forked from ScoDoc/ScoDoc
Compare commits
515 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
98e709bfd0 | ||
cdcb4a2468 | |||
03db1e183e | |||
6194bdc5ed | |||
bfa61cf035 | |||
a267c69501 | |||
e16e4a0ff3 | |||
fec6ccfb28 | |||
d79cd95dff | |||
c732536922 | |||
92d5bd9454 | |||
7eef38aefe | |||
94ec3266ed | |||
5ed92f9080 | |||
212616655b | |||
1f818da064 | |||
c6f81d1301 | |||
23a118a8cc | |||
ca5abc9c22 | |||
0e2a2d4b3b | |||
963a326426 | |||
0a093f420f | |||
9d76ef4d5d | |||
59cfb94b9d | |||
38262c066a | |||
fabe9c90cf | |||
e2ac77332c | |||
dbcd65e2d4 | |||
7996ec2ecd | |||
3f499f7631 | |||
40b6722743 | |||
f214aa8507 | |||
cd56337958 | |||
257ad34724 | |||
99812ca25d | |||
60711d674b | |||
d5dfa37b91 | |||
ee2c9ccb84 | |||
407129da0f | |||
422a200e88 | |||
d05ec0e4f1 | |||
98accd7a6a | |||
0011427302 | |||
e02db2a751 | |||
6ec4011a3d | |||
9c2c8d9047 | |||
be76fc8f42 | |||
e159ce883c | |||
b1cb4ddea3 | |||
8fce660173 | |||
3c42bae235 | |||
b1f203bf25 | |||
8dbe5f7926 | |||
0196d96543 | |||
40017ad69b | |||
31581419a7 | |||
25bd2b6e45 | |||
2a8ee95df7 | |||
b98d9c2036 | |||
76236f1125 | |||
3458e5f611 | |||
12d1e7fe99 | |||
a407856cbb | |||
a7437bfdc5 | |||
d3ba09e6da | |||
6a0713b432 | |||
935ae99e03 | |||
363de7be76 | |||
2f84d9968c | |||
94d49ac870 | |||
175f65cd1f | |||
8ddb3eb427 | |||
1a6aa269ee | |||
9264ac7b31 | |||
fdc819e904 | |||
8dce157d06 | |||
50191e6f77 | |||
b359aa5c93 | |||
88e092f76b | |||
de11836479 | |||
c7731f0455 | |||
88320ce95f | |||
784b3eea8c | |||
6dc19b6f80 | |||
6c7f0ef72f | |||
c8a70670be | |||
78fa0a88cf | |||
07f478d6ea | |||
8e7509b035 | |||
5d77d415a2 | |||
6809f24cee | |||
e62038ea59 | |||
fd34984b29 | |||
3ac806220c | |||
f0c0490816 | |||
bbcd6d7b33 | |||
c98df4529e | |||
|
4f71575154 | ||
|
b73a02ac67 | ||
4c648212dd | |||
157adf76e4 | |||
29d8c7b0e6 | |||
300dd4ac22 | |||
baaf8b0244 | |||
f60eba1b9c | |||
48a3950cfa | |||
32efafd61d | |||
1a80202468 | |||
77723982f5 | |||
fd5fefec71 | |||
693f6c9cbd | |||
a9b809655b | |||
1e0645fd14 | |||
83e271ad8c | |||
a84a5da836 | |||
6235f2346e | |||
831b6a1039 | |||
8d91505b8b | |||
cc0eca20fa | |||
0d9b810dd9 | |||
58fb6ecea0 | |||
77b877c063 | |||
67e7e6cb1d | |||
a3260f05b0 | |||
64f6b01140 | |||
0b118a6947 | |||
1b022231f8 | |||
c78745cd3d | |||
71114c391d | |||
39f7e1b63f | |||
e68c957c62 | |||
aa631a8a27 | |||
6dc770f79b | |||
72e96abfd0 | |||
0bc546853d | |||
120901d9dc | |||
6efd2d2e6e | |||
8e77689ff0 | |||
c649c05628 | |||
5861a2d802 | |||
86cf10d26d | |||
87776b412c | |||
3dcf8495b0 | |||
8d7958c80d | |||
0e5b4f9cb7 | |||
42a63298b4 | |||
574f7fc376 | |||
5f9c525d39 | |||
527a73b65a | |||
28baca0696 | |||
c0a4f40803 | |||
95b7f813ff | |||
|
3c227562ff | ||
|
b9554cd6b5 | ||
|
6468bb30e4 | ||
|
104a7058a1 | ||
|
345a842f59 | ||
|
5aeffcbf1d | ||
84ac334b13 | |||
84a333c6d9 | |||
f78653c184 | |||
ca5ea5148d | |||
7c25e14dda | |||
06ad4f39a9 | |||
41a4610b7a | |||
12ce1d16f2 | |||
462db3f9ef | |||
107341efab | |||
7739530383 | |||
7018a41425 | |||
a992d80982 | |||
a8f97bedde | |||
73a242663f | |||
eae49810dd | |||
6bafcdbcac | |||
323522a27c | |||
9a7c98e906 | |||
c5e7012237 | |||
83d0200e57 | |||
18d5dc5fbd | |||
3fb157e316 | |||
613837ac37 | |||
bd1c2f1cb3 | |||
8ecdfd4e62 | |||
db58c57a78 | |||
c49bc44700 | |||
ecb3748c85 | |||
40919063f8 | |||
6a985f8558 | |||
91176d282c | |||
c45a229e95 | |||
b8f5cf712f | |||
705fcc31cd | |||
b7b12d20ad | |||
5144f9b8e1 | |||
0bd873ba65 | |||
e84e55467a | |||
87ff6b793c | |||
95a3a74ce8 | |||
48f54fa232 | |||
bf6d718e47 | |||
d77f745437 | |||
|
0f3e1ea95e | ||
|
aa956f4530 | ||
|
21f57aab8f | ||
|
53c9658ce1 | ||
|
e18990d804 | ||
|
c11599b64f | ||
|
095eb6ce20 | ||
|
61d4186ad3 | ||
|
4d72fec42d | ||
a63e14ce06 | |||
ba909d72f0 | |||
44a72c1ab9 | |||
|
2fd1b039f4 | ||
0f8998c891 | |||
be367de2a1 | |||
e81ad610b6 | |||
0f45101000 | |||
1224b46846 | |||
6b2ea5c5bc | |||
|
a7b856b1ec | ||
|
547040bb93 | ||
|
8bc780f2cf | ||
86f5751e79 | |||
b160f64e4f | |||
ee2ac9d986 | |||
fc78484186 | |||
21b5474a6f | |||
3c1acc9c00 | |||
2548a97515 | |||
ce1cb7516b | |||
|
cf3258f5f9 | ||
3998b5a366 | |||
728010bf69 | |||
e9f23d8b3e | |||
f6d442beb4 | |||
b19c94a1f4 | |||
9fb70aef5d | |||
c8c05ecd77 | |||
|
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 | |||
|
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 | ||
3bae99c5cd | |||
ae2f8a97fd | |||
1598537f24 | |||
557a0c3f6d | |||
fa84f9ed89 | |||
dac46b8366 | |||
a92d2a6edf | |||
caf88c5909 | |||
4da21bf4d3 | |||
bdaf416ccb | |||
066e03dae8 | |||
1847250bab | |||
1ce4ffecad | |||
91e77dd2dc | |||
7dcebf4b83 | |||
10caea92ae | |||
75c5256ba9 | |||
678959c76a | |||
77c0294c83 | |||
8d72229e8b | |||
84b02edd48 | |||
ec4aa0e26f | |||
50e8f2b4fe | |||
318f6f8a65 | |||
91e508bf9f | |||
036ce650c6 | |||
37a8b3bb0b | |||
ad46a190ab | |||
f69ce75b1f | |||
1813e3c7ce | |||
7bbdff67a0 | |||
dcf0f73c1b | |||
06cbd65365 | |||
268b75d441 | |||
0c5e338970 | |||
2731a4728b | |||
4f87f22586 | |||
a3e4c34745 | |||
78bb9a706e | |||
d6be0e131f | |||
9527240ea8 | |||
c6c7187c34 | |||
453f11084b | |||
6b29a205b6 |
|
@ -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`.
|
||||||
|
|
||||||
|
|
117
app/__init__.py
117
app/__init__.py
|
@ -19,19 +19,26 @@ from flask import abort, flash, has_request_context, jsonify
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
from flask.json import JSONEncoder
|
from flask.json import JSONEncoder
|
||||||
from flask.logging import default_handler
|
from flask.logging import default_handler
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
|
||||||
from flask_migrate import Migrate
|
from flask_bootstrap import Bootstrap
|
||||||
|
from flask_caching import Cache
|
||||||
from flask_login import LoginManager, current_user
|
from flask_login import LoginManager, current_user
|
||||||
from flask_mail import Mail
|
from flask_mail import Mail
|
||||||
from flask_bootstrap import Bootstrap
|
from flask_migrate import Migrate
|
||||||
from flask_moment import Moment
|
from flask_moment import Moment
|
||||||
from flask_caching import Cache
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
from jinja2 import select_autoescape
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
|
from flask_cas import CAS
|
||||||
|
|
||||||
from app.scodoc.sco_exceptions import (
|
from app.scodoc.sco_exceptions import (
|
||||||
AccessDenied,
|
AccessDenied,
|
||||||
ScoBugCatcher,
|
ScoBugCatcher,
|
||||||
|
ScoException,
|
||||||
ScoGenError,
|
ScoGenError,
|
||||||
|
ScoInvalidCSRF,
|
||||||
ScoValueError,
|
ScoValueError,
|
||||||
APIInvalidParams,
|
APIInvalidParams,
|
||||||
)
|
)
|
||||||
|
@ -60,11 +67,20 @@ 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 handle_invalid_csrf(exc):
|
||||||
|
"""Form submit with invalid CSRF token"""
|
||||||
|
# logout user and go back to login page with an error message
|
||||||
|
from app import auth
|
||||||
|
|
||||||
|
auth.logic.logout()
|
||||||
|
return render_template("error_csrf.j2", exc=exc), 404
|
||||||
|
|
||||||
|
|
||||||
def internal_server_error(exc):
|
def internal_server_error(exc):
|
||||||
|
@ -74,7 +90,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 +108,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)
|
||||||
|
|
||||||
|
@ -119,7 +138,7 @@ def handle_invalid_usage(error):
|
||||||
# JSON ENCODING
|
# JSON ENCODING
|
||||||
class ScoDocJSONEncoder(JSONEncoder):
|
class ScoDocJSONEncoder(JSONEncoder):
|
||||||
def default(self, o):
|
def default(self, o):
|
||||||
if isinstance(o, datetime.datetime):
|
if isinstance(o, (datetime.datetime, datetime.date)):
|
||||||
return o.isoformat()
|
return o.isoformat()
|
||||||
|
|
||||||
return super().default(o)
|
return super().default(o)
|
||||||
|
@ -127,7 +146,7 @@ class ScoDocJSONEncoder(JSONEncoder):
|
||||||
|
|
||||||
def render_raw_html(template_filename: str, **args) -> str:
|
def render_raw_html(template_filename: str, **args) -> str:
|
||||||
"""Load and render an HTML file _without_ using Flask
|
"""Load and render an HTML file _without_ using Flask
|
||||||
Necessary for 503 error mesage, when DB is down and Flask may be broken.
|
Necessary for 503 error message, when DB is down and Flask may be broken.
|
||||||
"""
|
"""
|
||||||
template_path = os.path.join(
|
template_path = os.path.join(
|
||||||
current_app.config["SCODOC_DIR"],
|
current_app.config["SCODOC_DIR"],
|
||||||
|
@ -142,7 +161,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):
|
||||||
|
@ -221,14 +240,16 @@ class ReverseProxied(object):
|
||||||
|
|
||||||
def create_app(config_class=DevConfig):
|
def create_app(config_class=DevConfig):
|
||||||
app = Flask(__name__, static_url_path="/ScoDoc/static", static_folder="static")
|
app = Flask(__name__, static_url_path="/ScoDoc/static", static_folder="static")
|
||||||
|
from app.auth import cas
|
||||||
|
|
||||||
|
CAS(app, url_prefix="/cas", configuration_function=cas.set_cas_configuration)
|
||||||
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
||||||
app.json_encoder = ScoDocJSONEncoder
|
app.json_encoder = ScoDocJSONEncoder
|
||||||
app.logger.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
# Evite de logguer toutes les requetes dans notre log
|
|
||||||
logging.getLogger("werkzeug").disabled = True
|
|
||||||
|
|
||||||
app.config.from_object(config_class)
|
app.config.from_object(config_class)
|
||||||
|
# Evite de logguer toutes les requetes dans notre log
|
||||||
|
logging.getLogger("werkzeug").disabled = True
|
||||||
|
app.logger.setLevel(app.config["LOG_LEVEL"])
|
||||||
|
|
||||||
# Vérifie/crée lien sym pour les URL statiques
|
# Vérifie/crée lien sym pour les URL statiques
|
||||||
link_filename = f"{app.root_path}/static/links/{sco_version.SCOVERSION}"
|
link_filename = f"{app.root_path}/static/links/{sco_version.SCOVERSION}"
|
||||||
|
@ -240,6 +261,7 @@ def create_app(config_class=DevConfig):
|
||||||
migrate.init_app(app, db)
|
migrate.init_app(app, db)
|
||||||
login.init_app(app)
|
login.init_app(app)
|
||||||
mail.init_app(app)
|
mail.init_app(app)
|
||||||
|
app.extensions["mail"].debug = 0 # disable copy of mails to stderr
|
||||||
bootstrap.init_app(app)
|
bootstrap.init_app(app)
|
||||||
moment.init_app(app)
|
moment.init_app(app)
|
||||||
cache.init_app(app)
|
cache.init_app(app)
|
||||||
|
@ -250,6 +272,7 @@ def create_app(config_class=DevConfig):
|
||||||
app.register_error_handler(ScoGenError, handle_sco_value_error)
|
app.register_error_handler(ScoGenError, handle_sco_value_error)
|
||||||
app.register_error_handler(ScoValueError, handle_sco_value_error)
|
app.register_error_handler(ScoValueError, handle_sco_value_error)
|
||||||
app.register_error_handler(ScoBugCatcher, handle_sco_bug)
|
app.register_error_handler(ScoBugCatcher, handle_sco_bug)
|
||||||
|
app.register_error_handler(ScoInvalidCSRF, handle_invalid_csrf)
|
||||||
app.register_error_handler(AccessDenied, handle_access_denied)
|
app.register_error_handler(AccessDenied, handle_access_denied)
|
||||||
app.register_error_handler(500, internal_server_error)
|
app.register_error_handler(500, internal_server_error)
|
||||||
app.register_error_handler(503, postgresql_server_error)
|
app.register_error_handler(503, postgresql_server_error)
|
||||||
|
@ -271,6 +294,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/...
|
||||||
|
@ -370,6 +396,15 @@ def create_app(config_class=DevConfig):
|
||||||
|
|
||||||
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorExample)
|
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorExample)
|
||||||
|
|
||||||
|
from app.auth.cas import set_cas_configuration
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
try:
|
||||||
|
set_cas_configuration(app)
|
||||||
|
except sqlalchemy.exc.ProgrammingError:
|
||||||
|
# Si la base n'a pas été upgradée (arrive durrant l'install)
|
||||||
|
# il se peut que la table scodoc_site_config n'existe pas encore.
|
||||||
|
pass
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
@ -435,8 +470,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 +496,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 +533,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}"
|
||||||
|
@ -510,10 +561,9 @@ def log_call_stack():
|
||||||
|
|
||||||
# Alarms by email:
|
# Alarms by email:
|
||||||
def send_scodoc_alarm(subject, txt):
|
def send_scodoc_alarm(subject, txt):
|
||||||
from app.scodoc import sco_preferences
|
|
||||||
from app import email
|
from app import email
|
||||||
|
|
||||||
sender = sco_preferences.get_preference("email_from_addr")
|
sender = email.get_from_addr()
|
||||||
email.send_email(subject, sender, ["exception@scodoc.org"], txt)
|
email.send_email(subject, sender, ["exception@scodoc.org"], txt)
|
||||||
|
|
||||||
|
|
||||||
|
@ -530,3 +580,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,13 +2,17 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
api_bp = Blueprint("api", __name__)
|
api_bp = Blueprint("api", __name__)
|
||||||
api_web_bp = Blueprint("apiweb", __name__)
|
api_web_bp = Blueprint("apiweb", __name__)
|
||||||
|
|
||||||
|
# HTTP ERROR STATUS
|
||||||
|
API_CLIENT_ERROR = 400 # erreur dans les paramètres fournis par le client
|
||||||
|
|
||||||
|
|
||||||
@api_bp.errorhandler(ScoException)
|
@api_bp.errorhandler(ScoException)
|
||||||
@api_bp.errorhandler(404)
|
@api_bp.errorhandler(404)
|
||||||
|
@ -31,9 +35,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 +62,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
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
from flask import jsonify
|
from flask import jsonify
|
||||||
|
|
||||||
from app.api import api_bp as bp
|
from app.api import api_bp as bp, API_CLIENT_ERROR
|
||||||
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
|
||||||
from app.models import Identite
|
from app.models import Identite
|
||||||
|
|
646
app/api/assiduites.py
Normal file
646
app/api/assiduites.py
Normal file
|
@ -0,0 +1,646 @@
|
||||||
|
##############################################################################
|
||||||
|
# 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, current_user
|
||||||
|
|
||||||
|
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.auth.models import User
|
||||||
|
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",
|
||||||
|
"user_id: 1 or null,
|
||||||
|
"est_just": False or True,
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
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?formsemestre_id=[int]
|
||||||
|
ex query?formsemestre_id=3
|
||||||
|
user_id (l'id de l'auteur de l'assiduité)
|
||||||
|
query?user_id=[int]
|
||||||
|
ex query?user_id=3
|
||||||
|
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||||
|
query?est_just=[bool]
|
||||||
|
query?est_just=f
|
||||||
|
query?est_just=t
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
user_id (l'id de l'auteur de l'assiduité)
|
||||||
|
query?user_id=[int]
|
||||||
|
ex query?user_id=3
|
||||||
|
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||||
|
query?est_just=[bool]
|
||||||
|
query?est_just=f
|
||||||
|
query?est_just=t
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""Retourne toutes les assiduités du formsemestre"""
|
||||||
|
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
|
||||||
|
):
|
||||||
|
"""Comptage des assiduités du formsemestre"""
|
||||||
|
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
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
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,
|
||||||
|
user_id=current_user.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(nouv_assiduite)
|
||||||
|
db.session.commit()
|
||||||
|
return (200, {"assiduite_id": nouv_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_delete():
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
"est_just"?: bool
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
# Cas 4 : est_just
|
||||||
|
est_just = data.get("est_just")
|
||||||
|
if est_just is not None:
|
||||||
|
if not isinstance(est_just, bool):
|
||||||
|
errors.append("param 'est_just' : booléen non reconnu")
|
||||||
|
else:
|
||||||
|
assiduite_unique.est_just = est_just
|
||||||
|
|
||||||
|
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", "").replace(" ", "+")
|
||||||
|
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", "").replace(" ", "+")
|
||||||
|
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")
|
||||||
|
|
||||||
|
# cas 7 : est_just
|
||||||
|
|
||||||
|
est_just: str = requested.args.get("est_just")
|
||||||
|
if est_just is not None:
|
||||||
|
trues: tuple[str] = ("v", "t", "vrai", "true")
|
||||||
|
falses: tuple[str] = ("f", "faux", "false")
|
||||||
|
|
||||||
|
if est_just.lower() in trues:
|
||||||
|
filtered["est_just"] = True
|
||||||
|
elif est_just.lower() in falses:
|
||||||
|
filtered["est_just"] = False
|
||||||
|
|
||||||
|
# cas 8 : user_id
|
||||||
|
|
||||||
|
user_id = requested.args.get("user_id", False)
|
||||||
|
if user_id is not False:
|
||||||
|
filtered["user_id"] = user_id
|
||||||
|
|
||||||
|
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", "").replace(" ", "+")
|
||||||
|
deb: datetime = scu.is_iso_formated(deb, True)
|
||||||
|
|
||||||
|
# cas 3 : date de fin
|
||||||
|
fin = requested.args.get("date_fin", "").replace(" ", "+")
|
||||||
|
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)
|
||||||
|
|
||||||
|
# cas 6 : est_just
|
||||||
|
|
||||||
|
est_just: str = requested.args.get("est_just")
|
||||||
|
if est_just is not None:
|
||||||
|
trues: tuple[str] = ("v", "t", "vrai", "true")
|
||||||
|
falses: tuple[str] = ("f", "faux", "false")
|
||||||
|
|
||||||
|
if est_just.lower() in trues:
|
||||||
|
assiduites_query: Assiduite = scass.filter_assiduites_by_est_just(
|
||||||
|
assiduites_query, True
|
||||||
|
)
|
||||||
|
elif est_just.lower() in falses:
|
||||||
|
assiduites_query: Assiduite = scass.filter_assiduites_by_est_just(
|
||||||
|
assiduites_query, False
|
||||||
|
)
|
||||||
|
|
||||||
|
# cas 8 : user_id
|
||||||
|
|
||||||
|
user_id = requested.args.get("user_id", False)
|
||||||
|
if user_id is not False:
|
||||||
|
assiduites_query: Assiduite = scass.filter_by_user_id(assiduites_query, user_id)
|
||||||
|
|
||||||
|
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
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -11,7 +11,6 @@
|
||||||
from flask import g, jsonify, request
|
from flask import g, jsonify, request
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
import app
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.api import api_bp as bp, api_web_bp
|
from app.api import api_bp as bp, api_web_bp
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
|
@ -48,12 +47,9 @@ def billets_absence_create():
|
||||||
justified = data.get("justified", False)
|
justified = data.get("justified", False)
|
||||||
if None in (etudid, abs_begin, abs_end):
|
if None in (etudid, abs_begin, abs_end):
|
||||||
return json_error(
|
return json_error(
|
||||||
404, message="Paramètre manquant: etudid, abs_bein, abs_end requis"
|
404, message="Paramètre manquant: etudid, abs_begin, abs_end requis"
|
||||||
)
|
)
|
||||||
query = Identite.query.filter_by(etudid=etudid)
|
etud = Identite.get_etud(etudid)
|
||||||
if g.scodoc_dept:
|
|
||||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
|
||||||
etud = query.first_or_404()
|
|
||||||
billet = BilletAbsence(
|
billet = BilletAbsence(
|
||||||
etudid=etud.id,
|
etudid=etud.id,
|
||||||
abs_begin=abs_begin,
|
abs_begin=abs_begin,
|
||||||
|
|
|
@ -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,13 +10,14 @@
|
||||||
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, API_CLIENT_ERROR
|
||||||
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
|
||||||
from app.models import Departement, FormSemestre
|
from app.models import Departement, FormSemestre
|
||||||
|
@ -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>")
|
||||||
|
@ -103,12 +105,12 @@ def departement_create():
|
||||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
acronym = str(data.get("acronym", ""))
|
acronym = str(data.get("acronym", ""))
|
||||||
if not acronym:
|
if not acronym:
|
||||||
return json_error(404, "missing acronym")
|
return json_error(API_CLIENT_ERROR, "missing acronym")
|
||||||
visible = bool(data.get("visible", True))
|
visible = bool(data.get("visible", True))
|
||||||
try:
|
try:
|
||||||
dept = departements.create_dept(acronym, visible=visible)
|
dept = departements.create_dept(acronym, visible=visible)
|
||||||
except ScoValueError as exc:
|
except ScoValueError as exc:
|
||||||
return json_error(404, exc.args[0] if exc.args else "")
|
return json_error(500, exc.args[0] if exc.args else "")
|
||||||
return jsonify(dept.to_dict())
|
return jsonify(dept.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@ -128,7 +130,7 @@ def departement_edit(acronym):
|
||||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
visible = bool(data.get("visible", None))
|
visible = bool(data.get("visible", None))
|
||||||
if visible is None:
|
if visible is None:
|
||||||
return json_error(404, "missing argument: visible")
|
return json_error(API_CLIENT_ERROR, "missing argument: visible")
|
||||||
visible = bool(visible)
|
visible = bool(visible)
|
||||||
dept.visible = visible
|
dept.visible = visible
|
||||||
db.session.add(dept)
|
db.session.add(dept)
|
||||||
|
@ -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,88 @@ 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(
|
@bp.route(
|
||||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
|
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>/pdf/nosig",
|
||||||
methods=["GET"],
|
defaults={"pdf": True, "with_img_signatures_pdf": False},
|
||||||
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(
|
@api_web_bp.route(
|
||||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
|
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>/pdf/nosig",
|
||||||
methods=["GET"],
|
defaults={"pdf": True, "with_img_signatures_pdf": False},
|
||||||
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,
|
||||||
|
with_img_signatures_pdf: bool = True,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
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 inexistant")
|
||||||
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,
|
||||||
|
version=version,
|
||||||
|
format="pdf",
|
||||||
|
with_img_signatures_pdf=with_img_signatures_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
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -15,13 +15,50 @@ import app
|
||||||
|
|
||||||
from app.api import api_bp as bp, api_web_bp
|
from app.api import api_bp as bp, api_web_bp
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
from app.scodoc.sco_utils import json_error
|
|
||||||
from app.models import Evaluation, ModuleImpl, FormSemestre
|
from app.models import Evaluation, ModuleImpl, FormSemestre
|
||||||
from app.scodoc import sco_evaluation_db
|
from app.scodoc import sco_evaluation_db
|
||||||
from app.scodoc.sco_permissions import Permission
|
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 +70,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
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ from flask import g, jsonify, request
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app.api import api_bp as bp, api_web_bp
|
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
from app.scodoc.sco_utils import json_error
|
from app.scodoc.sco_utils import json_error
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
|
@ -22,6 +22,8 @@ from app.models import (
|
||||||
Evaluation,
|
Evaluation,
|
||||||
FormSemestre,
|
FormSemestre,
|
||||||
FormSemestreEtape,
|
FormSemestreEtape,
|
||||||
|
FormSemestreInscription,
|
||||||
|
Identite,
|
||||||
ModuleImpl,
|
ModuleImpl,
|
||||||
NotesNotes,
|
NotesNotes,
|
||||||
)
|
)
|
||||||
|
@ -30,6 +32,7 @@ from app.scodoc import sco_groups
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
from app.tables.recap import TableRecap
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/formsemestre/<int:formsemestre_id>")
|
@bp.route("/formsemestre/<int:formsemestre_id>")
|
||||||
|
@ -95,11 +98,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)
|
||||||
|
@ -107,7 +113,7 @@ def formsemestres_query():
|
||||||
try:
|
try:
|
||||||
annee_scolaire_int = int(annee_scolaire)
|
annee_scolaire_int = int(annee_scolaire)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return json_error(404, "invalid annee_scolaire: not int")
|
return json_error(API_CLIENT_ERROR, "invalid annee_scolaire: not int")
|
||||||
debut_annee = scu.date_debut_anne_scolaire(annee_scolaire_int)
|
debut_annee = scu.date_debut_anne_scolaire(annee_scolaire_int)
|
||||||
fin_annee = scu.date_fin_anne_scolaire(annee_scolaire_int)
|
fin_annee = scu.date_fin_anne_scolaire(annee_scolaire_int)
|
||||||
formsemestres = formsemestres.filter(
|
formsemestres = formsemestres.filter(
|
||||||
|
@ -119,22 +125,36 @@ def formsemestres_query():
|
||||||
try:
|
try:
|
||||||
dept_id = int(dept_id)
|
dept_id = int(dept_id)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return json_error(404, "invalid dept_id: not int")
|
return json_error(404, "invalid dept_id: integer expected")
|
||||||
formsemestres = formsemestres.filter_by(dept_id=dept_id)
|
formsemestres = formsemestres.filter_by(dept_id=dept_id)
|
||||||
if etape_apo is not None:
|
if etape_apo is not None:
|
||||||
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 +165,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 +405,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
|
||||||
|
@ -444,7 +468,7 @@ def formsemestre_resultat(formsemestre_id: int):
|
||||||
"""
|
"""
|
||||||
format_spec = request.args.get("format", None)
|
format_spec = request.args.get("format", None)
|
||||||
if format_spec is not None and format_spec != "raw":
|
if format_spec is not None and format_spec != "raw":
|
||||||
return json_error(404, "invalid format specification")
|
return json_error(API_CLIENT_ERROR, "invalid format specification")
|
||||||
convert_values = format_spec != "raw"
|
convert_values = format_spec != "raw"
|
||||||
|
|
||||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||||
|
@ -453,16 +477,14 @@ def formsemestre_resultat(formsemestre_id: int):
|
||||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||||
app.set_sco_dept(formsemestre.departement.acronym)
|
app.set_sco_dept(formsemestre.departement.acronym)
|
||||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
rows, footer_rows, titles, column_ids = res.get_table_recap(
|
table = TableRecap(
|
||||||
convert_values=convert_values,
|
res, convert_values=convert_values, include_evaluations=False, mode_jury=False
|
||||||
include_evaluations=False,
|
|
||||||
mode_jury=False,
|
|
||||||
allow_html=False,
|
|
||||||
)
|
)
|
||||||
# Supprime les champs inutiles (mise en forme)
|
# Supprime les champs inutiles (mise en forme)
|
||||||
table = [{k: row[k] for k in row if not k[0] == "_"} for row in rows]
|
rows = table.to_list()
|
||||||
# Ajoute les groupes
|
# Ajoute le groupe de chaque partition:
|
||||||
etud_groups = sco_groups.get_formsemestre_etuds_groups(formsemestre_id)
|
etud_groups = sco_groups.get_formsemestre_etuds_groups(formsemestre_id)
|
||||||
for row in table:
|
for row in rows:
|
||||||
row["partitions"] = etud_groups.get(row["etudid"], {})
|
row["partitions"] = etud_groups.get(row["etudid"], {})
|
||||||
return jsonify(table)
|
|
||||||
|
return jsonify(rows)
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
##############################################################################
|
##############################################################################
|
||||||
# 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 : jury
|
ScoDoc 9 API : jury WIP
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import g, jsonify, request
|
from flask import jsonify
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app import db, log
|
|
||||||
from app.api import api_bp as bp, api_web_bp
|
from app.api import api_bp as bp, api_web_bp
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
from app.scodoc.sco_exceptions import ScoException
|
from app.scodoc.sco_exceptions import ScoException
|
||||||
from app.scodoc.sco_utils import json_error
|
from app.but import jury_but_results
|
||||||
from app.but import jury_but_recap
|
from app.models import FormSemestre
|
||||||
from app.models import FormSemestre, FormSemestreInscription, Identite
|
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,7 +31,7 @@ def decisions_jury(formsemestre_id: int):
|
||||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||||
if formsemestre.formation.is_apc():
|
if formsemestre.formation.is_apc():
|
||||||
app.set_sco_dept(formsemestre.departement.acronym)
|
app.set_sco_dept(formsemestre.departement.acronym)
|
||||||
rows = jury_but_recap.get_jury_but_results(formsemestre)
|
rows = jury_but_results.get_jury_but_results(formsemestre)
|
||||||
return jsonify(rows)
|
return jsonify(rows)
|
||||||
else:
|
else:
|
||||||
raise ScoException("non implemente")
|
raise ScoException("non implemente")
|
||||||
|
|
591
app/api/justificatifs.py
Normal file
591
app/api/justificatifs.py
Normal file
|
@ -0,0 +1,591 @@
|
||||||
|
##############################################################################
|
||||||
|
# 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, current_user
|
||||||
|
|
||||||
|
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 compute_assiduites_justified
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# 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",
|
||||||
|
"user_id": 1 or null,
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
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
|
||||||
|
user_id (l'id de l'auteur du justificatif)
|
||||||
|
query?user_id=[int]
|
||||||
|
ex query?user_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)
|
||||||
|
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
|
||||||
|
compute_assiduites_justified(Justificatif.query.filter_by(etudid=etudid), True)
|
||||||
|
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,
|
||||||
|
user_id=current_user.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(nouv_justificatif)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return (
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"justif_id": nouv_justificatif.id,
|
||||||
|
"couverture": scass.justifies(nouv_justificatif),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
avant_ids: list[int] = scass.justifies(justificatif_unique)
|
||||||
|
# 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.replace(" ", "+"), 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.replace(" ", "+"), 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 ")
|
||||||
|
|
||||||
|
# Mise à jour des dates
|
||||||
|
deb = deb if deb is not None else justificatif_unique.date_debut
|
||||||
|
fin = fin if fin is not None else justificatif_unique.date_fin
|
||||||
|
|
||||||
|
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(
|
||||||
|
{
|
||||||
|
"couverture": {
|
||||||
|
"avant": avant_ids,
|
||||||
|
"après": compute_assiduites_justified(
|
||||||
|
Justificatif.query.filter_by(etudid=justificatif_unique.etudid),
|
||||||
|
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/<int:justif_id>/import", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>/import", 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/<int:justif_id>/export/<filename>", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>/export/<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/<int:justif_id>/remove", methods=["POST"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>/remove", 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/<int:justif_id>/list", methods=["GET"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>/list", 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/<int:justif_id>/justifies", methods=["GET"])
|
||||||
|
@api_web_bp.route("/justificatif/<int:justif_id>/justifies", methods=["GET"])
|
||||||
|
@scodoc
|
||||||
|
@login_required
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
# @permission_required(Permission.ScoAssiduiteChange)
|
||||||
|
def justif_justifies(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", "").replace(" ", "+")
|
||||||
|
deb: datetime = scu.is_iso_formated(deb, True)
|
||||||
|
|
||||||
|
# cas 3 : date de fin
|
||||||
|
fin = requested.args.get("date_fin", "").replace(" ", "+")
|
||||||
|
fin = scu.is_iso_formated(fin, True)
|
||||||
|
|
||||||
|
if (deb, fin) != (None, None):
|
||||||
|
justificatifs_query: Justificatif = scass.filter_by_date(
|
||||||
|
justificatifs_query, Justificatif, deb, fin
|
||||||
|
)
|
||||||
|
|
||||||
|
user_id = requested.args.get("user_id", False)
|
||||||
|
if user_id is not False:
|
||||||
|
justificatif_query: Justificatif = scass.filter_by_user_id(
|
||||||
|
justificatif_query, user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -12,13 +12,14 @@ from flask_login import login_required
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app import db, log
|
from app import db, log
|
||||||
from app.api import api_bp as bp, api_web_bp
|
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
from app.scodoc.sco_utils import json_error
|
from app.scodoc.sco_utils import json_error
|
||||||
from app.models import FormSemestre, FormSemestreInscription, Identite
|
from app.models import FormSemestre, FormSemestreInscription, Identite
|
||||||
from app.models import GroupDescr, Partition
|
from app.models import GroupDescr, Partition, Scolog
|
||||||
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
|
||||||
|
|
||||||
|
@ -137,7 +138,7 @@ def etud_in_group_query(group_id: int):
|
||||||
"""Étudiants du groupe, filtrés par état"""
|
"""Étudiants du groupe, filtrés par état"""
|
||||||
etat = request.args.get("etat")
|
etat = request.args.get("etat")
|
||||||
if etat not in {None, scu.INSCRIT, scu.DEMISSION, scu.DEF}:
|
if etat not in {None, scu.INSCRIT, scu.DEMISSION, scu.DEF}:
|
||||||
return json_error(404, "etat: valeur invalide")
|
return json_error(API_CLIENT_ERROR, "etat: valeur invalide")
|
||||||
query = GroupDescr.query.filter_by(id=group_id)
|
query = GroupDescr.query.filter_by(id=group_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = (
|
query = (
|
||||||
|
@ -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,9 +199,19 @@ 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()
|
||||||
|
Scolog.logdb(
|
||||||
|
method="group_remove_etud",
|
||||||
|
etudid=etud.id,
|
||||||
|
msg=f"Retrait du groupe {group.group_name} de {group.partition.partition_name}",
|
||||||
|
commit=True,
|
||||||
|
)
|
||||||
|
# Update parcours
|
||||||
|
group.partition.formsemestre.update_inscriptions_parcours_from_groups()
|
||||||
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
|
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
|
||||||
return jsonify({"group_id": group_id, "etudid": etudid})
|
return jsonify({"group_id": group_id, "etudid": etudid})
|
||||||
|
|
||||||
|
@ -232,6 +234,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)
|
||||||
|
@ -239,7 +243,15 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
||||||
)
|
)
|
||||||
for group in groups:
|
for group in groups:
|
||||||
group.etuds.remove(etud)
|
group.etuds.remove(etud)
|
||||||
|
Scolog.logdb(
|
||||||
|
method="partition_remove_etud",
|
||||||
|
etudid=etud.id,
|
||||||
|
msg=f"Retrait du groupe {group.group_name} de {group.partition.partition_name}",
|
||||||
|
commit=True,
|
||||||
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
# Update parcours
|
||||||
|
partition.formsemestre.update_inscriptions_parcours_from_groups()
|
||||||
app.set_sco_dept(partition.formsemestre.departement.acronym)
|
app.set_sco_dept(partition.formsemestre.departement.acronym)
|
||||||
sco_cache.invalidate_formsemestre(partition.formsemestre_id)
|
sco_cache.invalidate_formsemestre(partition.formsemestre_id)
|
||||||
return jsonify({"partition_id": partition_id, "etudid": etudid})
|
return jsonify({"partition_id": partition_id, "etudid": etudid})
|
||||||
|
@ -262,14 +274,16 @@ 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:
|
||||||
return json_error(404, "missing group name or invalid data format")
|
return json_error(API_CLIENT_ERROR, "missing group name or invalid data format")
|
||||||
if not GroupDescr.check_name(partition, group_name):
|
if not GroupDescr.check_name(partition, group_name):
|
||||||
return json_error(404, "invalid group_name")
|
return json_error(API_CLIENT_ERROR, "invalid group_name")
|
||||||
group_name = group_name.strip()
|
group_name = group_name.strip()
|
||||||
|
|
||||||
group = GroupDescr(group_name=group_name, partition_id=partition_id)
|
group = GroupDescr(group_name=group_name, partition_id=partition_id)
|
||||||
|
@ -294,8 +308,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,14 +334,16 @@ 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:
|
||||||
group_name = group_name.strip()
|
group_name = group_name.strip()
|
||||||
if not GroupDescr.check_name(group.partition, group_name, existing=True):
|
if not GroupDescr.check_name(group.partition, group_name, existing=True):
|
||||||
return json_error(404, "invalid group_name")
|
return json_error(API_CLIENT_ERROR, "invalid group_name")
|
||||||
group.group_name = group_name
|
group.group_name = group_name
|
||||||
db.session.add(group)
|
db.session.add(group)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -358,17 +376,23 @@ 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:
|
||||||
return json_error(404, "missing partition_name or invalid data format")
|
return json_error(
|
||||||
|
API_CLIENT_ERROR, "missing partition_name or invalid data format"
|
||||||
|
)
|
||||||
if partition_name == scu.PARTITION_PARCOURS:
|
if partition_name == scu.PARTITION_PARCOURS:
|
||||||
return json_error(404, f"invalid partition_name {scu.PARTITION_PARCOURS}")
|
return json_error(
|
||||||
|
API_CLIENT_ERROR, f"invalid partition_name {scu.PARTITION_PARCOURS}"
|
||||||
|
)
|
||||||
if not Partition.check_name(formsemestre, partition_name):
|
if not Partition.check_name(formsemestre, partition_name):
|
||||||
return json_error(404, "invalid partition_name")
|
return json_error(API_CLIENT_ERROR, "invalid partition_name")
|
||||||
numero = data.get("numero", 0)
|
numero = data.get("numero", 0)
|
||||||
if not isinstance(numero, int):
|
if not isinstance(numero, int):
|
||||||
return json_error(404, "invalid type for numero")
|
return json_error(API_CLIENT_ERROR, "invalid type for numero")
|
||||||
args = {
|
args = {
|
||||||
"formsemestre_id": formsemestre_id,
|
"formsemestre_id": formsemestre_id,
|
||||||
"partition_name": partition_name.strip(),
|
"partition_name": partition_name.strip(),
|
||||||
|
@ -379,7 +403,7 @@ def partition_create(formsemestre_id: int):
|
||||||
boolean_field, False if boolean_field != "groups_editable" else True
|
boolean_field, False if boolean_field != "groups_editable" else True
|
||||||
)
|
)
|
||||||
if not isinstance(value, bool):
|
if not isinstance(value, bool):
|
||||||
return json_error(404, f"invalid type for {boolean_field}")
|
return json_error(API_CLIENT_ERROR, f"invalid type for {boolean_field}")
|
||||||
args[boolean_field] = value
|
args[boolean_field] = value
|
||||||
|
|
||||||
partition = Partition(**args)
|
partition = Partition(**args)
|
||||||
|
@ -406,12 +430,14 @@ 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
|
||||||
):
|
):
|
||||||
return json_error(
|
return json_error(
|
||||||
404,
|
API_CLIENT_ERROR,
|
||||||
message="paramètre liste des partitions invalide",
|
message="paramètre liste des partitions invalide",
|
||||||
)
|
)
|
||||||
for p_id, numero in zip(partition_ids, range(len(partition_ids))):
|
for p_id, numero in zip(partition_ids, range(len(partition_ids))):
|
||||||
|
@ -443,12 +469,14 @@ 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
|
||||||
):
|
):
|
||||||
return json_error(
|
return json_error(
|
||||||
404,
|
API_CLIENT_ERROR,
|
||||||
message="paramètre liste de groupe invalide",
|
message="paramètre liste de groupe invalide",
|
||||||
)
|
)
|
||||||
for group_id, numero in zip(group_ids, range(len(group_ids))):
|
for group_id, numero in zip(group_ids, range(len(group_ids))):
|
||||||
|
@ -484,24 +512,28 @@ 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")
|
||||||
#
|
#
|
||||||
if partition_name is not None and partition_name != partition.partition_name:
|
if partition_name is not None and partition_name != partition.partition_name:
|
||||||
if partition.is_parcours():
|
if partition.is_parcours():
|
||||||
return json_error(404, f"can't rename {scu.PARTITION_PARCOURS}")
|
return json_error(
|
||||||
|
API_CLIENT_ERROR, f"can't rename {scu.PARTITION_PARCOURS}"
|
||||||
|
)
|
||||||
if not Partition.check_name(
|
if not Partition.check_name(
|
||||||
partition.formsemestre, partition_name, existing=True
|
partition.formsemestre, partition_name, existing=True
|
||||||
):
|
):
|
||||||
return json_error(404, "invalid partition_name")
|
return json_error(API_CLIENT_ERROR, "invalid partition_name")
|
||||||
partition.partition_name = partition_name.strip()
|
partition.partition_name = partition_name.strip()
|
||||||
modified = True
|
modified = True
|
||||||
|
|
||||||
numero = data.get("numero")
|
numero = data.get("numero")
|
||||||
if numero is not None and numero != partition.numero:
|
if numero is not None and numero != partition.numero:
|
||||||
if not isinstance(numero, int):
|
if not isinstance(numero, int):
|
||||||
return json_error(404, "invalid type for numero")
|
return json_error(API_CLIENT_ERROR, "invalid type for numero")
|
||||||
partition.numero = numero
|
partition.numero = numero
|
||||||
modified = True
|
modified = True
|
||||||
|
|
||||||
|
@ -509,9 +541,11 @@ def partition_edit(partition_id: int):
|
||||||
value = data.get(boolean_field)
|
value = data.get(boolean_field)
|
||||||
if value is not None and value != getattr(partition, boolean_field):
|
if value is not None and value != getattr(partition, boolean_field):
|
||||||
if not isinstance(value, bool):
|
if not isinstance(value, bool):
|
||||||
return json_error(404, f"invalid type for {boolean_field}")
|
return json_error(API_CLIENT_ERROR, f"invalid type for {boolean_field}")
|
||||||
if boolean_field == "groups_editable" and partition.is_parcours():
|
if boolean_field == "groups_editable" and partition.is_parcours():
|
||||||
return json_error(404, f"can't change {scu.PARTITION_PARCOURS}")
|
return json_error(
|
||||||
|
API_CLIENT_ERROR, f"can't change {scu.PARTITION_PARCOURS}"
|
||||||
|
)
|
||||||
setattr(partition, boolean_field, value)
|
setattr(partition, boolean_field, value)
|
||||||
modified = True
|
modified = True
|
||||||
|
|
||||||
|
@ -542,8 +576,12 @@ 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(
|
||||||
|
API_CLIENT_ERROR, "ne peut pas supprimer la partition par défaut"
|
||||||
|
)
|
||||||
is_parcours = partition.is_parcours()
|
is_parcours = partition.is_parcours()
|
||||||
formsemestre: FormSemestre = partition.formsemestre
|
formsemestre: FormSemestre = partition.formsemestre
|
||||||
log(f"deleting partition {partition}")
|
log(f"deleting partition {partition}")
|
||||||
|
|
|
@ -18,6 +18,8 @@ def get_token():
|
||||||
@token_auth.login_required
|
@token_auth.login_required
|
||||||
def revoke_token():
|
def revoke_token():
|
||||||
"révoque le jeton de l'utilisateur courant"
|
"révoque le jeton de l'utilisateur courant"
|
||||||
token_auth.current_user().revoke_token()
|
user = token_auth.current_user()
|
||||||
|
user.revoke_token()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
log(f"API: revoking token for {user}")
|
||||||
return "", 204
|
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
|
||||||
##############################################################################
|
##############################################################################
|
||||||
"""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
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -12,8 +12,8 @@
|
||||||
from flask import g, jsonify, request
|
from flask import g, jsonify, request
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
|
|
||||||
from app import db, log
|
from app import db
|
||||||
from app.api import api_bp as bp, api_web_bp
|
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||||
from app.scodoc.sco_utils import json_error
|
from app.scodoc.sco_utils import json_error
|
||||||
from app.auth.models import User, Role, UserRole
|
from app.auth.models import User, Role, UserRole
|
||||||
from app.auth.models import is_valid_password
|
from app.auth.models import is_valid_password
|
||||||
|
@ -187,7 +187,7 @@ def user_password(uid: int):
|
||||||
if not password:
|
if not password:
|
||||||
return json_error(404, "user_password: missing password")
|
return json_error(404, "user_password: missing password")
|
||||||
if not is_valid_password(password):
|
if not is_valid_password(password):
|
||||||
return json_error(400, "user_password: invalid password")
|
return json_error(API_CLIENT_ERROR, "user_password: invalid password")
|
||||||
allowed_depts = current_user.get_depts_with_permission(Permission.ScoUsersAdmin)
|
allowed_depts = current_user.get_depts_with_permission(Permission.ScoUsersAdmin)
|
||||||
if (None not in allowed_depts) and ((user.dept not in allowed_depts)):
|
if (None not in allowed_depts) and ((user.dept not in allowed_depts)):
|
||||||
return json_error(403, "user_password: departement non autorise")
|
return json_error(403, "user_password: departement non autorise")
|
||||||
|
|
|
@ -6,3 +6,4 @@ from flask import Blueprint
|
||||||
bp = Blueprint("auth", __name__)
|
bp = Blueprint("auth", __name__)
|
||||||
|
|
||||||
from app.auth import routes
|
from app.auth import routes
|
||||||
|
from app.auth import cas
|
||||||
|
|
251
app/auth/cas.py
Normal file
251
app/auth/cas.py
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
# -*- coding: UTF-8 -*
|
||||||
|
"""
|
||||||
|
auth.cas.py
|
||||||
|
"""
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import flask
|
||||||
|
from flask import current_app, flash, url_for
|
||||||
|
from flask_login import current_user, login_user
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.auth import bp
|
||||||
|
from app.auth.models import User
|
||||||
|
from app.models.config import ScoDocSiteConfig
|
||||||
|
from app.scodoc import sco_excel
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError, AccessDenied
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
|
# after_cas_login/after_cas_logout : routes appelées par redirect depuis le serveur CAS.
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/after_cas_login")
|
||||||
|
def after_cas_login():
|
||||||
|
"Called by CAS after CAS authentication"
|
||||||
|
# Ici on a les infos dans flask.session["CAS_ATTRIBUTES"]
|
||||||
|
if ScoDocSiteConfig.is_cas_enabled() and ("CAS_ATTRIBUTES" in flask.session):
|
||||||
|
# Lookup user:
|
||||||
|
cas_id = flask.session["CAS_ATTRIBUTES"].get(
|
||||||
|
"cas:" + ScoDocSiteConfig.get("cas_attribute_id"),
|
||||||
|
flask.session.get("CAS_USERNAME"),
|
||||||
|
)
|
||||||
|
if cas_id is not None:
|
||||||
|
user: User = User.query.filter_by(cas_id=cas_id).first()
|
||||||
|
if user and user.active:
|
||||||
|
if user.cas_allow_login:
|
||||||
|
current_app.logger.info(f"CAS: login {user.user_name}")
|
||||||
|
if login_user(user):
|
||||||
|
flask.session[
|
||||||
|
"scodoc_cas_login_date"
|
||||||
|
] = datetime.datetime.now().isoformat()
|
||||||
|
user.cas_last_login = datetime.datetime.utcnow()
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
return flask.redirect(url_for("scodoc.index"))
|
||||||
|
else:
|
||||||
|
current_app.logger.info(
|
||||||
|
f"CAS login denied for {user.user_name} (not allowed to use CAS)"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
current_app.logger.info(
|
||||||
|
f"""CAS login denied for {
|
||||||
|
user.user_name if user else ""
|
||||||
|
} cas_id={cas_id} (unknown or inactive)"""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
current_app.logger.info(
|
||||||
|
f"""CAS attribute '{ScoDocSiteConfig.get("cas_attribute_id")}' not found !
|
||||||
|
(check your ScoDoc config)"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Echec:
|
||||||
|
flash("échec de l'authentification")
|
||||||
|
return flask.redirect(url_for("auth.login"))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/after_cas_logout")
|
||||||
|
def after_cas_logout():
|
||||||
|
"Called by CAS after CAS logout"
|
||||||
|
flash("Vous êtes déconnecté")
|
||||||
|
current_app.logger.info("after_cas_logout")
|
||||||
|
return flask.redirect(url_for("scodoc.index"))
|
||||||
|
|
||||||
|
|
||||||
|
def cas_error_callback(message):
|
||||||
|
"Called by CAS when an error occurs, with a message"
|
||||||
|
raise ScoValueError(f"Erreur authentification CAS: {message}")
|
||||||
|
|
||||||
|
|
||||||
|
def set_cas_configuration(app: flask.app.Flask = None):
|
||||||
|
"""Force la configuration du module flask_cas à partir des paramètres de
|
||||||
|
la config de ScoDoc.
|
||||||
|
Appelé au démarrage et à chaque modif des paramètres.
|
||||||
|
"""
|
||||||
|
app = app or current_app
|
||||||
|
if ScoDocSiteConfig.is_cas_enabled():
|
||||||
|
current_app.logger.debug("CAS: set_cas_configuration")
|
||||||
|
app.config["CAS_SERVER"] = ScoDocSiteConfig.get("cas_server")
|
||||||
|
app.config["CAS_LOGIN_ROUTE"] = ScoDocSiteConfig.get("cas_login_route", "/cas")
|
||||||
|
app.config["CAS_LOGOUT_ROUTE"] = ScoDocSiteConfig.get(
|
||||||
|
"cas_logout_route", "/cas/logout"
|
||||||
|
)
|
||||||
|
app.config["CAS_VALIDATE_ROUTE"] = ScoDocSiteConfig.get(
|
||||||
|
"cas_validate_route", "/cas/serviceValidate"
|
||||||
|
)
|
||||||
|
app.config["CAS_AFTER_LOGIN"] = "auth.after_cas_login"
|
||||||
|
app.config["CAS_AFTER_LOGOUT"] = "auth.after_cas_logout"
|
||||||
|
app.config["CAS_ERROR_CALLBACK"] = cas_error_callback
|
||||||
|
app.config["CAS_SSL_VERIFY"] = ScoDocSiteConfig.get("cas_ssl_verify")
|
||||||
|
app.config["CAS_SSL_CERTIFICATE"] = ScoDocSiteConfig.get("cas_ssl_certificate")
|
||||||
|
else:
|
||||||
|
app.config.pop("CAS_SERVER", None)
|
||||||
|
app.config.pop("CAS_AFTER_LOGIN", None)
|
||||||
|
app.config.pop("CAS_AFTER_LOGOUT", None)
|
||||||
|
app.config.pop("CAS_SSL_VERIFY", None)
|
||||||
|
app.config.pop("CAS_SSL_CERTIFICATE", None)
|
||||||
|
|
||||||
|
|
||||||
|
CAS_USER_INFO_IDS = (
|
||||||
|
"user_name",
|
||||||
|
"nom",
|
||||||
|
"prenom",
|
||||||
|
"email",
|
||||||
|
"roles_string",
|
||||||
|
"active",
|
||||||
|
"dept",
|
||||||
|
"cas_id",
|
||||||
|
"cas_allow_login",
|
||||||
|
"cas_allow_scodoc_login",
|
||||||
|
"email_institutionnel",
|
||||||
|
)
|
||||||
|
CAS_USER_INFO_COMMENTS = (
|
||||||
|
"""user_name:
|
||||||
|
L'identifiant (login).
|
||||||
|
""",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"Pour info: 0 si compte inactif",
|
||||||
|
"""Pour info: roles:
|
||||||
|
chaînes séparées par _:
|
||||||
|
1. Le rôle (Ens, Secr ou Admin)
|
||||||
|
2. Le département (en majuscule)
|
||||||
|
""",
|
||||||
|
"""dept:
|
||||||
|
Le département d'appartenance de l'utilisateur. Vide si l'utilisateur
|
||||||
|
intervient dans plusieurs départements.
|
||||||
|
""",
|
||||||
|
"""cas_id:
|
||||||
|
identifiant de l'utilisateur sur CAS (requis pour CAS).
|
||||||
|
""",
|
||||||
|
"""cas_allow_login:
|
||||||
|
autorise la connexion via CAS (optionnel, faux par défaut)
|
||||||
|
""",
|
||||||
|
"""cas_allow_scodoc_login
|
||||||
|
autorise connexion via ScoDoc même si CAS obligatoire (optionnel, faux par défaut)
|
||||||
|
""",
|
||||||
|
"""email_institutionnel
|
||||||
|
optionnel, le mail officiel de l'utilisateur.
|
||||||
|
Maximum 120 caractères.""",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cas_users_generate_excel_sample() -> bytes:
|
||||||
|
"""generate an excel document suitable to import users CAS information"""
|
||||||
|
style = sco_excel.excel_make_style(bold=True)
|
||||||
|
titles = CAS_USER_INFO_IDS
|
||||||
|
titles_styles = [style] * len(titles)
|
||||||
|
# Extrait tous les utilisateurs (tous dept et statuts)
|
||||||
|
rows = []
|
||||||
|
for user in User.query.order_by(User.user_name):
|
||||||
|
u_dict = user.to_dict()
|
||||||
|
rows.append([u_dict.get(k) for k in CAS_USER_INFO_IDS])
|
||||||
|
return sco_excel.excel_simple_table(
|
||||||
|
lines=rows,
|
||||||
|
titles=titles,
|
||||||
|
titles_styles=titles_styles,
|
||||||
|
sheet_name="Utilisateurs ScoDoc",
|
||||||
|
comments=CAS_USER_INFO_COMMENTS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cas_users_import_excel_file(datafile) -> int:
|
||||||
|
"""
|
||||||
|
Import users CAS configuration from Excel file.
|
||||||
|
May change cas_id, cas_allow_login, cas_allow_scodoc_login
|
||||||
|
and active.
|
||||||
|
:param datafile: stream to be imported
|
||||||
|
:return: nb de comptes utilisateurs modifiés
|
||||||
|
"""
|
||||||
|
from app.scodoc import sco_import_users
|
||||||
|
|
||||||
|
if not current_user.is_administrator():
|
||||||
|
raise AccessDenied(f"invalid user ({current_user}) must be SuperAdmin")
|
||||||
|
current_app.logger.info("cas_users_import_excel_file by {current_user}")
|
||||||
|
|
||||||
|
users_infos = sco_import_users.read_users_excel_file(
|
||||||
|
datafile, titles=CAS_USER_INFO_IDS
|
||||||
|
)
|
||||||
|
|
||||||
|
return cas_users_import_data(users_infos=users_infos)
|
||||||
|
|
||||||
|
|
||||||
|
def cas_users_import_data(users_infos: list[dict]) -> int:
|
||||||
|
"""Import informations configuration CAS
|
||||||
|
users est une liste de dict, on utilise seulement les champs:
|
||||||
|
- user_name : la clé, l'utilisateur DOIT déjà exister
|
||||||
|
- cas_id : l'ID CAS a enregistrer.
|
||||||
|
- cas_allow_login
|
||||||
|
- cas_allow_scodoc_login
|
||||||
|
Les éventuels autres champs sont ignorés.
|
||||||
|
|
||||||
|
Return: nb de comptes modifiés.
|
||||||
|
"""
|
||||||
|
nb_modif = 0
|
||||||
|
users = []
|
||||||
|
for info in users_infos:
|
||||||
|
user: User = User.query.filter_by(user_name=info["user_name"]).first()
|
||||||
|
if not user:
|
||||||
|
db.session.rollback() # au cas où auto-flush
|
||||||
|
raise ScoValueError(f"""Utilisateur '{info["user_name"]}' inexistant""")
|
||||||
|
modif = False
|
||||||
|
new_cas_id = info["cas_id"].strip()
|
||||||
|
if new_cas_id != (user.cas_id or ""):
|
||||||
|
# check unicity
|
||||||
|
other = User.query.filter_by(cas_id=new_cas_id).first()
|
||||||
|
if other and other.id != user.id:
|
||||||
|
db.session.rollback() # au cas où auto-flush
|
||||||
|
raise ScoValueError(f"cas_id {new_cas_id} dupliqué")
|
||||||
|
user.cas_id = info["cas_id"].strip() or None
|
||||||
|
modif = True
|
||||||
|
val = scu.to_bool(info["cas_allow_login"])
|
||||||
|
if val != user.cas_allow_login:
|
||||||
|
user.cas_allow_login = val
|
||||||
|
modif = True
|
||||||
|
val = scu.to_bool(info["cas_allow_scodoc_login"])
|
||||||
|
if val != user.cas_allow_scodoc_login:
|
||||||
|
user.cas_allow_scodoc_login = val
|
||||||
|
modif = True
|
||||||
|
val = scu.to_bool(info["active"])
|
||||||
|
if val != (user.active or False):
|
||||||
|
user.active = val
|
||||||
|
modif = True
|
||||||
|
if modif:
|
||||||
|
nb_modif += 1
|
||||||
|
# Record modifications
|
||||||
|
for user in users:
|
||||||
|
try:
|
||||||
|
db.session.add(user)
|
||||||
|
except Exception as exc:
|
||||||
|
db.session.rollback()
|
||||||
|
raise ScoValueError(
|
||||||
|
"Erreur (1) durant l'importation des modifications"
|
||||||
|
) from exc
|
||||||
|
try:
|
||||||
|
db.session.commit()
|
||||||
|
except Exception as exc:
|
||||||
|
db.session.rollback()
|
||||||
|
raise ScoValueError(
|
||||||
|
"Erreur (2) durant l'importation des modifications"
|
||||||
|
) from exc
|
||||||
|
return nb_modif
|
|
@ -1,15 +1,20 @@
|
||||||
# -*- coding: UTF-8 -*
|
# -*- coding: UTF-8 -*
|
||||||
from flask import render_template, current_app
|
|
||||||
from flask_babel import _
|
from flask import render_template
|
||||||
from app.email import send_email
|
from app.auth.models import User
|
||||||
|
from app.email import get_from_addr, send_email
|
||||||
|
|
||||||
|
|
||||||
def send_password_reset_email(user):
|
def send_password_reset_email(user: User):
|
||||||
|
"""Send message allowing to reset password"""
|
||||||
|
recipients = user.get_emails()
|
||||||
|
if not recipients:
|
||||||
|
return
|
||||||
token = user.get_reset_password_token()
|
token = user.get_reset_password_token()
|
||||||
send_email(
|
send_email(
|
||||||
"[ScoDoc] Réinitialisation de votre mot de passe",
|
"[ScoDoc] Réinitialisation de votre mot de passe",
|
||||||
sender=current_app.config["SCODOC_MAIL_FROM"],
|
sender=get_from_addr(),
|
||||||
recipients=[user.email],
|
recipients=recipients,
|
||||||
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),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
# -*- coding: UTF-8 -*
|
# -*- coding: UTF-8 -*
|
||||||
|
|
||||||
"""Formulaires authentification
|
"""Formulaires authentification
|
||||||
|
|
||||||
TODO: à revoir complètement pour reprendre ZScoUsers et les pages d'authentification
|
|
||||||
"""
|
"""
|
||||||
from urllib.parse import urlparse, urljoin
|
from urllib.parse import urlparse, urljoin
|
||||||
from flask import request, url_for, redirect
|
from flask import request, url_for, redirect
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import BooleanField, HiddenField, PasswordField, StringField, SubmitField
|
from wtforms import BooleanField, HiddenField, PasswordField, StringField, SubmitField
|
||||||
|
from wtforms.fields.simple import FileField
|
||||||
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
|
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
|
||||||
from app.auth.models import User, is_valid_password
|
from app.auth.models import User, is_valid_password
|
||||||
|
|
||||||
|
@ -98,3 +97,12 @@ class ResetPasswordForm(FlaskForm):
|
||||||
class DeactivateUserForm(FlaskForm):
|
class DeactivateUserForm(FlaskForm):
|
||||||
submit = SubmitField("Modifier l'utilisateur")
|
submit = SubmitField("Modifier l'utilisateur")
|
||||||
cancel = SubmitField(label="Annuler", render_kw={"formnovalidate": True})
|
cancel = SubmitField(label="Annuler", render_kw={"formnovalidate": True})
|
||||||
|
|
||||||
|
|
||||||
|
class CASUsersImportConfigForm(FlaskForm):
|
||||||
|
user_config_file = FileField(
|
||||||
|
label="Fichier Excel à réimporter",
|
||||||
|
description="""fichier avec les paramètres CAS renseignés""",
|
||||||
|
)
|
||||||
|
submit = SubmitField("Importer le fichier utilisateurs")
|
||||||
|
cancel = SubmitField(label="Annuler", render_kw={"formnovalidate": True})
|
||||||
|
|
|
@ -5,12 +5,14 @@
|
||||||
import http
|
import http
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask import g, redirect, request, url_for
|
from flask import current_app, g, redirect, request, url_for
|
||||||
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
|
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
|
||||||
import flask_login
|
import flask_login
|
||||||
|
|
||||||
from app import login
|
from app import login
|
||||||
from app.scodoc.sco_utils import json_error
|
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
|
from app.models.config import ScoDocSiteConfig
|
||||||
|
from app.scodoc.sco_utils import json_error
|
||||||
|
|
||||||
basic_auth = HTTPBasicAuth()
|
basic_auth = HTTPBasicAuth()
|
||||||
token_auth = HTTPTokenAuth()
|
token_auth = HTTPTokenAuth()
|
||||||
|
@ -83,3 +85,15 @@ def unauthorized():
|
||||||
if request.blueprint == "api" or request.blueprint == "apiweb":
|
if request.blueprint == "api" or request.blueprint == "apiweb":
|
||||||
return json_error(http.HTTPStatus.UNAUTHORIZED, "Non autorise (logic)")
|
return json_error(http.HTTPStatus.UNAUTHORIZED, "Non autorise (logic)")
|
||||||
return redirect(url_for("auth.login"))
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
|
|
||||||
|
def logout() -> flask.Response:
|
||||||
|
"""Logout the current user: If CAS session, logout from CAS. Redirect."""
|
||||||
|
if flask_login.current_user:
|
||||||
|
user_name = getattr(flask_login.current_user, "user_name", "anonymous")
|
||||||
|
current_app.logger.info(f"logout user {user_name}")
|
||||||
|
flask_login.logout_user()
|
||||||
|
if ScoDocSiteConfig.is_cas_enabled() and flask.session.get("scodoc_cas_login_date"):
|
||||||
|
flask.session.pop("scodoc_cas_login_date", None)
|
||||||
|
return redirect(url_for("cas.logout"))
|
||||||
|
return redirect(url_for("scodoc.index"))
|
||||||
|
|
|
@ -19,9 +19,10 @@ from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
from app import db, log, login
|
from app import db, email, log, login
|
||||||
from app.models import Departement
|
from app.models import Departement
|
||||||
from app.models import SHORT_STR_LEN
|
from app.models import SHORT_STR_LEN, USERNAME_STR_LEN
|
||||||
|
from app.models.config import ScoDocSiteConfig
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS
|
from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS
|
||||||
|
@ -31,7 +32,7 @@ from app.scodoc import sco_etud # a deplacer dans scu
|
||||||
VALID_LOGIN_EXP = re.compile(r"^[a-zA-Z0-9@\\\-_\.]+$")
|
VALID_LOGIN_EXP = re.compile(r"^[a-zA-Z0-9@\\\-_\.]+$")
|
||||||
|
|
||||||
|
|
||||||
def is_valid_password(cleartxt):
|
def is_valid_password(cleartxt) -> bool:
|
||||||
"""Check password.
|
"""Check password.
|
||||||
returns True if OK.
|
returns True if OK.
|
||||||
"""
|
"""
|
||||||
|
@ -48,17 +49,45 @@ def is_valid_password(cleartxt):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def invalid_user_name(user_name: str) -> bool:
|
||||||
|
"Check that user_name (aka login) is invalid"
|
||||||
|
return (
|
||||||
|
(len(user_name) < 2)
|
||||||
|
or (len(user_name) >= USERNAME_STR_LEN)
|
||||||
|
or not VALID_LOGIN_EXP.match(user_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class User(UserMixin, db.Model):
|
class User(UserMixin, db.Model):
|
||||||
"""ScoDoc users, handled by Flask / SQLAlchemy"""
|
"""ScoDoc users, handled by Flask / SQLAlchemy"""
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
user_name = db.Column(db.String(64), index=True, unique=True)
|
user_name = db.Column(db.String(USERNAME_STR_LEN), index=True, unique=True)
|
||||||
|
"le login"
|
||||||
email = db.Column(db.String(120))
|
email = db.Column(db.String(120))
|
||||||
|
"email à utiliser par ScoDoc"
|
||||||
nom = db.Column(db.String(64))
|
email_institutionnel = db.Column(db.String(120))
|
||||||
prenom = db.Column(db.String(64))
|
"email dans l'établissement, facultatif"
|
||||||
|
nom = db.Column(db.String(USERNAME_STR_LEN))
|
||||||
|
prenom = db.Column(db.String(USERNAME_STR_LEN))
|
||||||
dept = db.Column(db.String(SHORT_STR_LEN), index=True)
|
dept = db.Column(db.String(SHORT_STR_LEN), index=True)
|
||||||
|
"acronyme du département de l'utilisateur"
|
||||||
active = db.Column(db.Boolean, default=True, index=True)
|
active = db.Column(db.Boolean, default=True, index=True)
|
||||||
|
"si faux, compte utilisateur désactivé"
|
||||||
|
cas_id = db.Column(db.Text(), index=True, unique=True, nullable=True)
|
||||||
|
"uid sur le CAS (id, mail ou autre attribut, selon config.cas_attribute_id)"
|
||||||
|
cas_allow_login = db.Column(
|
||||||
|
db.Boolean, default=False, server_default="false", nullable=False
|
||||||
|
)
|
||||||
|
"Peut-on se logguer via le CAS ?"
|
||||||
|
cas_allow_scodoc_login = db.Column(
|
||||||
|
db.Boolean, default=False, server_default="false", nullable=False
|
||||||
|
)
|
||||||
|
"""Si CAS forcé (cas_force), peut-on se logguer sur ScoDoc directement ?
|
||||||
|
(le rôle ScoSuperAdmin peut toujours, mettre à True pour les utilisateur API)
|
||||||
|
"""
|
||||||
|
cas_last_login = db.Column(db.DateTime, nullable=True)
|
||||||
|
"""date du dernier login via CAS"""
|
||||||
|
|
||||||
password_hash = db.Column(db.String(128))
|
password_hash = db.Column(db.String(128))
|
||||||
password_scodoc7 = db.Column(db.String(42))
|
password_scodoc7 = db.Column(db.String(42))
|
||||||
|
@ -67,6 +96,8 @@ class User(UserMixin, db.Model):
|
||||||
date_created = db.Column(db.DateTime, default=datetime.utcnow)
|
date_created = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
date_expiration = db.Column(db.DateTime, default=None)
|
date_expiration = db.Column(db.DateTime, default=None)
|
||||||
passwd_temp = db.Column(db.Boolean, default=False)
|
passwd_temp = db.Column(db.Boolean, default=False)
|
||||||
|
"""champ obsolete. Si connexion alors que passwd_temp est vrai,
|
||||||
|
efface mot de passe et redirige vers accueil."""
|
||||||
token = db.Column(db.Text(), index=True, unique=True)
|
token = db.Column(db.Text(), index=True, unique=True)
|
||||||
token_expiration = db.Column(db.DateTime)
|
token_expiration = db.Column(db.DateTime)
|
||||||
|
|
||||||
|
@ -86,7 +117,7 @@ class User(UserMixin, db.Model):
|
||||||
self.roles = []
|
self.roles = []
|
||||||
self.user_roles = []
|
self.user_roles = []
|
||||||
# check login:
|
# check login:
|
||||||
if kwargs.get("user_name") and not VALID_LOGIN_EXP.match(kwargs["user_name"]):
|
if kwargs.get("user_name") and invalid_user_name(kwargs["user_name"]):
|
||||||
raise ValueError(f"invalid user_name: {kwargs['user_name']}")
|
raise ValueError(f"invalid user_name: {kwargs['user_name']}")
|
||||||
super(User, self).__init__(**kwargs)
|
super(User, self).__init__(**kwargs)
|
||||||
# Ajoute roles:
|
# Ajoute roles:
|
||||||
|
@ -103,7 +134,8 @@ class User(UserMixin, db.Model):
|
||||||
# current_app.logger.info("creating user with roles={}".format(self.roles))
|
# current_app.logger.info("creating user with roles={}".format(self.roles))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<User {self.user_name} id={self.id} dept={self.dept}{' (inactive)' if not self.active else ''}>"
|
return f"""<User {self.user_name} id={self.id} dept={self.dept}{
|
||||||
|
' (inactive)' if not self.active else ''}>"""
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.user_name
|
return self.user_name
|
||||||
|
@ -115,30 +147,56 @@ class User(UserMixin, db.Model):
|
||||||
self.password_hash = generate_password_hash(password)
|
self.password_hash = generate_password_hash(password)
|
||||||
else:
|
else:
|
||||||
self.password_hash = None
|
self.password_hash = None
|
||||||
|
# La création d'un mot de passe efface l'éventuel mot de passe historique
|
||||||
|
self.password_scodoc7 = None
|
||||||
self.passwd_temp = False
|
self.passwd_temp = False
|
||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password: str) -> bool:
|
||||||
"""Check given password vs current one.
|
"""Check given password vs current one.
|
||||||
Returns `True` if the password matched, `False` otherwise.
|
Returns `True` if the password matched, `False` otherwise.
|
||||||
"""
|
"""
|
||||||
if not self.active: # inactived users can't login
|
if not self.active: # inactived users can't login
|
||||||
|
current_app.logger.warning(
|
||||||
|
f"auth: login attempt from inactive account {self}"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
if (not self.password_hash) and self.password_scodoc7:
|
if self.passwd_temp:
|
||||||
# Special case: user freshly migrated from ScoDoc7
|
# Anciens comptes ScoDoc 7 non migrés
|
||||||
if scu.check_scodoc7_password(self.password_scodoc7, password):
|
# désactive le compte par sécurité.
|
||||||
current_app.logger.warning(
|
current_app.logger.warning(f"auth: desactivating legacy account {self}")
|
||||||
f"migrating legacy ScoDoc7 password for {self}"
|
self.active = False
|
||||||
)
|
self.passwd_temp = True
|
||||||
self.set_password(password)
|
db.session.add(self)
|
||||||
self.password_scodoc7 = None
|
db.session.commit()
|
||||||
db.session.add(self)
|
send_notif_desactivation_user(self)
|
||||||
db.session.commit()
|
|
||||||
return True
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# if CAS activated and forced, allow only super-user and users with cas_allow_scodoc_login
|
||||||
|
if ScoDocSiteConfig.is_cas_enabled() and ScoDocSiteConfig.get("cas_force"):
|
||||||
|
if (not self.is_administrator()) and not self.cas_allow_scodoc_login:
|
||||||
|
return False
|
||||||
|
|
||||||
if not self.password_hash: # user without password can't login
|
if not self.password_hash: # user without password can't login
|
||||||
|
if self.password_scodoc7:
|
||||||
|
# Special case: user freshly migrated from ScoDoc7
|
||||||
|
return self._migrate_scodoc7_password(password)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return check_password_hash(self.password_hash, password)
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
|
def _migrate_scodoc7_password(self, password) -> bool:
|
||||||
|
"""After migration, rehash password."""
|
||||||
|
if scu.check_scodoc7_password(self.password_scodoc7, password):
|
||||||
|
current_app.logger.warning(
|
||||||
|
f"auth: migrating legacy ScoDoc7 password for {self}"
|
||||||
|
)
|
||||||
|
self.set_password(password)
|
||||||
|
self.password_scodoc7 = None
|
||||||
|
db.session.add(self)
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def get_reset_password_token(self, expires_in=600):
|
def get_reset_password_token(self, expires_in=600):
|
||||||
"Un token pour réinitialiser son mot de passe"
|
"Un token pour réinitialiser son mot de passe"
|
||||||
return jwt.encode(
|
return jwt.encode(
|
||||||
|
@ -155,7 +213,7 @@ class User(UserMixin, db.Model):
|
||||||
token, current_app.config["SECRET_KEY"], algorithms=["HS256"]
|
token, current_app.config["SECRET_KEY"], algorithms=["HS256"]
|
||||||
)
|
)
|
||||||
except jwt.exceptions.ExpiredSignatureError:
|
except jwt.exceptions.ExpiredSignatureError:
|
||||||
log(f"verify_reset_password_token: token expired")
|
log("verify_reset_password_token: token expired")
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
|
@ -184,6 +242,12 @@ class User(UserMixin, db.Model):
|
||||||
"dept": self.dept,
|
"dept": self.dept,
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"active": self.active,
|
"active": self.active,
|
||||||
|
"cas_id": self.cas_id,
|
||||||
|
"cas_allow_login": self.cas_allow_login,
|
||||||
|
"cas_allow_scodoc_login": self.cas_allow_scodoc_login,
|
||||||
|
"cas_last_login": self.cas_last_login.isoformat() + "Z"
|
||||||
|
if self.cas_last_login
|
||||||
|
else None,
|
||||||
"status_txt": "actif" if self.active else "fermé",
|
"status_txt": "actif" if self.active else "fermé",
|
||||||
"last_seen": self.last_seen.isoformat() + "Z" if self.last_seen else None,
|
"last_seen": self.last_seen.isoformat() + "Z" if self.last_seen else None,
|
||||||
"nom": (self.nom or ""), # sco8
|
"nom": (self.nom or ""), # sco8
|
||||||
|
@ -200,22 +264,39 @@ class User(UserMixin, db.Model):
|
||||||
}
|
}
|
||||||
if include_email:
|
if include_email:
|
||||||
data["email"] = self.email or ""
|
data["email"] = self.email or ""
|
||||||
|
data["email_institutionnel"] = self.email_institutionnel or ""
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def from_dict(self, data, new_user=False):
|
def from_dict(self, data: dict, new_user=False):
|
||||||
"""Set users' attributes from given dict values.
|
"""Set users' attributes from given dict values.
|
||||||
Roles must be encoded as "roles_string", like "Ens_RT, Secr_CJ"
|
Roles must be encoded as "roles_string", like "Ens_RT, Secr_CJ"
|
||||||
"""
|
"""
|
||||||
for field in ["nom", "prenom", "dept", "active", "email", "date_expiration"]:
|
for field in [
|
||||||
|
"nom",
|
||||||
|
"prenom",
|
||||||
|
"dept",
|
||||||
|
"active",
|
||||||
|
"email",
|
||||||
|
"email_institutionnel",
|
||||||
|
"date_expiration",
|
||||||
|
"cas_id",
|
||||||
|
]:
|
||||||
if field in data:
|
if field in data:
|
||||||
setattr(self, field, data[field] or None)
|
setattr(self, field, data[field] or None)
|
||||||
|
# required boolean fields
|
||||||
|
for field in [
|
||||||
|
"cas_allow_login",
|
||||||
|
"cas_allow_scodoc_login",
|
||||||
|
]:
|
||||||
|
setattr(self, field, scu.to_bool(data.get(field, False)))
|
||||||
|
|
||||||
if new_user:
|
if new_user:
|
||||||
if "user_name" in data:
|
if "user_name" in data:
|
||||||
# never change name of existing users
|
# never change name of existing users
|
||||||
self.user_name = data["user_name"]
|
self.user_name = data["user_name"]
|
||||||
if "password" in data:
|
if "password" in data:
|
||||||
self.set_password(data["password"])
|
self.set_password(data["password"])
|
||||||
if not VALID_LOGIN_EXP.match(self.user_name):
|
if invalid_user_name(self.user_name):
|
||||||
raise ValueError(f"invalid user_name: {self.user_name}")
|
raise ValueError(f"invalid user_name: {self.user_name}")
|
||||||
# Roles: roles_string is "Ens_RT, Secr_RT, ..."
|
# Roles: roles_string is "Ens_RT, Secr_RT, ..."
|
||||||
if "roles_string" in data:
|
if "roles_string" in data:
|
||||||
|
@ -241,7 +322,7 @@ class User(UserMixin, db.Model):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_token(token):
|
def check_token(token):
|
||||||
"""Retreive user for given token, chek token's validity
|
"""Retreive user for given token, check token's validity
|
||||||
and returns the user object.
|
and returns the user object.
|
||||||
"""
|
"""
|
||||||
user = User.query.filter_by(token=token).first()
|
user = User.query.filter_by(token=token).first()
|
||||||
|
@ -255,6 +336,15 @@ class User(UserMixin, db.Model):
|
||||||
return self._departement.id
|
return self._departement.id
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_emails(self):
|
||||||
|
"List mail adresses to contact this user"
|
||||||
|
mails = []
|
||||||
|
if self.email:
|
||||||
|
mails.append(self.email)
|
||||||
|
if self.email_institutionnel:
|
||||||
|
mails.append(self.email_institutionnel)
|
||||||
|
return mails
|
||||||
|
|
||||||
# Permissions management:
|
# Permissions management:
|
||||||
def has_permission(self, perm: int, dept=False):
|
def has_permission(self, perm: int, dept=False):
|
||||||
"""Check if user has permission `perm` in given `dept`.
|
"""Check if user has permission `perm` in given `dept`.
|
||||||
|
@ -310,7 +400,7 @@ class User(UserMixin, db.Model):
|
||||||
"""string repr. of user's roles (with depts)
|
"""string repr. of user's roles (with depts)
|
||||||
e.g. "Ens_RT, Ens_Info, Secr_CJ"
|
e.g. "Ens_RT, Ens_Info, Secr_CJ"
|
||||||
"""
|
"""
|
||||||
return ",".join(
|
return ", ".join(
|
||||||
f"{r.role.name or ''}_{r.dept or ''}"
|
f"{r.role.name or ''}_{r.dept or ''}"
|
||||||
for r in self.user_roles
|
for r in self.user_roles
|
||||||
if r is not None
|
if r is not None
|
||||||
|
@ -339,24 +429,17 @@ class User(UserMixin, db.Model):
|
||||||
"""nomplogin est le nom en majuscules suivi du prénom et du login
|
"""nomplogin est le nom en majuscules suivi du prénom et du login
|
||||||
e.g. Dupont Pierre (dupont)
|
e.g. Dupont Pierre (dupont)
|
||||||
"""
|
"""
|
||||||
if self.nom:
|
nom = sco_etud.format_nom(self.nom) if self.nom else self.user_name.upper()
|
||||||
n = sco_etud.format_nom(self.nom)
|
return f"{nom} {sco_etud.format_prenom(self.prenom)} ({self.user_name})"
|
||||||
else:
|
|
||||||
n = self.user_name.upper()
|
|
||||||
return "%s %s (%s)" % (
|
|
||||||
n,
|
|
||||||
sco_etud.format_prenom(self.prenom),
|
|
||||||
self.user_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_user_id_from_nomplogin(nomplogin: str) -> Optional[int]:
|
def get_user_id_from_nomplogin(nomplogin: str) -> Optional[int]:
|
||||||
"""Returns id from the string "Dupont Pierre (dupont)"
|
"""Returns id from the string "Dupont Pierre (dupont)"
|
||||||
or None if user does not exist
|
or None if user does not exist
|
||||||
"""
|
"""
|
||||||
m = re.match(r".*\((.*)\)", nomplogin.strip())
|
match = re.match(r".*\((.*)\)", nomplogin.strip())
|
||||||
if m:
|
if match:
|
||||||
user_name = m.group(1)
|
user_name = match.group(1)
|
||||||
u = User.query.filter_by(user_name=user_name).first()
|
u = User.query.filter_by(user_name=user_name).first()
|
||||||
if u:
|
if u:
|
||||||
return u.id
|
return u.id
|
||||||
|
@ -393,6 +476,8 @@ class User(UserMixin, db.Model):
|
||||||
|
|
||||||
|
|
||||||
class AnonymousUser(AnonymousUserMixin):
|
class AnonymousUser(AnonymousUserMixin):
|
||||||
|
"Notre utilisateur anonyme"
|
||||||
|
|
||||||
def has_permission(self, perm, dept=None):
|
def has_permission(self, perm, dept=None):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -509,7 +594,7 @@ class UserRole(db.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<UserRole u={} r={} dept={}>".format(self.user, self.role, self.dept)
|
return f"<UserRole u={self.user} r={self.role} dept={self.dept}>"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def role_dept_from_string(role_dept: str):
|
def role_dept_from_string(role_dept: str):
|
||||||
|
@ -517,18 +602,21 @@ class UserRole(db.Model):
|
||||||
role_dept, of the forme "Role_Dept".
|
role_dept, of the forme "Role_Dept".
|
||||||
role is a Role instance, dept is a string, or None.
|
role is a Role instance, dept is a string, or None.
|
||||||
"""
|
"""
|
||||||
fields = role_dept.split("_", 1) # maxsplit=1, le dept peut contenir un "_"
|
fields = role_dept.strip().split("_", 1)
|
||||||
|
# maxsplit=1, le dept peut contenir un "_"
|
||||||
if len(fields) != 2:
|
if len(fields) != 2:
|
||||||
current_app.logger.warning(
|
current_app.logger.warning(
|
||||||
f"role_dept_from_string: Invalid role_dept '{role_dept}'"
|
f"auth: role_dept_from_string: Invalid role_dept '{role_dept}'"
|
||||||
)
|
)
|
||||||
raise ScoValueError("Invalid role_dept")
|
raise ScoValueError("Invalid role_dept")
|
||||||
role_name, dept = fields
|
role_name, dept = fields
|
||||||
|
dept = dept.strip() if dept else ""
|
||||||
if dept == "":
|
if dept == "":
|
||||||
dept = None
|
dept = None
|
||||||
|
|
||||||
role = Role.query.filter_by(name=role_name).first()
|
role = Role.query.filter_by(name=role_name).first()
|
||||||
if role is None:
|
if role is None:
|
||||||
raise ScoValueError("role %s does not exists" % role_name)
|
raise ScoValueError(f"role {role_name} does not exists")
|
||||||
return (role, dept)
|
return (role, dept)
|
||||||
|
|
||||||
|
|
||||||
|
@ -545,3 +633,22 @@ def get_super_admin():
|
||||||
)
|
)
|
||||||
assert admin_user
|
assert admin_user
|
||||||
return admin_user
|
return admin_user
|
||||||
|
|
||||||
|
|
||||||
|
def send_notif_desactivation_user(user: User):
|
||||||
|
"""Envoi un message mail de notification à l'admin et à l'adresse du compte désactivé"""
|
||||||
|
recipients = user.get_emails() + [current_app.config.get("SCODOC_ADMIN_MAIL")]
|
||||||
|
txt = [
|
||||||
|
f"""Le compte ScoDoc '{user.user_name}' associé à votre adresse <{user.email}>""",
|
||||||
|
"""a été désactivé par le système car son mot de passe n'était pas valide.\n""",
|
||||||
|
"""Contactez votre responsable pour le ré-activer.\n""",
|
||||||
|
"""Ceci est un message automatique, ne pas répondre.""",
|
||||||
|
]
|
||||||
|
txt = "\n".join(txt)
|
||||||
|
email.send_email(
|
||||||
|
f"ScoDoc: désactivation automatique du compte {user.user_name}",
|
||||||
|
email.get_from_addr(),
|
||||||
|
recipients,
|
||||||
|
txt,
|
||||||
|
)
|
||||||
|
return txt
|
||||||
|
|
|
@ -3,54 +3,88 @@
|
||||||
auth.routes.py
|
auth.routes.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import flask
|
||||||
from flask import current_app, flash, render_template
|
from flask import current_app, flash, render_template
|
||||||
from flask import redirect, url_for, request
|
from flask import redirect, url_for, request
|
||||||
from flask_login import login_user, logout_user, current_user
|
from flask_login import login_user, current_user
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.auth import bp
|
from app.auth import bp, cas, logic
|
||||||
from app.auth.forms import (
|
from app.auth.forms import (
|
||||||
|
CASUsersImportConfigForm,
|
||||||
LoginForm,
|
LoginForm,
|
||||||
UserCreationForm,
|
|
||||||
ResetPasswordRequestForm,
|
|
||||||
ResetPasswordForm,
|
ResetPasswordForm,
|
||||||
|
ResetPasswordRequestForm,
|
||||||
|
UserCreationForm,
|
||||||
)
|
)
|
||||||
from app.auth.models import Role
|
from app.auth.models import Role, User, invalid_user_name
|
||||||
from app.auth.models import User
|
|
||||||
from app.auth.email import send_password_reset_email
|
from app.auth.email import send_password_reset_email
|
||||||
from app.decorators import admin_required
|
from app.decorators import admin_required
|
||||||
|
from app.models.config import ScoDocSiteConfig
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
_ = lambda x: x # sans babel
|
_ = lambda x: x # sans babel
|
||||||
_l = _
|
_l = _
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/login", methods=["GET", "POST"])
|
def _login_form():
|
||||||
def login():
|
"""le formulaire de login, avec un lien CAS s'il est configuré."""
|
||||||
"ScoDoc Login form"
|
|
||||||
if current_user.is_authenticated:
|
|
||||||
return redirect(url_for("scodoc.index"))
|
|
||||||
form = LoginForm()
|
form = LoginForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
user = User.query.filter_by(user_name=form.user_name.data).first()
|
# note: ceci est la première requête SQL déclenchée par un utilisateur arrivant
|
||||||
|
if invalid_user_name(form.user_name.data):
|
||||||
|
user = None
|
||||||
|
else:
|
||||||
|
user = User.query.filter_by(user_name=form.user_name.data).first()
|
||||||
if user is None or not user.check_password(form.password.data):
|
if user is None or not user.check_password(form.password.data):
|
||||||
current_app.logger.info("login: invalid (%s)", form.user_name.data)
|
current_app.logger.info("login: invalid (%s)", form.user_name.data)
|
||||||
flash(_("Nom ou mot de passe invalide"))
|
flash(_("Nom ou mot de passe invalide"))
|
||||||
return redirect(url_for("auth.login"))
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
login_user(user, remember=form.remember_me.data)
|
login_user(user, remember=form.remember_me.data)
|
||||||
|
|
||||||
current_app.logger.info("login: success (%s)", form.user_name.data)
|
current_app.logger.info("login: success (%s)", form.user_name.data)
|
||||||
return form.redirect("scodoc.index")
|
return form.redirect("scodoc.index")
|
||||||
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,
|
||||||
|
is_cas_enabled=ScoDocSiteConfig.is_cas_enabled(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/login", methods=["GET", "POST"])
|
||||||
|
def login():
|
||||||
|
"""ScoDoc Login form
|
||||||
|
Si paramètre cas_force, redirige vers le CAS.
|
||||||
|
"""
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return redirect(url_for("scodoc.index"))
|
||||||
|
|
||||||
|
if ScoDocSiteConfig.get("cas_force"):
|
||||||
|
current_app.logger.info("login: forcing CAS")
|
||||||
|
return redirect(url_for("cas.login"))
|
||||||
|
|
||||||
|
return _login_form()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/login_scodoc", methods=["GET", "POST"])
|
||||||
|
def login_scodoc():
|
||||||
|
"""ScoDoc Login form.
|
||||||
|
Formulaire login, sans redirection immédiate sur CAS si ce dernier est configuré.
|
||||||
|
Sans CAS, ce formulaire est identique à /login
|
||||||
|
"""
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return redirect(url_for("scodoc.index"))
|
||||||
|
return _login_form()
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/logout")
|
@bp.route("/logout")
|
||||||
def logout():
|
def logout() -> flask.Response:
|
||||||
"Logout current user and redirect to home page"
|
"Logout a scodoc user. If CAS session, logout from CAS. Redirect."
|
||||||
logout_user()
|
return logic.logout()
|
||||||
return redirect(url_for("scodoc.index"))
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/create_user", methods=["GET", "POST"])
|
@bp.route("/create_user", methods=["GET", "POST"])
|
||||||
|
@ -63,11 +97,9 @@ def create_user():
|
||||||
user.set_password(form.password.data)
|
user.set_password(form.password.data)
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("User {} created".format(user.user_name))
|
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,13 +130,16 @@ 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,
|
||||||
|
is_cas_enabled=ScoDocSiteConfig.is_cas_enabled(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/reset_password/<token>", methods=["GET", "POST"])
|
@bp.route("/reset_password/<token>", methods=["GET", "POST"])
|
||||||
def reset_password(token):
|
def reset_password(token):
|
||||||
"Reset passord après demande par mail"
|
"Reset password après demande par mail"
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return redirect(url_for("scodoc.index"))
|
return redirect(url_for("scodoc.index"))
|
||||||
user: User = User.verify_reset_password_token(token)
|
user: User = User.verify_reset_password_token(token)
|
||||||
|
@ -116,7 +151,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"])
|
||||||
|
@ -126,3 +161,34 @@ def reset_standard_roles_permissions():
|
||||||
Role.reset_standard_roles_permissions()
|
Role.reset_standard_roles_permissions()
|
||||||
flash("rôles standards réinitialisés !")
|
flash("rôles standards réinitialisés !")
|
||||||
return redirect(url_for("scodoc.configuration"))
|
return redirect(url_for("scodoc.configuration"))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/cas_users_generate_excel_sample")
|
||||||
|
@admin_required
|
||||||
|
def cas_users_generate_excel_sample():
|
||||||
|
"une feuille excel pour importation config CAS"
|
||||||
|
data = cas.cas_users_generate_excel_sample()
|
||||||
|
return scu.send_file(data, "ImportConfigCAS", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/cas_users_import_config", methods=["GET", "POST"])
|
||||||
|
@admin_required
|
||||||
|
def cas_users_import_config():
|
||||||
|
"""Import utilisateurs depuis feuille Excel"""
|
||||||
|
form = CASUsersImportConfigForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
if form.cancel.data: # cancel button
|
||||||
|
return redirect(url_for("scodoc.configuration"))
|
||||||
|
datafile = request.files[form.user_config_file.name]
|
||||||
|
nb_modif = cas.cas_users_import_excel_file(datafile)
|
||||||
|
current_app.logger.info(f"cas_users_import_config: {nb_modif} comptes modifiés")
|
||||||
|
flash(f"Config. CAS de {nb_modif} comptes modifiée.")
|
||||||
|
return redirect(url_for("scodoc.configuration"))
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"auth/cas_users_import_config.j2",
|
||||||
|
title=_("Importation configuration CAS utilisateurs"),
|
||||||
|
form=form,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
|
@ -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,15 +8,15 @@
|
||||||
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.scodoc import codes_cursus
|
||||||
from app.models.but_refcomp import ApcNiveau
|
|
||||||
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.
|
||||||
if ue.type != sco_codes_parcours.UE_STANDARD:
|
Le menu select lui meême est vide et rempli en JS par appel à get_ue_niveaux_options_html
|
||||||
|
"""
|
||||||
|
if ue.type != codes_cursus.UE_STANDARD:
|
||||||
return ""
|
return ""
|
||||||
ref_comp = ue.formation.referentiel_competence
|
ref_comp = ue.formation.referentiel_competence
|
||||||
if ref_comp is None:
|
if ref_comp is None:
|
||||||
|
@ -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
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -19,10 +19,10 @@ from app.models.ues import UniteEns
|
||||||
from app.scodoc import sco_bulletins, sco_utils as scu
|
from app.scodoc import sco_bulletins, sco_utils as scu
|
||||||
from app.scodoc import sco_bulletins_json
|
from app.scodoc import sco_bulletins_json
|
||||||
from app.scodoc import sco_bulletins_pdf
|
from app.scodoc import sco_bulletins_pdf
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc import codes_cursus
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
|
from app.scodoc.codes_cursus import UE_SPORT, DEF
|
||||||
from app.scodoc.sco_utils import fmt_note
|
from app.scodoc.sco_utils import fmt_note
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -154,7 +157,7 @@ class BulletinBUT:
|
||||||
for _, ue_capitalisee in self.res.validations.ue_capitalisees.loc[
|
for _, ue_capitalisee in self.res.validations.ue_capitalisees.loc[
|
||||||
[etud.id]
|
[etud.id]
|
||||||
].iterrows():
|
].iterrows():
|
||||||
if sco_codes_parcours.code_ue_validant(ue_capitalisee.code):
|
if codes_cursus.code_ue_validant(ue_capitalisee.code):
|
||||||
ue = UniteEns.query.get(ue_capitalisee.ue_id) # XXX cacher ?
|
ue = UniteEns.query.get(ue_capitalisee.ue_id) # XXX cacher ?
|
||||||
# déjà capitalisé ? montre la meilleure
|
# déjà capitalisé ? montre la meilleure
|
||||||
if ue.acronyme in d:
|
if ue.acronyme in d:
|
||||||
|
@ -184,6 +187,8 @@ class BulletinBUT:
|
||||||
)
|
)
|
||||||
if ue_capitalisee.formsemestre_id
|
if ue_capitalisee.formsemestre_id
|
||||||
else None,
|
else None,
|
||||||
|
"ressources": {}, # sans détail en BUT
|
||||||
|
"saes": {},
|
||||||
}
|
}
|
||||||
if self.prefs["bul_show_ects"]:
|
if self.prefs["bul_show_ects"]:
|
||||||
d[ue.acronyme]["ECTS"] = {
|
d[ue.acronyme]["ECTS"] = {
|
||||||
|
@ -239,6 +244,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 +262,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 +363,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:
|
||||||
|
@ -380,7 +387,7 @@ class BulletinBUT:
|
||||||
"injustifie": nbabs - nbabsjust,
|
"injustifie": nbabs - nbabsjust,
|
||||||
"total": nbabs,
|
"total": nbabs,
|
||||||
}
|
}
|
||||||
decisions_ues = self.res.get_etud_decision_ues(etud.id) or {}
|
decisions_ues = self.res.get_etud_decisions_ue(etud.id) or {}
|
||||||
if self.prefs["bul_show_ects"]:
|
if self.prefs["bul_show_ects"]:
|
||||||
ects_tot = res.etud_ects_tot_sem(etud.id)
|
ects_tot = res.etud_ects_tot_sem(etud.id)
|
||||||
ects_acquis = res.get_etud_ects_valides(etud.id, decisions_ues)
|
ects_acquis = res.get_etud_ects_valides(etud.id, decisions_ues)
|
||||||
|
@ -460,6 +467,7 @@ class BulletinBUT:
|
||||||
"ressources": {},
|
"ressources": {},
|
||||||
"saes": {},
|
"saes": {},
|
||||||
"ues": {},
|
"ues": {},
|
||||||
|
"ues_capitalisees": {},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -467,6 +475,7 @@ class BulletinBUT:
|
||||||
|
|
||||||
def bulletin_etud_complet(self, etud: Identite, version="long") -> dict:
|
def bulletin_etud_complet(self, etud: Identite, version="long") -> dict:
|
||||||
"""Bulletin dict complet avec toutes les infos pour les bulletins BUT pdf
|
"""Bulletin dict complet avec toutes les infos pour les bulletins BUT pdf
|
||||||
|
(pas utilisé pour json/html)
|
||||||
Résultat compatible avec celui de sco_bulletins.formsemestre_bulletinetud_dict
|
Résultat compatible avec celui de sco_bulletins.formsemestre_bulletinetud_dict
|
||||||
"""
|
"""
|
||||||
d = self.bulletin_etud(
|
d = self.bulletin_etud(
|
||||||
|
@ -495,7 +504,7 @@ class BulletinBUT:
|
||||||
# --- Decision Jury
|
# --- Decision Jury
|
||||||
infos, dpv = sco_bulletins.etud_descr_situation_semestre(
|
infos, dpv = sco_bulletins.etud_descr_situation_semestre(
|
||||||
etud.id,
|
etud.id,
|
||||||
self.res.formsemestre.id,
|
self.res.formsemestre,
|
||||||
format="html",
|
format="html",
|
||||||
show_date_inscr=self.prefs["bul_show_date_inscr"],
|
show_date_inscr=self.prefs["bul_show_date_inscr"],
|
||||||
show_decisions=self.prefs["bul_show_decision"],
|
show_decisions=self.prefs["bul_show_decision"],
|
||||||
|
|
|
@ -1,10 +1,24 @@
|
||||||
##############################################################################
|
##############################################################################
|
||||||
# ScoDoc
|
# ScoDoc
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""Génération bulletin BUT au format PDF standard
|
"""Génération bulletin BUT au format PDF standard
|
||||||
|
|
||||||
|
La génération du bulletin PDF suit le chemin suivant:
|
||||||
|
|
||||||
|
- vue formsemestre_bulletinetud -> sco_bulletins.formsemestre_bulletinetud
|
||||||
|
|
||||||
|
bul_dict = bulletin_but.BulletinBUT(formsemestre).bulletin_etud_complet(etud)
|
||||||
|
|
||||||
|
- sco_bulletins_generator.make_formsemestre_bulletinetud(infos)
|
||||||
|
- instance de BulletinGeneratorStandardBUT(infos)
|
||||||
|
- BulletinGeneratorStandardBUT.generate(format="pdf")
|
||||||
|
sco_bulletins_generator.BulletinGenerator.generate()
|
||||||
|
.generate_pdf()
|
||||||
|
.bul_table() (ci-dessous)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from reportlab.lib.colors import blue
|
from reportlab.lib.colors import blue
|
||||||
from reportlab.lib.units import cm, mm
|
from reportlab.lib.units import cm, mm
|
||||||
|
@ -12,7 +26,7 @@ from reportlab.platypus import Paragraph, Spacer
|
||||||
|
|
||||||
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
|
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
|
||||||
from app.scodoc import gen_tables
|
from app.scodoc import gen_tables
|
||||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
from app.scodoc.codes_cursus import UE_SPORT
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
|
@ -65,7 +79,9 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||||
|
|
||||||
return objects
|
return objects
|
||||||
|
|
||||||
def but_table_synthese_ues(self, title_bg=(182, 235, 255)):
|
def but_table_synthese_ues(
|
||||||
|
self, title_bg=(182, 235, 255), title_ue_cap_bg=(150, 207, 147)
|
||||||
|
):
|
||||||
"""La table de synthèse; pour chaque UE, liste des ressources et SAÉs avec leurs notes
|
"""La table de synthèse; pour chaque UE, liste des ressources et SAÉs avec leurs notes
|
||||||
et leurs coefs.
|
et leurs coefs.
|
||||||
Renvoie: colkeys, P, pdf_style, colWidths
|
Renvoie: colkeys, P, pdf_style, colWidths
|
||||||
|
@ -74,6 +90,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||||
- pdf_style : commandes table Platypus
|
- pdf_style : commandes table Platypus
|
||||||
- largeurs de colonnes pour PDF
|
- largeurs de colonnes pour PDF
|
||||||
"""
|
"""
|
||||||
|
# nb: self.infos a ici été donné par BulletinBUT.bulletin_etud_complet()
|
||||||
col_widths = {
|
col_widths = {
|
||||||
"titre": None,
|
"titre": None,
|
||||||
"min": 1.5 * cm,
|
"min": 1.5 * cm,
|
||||||
|
@ -95,6 +112,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||||
col_keys += ["coef", "moyenne"]
|
col_keys += ["coef", "moyenne"]
|
||||||
# Couleur fond:
|
# Couleur fond:
|
||||||
title_bg = tuple(x / 255.0 for x in title_bg)
|
title_bg = tuple(x / 255.0 for x in title_bg)
|
||||||
|
title_ue_cap_bg = tuple(x / 255.0 for x in title_ue_cap_bg)
|
||||||
# elems pour générer table avec gen_table (liste de dicts)
|
# elems pour générer table avec gen_table (liste de dicts)
|
||||||
rows = [
|
rows = [
|
||||||
# Ligne de titres
|
# Ligne de titres
|
||||||
|
@ -141,9 +159,17 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||||
blue,
|
blue,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
ues = self.infos["ues"]
|
||||||
for ue_acronym, ue in self.infos["ues"].items():
|
ues_capitalisees = self.infos.get("ues_capitalisees", {})
|
||||||
self.ue_rows(rows, ue_acronym, ue, title_bg)
|
ues_tup = sorted(
|
||||||
|
list(ues.items()) + list(ues_capitalisees.items()),
|
||||||
|
key=lambda x: x[1]["numero"],
|
||||||
|
)
|
||||||
|
for ue_acronym, ue in ues_tup:
|
||||||
|
is_capitalized = "date_capitalisation" in ue
|
||||||
|
self._ue_rows(
|
||||||
|
rows, ue_acronym, ue, title_ue_cap_bg if is_capitalized else title_bg
|
||||||
|
)
|
||||||
|
|
||||||
# Global pdf style commands:
|
# Global pdf style commands:
|
||||||
pdf_style = [
|
pdf_style = [
|
||||||
|
@ -152,20 +178,18 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||||
]
|
]
|
||||||
return col_keys, rows, pdf_style, col_widths
|
return col_keys, rows, pdf_style, col_widths
|
||||||
|
|
||||||
def ue_rows(self, rows: list, ue_acronym: str, ue: dict, title_bg: tuple):
|
def _ue_rows(self, rows: list, ue_acronym: str, ue: dict, title_bg: tuple):
|
||||||
"Décrit une UE dans la table synthèse: titre, sous-titre et liste modules"
|
"Décrit une UE dans la table synthèse: titre, sous-titre et liste modules"
|
||||||
if (ue["type"] == UE_SPORT) and len(ue.get("modules", [])) == 0:
|
if (ue["type"] == UE_SPORT) and len(ue.get("modules", [])) == 0:
|
||||||
# ne mentionne l'UE que s'il y a des modules
|
# ne mentionne l'UE que s'il y a des modules
|
||||||
return
|
return
|
||||||
# 1er ligne titre UE
|
# 1er ligne titre UE
|
||||||
moy_ue = ue.get("moyenne")
|
moy_ue = ue.get("moyenne", "-")
|
||||||
|
if isinstance(moy_ue, dict):
|
||||||
|
moy_ue = moy_ue.get("value", "-") if moy_ue is not None else "-"
|
||||||
t = {
|
t = {
|
||||||
"titre": f"{ue_acronym} - {ue['titre']}",
|
"titre": f"{ue_acronym} - {ue['titre']}",
|
||||||
"moyenne": Paragraph(
|
"moyenne": Paragraph(f"""<para align=right><b>{moy_ue}</b></para>"""),
|
||||||
f"""<para align=right><b>{moy_ue.get("value", "-")
|
|
||||||
if moy_ue is not None else "-"
|
|
||||||
}</b></para>"""
|
|
||||||
),
|
|
||||||
"_css_row_class": "note_bold",
|
"_css_row_class": "note_bold",
|
||||||
"_pdf_row_markup": ["b"],
|
"_pdf_row_markup": ["b"],
|
||||||
"_pdf_style": [
|
"_pdf_style": [
|
||||||
|
@ -196,25 +220,40 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||||
# case Bonus/Malus/Rang "bmr"
|
# case Bonus/Malus/Rang "bmr"
|
||||||
fields_bmr = []
|
fields_bmr = []
|
||||||
try:
|
try:
|
||||||
value = float(ue["bonus"])
|
value = float(ue.get("bonus", 0.0))
|
||||||
if value != 0:
|
if value != 0:
|
||||||
fields_bmr.append(f"Bonus: {ue['bonus']}")
|
fields_bmr.append(f"Bonus: {ue['bonus']}")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
value = float(ue["malus"])
|
value = float(ue.get("malus", 0.0))
|
||||||
if value != 0:
|
if value != 0:
|
||||||
fields_bmr.append(f"Malus: {ue['malus']}")
|
fields_bmr.append(f"Malus: {ue['malus']}")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
if self.preferences["bul_show_ue_rangs"]:
|
|
||||||
fields_bmr.append(
|
moy_ue = ue.get("moyenne", "-")
|
||||||
f"Rang: {ue['moyenne']['rang']} / {ue['moyenne']['total']}"
|
if isinstance(moy_ue, dict): # UE non capitalisées
|
||||||
|
if self.preferences["bul_show_ue_rangs"]:
|
||||||
|
fields_bmr.append(
|
||||||
|
f"Rang: {ue['moyenne']['rang']} / {ue['moyenne']['total']}"
|
||||||
|
)
|
||||||
|
ue_min, ue_max, ue_moy = (
|
||||||
|
ue["moyenne"]["min"],
|
||||||
|
ue["moyenne"]["max"],
|
||||||
|
ue["moyenne"]["moy"],
|
||||||
)
|
)
|
||||||
|
else: # UE capitalisée
|
||||||
|
ue_min, ue_max, ue_moy = "", "", moy_ue
|
||||||
|
date_capitalisation = ue.get("date_capitalisation")
|
||||||
|
if date_capitalisation:
|
||||||
|
fields_bmr.append(
|
||||||
|
f"""Capitalisée le {date_capitalisation.strftime("%d/%m/%Y")}"""
|
||||||
|
)
|
||||||
t = {
|
t = {
|
||||||
"titre": " - ".join(fields_bmr),
|
"titre": " - ".join(fields_bmr),
|
||||||
"coef": ects_txt,
|
"coef": ects_txt,
|
||||||
"_coef_pdf": Paragraph(f"""<para align=left>{ects_txt}</para>"""),
|
"_coef_pdf": Paragraph(f"""<para align=right>{ects_txt}</para>"""),
|
||||||
"_coef_colspan": 2,
|
"_coef_colspan": 2,
|
||||||
"_pdf_style": [
|
"_pdf_style": [
|
||||||
("BACKGROUND", (0, 0), (-1, 0), title_bg),
|
("BACKGROUND", (0, 0), (-1, 0), title_bg),
|
||||||
|
@ -222,9 +261,9 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||||
# ligne au dessus du bonus/malus, gris clair
|
# ligne au dessus du bonus/malus, gris clair
|
||||||
("LINEABOVE", (0, 0), (-1, 0), self.PDF_LINEWIDTH, (0.7, 0.7, 0.7)),
|
("LINEABOVE", (0, 0), (-1, 0), self.PDF_LINEWIDTH, (0.7, 0.7, 0.7)),
|
||||||
],
|
],
|
||||||
"min": ue["moyenne"]["min"],
|
"min": ue_min,
|
||||||
"max": ue["moyenne"]["max"],
|
"max": ue_max,
|
||||||
"moy": ue["moyenne"]["moy"],
|
"moy": ue_moy,
|
||||||
}
|
}
|
||||||
rows.append(t)
|
rows.append(t)
|
||||||
|
|
||||||
|
|
|
@ -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 codes_cursus
|
||||||
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(
|
||||||
|
@ -65,11 +65,10 @@ def bulletin_but_xml_compat(
|
||||||
from app.scodoc import sco_bulletins
|
from app.scodoc import sco_bulletins
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"bulletin_but_xml_compat( formsemestre_id=%s, etudid=%s )"
|
f"bulletin_but_xml_compat( formsemestre_id={formsemestre_id}, etudid={etudid} )"
|
||||||
% (formsemestre_id, etudid)
|
|
||||||
)
|
)
|
||||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
etud = Identite.get_etud(etudid)
|
||||||
etud: Identite = Identite.query.get_or_404(etudid)
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
results = bulletin_but.ResultatsSemestreBUT(formsemestre)
|
results = bulletin_but.ResultatsSemestreBUT(formsemestre)
|
||||||
nb_inscrits = results.get_inscriptions_counts()[scu.INSCRIT]
|
nb_inscrits = results.get_inscriptions_counts()[scu.INSCRIT]
|
||||||
# etat_inscription = etud.inscription_etat(formsemestre.id)
|
# etat_inscription = etud.inscription_etat(formsemestre.id)
|
||||||
|
@ -108,13 +107,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,13 +152,13 @@ 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 != codes_cursus.UE_SPORT:
|
||||||
v = results.etud_moy_ue[ue.id][etud.id]
|
v = results.etud_moy_ue[ue.id][etud.id]
|
||||||
vmin = results.etud_moy_ue[ue.id].min()
|
vmin = results.etud_moy_ue[ue.id].min()
|
||||||
vmax = results.etud_moy_ue[ue.id].max()
|
vmax = results.etud_moy_ue[ue.id].max()
|
||||||
|
@ -192,11 +191,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 +212,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),
|
||||||
)
|
)
|
||||||
|
@ -255,14 +252,14 @@ def bulletin_but_xml_compat(
|
||||||
):
|
):
|
||||||
infos, dpv = sco_bulletins.etud_descr_situation_semestre(
|
infos, dpv = sco_bulletins.etud_descr_situation_semestre(
|
||||||
etudid,
|
etudid,
|
||||||
formsemestre_id,
|
formsemestre,
|
||||||
format="xml",
|
format="xml",
|
||||||
show_uevalid=sco_preferences.get_preference(
|
show_uevalid=sco_preferences.get_preference(
|
||||||
"bul_show_uevalid", formsemestre_id
|
"bul_show_uevalid", formsemestre_id
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
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 +294,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 +319,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
|
||||||
|
@ -44,15 +44,17 @@ from app.models.formations import Formation
|
||||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||||
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 as sco_codes
|
from app.scodoc import codes_cursus as sco_codes
|
||||||
from app.scodoc.sco_codes_parcours import RED, UE_STANDARD
|
from app.scodoc.codes_cursus 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,140 @@ 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 (la "meilleure"), ... } }"""
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
# XXX TODO OPTIMISATION ACCESS TABLE JURY
|
||||||
|
def to_dict_codes(self) -> dict[int, dict[str, int]]:
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
competence_id : {
|
||||||
|
annee : { validation}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
où validation est un petit dict avec niveau_id, etc.
|
||||||
|
"""
|
||||||
|
d = {}
|
||||||
|
for competence in self.competences.values():
|
||||||
|
d[competence.id] = {}
|
||||||
|
for annee in ("BUT1", "BUT2", "BUT3"):
|
||||||
|
validation_rcue: ApcValidationRCUE = (
|
||||||
|
self.validation_par_competence_et_annee.get(competence.id, {}).get(
|
||||||
|
annee
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
d[competence.id][annee] = (
|
||||||
|
validation_rcue.to_dict_codes() if validation_rcue else None
|
||||||
|
)
|
||||||
|
return d
|
||||||
|
|
|
@ -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
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -26,78 +26,34 @@ def _descr_cursus_but(etud: Identite) -> str:
|
||||||
# prend simplement tous les semestre de type APC, ce qui sera faux si
|
# prend simplement tous les semestre de type APC, ce qui sera faux si
|
||||||
# l'étudiant change de spécialité au sein du même département
|
# l'étudiant change de spécialité au sein du même département
|
||||||
# (ce qui ne peut normalement pas se produire)
|
# (ce qui ne peut normalement pas se produire)
|
||||||
indices = sorted(
|
inscriptions = sorted(
|
||||||
[
|
[
|
||||||
ins.formsemestre.semestre_id
|
ins
|
||||||
if ins.formsemestre.semestre_id is not None
|
|
||||||
else -1
|
|
||||||
for ins in etud.formsemestre_inscriptions
|
for ins in etud.formsemestre_inscriptions
|
||||||
if ins.formsemestre.formation.is_apc()
|
if ins.formsemestre.formation.is_apc()
|
||||||
]
|
],
|
||||||
|
key=lambda i: i.formsemestre.date_debut,
|
||||||
)
|
)
|
||||||
|
indices = [
|
||||||
|
ins.formsemestre.semestre_id if ins.formsemestre.semestre_id is not None else -1
|
||||||
|
for ins in inscriptions
|
||||||
|
]
|
||||||
|
|
||||||
return ", ".join(f"S{indice}" for indice in indices)
|
return ", ".join(f"S{indice}" for indice in indices)
|
||||||
|
|
||||||
|
|
||||||
def pvjury_table_but(formsemestre_id: int, format="html"):
|
def pvjury_page_but(formsemestre_id: int, fmt="html"):
|
||||||
"""Page récapitulant les décisions de jury BUT
|
"""Page récapitulant les décisions de jury BUT
|
||||||
formsemestre peut être pair ou impair
|
formsemestre peut être pair ou impair
|
||||||
"""
|
"""
|
||||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
assert formsemestre.formation.is_apc()
|
assert formsemestre.formation.is_apc()
|
||||||
title = "Procès-verbal de jury BUT annuel"
|
title = "Procès-verbal de jury BUT"
|
||||||
|
if fmt == "html":
|
||||||
if format == "html":
|
line_sep = "<br>"
|
||||||
line_sep = "<br/>"
|
|
||||||
else:
|
else:
|
||||||
line_sep = "\n"
|
line_sep = "\n"
|
||||||
# remplace pour le BUT la fonction sco_pvjury.pvjury_table
|
rows, titles = pvjury_table_but(formsemestre, line_sep=line_sep)
|
||||||
annee_but = (formsemestre.semestre_id + 1) // 2
|
|
||||||
titles = {
|
|
||||||
"nom": "Nom",
|
|
||||||
"cursus": "Cursus",
|
|
||||||
"ues": "UE validées",
|
|
||||||
"niveaux": "Niveaux de compétences validés",
|
|
||||||
"decision_but": f"Décision BUT{annee_but}",
|
|
||||||
"diplome": "Résultat au diplôme",
|
|
||||||
"devenir": "Devenir",
|
|
||||||
"observations": "Observations",
|
|
||||||
}
|
|
||||||
rows = []
|
|
||||||
for etudid in formsemestre.etuds_inscriptions:
|
|
||||||
etud: Identite = Identite.query.get(etudid)
|
|
||||||
try:
|
|
||||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
|
||||||
if deca.annee_but != annee_but: # wtf ?
|
|
||||||
log(
|
|
||||||
f"pvjury_table_but: inconsistent annee_but {deca.annee_but} != {annee_but}"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
except ScoValueError:
|
|
||||||
deca = None
|
|
||||||
row = {
|
|
||||||
"nom": etud.etat_civil_pv(line_sep=line_sep),
|
|
||||||
"_nom_order": etud.sort_key,
|
|
||||||
"_nom_target_attrs": f'class="etudinfo" id="{etud.id}"',
|
|
||||||
"_nom_td_attrs": f'id="{etud.id}" class="etudinfo"',
|
|
||||||
"_nom_target": url_for(
|
|
||||||
"scolar.ficheEtud",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
etudid=etud.id,
|
|
||||||
),
|
|
||||||
"cursus": _descr_cursus_but(etud),
|
|
||||||
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
|
|
||||||
"niveaux": deca.descr_niveaux_validation(line_sep=line_sep)
|
|
||||||
if deca
|
|
||||||
else "-",
|
|
||||||
"decision_but": deca.code_valide if deca else "",
|
|
||||||
"devenir": ", ".join([f"S{i}" for i in deca.get_autorisations_passage()])
|
|
||||||
if deca
|
|
||||||
else "",
|
|
||||||
}
|
|
||||||
|
|
||||||
rows.append(row)
|
|
||||||
|
|
||||||
rows.sort(key=lambda x: x["_nom_order"])
|
|
||||||
|
|
||||||
# Style excel... passages à la ligne sur \n
|
# Style excel... passages à la ligne sur \n
|
||||||
xls_style_base = sco_excel.excel_make_style()
|
xls_style_base = sco_excel.excel_make_style()
|
||||||
|
@ -109,10 +65,11 @@ def pvjury_table_but(formsemestre_id: int, format="html"):
|
||||||
columns_ids=titles.keys(),
|
columns_ids=titles.keys(),
|
||||||
html_caption=title,
|
html_caption=title,
|
||||||
html_class="pvjury_table_but table_leftalign",
|
html_class="pvjury_table_but table_leftalign",
|
||||||
html_title=f"""<div style="margin-bottom: 8px;"><span style="font-size: 120%; font-weight: bold;">{title}</span>
|
html_title=f"""<div style="margin-bottom: 8px;"><span
|
||||||
|
style="font-size: 120%; font-weight: bold;">{title}</span>
|
||||||
<span style="padding-left: 20px;">
|
<span style="padding-left: 20px;">
|
||||||
<a href="{url_for("notes.pvjury_table_but",
|
<a href="{url_for("notes.pvjury_page_but",
|
||||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, format="xlsx")}"
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, fmt="xlsx")}"
|
||||||
class="stdlink">version excel</a></span></div>
|
class="stdlink">version excel</a></span></div>
|
||||||
|
|
||||||
""",
|
""",
|
||||||
|
@ -136,4 +93,78 @@ def pvjury_table_but(formsemestre_id: int, format="html"):
|
||||||
},
|
},
|
||||||
xls_style_base=xls_style_base,
|
xls_style_base=xls_style_base,
|
||||||
)
|
)
|
||||||
return tab.make_page(format=format, javascripts=["js/etud_info.js"], init_qtip=True)
|
return tab.make_page(format=fmt, javascripts=["js/etud_info.js"], init_qtip=True)
|
||||||
|
|
||||||
|
|
||||||
|
def pvjury_table_but(
|
||||||
|
formsemestre: FormSemestre,
|
||||||
|
etudids: list[int] = None,
|
||||||
|
line_sep: str = "\n",
|
||||||
|
only_diplome=False,
|
||||||
|
anonymous=False,
|
||||||
|
with_paragraph_nom=False,
|
||||||
|
) -> tuple[list[dict], dict]:
|
||||||
|
"""Table avec résultats jury BUT pour PV.
|
||||||
|
Si etudids est None, prend tous les étudiants inscrits.
|
||||||
|
"""
|
||||||
|
# remplace pour le BUT la fonction sco_pv_forms.pvjury_table
|
||||||
|
annee_but = (formsemestre.semestre_id + 1) // 2
|
||||||
|
titles = {
|
||||||
|
"nom": "Code" if anonymous else "Nom",
|
||||||
|
"cursus": "Cursus",
|
||||||
|
"ects": "ECTS",
|
||||||
|
"ues": "UE validées",
|
||||||
|
"niveaux": "Niveaux de compétences validés",
|
||||||
|
"decision_but": f"Décision BUT{annee_but}",
|
||||||
|
"diplome": "Résultat au diplôme",
|
||||||
|
"devenir": "Devenir",
|
||||||
|
"observations": "Observations",
|
||||||
|
}
|
||||||
|
rows = []
|
||||||
|
formsemestre_etudids = formsemestre.etuds_inscriptions.keys()
|
||||||
|
if etudids is None:
|
||||||
|
etudids = formsemestre_etudids
|
||||||
|
for etudid in etudids:
|
||||||
|
if not etudid in formsemestre_etudids:
|
||||||
|
continue # garde fou
|
||||||
|
etud = Identite.get_etud(etudid)
|
||||||
|
try:
|
||||||
|
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||||
|
if deca.annee_but != annee_but: # wtf ?
|
||||||
|
log(
|
||||||
|
f"pvjury_table_but: inconsistent annee_but {deca.annee_but} != {annee_but}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
except ScoValueError:
|
||||||
|
deca = None
|
||||||
|
|
||||||
|
row = {
|
||||||
|
"nom": etud.code_ine or etud.code_nip or etud.id
|
||||||
|
if anonymous # Mode anonyme: affiche INE ou sinon NIP, ou id
|
||||||
|
else etud.etat_civil_pv(
|
||||||
|
line_sep=line_sep, with_paragraph=with_paragraph_nom
|
||||||
|
),
|
||||||
|
"_nom_order": etud.sort_key,
|
||||||
|
"_nom_target_attrs": f'class="etudinfo" id="{etud.id}"',
|
||||||
|
"_nom_td_attrs": f'id="{etud.id}" class="etudinfo"',
|
||||||
|
"_nom_target": url_for(
|
||||||
|
"scolar.ficheEtud",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
etudid=etud.id,
|
||||||
|
),
|
||||||
|
"cursus": _descr_cursus_but(etud),
|
||||||
|
"ects": f"{deca.formsemestre_ects():g}",
|
||||||
|
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
|
||||||
|
"niveaux": deca.descr_niveaux_validation(line_sep=line_sep)
|
||||||
|
if deca
|
||||||
|
else "-",
|
||||||
|
"decision_but": deca.code_valide if deca else "",
|
||||||
|
"devenir": ", ".join([f"S{i}" for i in deca.get_autorisations_passage()])
|
||||||
|
if deca
|
||||||
|
else "",
|
||||||
|
}
|
||||||
|
if deca.valide_diplome() or not only_diplome:
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
rows.sort(key=lambda x: x["_nom_order"])
|
||||||
|
return rows, titles
|
||||||
|
|
|
@ -1,525 +0,0 @@
|
||||||
##############################################################################
|
|
||||||
# ScoDoc
|
|
||||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
|
||||||
# See LICENSE
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
"""Jury BUT: table recap annuelle et liens saisie
|
|
||||||
"""
|
|
||||||
|
|
||||||
import time
|
|
||||||
import numpy as np
|
|
||||||
from flask import g, url_for
|
|
||||||
|
|
||||||
from app.but import jury_but
|
|
||||||
from app.but.jury_but import (
|
|
||||||
DecisionsProposeesAnnee,
|
|
||||||
DecisionsProposeesRCUE,
|
|
||||||
DecisionsProposeesUE,
|
|
||||||
)
|
|
||||||
from app.comp.res_but import ResultatsSemestreBUT
|
|
||||||
from app.comp import res_sem
|
|
||||||
from app.models.etudiants import Identite
|
|
||||||
from app.models.formsemestre import FormSemestre
|
|
||||||
from app.scodoc import html_sco_header
|
|
||||||
from app.scodoc.sco_codes_parcours import (
|
|
||||||
BUT_BARRE_RCUE,
|
|
||||||
BUT_BARRE_UE,
|
|
||||||
BUT_BARRE_UE8,
|
|
||||||
BUT_RCUE_SUFFISANT,
|
|
||||||
)
|
|
||||||
from app.scodoc import sco_formsemestre_status
|
|
||||||
from app.scodoc import sco_pvjury
|
|
||||||
from app.scodoc import sco_utils as scu
|
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_saisie_jury_but(
|
|
||||||
formsemestre2: FormSemestre,
|
|
||||||
read_only: bool = False,
|
|
||||||
selected_etudid: int = None,
|
|
||||||
mode="jury",
|
|
||||||
) -> str:
|
|
||||||
"""formsemestre est un semestre PAIR
|
|
||||||
Si readonly, ne montre pas le lien "saisir la décision"
|
|
||||||
|
|
||||||
=> page html complète
|
|
||||||
|
|
||||||
Si mode == "recap", table recap des codes, sans liens de saisie.
|
|
||||||
"""
|
|
||||||
# Quick & Dirty
|
|
||||||
# pour chaque etud de res2 trié
|
|
||||||
# S1: UE1, ..., UEn
|
|
||||||
# S2: UE1, ..., UEn
|
|
||||||
#
|
|
||||||
# UE1_s1, UE1_s2, moy_rcue, UE2... , Nbrcue_validables, Nbrcue<8, passage_de_droit, valide_moitie_rcue
|
|
||||||
#
|
|
||||||
# Pour chaque etud de res2 trié
|
|
||||||
# DecisionsProposeesAnnee(etud, formsemestre2)
|
|
||||||
# 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
|
|
||||||
if formsemestre2.semestre_id % 2 != 0:
|
|
||||||
raise ScoValueError("Cette page ne fonctionne que sur les semestres pairs")
|
|
||||||
|
|
||||||
if formsemestre2.formation.referentiel_competence is None:
|
|
||||||
raise ScoValueError(
|
|
||||||
"""
|
|
||||||
<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(
|
|
||||||
formsemestre2, read_only=read_only, mode=mode
|
|
||||||
)
|
|
||||||
if not rows:
|
|
||||||
return (
|
|
||||||
'<div class="table_recap"><div class="message">aucun étudiant !</div></div>'
|
|
||||||
)
|
|
||||||
filename = scu.sanitize_filename(
|
|
||||||
f"""jury-but-{formsemestre2.titre_num()}-{time.strftime("%Y-%m-%d")}"""
|
|
||||||
)
|
|
||||||
klass = "table_jury_but_bilan" if mode == "recap" else ""
|
|
||||||
table_html = build_table_jury_but_html(
|
|
||||||
filename, rows, titles, column_ids, selected_etudid=selected_etudid, klass=klass
|
|
||||||
)
|
|
||||||
H = [
|
|
||||||
html_sco_header.sco_header(
|
|
||||||
page_title=f"{formsemestre2.sem_modalite()}: jury BUT annuel",
|
|
||||||
no_side_bar=True,
|
|
||||||
init_qtip=True,
|
|
||||||
javascripts=["js/etud_info.js", "js/table_recap.js"],
|
|
||||||
),
|
|
||||||
sco_formsemestre_status.formsemestre_status_head(
|
|
||||||
formsemestre_id=formsemestre2.id
|
|
||||||
),
|
|
||||||
]
|
|
||||||
if mode == "recap":
|
|
||||||
H.append(
|
|
||||||
f"""<h3>Décisions de jury enregistrées pour les étudiants de ce semestre</h3>
|
|
||||||
<div class="table_jury_but_links">
|
|
||||||
<div>
|
|
||||||
<ul>
|
|
||||||
<li><a href="{url_for(
|
|
||||||
"notes.pvjury_table_but",
|
|
||||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
|
|
||||||
}" class="stdlink">Tableau PV de jury</a>
|
|
||||||
</li>
|
|
||||||
<li><a href="{url_for(
|
|
||||||
"notes.formsemestre_lettres_individuelles",
|
|
||||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
|
|
||||||
}" class="stdlink">Courriers individuels (classeur pdf)</a>
|
|
||||||
</li>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
H.append(
|
|
||||||
f"""
|
|
||||||
|
|
||||||
{table_html}
|
|
||||||
|
|
||||||
<div class="table_jury_but_links">
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
if (mode == "recap") and not read_only:
|
|
||||||
H.append(
|
|
||||||
f"""
|
|
||||||
<p><a class="stdlink" href="{url_for(
|
|
||||||
"notes.formsemestre_saisie_jury",
|
|
||||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
|
|
||||||
}">Saisie des décisions du jury</a>
|
|
||||||
</p>"""
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
H.append(
|
|
||||||
f"""
|
|
||||||
<p><a class="stdlink" href="{url_for(
|
|
||||||
"notes.formsemestre_validation_auto_but",
|
|
||||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
|
|
||||||
}">Calcul automatique des décisions du jury</a>
|
|
||||||
</p>
|
|
||||||
<p><a class="stdlink" href="{url_for(
|
|
||||||
"notes.formsemestre_jury_but_recap",
|
|
||||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
|
|
||||||
}">Tableau récapitulatif des décisions du jury</a>
|
|
||||||
</p>
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
H.append(
|
|
||||||
f"""
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{html_sco_header.sco_footer()}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
return "\n".join(H)
|
|
||||||
|
|
||||||
|
|
||||||
def build_table_jury_but_html(
|
|
||||||
filename: str, rows, titles, column_ids, selected_etudid: int = None, klass=""
|
|
||||||
) -> str:
|
|
||||||
"""assemble la table html"""
|
|
||||||
footer_rows = [] # inutilisé pour l'instant
|
|
||||||
H = [
|
|
||||||
f"""<div class="table_recap"><table class="table_recap apc jury table_jury_but {klass}"
|
|
||||||
data-filename="{filename}">"""
|
|
||||||
]
|
|
||||||
# header
|
|
||||||
H.append(
|
|
||||||
f"""
|
|
||||||
<thead>
|
|
||||||
{scu.gen_row(column_ids, titles, "th")}
|
|
||||||
</thead>
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
# body
|
|
||||||
H.append("<tbody>")
|
|
||||||
for row in rows:
|
|
||||||
H.append(f"{scu.gen_row(column_ids, row, selected_etudid=selected_etudid)}\n")
|
|
||||||
H.append("</tbody>\n")
|
|
||||||
# footer
|
|
||||||
H.append("<tfoot>")
|
|
||||||
idx_last = len(footer_rows) - 1
|
|
||||||
for i, row in enumerate(footer_rows):
|
|
||||||
H.append(f'{scu.gen_row(column_ids, row, "th" if i == idx_last else "td")}\n')
|
|
||||||
H.append(
|
|
||||||
"""
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
return "".join(H)
|
|
||||||
|
|
||||||
|
|
||||||
class RowCollector:
|
|
||||||
"""Une ligne de la table"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
cells: dict = None,
|
|
||||||
titles: dict = None,
|
|
||||||
convert_values=True,
|
|
||||||
column_classes: dict = None,
|
|
||||||
):
|
|
||||||
self.titles = titles
|
|
||||||
self.row = cells or {} # col_id : str
|
|
||||||
self.column_classes = column_classes # col_id : str, css class
|
|
||||||
self.idx = 0
|
|
||||||
self.last_etud_cell_idx = 0
|
|
||||||
if convert_values:
|
|
||||||
self.fmt_note = scu.fmt_note
|
|
||||||
else:
|
|
||||||
self.fmt_note = lambda x: x
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
self.row[key] = value
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
return self.row[key]
|
|
||||||
|
|
||||||
def get_row_dict(self):
|
|
||||||
"La ligne, comme un dict"
|
|
||||||
# create empty cells
|
|
||||||
for col_id in self.titles:
|
|
||||||
if col_id not in self.row:
|
|
||||||
self.row[col_id] = ""
|
|
||||||
klass = self.column_classes.get(col_id)
|
|
||||||
if klass:
|
|
||||||
self.row[f"_{col_id}_class"] = klass
|
|
||||||
return self.row
|
|
||||||
|
|
||||||
def add_cell(
|
|
||||||
self,
|
|
||||||
col_id: str,
|
|
||||||
title: str,
|
|
||||||
content: str,
|
|
||||||
classes: str = "",
|
|
||||||
idx: int = None,
|
|
||||||
column_class="",
|
|
||||||
):
|
|
||||||
"""Add a row to our table. classes is a list of css class names"""
|
|
||||||
self.idx = idx if idx is not None else self.idx
|
|
||||||
self.row[col_id] = content
|
|
||||||
if classes:
|
|
||||||
self.row[f"_{col_id}_class"] = classes + f" c{self.idx}"
|
|
||||||
if not col_id in self.titles:
|
|
||||||
self.titles[col_id] = title
|
|
||||||
self.titles[f"_{col_id}_col_order"] = self.idx
|
|
||||||
if classes:
|
|
||||||
self.titles[f"_{col_id}_class"] = classes
|
|
||||||
self.column_classes[col_id] = column_class
|
|
||||||
self.idx += 1
|
|
||||||
|
|
||||||
def add_etud_cells(
|
|
||||||
self, etud: Identite, formsemestre: FormSemestre, with_links=True
|
|
||||||
):
|
|
||||||
"Les cells code, nom, prénom etc."
|
|
||||||
# --- Codes (seront cachés, mais exportés en excel)
|
|
||||||
self.add_cell("etudid", "etudid", etud.id, "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)
|
|
||||||
self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail")
|
|
||||||
self.add_cell("nom_disp", "Nom", etud.nom_disp(), "identite_detail")
|
|
||||||
self["_nom_disp_order"] = etud.sort_key
|
|
||||||
self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
|
|
||||||
self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court")
|
|
||||||
if with_links:
|
|
||||||
self["_nom_short_order"] = etud.sort_key
|
|
||||||
self["_nom_short_target"] = url_for(
|
|
||||||
"notes.formsemestre_bulletinetud",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
formsemestre_id=formsemestre.id,
|
|
||||||
etudid=etud.id,
|
|
||||||
)
|
|
||||||
self["_nom_short_target_attrs"] = f'class="etudinfo" id="{etud.id}"'
|
|
||||||
self["_nom_disp_target"] = self["_nom_short_target"]
|
|
||||||
self["_nom_disp_target_attrs"] = self["_nom_short_target_attrs"]
|
|
||||||
self.last_etud_cell_idx = self.idx
|
|
||||||
|
|
||||||
def add_ue_cells(self, dec_ue: DecisionsProposeesUE):
|
|
||||||
"cell de moyenne d'UE"
|
|
||||||
col_id = f"moy_ue_{dec_ue.ue.id}"
|
|
||||||
note_class = ""
|
|
||||||
val = dec_ue.moy_ue
|
|
||||||
if isinstance(val, float):
|
|
||||||
if val < BUT_BARRE_UE:
|
|
||||||
note_class = " moy_inf"
|
|
||||||
elif val >= BUT_BARRE_UE:
|
|
||||||
note_class = " moy_ue_valid"
|
|
||||||
if val < BUT_BARRE_UE8:
|
|
||||||
note_class = " moy_ue_warning" # notes très basses
|
|
||||||
self.add_cell(
|
|
||||||
col_id,
|
|
||||||
dec_ue.ue.acronyme,
|
|
||||||
self.fmt_note(val),
|
|
||||||
"col_ue" + note_class,
|
|
||||||
column_class="col_ue",
|
|
||||||
)
|
|
||||||
self.add_cell(
|
|
||||||
col_id + "_code",
|
|
||||||
dec_ue.ue.acronyme,
|
|
||||||
dec_ue.code_valide or "",
|
|
||||||
"col_ue_code recorded_code",
|
|
||||||
column_class="col_ue",
|
|
||||||
)
|
|
||||||
|
|
||||||
def add_rcue_cells(self, dec_rcue: DecisionsProposeesRCUE):
|
|
||||||
"2 cells: moyenne du RCUE, code enregistré"
|
|
||||||
rcue = dec_rcue.rcue
|
|
||||||
col_id = f"moy_rcue_{rcue.ue_1.niveau_competence_id}" # le niveau_id
|
|
||||||
note_class = ""
|
|
||||||
val = rcue.moy_rcue
|
|
||||||
if isinstance(val, float):
|
|
||||||
if val < BUT_BARRE_RCUE:
|
|
||||||
note_class = " moy_ue_inf"
|
|
||||||
elif val >= BUT_BARRE_RCUE:
|
|
||||||
note_class = " moy_ue_valid"
|
|
||||||
if val < BUT_RCUE_SUFFISANT:
|
|
||||||
note_class = " moy_ue_warning" # notes très basses
|
|
||||||
self.add_cell(
|
|
||||||
col_id,
|
|
||||||
f"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>",
|
|
||||||
self.fmt_note(val),
|
|
||||||
"col_rcue" + note_class,
|
|
||||||
column_class="col_rcue",
|
|
||||||
)
|
|
||||||
self.add_cell(
|
|
||||||
col_id + "_code",
|
|
||||||
f"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>",
|
|
||||||
dec_rcue.code_valide or "",
|
|
||||||
"col_rcue_code recorded_code",
|
|
||||||
column_class="col_rcue",
|
|
||||||
)
|
|
||||||
|
|
||||||
def add_nb_rcues_cell(self, deca: DecisionsProposeesAnnee):
|
|
||||||
"cell avec nb niveaux validables / total"
|
|
||||||
klass = " "
|
|
||||||
if deca.nb_rcues_under_8 > 0:
|
|
||||||
klass += "moy_ue_warning"
|
|
||||||
elif deca.nb_validables < deca.nb_competences:
|
|
||||||
klass += "moy_ue_inf"
|
|
||||||
else:
|
|
||||||
klass += "moy_ue_valid"
|
|
||||||
self.add_cell(
|
|
||||||
"rcues_validables",
|
|
||||||
"RCUEs",
|
|
||||||
f"""{deca.nb_validables}/{deca.nb_competences}"""
|
|
||||||
+ ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""),
|
|
||||||
"col_rcue col_rcues_validables" + klass,
|
|
||||||
)
|
|
||||||
self["_rcues_validables_data"] = {
|
|
||||||
"etudid": deca.etud.id,
|
|
||||||
"nomprenom": deca.etud.nomprenom,
|
|
||||||
}
|
|
||||||
if len(deca.rcues_annee) > 0:
|
|
||||||
# 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:
|
|
||||||
moy = deca.res_pair.etud_moy_gen[deca.etud.id]
|
|
||||||
if np.isnan(moy):
|
|
||||||
moy_gen_d = "x"
|
|
||||||
else:
|
|
||||||
moy_gen_d = f"{int(moy*1000):05}"
|
|
||||||
else:
|
|
||||||
moy_gen_d = "x"
|
|
||||||
self["_rcues_validables_order"] = f"{deca.nb_validables:04d}-{moy_gen_d}"
|
|
||||||
else:
|
|
||||||
# etudiants sans RCUE: pas de semestre impair, ...
|
|
||||||
# les classe à la fin
|
|
||||||
self[
|
|
||||||
"_rcues_validables_order"
|
|
||||||
] = f"{deca.nb_validables:04d}-00000-{deca.etud.sort_key}"
|
|
||||||
|
|
||||||
|
|
||||||
def get_jury_but_table(
|
|
||||||
formsemestre2: FormSemestre, read_only: bool = False, mode="jury", with_links=True
|
|
||||||
) -> tuple[list[dict], list[str], list[str]]:
|
|
||||||
"""Construit la table des résultats annuels pour le jury BUT"""
|
|
||||||
res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2)
|
|
||||||
titles = {} # column_id : title
|
|
||||||
column_classes = {}
|
|
||||||
rows = []
|
|
||||||
for etudid in formsemestre2.etuds_inscriptions:
|
|
||||||
etud: Identite = Identite.query.get(etudid)
|
|
||||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre2)
|
|
||||||
row = RowCollector(titles=titles, column_classes=column_classes)
|
|
||||||
row.add_etud_cells(etud, formsemestre2, with_links=with_links)
|
|
||||||
row.idx = 100 # laisse place pour les colonnes de groupes
|
|
||||||
# --- Nombre de niveaux
|
|
||||||
row.add_nb_rcues_cell(deca)
|
|
||||||
# --- Les RCUEs
|
|
||||||
for rcue in deca.rcues_annee:
|
|
||||||
dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id)
|
|
||||||
if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau
|
|
||||||
row.add_ue_cells(deca.decisions_ues[rcue.ue_1.id])
|
|
||||||
row.add_ue_cells(deca.decisions_ues[rcue.ue_2.id])
|
|
||||||
row.add_rcue_cells(dec_rcue)
|
|
||||||
# --- Les ECTS validés
|
|
||||||
ects_valides = 0.0
|
|
||||||
if deca.res_impair:
|
|
||||||
ects_valides += deca.res_impair.get_etud_ects_valides(etudid)
|
|
||||||
if deca.res_pair:
|
|
||||||
ects_valides += deca.res_pair.get_etud_ects_valides(etudid)
|
|
||||||
row.add_cell(
|
|
||||||
"ects_annee",
|
|
||||||
"ECTS",
|
|
||||||
f"""{int(ects_valides)}""",
|
|
||||||
"col_code_annee",
|
|
||||||
)
|
|
||||||
# --- Le code annuel existant
|
|
||||||
row.add_cell(
|
|
||||||
"code_annee",
|
|
||||||
"Année",
|
|
||||||
f"""{deca.code_valide or ''}""",
|
|
||||||
"col_code_annee",
|
|
||||||
)
|
|
||||||
# --- Le lien de saisie
|
|
||||||
if mode != "recap" and with_links:
|
|
||||||
row.add_cell(
|
|
||||||
"lien_saisie",
|
|
||||||
"",
|
|
||||||
f"""
|
|
||||||
<a href="{url_for(
|
|
||||||
'notes.formsemestre_validation_but',
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
etudid=etud.id,
|
|
||||||
formsemestre_id=formsemestre2.id,
|
|
||||||
)}" class="stdlink">
|
|
||||||
{"voir" if read_only else ("modif." if deca.code_valide else "saisie")}
|
|
||||||
décision</a>
|
|
||||||
"""
|
|
||||||
if deca.inscription_etat == scu.INSCRIT
|
|
||||||
else deca.inscription_etat,
|
|
||||||
"col_lien_saisie_but",
|
|
||||||
)
|
|
||||||
rows.append(row)
|
|
||||||
rows_dict = [row.get_row_dict() for row in rows]
|
|
||||||
if len(rows_dict) > 0:
|
|
||||||
res2.recap_add_partitions(rows_dict, titles, col_idx=row.last_etud_cell_idx + 1)
|
|
||||||
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))
|
|
||||||
rows_dict.sort(key=lambda row: row["_nom_disp_order"])
|
|
||||||
return rows_dict, titles, column_ids
|
|
||||||
|
|
||||||
|
|
||||||
def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]:
|
|
||||||
"""Liste des résultats jury BUT sous forme de dict, pour API"""
|
|
||||||
if formsemestre.formation.referentiel_competence is None:
|
|
||||||
# pas de ref. comp., donc pas de decisions de jury (ne lance pas d'exception)
|
|
||||||
return []
|
|
||||||
dpv = sco_pvjury.dict_pvjury(formsemestre.id)
|
|
||||||
rows = []
|
|
||||||
for etudid in formsemestre.etuds_inscriptions:
|
|
||||||
rows.append(get_jury_but_etud_result(formsemestre, dpv, etudid))
|
|
||||||
return rows
|
|
||||||
|
|
||||||
|
|
||||||
def get_jury_but_etud_result(
|
|
||||||
formsemestre: FormSemestre, dpv: dict, etudid: int
|
|
||||||
) -> dict:
|
|
||||||
"""Résultats de jury d'un étudiant sur un semestre pair de BUT"""
|
|
||||||
etud: Identite = Identite.query.get(etudid)
|
|
||||||
dec_etud = dpv["decisions_dict"][etudid]
|
|
||||||
if formsemestre.formation.is_apc():
|
|
||||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
|
||||||
else:
|
|
||||||
deca = None
|
|
||||||
row = {
|
|
||||||
"etudid": etud.id,
|
|
||||||
"code_nip": etud.code_nip,
|
|
||||||
"code_ine": etud.code_ine,
|
|
||||||
"is_apc": dpv["is_apc"], # BUT ou classic ?
|
|
||||||
"etat": dec_etud["etat"], # I ou D ou DEF
|
|
||||||
"nb_competences": deca.nb_competences if deca else 0,
|
|
||||||
}
|
|
||||||
# --- Les RCUEs
|
|
||||||
rcue_list = []
|
|
||||||
if deca:
|
|
||||||
for rcue in deca.rcues_annee:
|
|
||||||
dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id)
|
|
||||||
if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau
|
|
||||||
dec_ue1 = deca.decisions_ues[rcue.ue_1.id]
|
|
||||||
dec_ue2 = deca.decisions_ues[rcue.ue_2.id]
|
|
||||||
rcue_dict = {
|
|
||||||
"ue_1": {
|
|
||||||
"ue_id": rcue.ue_1.id,
|
|
||||||
"moy": None
|
|
||||||
if (dec_ue1.moy_ue is None or np.isnan(dec_ue1.moy_ue))
|
|
||||||
else dec_ue1.moy_ue,
|
|
||||||
"code": dec_ue1.code_valide,
|
|
||||||
},
|
|
||||||
"ue_2": {
|
|
||||||
"ue_id": rcue.ue_2.id,
|
|
||||||
"moy": None
|
|
||||||
if (dec_ue2.moy_ue is None or np.isnan(dec_ue2.moy_ue))
|
|
||||||
else dec_ue2.moy_ue,
|
|
||||||
"code": dec_ue2.code_valide,
|
|
||||||
},
|
|
||||||
"moy": rcue.moy_rcue,
|
|
||||||
"code": dec_rcue.code_valide,
|
|
||||||
}
|
|
||||||
rcue_list.append(rcue_dict)
|
|
||||||
row["rcues"] = rcue_list
|
|
||||||
# --- Les UEs
|
|
||||||
ue_list = []
|
|
||||||
if dec_etud["decisions_ue"]:
|
|
||||||
for ue_id, ue_dec in dec_etud["decisions_ue"].items():
|
|
||||||
ue_dict = {
|
|
||||||
"ue_id": ue_id,
|
|
||||||
"code": ue_dec["code"],
|
|
||||||
"ects": ue_dec["ects"],
|
|
||||||
}
|
|
||||||
ue_list.append(ue_dict)
|
|
||||||
row["ues"] = ue_list
|
|
||||||
# --- Le semestre (pour les formations classiques)
|
|
||||||
if dec_etud["decision_sem"]:
|
|
||||||
row["semestre"] = {"code": dec_etud["decision_sem"].get("code")}
|
|
||||||
else:
|
|
||||||
row["semestre"] = {} # APC, ...
|
|
||||||
# --- Autorisations
|
|
||||||
row["autorisations"] = dec_etud["autorisations"]
|
|
||||||
return row
|
|
94
app/but/jury_but_results.py
Normal file
94
app/but/jury_but_results.py
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""Jury BUT et classiques: récupération des résults pour API
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from app.but import jury_but
|
||||||
|
from app.models.etudiants import Identite
|
||||||
|
from app.models.formsemestre import FormSemestre
|
||||||
|
from app.scodoc import sco_pv_dict
|
||||||
|
|
||||||
|
|
||||||
|
def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]:
|
||||||
|
"""Liste des résultats jury BUT sous forme de dict, pour API"""
|
||||||
|
if formsemestre.formation.referentiel_competence is None:
|
||||||
|
# pas de ref. comp., donc pas de decisions de jury (ne lance pas d'exception)
|
||||||
|
return []
|
||||||
|
dpv = sco_pv_dict.dict_pvjury(formsemestre.id)
|
||||||
|
rows = []
|
||||||
|
for etudid in formsemestre.etuds_inscriptions:
|
||||||
|
rows.append(_get_jury_but_etud_result(formsemestre, dpv, etudid))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _get_jury_but_etud_result(
|
||||||
|
formsemestre: FormSemestre, dpv: dict, etudid: int
|
||||||
|
) -> dict:
|
||||||
|
"""Résultats de jury d'un étudiant sur un semestre pair de BUT"""
|
||||||
|
etud = Identite.get_etud(etudid)
|
||||||
|
dec_etud = dpv["decisions_dict"][etudid]
|
||||||
|
if formsemestre.formation.is_apc():
|
||||||
|
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||||
|
else:
|
||||||
|
deca = None
|
||||||
|
row = {
|
||||||
|
"etudid": etud.id,
|
||||||
|
"code_nip": etud.code_nip,
|
||||||
|
"code_ine": etud.code_ine,
|
||||||
|
"is_apc": dpv["is_apc"], # BUT ou classic ?
|
||||||
|
"etat": dec_etud["etat"], # I ou D ou DEF
|
||||||
|
"nb_competences": deca.nb_competences if deca else 0,
|
||||||
|
}
|
||||||
|
# --- Les RCUEs
|
||||||
|
rcue_list = []
|
||||||
|
if deca:
|
||||||
|
for rcue in deca.rcues_annee:
|
||||||
|
dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id)
|
||||||
|
if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau
|
||||||
|
dec_ue1 = deca.decisions_ues[rcue.ue_1.id]
|
||||||
|
dec_ue2 = deca.decisions_ues[rcue.ue_2.id]
|
||||||
|
rcue_dict = {
|
||||||
|
"ue_1": {
|
||||||
|
"ue_id": rcue.ue_1.id,
|
||||||
|
"moy": None
|
||||||
|
if (dec_ue1.moy_ue is None or np.isnan(dec_ue1.moy_ue))
|
||||||
|
else dec_ue1.moy_ue,
|
||||||
|
"code": dec_ue1.code_valide,
|
||||||
|
},
|
||||||
|
"ue_2": {
|
||||||
|
"ue_id": rcue.ue_2.id,
|
||||||
|
"moy": None
|
||||||
|
if (dec_ue2.moy_ue is None or np.isnan(dec_ue2.moy_ue))
|
||||||
|
else dec_ue2.moy_ue,
|
||||||
|
"code": dec_ue2.code_valide,
|
||||||
|
},
|
||||||
|
"moy": rcue.moy_rcue,
|
||||||
|
"code": dec_rcue.code_valide,
|
||||||
|
}
|
||||||
|
rcue_list.append(rcue_dict)
|
||||||
|
row["rcues"] = rcue_list
|
||||||
|
# --- Les UEs
|
||||||
|
ue_list = []
|
||||||
|
if dec_etud["decisions_ue"]:
|
||||||
|
for ue_id, ue_dec in dec_etud["decisions_ue"].items():
|
||||||
|
ue_dict = {
|
||||||
|
"ue_id": ue_id,
|
||||||
|
"code": ue_dec["code"],
|
||||||
|
"ects": ue_dec["ects"],
|
||||||
|
}
|
||||||
|
ue_list.append(ue_dict)
|
||||||
|
row["ues"] = ue_list
|
||||||
|
# --- Le semestre (pour les formations classiques)
|
||||||
|
if dec_etud["decision_sem"]:
|
||||||
|
row["semestre"] = {"code": dec_etud["decision_sem"].get("code")}
|
||||||
|
else:
|
||||||
|
row["semestre"] = {} # APC, ...
|
||||||
|
# --- Autorisations
|
||||||
|
row["autorisations"] = dec_etud["autorisations"]
|
||||||
|
return row
|
|
@ -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.get_etud(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,18 +1,42 @@
|
||||||
##############################################################################
|
##############################################################################
|
||||||
# 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: affichage/formulaire
|
"""Jury BUT: affichage/formulaire
|
||||||
"""
|
"""
|
||||||
from flask import g, url_for
|
|
||||||
from app.models.etudiants import Identite
|
|
||||||
|
|
||||||
from app.scodoc import sco_utils as scu
|
import re
|
||||||
from app.but.jury_but import DecisionsProposeesAnnee, DecisionsProposeesUE
|
import numpy as np
|
||||||
from app.models import FormSemestre, FormSemestreInscription, UniteEns
|
|
||||||
|
import flask
|
||||||
|
from flask import flash, render_template, url_for
|
||||||
|
from flask import g, request
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.but import jury_but
|
||||||
|
from app.but.jury_but import (
|
||||||
|
DecisionsProposeesAnnee,
|
||||||
|
DecisionsProposeesRCUE,
|
||||||
|
DecisionsProposeesUE,
|
||||||
|
)
|
||||||
|
from app.comp import res_sem
|
||||||
|
from app.comp.res_but import ResultatsSemestreBUT
|
||||||
|
from app.models import (
|
||||||
|
ApcNiveau,
|
||||||
|
FormSemestre,
|
||||||
|
FormSemestreInscription,
|
||||||
|
Identite,
|
||||||
|
UniteEns,
|
||||||
|
ScolarAutorisationInscription,
|
||||||
|
ScolarFormSemestreValidation,
|
||||||
|
)
|
||||||
|
from app.models.config import ScoDocSiteConfig
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
||||||
|
@ -20,35 +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"""<div class="but_section_annee">
|
H.append(
|
||||||
<div>
|
f"""
|
||||||
<b>Décision de jury pour l'année :</b> {
|
<div class="but_section_annee">
|
||||||
_gen_but_select("code_annee", deca.codes, deca.code_valide,
|
<div>
|
||||||
disabled=True, klass="manual")
|
<b>Décision de jury pour l'année :</b> {
|
||||||
}
|
_gen_but_select("code_annee", deca.codes, deca.code_valide,
|
||||||
<span>({'non ' if deca.code_valide is None else ''}enregistrée)</span>
|
disabled=True, klass="manual")
|
||||||
<span>{erase_span}</span>
|
}
|
||||||
|
<span>({deca.code_valide or 'non'} enregistrée)</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="but_explanation">{deca.explanation}</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>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
@ -58,42 +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
|
||||||
dec_rcue.rcue.moy_ue_1,
|
ues = [
|
||||||
deca.decisions_ues[dec_rcue.rcue.ue_1.id],
|
ue
|
||||||
disabled=read_only,
|
for ue in deca.ues_pair
|
||||||
)
|
if ue.niveau_competence and ue.niveau_competence.id == niveau.id
|
||||||
)
|
]
|
||||||
# Semestre pair
|
ue_pair = ues[0] if ues else None
|
||||||
H.append(
|
# Les UEs à afficher,
|
||||||
_gen_but_niveau_ue(
|
# qui seront toujours en readonly sur le formsemestre de l'année précédente du redoublant
|
||||||
dec_rcue.rcue.ue_2,
|
ues_ro = [
|
||||||
dec_rcue.rcue.moy_ue_2,
|
(
|
||||||
deca.decisions_ues[dec_rcue.rcue.ue_2.id],
|
ue_impair,
|
||||||
disabled=read_only,
|
(deca.a_cheval and deca.formsemestre_id != deca.formsemestre_impair.id),
|
||||||
)
|
),
|
||||||
)
|
(
|
||||||
# RCUE
|
ue_pair,
|
||||||
H.append(
|
deca.a_cheval and deca.formsemestre_id != deca.formsemestre_pair.id,
|
||||||
f"""<div class="but_niveau_rcue
|
),
|
||||||
{'recorded' if dec_rcue.code_valide is not None else ''}
|
]
|
||||||
">
|
# Ordonne selon les dates des 2 semestres considérés:
|
||||||
<div class="but_note">{scu.fmt_note(dec_rcue.rcue.moy_rcue)}</div>
|
if reverse_semestre:
|
||||||
<div class="but_code">{
|
ues_ro[0], ues_ro[1] = ues_ro[1], ues_ro[0]
|
||||||
_gen_but_select("code_rcue_"+str(niveau.id),
|
# Colonnes d'UE:
|
||||||
dec_rcue.codes,
|
for ue, ue_read_only in ues_ro:
|
||||||
dec_rcue.code_valide,
|
if ue:
|
||||||
disabled=True, klass="manual"
|
H.append(
|
||||||
|
_gen_but_niveau_ue(
|
||||||
|
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)
|
||||||
|
|
||||||
|
@ -104,48 +153,373 @@ 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(
|
||||||
|
formsemestre: FormSemestre,
|
||||||
|
etud: Identite,
|
||||||
|
read_only: bool,
|
||||||
|
navigation_div: str = "",
|
||||||
|
) -> str:
|
||||||
|
"""Page: formulaire saisie décision d'UE d'un semestre BUT isolé (pas jury annuel)."""
|
||||||
|
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
||||||
|
parcour, ues = jury_but.list_ue_parcour_etud(formsemestre, etud, res)
|
||||||
|
inscription_etat = etud.inscription_etat(formsemestre.id)
|
||||||
|
semestre_terminal = (
|
||||||
|
formsemestre.semestre_id >= formsemestre.formation.get_cursus().NB_SEM
|
||||||
|
)
|
||||||
|
autorisations_passage = ScolarAutorisationInscription.query.filter_by(
|
||||||
|
etudid=etud.id,
|
||||||
|
origin_formsemestre_id=formsemestre.id,
|
||||||
|
).all()
|
||||||
|
# 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 = {
|
||||||
|
ue.id: DecisionsProposeesUE(etud, formsemestre, ue, inscription_etat)
|
||||||
|
for ue in ues
|
||||||
|
}
|
||||||
|
for dec_ue in decisions_ues.values():
|
||||||
|
dec_ue.compute_codes()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
if not read_only:
|
||||||
|
for key in request.form:
|
||||||
|
code = request.form[key]
|
||||||
|
# Codes d'UE
|
||||||
|
code_match = re.match(r"^code_ue_(\d+)$", key)
|
||||||
|
if code_match:
|
||||||
|
ue_id = int(code_match.group(1))
|
||||||
|
dec_ue = decisions_ues.get(ue_id)
|
||||||
|
if not dec_ue:
|
||||||
|
raise ScoValueError(f"UE invalide ue_id={ue_id}")
|
||||||
|
dec_ue.record(code)
|
||||||
|
db.session.commit()
|
||||||
|
flash("codes enregistrés")
|
||||||
|
if not semestre_terminal:
|
||||||
|
if request.form.get("autorisation_passage"):
|
||||||
|
if not formsemestre.semestre_id + 1 in (
|
||||||
|
a.semestre_id for a in autorisations_passage
|
||||||
|
):
|
||||||
|
ScolarAutorisationInscription.delete_autorisation_etud(
|
||||||
|
etud.id, formsemestre.id
|
||||||
|
)
|
||||||
|
ScolarAutorisationInscription.autorise_etud(
|
||||||
|
etud.id,
|
||||||
|
formsemestre.formation.formation_code,
|
||||||
|
formsemestre.id,
|
||||||
|
formsemestre.semestre_id + 1,
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
flash(
|
||||||
|
f"""autorisation de passage en S{formsemestre.semestre_id + 1
|
||||||
|
} enregistrée"""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if est_autorise_a_passer:
|
||||||
|
ScolarAutorisationInscription.delete_autorisation_etud(
|
||||||
|
etud.id, formsemestre.id
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
flash(
|
||||||
|
f"autorisation de passage en S{formsemestre.semestre_id + 1} annulée"
|
||||||
|
)
|
||||||
|
return flask.redirect(
|
||||||
|
url_for(
|
||||||
|
"notes.formsemestre_validation_but",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre.id,
|
||||||
|
etudid=etud.id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# GET
|
||||||
|
if formsemestre.semestre_id % 2 == 0:
|
||||||
|
warning = f"""<div class="warning">
|
||||||
|
Cet étudiant de S{formsemestre.semestre_id} ne peut pas passer
|
||||||
|
en jury BUT annuel car il lui manque le semestre précédent.
|
||||||
|
</div>"""
|
||||||
|
else:
|
||||||
|
warning = ""
|
||||||
|
H = [
|
||||||
|
html_sco_header.sco_header(
|
||||||
|
page_title=f"Validation BUT S{formsemestre.semestre_id}",
|
||||||
|
formsemestre_id=formsemestre.id,
|
||||||
|
etudid=etud.id,
|
||||||
|
cssstyles=("css/jury_but.css",),
|
||||||
|
javascripts=("js/jury_but.js",),
|
||||||
|
),
|
||||||
|
f"""
|
||||||
|
<div class="jury_but">
|
||||||
|
<div>
|
||||||
|
<div class="bull_head">
|
||||||
|
<div>
|
||||||
|
<div class="titre_parcours">Jury BUT S{formsemestre.id}
|
||||||
|
- 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>
|
||||||
|
<h3>Jury sur un semestre BUT isolé (ne concerne que les UEs)</h3>
|
||||||
|
{warning}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" id="jury_but">
|
||||||
|
""",
|
||||||
|
]
|
||||||
|
|
||||||
|
erase_span = ""
|
||||||
|
if not read_only:
|
||||||
|
# Requête toutes les validations (pas seulement celles du deca courant),
|
||||||
|
# au cas où: changement d'architecture, saisie en mode classique, ...
|
||||||
|
validations = ScolarFormSemestreValidation.query.filter_by(
|
||||||
|
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(
|
||||||
|
f"""
|
||||||
|
<div class="but_section_annee">
|
||||||
|
</div>
|
||||||
|
<div><b>Unités d'enseignement de S{formsemestre.semestre_id}:</b></div>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
if not ues:
|
||||||
|
H.append(
|
||||||
|
"""<div class="warning">Aucune UE ! Vérifiez votre programme de
|
||||||
|
formation, et l'association UEs / Niveaux de compétences</div>"""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
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(
|
||||||
|
"""<div style=""></div>
|
||||||
|
<div class=""></div>"""
|
||||||
|
)
|
||||||
|
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:
|
||||||
|
H.append(
|
||||||
|
f"""<div class="but_explanation">
|
||||||
|
{"Vous n'avez pas la permission de modifier ces décisions."
|
||||||
|
if formsemestre.etat
|
||||||
|
else "Semestre verrouillé."}
|
||||||
|
Les champs entourés en vert sont enregistrés.
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if formsemestre.semestre_id < formsemestre.formation.get_cursus().NB_SEM:
|
||||||
|
H.append(
|
||||||
|
f"""
|
||||||
|
<div class="but_settings">
|
||||||
|
<input type="checkbox" name="autorisation_passage" value="1" {
|
||||||
|
"checked" if est_autorise_a_passer else ""}>
|
||||||
|
<em>autoriser à passer dans le semestre S{formsemestre.semestre_id+1}</em>
|
||||||
|
</input>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
H.append("""<div class="help">dernier semestre de la formation.</div>""")
|
||||||
|
H.append(
|
||||||
|
f"""
|
||||||
|
<div class="but_buttons">
|
||||||
|
<span><input type="submit" value="Enregistrer ces décisions"></span>
|
||||||
|
<span>{erase_span}</span>
|
||||||
|
</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)
|
||||||
|
|
||||||
|
|
||||||
|
# -------------
|
||||||
def infos_fiche_etud_html(etudid: int) -> str:
|
def infos_fiche_etud_html(etudid: int) -> str:
|
||||||
"""Section html pour fiche etudiant
|
"""Section html pour fiche etudiant
|
||||||
provisoire pour BUT 2022
|
provisoire pour BUT 2022
|
||||||
"""
|
"""
|
||||||
etud: Identite = Identite.query.get_or_404(etudid)
|
etud = Identite.get_etud(etudid)
|
||||||
inscriptions = (
|
inscriptions = (
|
||||||
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
|
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
|
||||||
.filter(
|
.filter(
|
||||||
|
@ -162,11 +536,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.codes_cursus import UE_SPORT
|
||||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
from app.scodoc.codes_cursus import CursusDUT, CursusDUTMono
|
||||||
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>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -649,11 +677,11 @@ class BonusCalais(BonusSportAdditif):
|
||||||
proportion_point = 0.06 # 6%
|
proportion_point = 0.06 # 6%
|
||||||
|
|
||||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||||
parcours = self.formsemestre.formation.get_parcours()
|
parcours = self.formsemestre.formation.get_cursus()
|
||||||
# Variantes de DUT ?
|
# Variantes de DUT ?
|
||||||
if (
|
if (
|
||||||
isinstance(parcours, ParcoursDUT)
|
isinstance(parcours, CursusDUT)
|
||||||
or parcours.TYPE_PARCOURS == ParcoursDUTMono.TYPE_PARCOURS
|
or parcours.TYPE_CURSUS == CursusDUTMono.TYPE_CURSUS
|
||||||
): # DUT
|
): # DUT
|
||||||
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
||||||
else:
|
else:
|
||||||
|
@ -808,7 +836,7 @@ class BonusLaRochelle(BonusSportAdditif):
|
||||||
"""Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle.
|
"""Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle.
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>Si la note de sport est comprise entre 0 et 10 : pas d’ajout de point.</li>
|
<li>Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point.</li>
|
||||||
<li>Si la note de sport est comprise entre 10 et 20 :
|
<li>Si la note de sport est comprise entre 10 et 20 :
|
||||||
<ul>
|
<ul>
|
||||||
<li>Pour le BUT, application pour chaque UE du semestre :
|
<li>Pour le BUT, application pour chaque UE du semestre :
|
||||||
|
@ -868,15 +896,15 @@ class BonusLeHavre(BonusSportAdditif):
|
||||||
<p>
|
<p>
|
||||||
Les enseignements optionnels de langue, préprofessionnalisation,
|
Les enseignements optionnels de langue, préprofessionnalisation,
|
||||||
PIX (compétences numériques), l'entrepreneuriat étudiant, l'engagement
|
PIX (compétences numériques), l'entrepreneuriat étudiant, l'engagement
|
||||||
bénévole au sein d’association dès lors qu’une grille d’évaluation des
|
bénévole au sein d'association dès lors qu'une grille d'évaluation des
|
||||||
compétences existe ainsi que les activités sportives et culturelles
|
compétences existe ainsi que les activités sportives et culturelles
|
||||||
seront traités au niveau semestriel.
|
seront traités au niveau semestriel.
|
||||||
</p><p>
|
</p><p>
|
||||||
Le maximum de bonification qu’un étudiant peut obtenir sur sa moyenne
|
Le maximum de bonification qu'un étudiant peut obtenir sur sa moyenne
|
||||||
est plafonné à 0.5 point.
|
est plafonné à 0.5 point.
|
||||||
</p><p>
|
</p><p>
|
||||||
Lorsqu’un étudiant suit plus de deux matières qui donnent droit à
|
Lorsqu'un étudiant suit plus de deux matières qui donnent droit à
|
||||||
bonification, l’étudiant choisit les deux notes à retenir.
|
bonification, l'étudiant choisit les deux notes à retenir.
|
||||||
</p><p>
|
</p><p>
|
||||||
Les points bonus ne sont acquis que pour une note supérieure à 10/20.
|
Les points bonus ne sont acquis que pour une note supérieure à 10/20.
|
||||||
</p><p>
|
</p><p>
|
||||||
|
@ -885,7 +913,7 @@ class BonusLeHavre(BonusSportAdditif):
|
||||||
Pour chaque matière (max. 2) donnant lieu à bonification :<br>
|
Pour chaque matière (max. 2) donnant lieu à bonification :<br>
|
||||||
|
|
||||||
Bonification = (N-10) x 0,05,
|
Bonification = (N-10) x 0,05,
|
||||||
N étant la note de l’activité sur 20.
|
N étant la note de l'activité sur 20.
|
||||||
</p>
|
</p>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -1097,13 +1125,13 @@ class BonusOrleans(BonusSportAdditif):
|
||||||
"""Calcul bonus modules optionnels (sport, culture), règle IUT d'Orléans
|
"""Calcul bonus modules optionnels (sport, culture), règle IUT d'Orléans
|
||||||
<p><b>Cadre général :</b>
|
<p><b>Cadre général :</b>
|
||||||
En reconnaissance de l'engagement des étudiants dans la vie associative,
|
En reconnaissance de l'engagement des étudiants dans la vie associative,
|
||||||
sociale ou professionnelle, l’IUT d’Orléans accorde, sous conditions,
|
sociale ou professionnelle, l'IUT d'Orléans accorde, sous conditions,
|
||||||
une bonification aux étudiants inscrits qui en font la demande en début
|
une bonification aux étudiants inscrits qui en font la demande en début
|
||||||
d’année universitaire.
|
d'année universitaire.
|
||||||
</p>
|
</p>
|
||||||
<p>Cet engagement doit être régulier et correspondre à une activité réelle
|
<p>Cet engagement doit être régulier et correspondre à une activité réelle
|
||||||
et sérieuse qui bénéficie à toute la communauté étudiante de l’IUT,
|
et sérieuse qui bénéficie à toute la communauté étudiante de l'IUT,
|
||||||
de l’Université ou à l’ensemble de la collectivité.</p>
|
de l'Université ou à l'ensemble de la collectivité.</p>
|
||||||
<p><b>Bonification :</b>
|
<p><b>Bonification :</b>
|
||||||
Pour les DUT et LP, cette bonification interviendra sur la moyenne générale
|
Pour les DUT et LP, cette bonification interviendra sur la moyenne générale
|
||||||
des semestres pairs :
|
des semestres pairs :
|
||||||
|
@ -1169,6 +1197,89 @@ class BonusRoanne(BonusSportAdditif):
|
||||||
proportion_point = 1
|
proportion_point = 1
|
||||||
|
|
||||||
|
|
||||||
|
class BonusSceaux(BonusSportAdditif): # atypique
|
||||||
|
"""IUT de Sceaux
|
||||||
|
|
||||||
|
L’IUT de Sceaux (Université de Paris-Saclay) propose aux étudiants un seul enseignement
|
||||||
|
non rattaché aux UE : l’option Sport.
|
||||||
|
<p>
|
||||||
|
Cette option donne à l’étudiant qui la suit une bonification qui s’applique uniquement
|
||||||
|
si sa note est supérieure à 10.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Cette bonification s’applique sur l’ensemble des UE d’un semestre de la façon suivante :
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<tt>
|
||||||
|
[ (Note – 10) / Nb UE du semestre ] / Total des coefficients de chaque UE
|
||||||
|
</tt>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Exemple : un étudiant qui a obtenu 16/20 à l’option Sport en S1
|
||||||
|
(composé par exemple de 3 UE:UE1.1, UE1.2 et UE1.3)
|
||||||
|
aurait les bonifications suivantes :
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>UE1.1 (Total des coefficients : 15) ⇒ Bonification UE1.1 = <tt>[ (16 – 10) / 3 ] /15
|
||||||
|
</tt>
|
||||||
|
</li>
|
||||||
|
<li>UE1.2 (Total des coefficients : 14) ⇒ Bonification UE1.2 = <tt>[ (16 – 10) / 3 ] /14
|
||||||
|
</tt>
|
||||||
|
</li>
|
||||||
|
<li>UE1.3 (Total des coefficients : 12,5) ⇒ Bonification UE1.3 = <tt>[ (16 – 10) / 3 ] /12,5
|
||||||
|
</tt>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "bonus_iut_sceaux"
|
||||||
|
displayed_name = "IUT de Sceaux"
|
||||||
|
proportion_point = 1.0
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
formsemestre: "FormSemestre",
|
||||||
|
sem_modimpl_moys: np.array,
|
||||||
|
ues: list,
|
||||||
|
modimpl_inscr_df: pd.DataFrame,
|
||||||
|
modimpl_coefs: np.array,
|
||||||
|
etud_moy_gen,
|
||||||
|
etud_moy_ue,
|
||||||
|
):
|
||||||
|
# Pour ce bonus, il faut conserver:
|
||||||
|
# - le nombre d'UEs
|
||||||
|
self.nb_ues = len([ue for ue in ues if ue.type != UE_SPORT])
|
||||||
|
# - le total des coefs de chaque UE
|
||||||
|
# modimpl_coefs : DataFrame, lignes modimpl, col UEs (sans sport)
|
||||||
|
self.sum_coefs_ues = modimpl_coefs.sum() # Series, index ue_id
|
||||||
|
super().__init__(
|
||||||
|
formsemestre,
|
||||||
|
sem_modimpl_moys,
|
||||||
|
ues,
|
||||||
|
modimpl_inscr_df,
|
||||||
|
modimpl_coefs,
|
||||||
|
etud_moy_gen,
|
||||||
|
etud_moy_ue,
|
||||||
|
)
|
||||||
|
|
||||||
|
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||||
|
"""Calcul du bonus IUT de Sceaux 2023
|
||||||
|
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)
|
||||||
|
|
||||||
|
Attention: si la somme des coefs de modules dans une UE est nulle, on a un bonus Inf
|
||||||
|
(moyenne d'UE cappée à 20).
|
||||||
|
"""
|
||||||
|
if (0 in sem_modimpl_moys_inscrits.shape) or (self.nb_ues == 0):
|
||||||
|
# pas d'étudiants ou pas d'UE ou pas de module...
|
||||||
|
return
|
||||||
|
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
||||||
|
if self.bonus_ues is not None:
|
||||||
|
self.bonus_ues = (self.bonus_ues / self.nb_ues) / self.sum_coefs_ues
|
||||||
|
|
||||||
|
|
||||||
class BonusStEtienne(BonusSportAdditif):
|
class BonusStEtienne(BonusSportAdditif):
|
||||||
"""IUT de Saint-Etienne.
|
"""IUT de Saint-Etienne.
|
||||||
|
|
||||||
|
@ -1199,7 +1310,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 +1332,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 +1441,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 +1528,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
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -9,10 +9,10 @@
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import FormSemestre, ScolarFormSemestreValidation, UniteEns
|
from app.models import FormSemestre, Identite, ScolarFormSemestreValidation, UniteEns
|
||||||
from app.comp.res_cache import ResultatsCache
|
from app.comp.res_cache import ResultatsCache
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc import codes_cursus
|
||||||
|
|
||||||
|
|
||||||
class ValidationsSemestre(ResultatsCache):
|
class ValidationsSemestre(ResultatsCache):
|
||||||
|
@ -53,7 +53,7 @@ class ValidationsSemestre(ResultatsCache):
|
||||||
self.comp_decisions_jury()
|
self.comp_decisions_jury()
|
||||||
|
|
||||||
def comp_decisions_jury(self):
|
def comp_decisions_jury(self):
|
||||||
"""Cherche les decisions du jury pour le semestre (pas les UE).
|
"""Cherche les decisions du jury pour le semestre (pas les RCUE).
|
||||||
Calcule les attributs:
|
Calcule les attributs:
|
||||||
decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}
|
decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}
|
||||||
decision_jury_ues={ etudid :
|
decision_jury_ues={ etudid :
|
||||||
|
@ -89,7 +89,7 @@ class ValidationsSemestre(ResultatsCache):
|
||||||
if decision.etudid not in decisions_jury_ues:
|
if decision.etudid not in decisions_jury_ues:
|
||||||
decisions_jury_ues[decision.etudid] = {}
|
decisions_jury_ues[decision.etudid] = {}
|
||||||
# Calcul des ECTS associés à cette UE:
|
# Calcul des ECTS associés à cette UE:
|
||||||
if sco_codes_parcours.code_ue_validant(decision.code) and decision.ue:
|
if codes_cursus.code_ue_validant(decision.code) and decision.ue:
|
||||||
ects = decision.ue.ects or 0.0 # 0 if None
|
ects = decision.ue.ects or 0.0 # 0 if None
|
||||||
else:
|
else:
|
||||||
ects = 0.0
|
ects = 0.0
|
||||||
|
@ -102,6 +102,12 @@ class ValidationsSemestre(ResultatsCache):
|
||||||
|
|
||||||
self.decisions_jury_ues = decisions_jury_ues
|
self.decisions_jury_ues = decisions_jury_ues
|
||||||
|
|
||||||
|
def has_decision(self, etud: Identite) -> bool:
|
||||||
|
"""Vrai si etud a au moins une décision enregistrée depuis
|
||||||
|
ce semestre (quelle qu'elle soit)
|
||||||
|
"""
|
||||||
|
return (etud.id in self.decisions_jury_ues) or (etud.id in self.decisions_jury)
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame:
|
def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame:
|
||||||
"""Liste des UE capitalisées (ADM) utilisables dans ce formsemestre
|
"""Liste des UE capitalisées (ADM) utilisables dans ce formsemestre
|
||||||
|
@ -122,6 +128,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
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import pandas as pd
|
||||||
from app.comp import moy_ue
|
from app.comp import moy_ue
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
|
|
||||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
from app.scodoc.codes_cursus import UE_SPORT
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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_utils as scu
|
|
||||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
from app.scodoc.codes_cursus import UE_SPORT
|
||||||
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
|
||||||
|
|
||||||
|
@ -83,6 +85,8 @@ class ModuleImplResults:
|
||||||
"{ evaluation.id : bool } indique si à prendre en compte ou non."
|
"{ evaluation.id : bool } indique si à prendre en compte ou non."
|
||||||
self.evaluations_etat = {}
|
self.evaluations_etat = {}
|
||||||
"{ evaluation_id: EvaluationEtat }"
|
"{ evaluation_id: EvaluationEtat }"
|
||||||
|
self.etudids_attente = set()
|
||||||
|
"etudids avec au moins une note ATT dans ce module"
|
||||||
self.en_attente = False
|
self.en_attente = False
|
||||||
"Vrai si au moins une évaluation a une note en attente"
|
"Vrai si au moins une évaluation a une note en attente"
|
||||||
#
|
#
|
||||||
|
@ -143,7 +147,6 @@ class ModuleImplResults:
|
||||||
evals_notes = pd.DataFrame(index=self.etudids, dtype=float)
|
evals_notes = pd.DataFrame(index=self.etudids, dtype=float)
|
||||||
self.evaluations_completes = []
|
self.evaluations_completes = []
|
||||||
self.evaluations_completes_dict = {}
|
self.evaluations_completes_dict = {}
|
||||||
self.en_attente = False
|
|
||||||
for evaluation in moduleimpl.evaluations:
|
for evaluation in moduleimpl.evaluations:
|
||||||
eval_df = self._load_evaluation_notes(evaluation)
|
eval_df = self._load_evaluation_notes(evaluation)
|
||||||
# is_complete ssi tous les inscrits (non dem) au semestre ont une note
|
# is_complete ssi tous les inscrits (non dem) au semestre ont une note
|
||||||
|
@ -170,15 +173,20 @@ class ModuleImplResults:
|
||||||
eval_df, how="left", left_index=True, right_index=True
|
eval_df, how="left", left_index=True, right_index=True
|
||||||
)
|
)
|
||||||
# Notes en attente: (ne prend en compte que les inscrits, non démissionnaires)
|
# Notes en attente: (ne prend en compte que les inscrits, non démissionnaires)
|
||||||
nb_att = sum(
|
eval_notes_inscr = evals_notes[str(evaluation.id)][list(inscrits_module)]
|
||||||
evals_notes[str(evaluation.id)][list(inscrits_module)]
|
eval_etudids_attente = set(
|
||||||
== scu.NOTES_ATTENTE
|
eval_notes_inscr.iloc[
|
||||||
|
(eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy()
|
||||||
|
].index
|
||||||
)
|
)
|
||||||
|
self.etudids_attente |= eval_etudids_attente
|
||||||
self.evaluations_etat[evaluation.id] = EvaluationEtat(
|
self.evaluations_etat[evaluation.id] = EvaluationEtat(
|
||||||
evaluation_id=evaluation.id, nb_attente=nb_att, is_complete=is_complete
|
evaluation_id=evaluation.id,
|
||||||
|
nb_attente=len(eval_etudids_attente),
|
||||||
|
is_complete=is_complete,
|
||||||
)
|
)
|
||||||
if nb_att > 0:
|
# au moins une note en ATT dans ce modimpl:
|
||||||
self.en_attente = True
|
self.en_attente = bool(self.etudids_attente)
|
||||||
|
|
||||||
# Force columns names to integers (evaluation ids)
|
# Force columns names to integers (evaluation ids)
|
||||||
evals_notes.columns = pd.Index([int(x) for x in evals_notes.columns], dtype=int)
|
evals_notes.columns = pd.Index([int(x) for x in evals_notes.columns], dtype=int)
|
||||||
|
@ -217,12 +225,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 +251,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 +383,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 +444,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 +453,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 +461,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 +491,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,12 +32,17 @@ 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 codes_cursus
|
||||||
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.codes_cursus import UE_SPORT
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
|
|
||||||
|
|
||||||
|
@ -56,7 +61,7 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
|
||||||
"""
|
"""
|
||||||
ues = (
|
ues = (
|
||||||
UniteEns.query.filter_by(formation_id=formation_id)
|
UniteEns.query.filter_by(formation_id=formation_id)
|
||||||
.filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
|
.filter(UniteEns.type != codes_cursus.UE_SPORT)
|
||||||
.order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme)
|
.order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme)
|
||||||
)
|
)
|
||||||
modules = (
|
modules = (
|
||||||
|
@ -64,14 +69,9 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
|
||||||
.filter(
|
.filter(
|
||||||
(Module.module_type == ModuleType.RESSOURCE)
|
(Module.module_type == ModuleType.RESSOURCE)
|
||||||
| (Module.module_type == ModuleType.SAE)
|
| (Module.module_type == ModuleType.SAE)
|
||||||
| (
|
| ((Module.ue_id == UniteEns.id) & (UniteEns.type == codes_cursus.UE_SPORT))
|
||||||
(Module.ue_id == UniteEns.id)
|
|
||||||
& (UniteEns.type == sco_codes_parcours.UE_SPORT)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.order_by(
|
|
||||||
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
|
|
||||||
)
|
)
|
||||||
|
.order_by(Module.semestre_id, Module.module_type, 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 +140,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 +200,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 +216,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 +231,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 +278,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 +297,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 +319,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 +328,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 +440,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 +471,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,9 +16,11 @@ 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.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
|
from app.scodoc.codes_cursus import UE_SPORT
|
||||||
|
from app.scodoc.sco_utils import ModuleType
|
||||||
|
|
||||||
|
|
||||||
class ResultatsSemestreBUT(NotesTableCompat):
|
class ResultatsSemestreBUT(NotesTableCompat):
|
||||||
|
@ -39,6 +41,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 +74,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 +120,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 +131,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()
|
||||||
|
|
||||||
|
@ -145,6 +160,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||||
# moyenne sur les UE:
|
# moyenne sur les UE:
|
||||||
if len(self.sem_cube[etud_idx, mod_idx]):
|
if len(self.sem_cube[etud_idx, mod_idx]):
|
||||||
return np.nanmean(self.sem_cube[etud_idx, mod_idx])
|
return np.nanmean(self.sem_cube[etud_idx, mod_idx])
|
||||||
|
# note: si toutes les valeurs sont nan, on va déclencher ici
|
||||||
|
# un RuntimeWarning: Mean of empty slice
|
||||||
return np.nan
|
return np.nan
|
||||||
|
|
||||||
def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float:
|
def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float:
|
||||||
|
@ -172,9 +189,15 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||||
modimpls = [
|
modimpls = [
|
||||||
modimpl
|
modimpl
|
||||||
for modimpl in self.formsemestre.modimpls_sorted
|
for modimpl in self.formsemestre.modimpls_sorted
|
||||||
if modimpl.module.ue.type != UE_SPORT
|
if (
|
||||||
and (coefs[modimpl.id][ue.id] != 0)
|
modimpl.module.ue.type != UE_SPORT
|
||||||
and self.modimpl_inscr_df[modimpl.id][etudid]
|
and (coefs[modimpl.id][ue.id] != 0)
|
||||||
|
and self.modimpl_inscr_df[modimpl.id][etudid]
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
modimpl.module.module_type == ModuleType.MALUS
|
||||||
|
and modimpl.module.ue_id == ue.id
|
||||||
|
)
|
||||||
]
|
]
|
||||||
if not with_bonus:
|
if not with_bonus:
|
||||||
return [
|
return [
|
||||||
|
@ -204,27 +227,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
|
||||||
|
)
|
||||||
|
# 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]:
|
||||||
|
@ -233,3 +262,18 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||||
"""
|
"""
|
||||||
s = self.ues_inscr_parcours_df.loc[etudid]
|
s = self.ues_inscr_parcours_df.loc[etudid]
|
||||||
return s.index[s.notna()]
|
return s.index[s.notna()]
|
||||||
|
|
||||||
|
def etud_has_decision(self, etudid):
|
||||||
|
"""True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
|
||||||
|
prend aussi en compte les autorisations de passage.
|
||||||
|
Sous-classée en BUT pour les RCUEs et années.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
super().etud_has_decision(etudid)
|
||||||
|
or ApcValidationAnnee.query.filter_by(
|
||||||
|
formsemestre_id=self.formsemestre.id, etudid=etudid
|
||||||
|
).count()
|
||||||
|
or ApcValidationRCUE.query.filter_by(
|
||||||
|
formsemestre_id=self.formsemestre.id, etudid=etudid
|
||||||
|
).count()
|
||||||
|
)
|
||||||
|
|
|
@ -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,7 +22,7 @@ from app.models import ScoDocSiteConfig
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
from app.scodoc.codes_cursus import UE_SPORT
|
||||||
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_preferences
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
|
@ -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(
|
||||||
|
@ -229,7 +230,7 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
||||||
f"""* oups: calcul coef UE impossible\nformsemestre_id='{self.formsemestre.id
|
f"""* oups: calcul coef UE impossible\nformsemestre_id='{self.formsemestre.id
|
||||||
}'\netudid='{etudid}'\nue={ue}"""
|
}'\netudid='{etudid}'\nue={ue}"""
|
||||||
)
|
)
|
||||||
etud: Identite = Identite.query.get(etudid)
|
etud = Identite.get_etud(etudid)
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
f"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée {ue.acronyme}
|
f"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée {ue.acronyme}
|
||||||
impossible à déterminer pour l'étudiant <a href="{
|
impossible à déterminer pour l'étudiant <a href="{
|
||||||
|
|
|
@ -1,13 +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
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""Résultats semestre: méthodes communes aux formations classiques et APC
|
"""Résultats semestre: méthodes communes aux formations classiques et APC
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections import Counter
|
from collections import Counter, defaultdict
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
@ -15,7 +15,6 @@ import pandas as pd
|
||||||
|
|
||||||
from flask import g, url_for
|
from flask import g, url_for
|
||||||
|
|
||||||
from app.auth.models import User
|
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_cache import ResultatsCache
|
from app.comp.res_cache import ResultatsCache
|
||||||
from app.comp.jury import ValidationsSemestre
|
from app.comp.jury import ValidationsSemestre
|
||||||
|
@ -23,12 +22,11 @@ from app.comp.moy_mod import ModuleImplResults
|
||||||
from app.models import FormSemestre, FormSemestreUECoef
|
from app.models import FormSemestre, FormSemestreUECoef
|
||||||
from app.models import Identite
|
from app.models import Identite
|
||||||
from app.models import ModuleImpl, ModuleImplInscription
|
from app.models import ModuleImpl, ModuleImplInscription
|
||||||
|
from app.models import ScolarAutorisationInscription
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
from app.scodoc.sco_cache import ResultatsSemestreCache
|
from app.scodoc.sco_cache import ResultatsSemestreCache
|
||||||
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF, DEM
|
from app.scodoc.codes_cursus import UE_SPORT
|
||||||
from app.scodoc import sco_evaluation_db
|
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc import sco_groups
|
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
# Il faut bien distinguer
|
# Il faut bien distinguer
|
||||||
|
@ -48,24 +46,27 @@ 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",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, formsemestre: FormSemestre):
|
def __init__(self, formsemestre: FormSemestre):
|
||||||
super().__init__(formsemestre, ResultatsSemestreCache)
|
super().__init__(formsemestre, ResultatsSemestreCache)
|
||||||
# BUT ou standard ? (apc == "approche par compétences")
|
# BUT ou standard ? (apc == "approche par compétences")
|
||||||
self.is_apc = formsemestre.formation.is_apc()
|
self.is_apc: bool = formsemestre.formation.is_apc()
|
||||||
# Attributs "virtuels", définis dans les sous-classes
|
# Attributs "virtuels", définis dans les sous-classes
|
||||||
self.bonus: pd.Series = None # virtuel
|
self.bonus: pd.Series = None # virtuel
|
||||||
"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"
|
||||||
|
@ -83,6 +84,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||||
"""Coefs APC: rows = UEs (sans bonus), columns = modimpl, value = coef."""
|
"""Coefs APC: rows = UEs (sans bonus), columns = modimpl, value = coef."""
|
||||||
|
|
||||||
self.validations = None
|
self.validations = None
|
||||||
|
self.autorisations_inscription = None
|
||||||
self.moyennes_matieres = {}
|
self.moyennes_matieres = {}
|
||||||
"""Moyennes de matières, si calculées. { matiere_id : Series, index etudid }"""
|
"""Moyennes de matières, si calculées. { matiere_id : Series, index etudid }"""
|
||||||
|
|
||||||
|
@ -123,7 +125,8 @@ class ResultatsSemestre(ResultatsCache):
|
||||||
return [ue.id for ue in self.ues if ue.type != UE_SPORT]
|
return [ue.id for ue in self.ues if ue.type != UE_SPORT]
|
||||||
|
|
||||||
def etud_ues(self, etudid: int) -> Generator[UniteEns]:
|
def etud_ues(self, etudid: int) -> Generator[UniteEns]:
|
||||||
"""Liste des UE auxquelles l'étudiant est inscrit."""
|
"""Liste des UE auxquelles l'étudiant est inscrit
|
||||||
|
(sans bonus, en BUT prend en compte le parcours de l'étudiant)."""
|
||||||
return (UniteEns.query.get(ue_id) for ue_id in self.etud_ues_ids(etudid))
|
return (UniteEns.query.get(ue_id) for ue_id in self.etud_ues_ids(etudid))
|
||||||
|
|
||||||
def etud_ects_tot_sem(self, etudid: int) -> float:
|
def etud_ects_tot_sem(self, etudid: int) -> float:
|
||||||
|
@ -171,13 +174,35 @@ class ResultatsSemestre(ResultatsCache):
|
||||||
if m.module.module_type == scu.ModuleType.SAE
|
if m.module.module_type == scu.ModuleType.SAE
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_etudids_attente(self) -> set[int]:
|
||||||
|
"""L'ensemble des etudids ayant au moins une note en ATTente"""
|
||||||
|
return set().union(
|
||||||
|
*[mr.etudids_attente for mr in self.modimpls_results.values()]
|
||||||
|
)
|
||||||
|
|
||||||
# --- JURY...
|
# --- JURY...
|
||||||
def load_validations(self) -> ValidationsSemestre:
|
def get_formsemestre_validations(self) -> ValidationsSemestre:
|
||||||
"""Load validations, set attribute and return value"""
|
"""Load validations if not already stored, set attribute and return value"""
|
||||||
if not self.validations:
|
if not self.validations:
|
||||||
self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
|
self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
|
||||||
return self.validations
|
return self.validations
|
||||||
|
|
||||||
|
def get_autorisations_inscription(self) -> dict[int : list[int]]:
|
||||||
|
"""Les autorisations d'inscription venant de ce formsemestre.
|
||||||
|
Lit en base et cache le résultat.
|
||||||
|
Resultat: { etudid : [ indices de semestres ]}
|
||||||
|
Note: les etudids peuvent ne plus être inscrits ici.
|
||||||
|
Seuls ceux avec des autorisations enregistrées sont présents dans le résultat.
|
||||||
|
"""
|
||||||
|
if not self.autorisations_inscription:
|
||||||
|
autorisations = ScolarAutorisationInscription.query.filter_by(
|
||||||
|
origin_formsemestre_id=self.formsemestre.id
|
||||||
|
)
|
||||||
|
self.autorisations_inscription = defaultdict(list)
|
||||||
|
for aut in autorisations:
|
||||||
|
self.autorisations_inscription[aut.etudid].append(aut.semestre_id)
|
||||||
|
return self.autorisations_inscription
|
||||||
|
|
||||||
def get_etud_ue_validables(self, etudid: int) -> list[UniteEns]:
|
def get_etud_ue_validables(self, etudid: int) -> list[UniteEns]:
|
||||||
"""Liste des UEs du semestre qui doivent être validées
|
"""Liste des UEs du semestre qui doivent être validées
|
||||||
|
|
||||||
|
@ -235,8 +260,8 @@ class ResultatsSemestre(ResultatsCache):
|
||||||
UE capitalisées.
|
UE capitalisées.
|
||||||
"""
|
"""
|
||||||
# Supposant qu'il y a peu d'UE capitalisées,
|
# Supposant qu'il y a peu d'UE capitalisées,
|
||||||
# on recalcule les moyennes gen des etuds ayant des UE capitalisée.
|
# on recalcule les moyennes gen des etuds ayant des UEs capitalisées.
|
||||||
self.load_validations()
|
self.get_formsemestre_validations()
|
||||||
ue_capitalisees = self.validations.ue_capitalisees
|
ue_capitalisees = self.validations.ue_capitalisees
|
||||||
for etudid in ue_capitalisees.index:
|
for etudid in ue_capitalisees.index:
|
||||||
recompute_mg = False
|
recompute_mg = False
|
||||||
|
@ -274,7 +299,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||||
|
|
||||||
def get_etud_etat(self, etudid: int) -> str:
|
def get_etud_etat(self, etudid: int) -> str:
|
||||||
"Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)"
|
"Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)"
|
||||||
ins = self.formsemestre.etuds_inscriptions.get(etudid, None)
|
ins = self.formsemestre.etuds_inscriptions.get(etudid)
|
||||||
if ins is None:
|
if ins is None:
|
||||||
return ""
|
return ""
|
||||||
return ins.etat
|
return ins.etat
|
||||||
|
@ -316,7 +341,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,
|
||||||
|
@ -363,7 +388,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||||
ue_capitalized = UniteEns.query.get(ue_cap["ue_id"])
|
ue_capitalized = UniteEns.query.get(ue_cap["ue_id"])
|
||||||
coef_ue = ue_capitalized.ects
|
coef_ue = ue_capitalized.ects
|
||||||
if coef_ue is None:
|
if coef_ue is None:
|
||||||
orig_sem = FormSemestre.query.get(ue_cap["formsemestre_id"])
|
orig_sem = FormSemestre.get_formsemestre(ue_cap["formsemestre_id"])
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
f"""L'UE capitalisée {ue_capitalized.acronyme}
|
f"""L'UE capitalisée {ue_capitalized.acronyme}
|
||||||
du semestre {orig_sem.titre_annee()}
|
du semestre {orig_sem.titre_annee()}
|
||||||
|
@ -428,588 +453,3 @@ class ResultatsSemestre(ResultatsCache):
|
||||||
# ici si l'étudiant est inscrit dans le semestre courant,
|
# ici si l'étudiant est inscrit dans le semestre courant,
|
||||||
# somme des coefs des modules de l'UE auxquels il est inscrit
|
# somme des coefs des modules de l'UE auxquels il est inscrit
|
||||||
return self.compute_etud_ue_coef(etudid, ue)
|
return self.compute_etud_ue_coef(etudid, ue)
|
||||||
|
|
||||||
# --- TABLEAU RECAP
|
|
||||||
|
|
||||||
def get_table_recap(
|
|
||||||
self,
|
|
||||||
convert_values=False,
|
|
||||||
include_evaluations=False,
|
|
||||||
mode_jury=False,
|
|
||||||
allow_html=True,
|
|
||||||
):
|
|
||||||
"""Table récap. des résultats.
|
|
||||||
allow_html: si vri, peut-mettre du HTML dans les valeurs
|
|
||||||
|
|
||||||
Result: tuple avec
|
|
||||||
- rows: liste de dicts { column_id : value }
|
|
||||||
- titles: { column_id : title }
|
|
||||||
- columns_ids: (liste des id de colonnes)
|
|
||||||
|
|
||||||
Si convert_values, transforme les notes en chaines ("12.34").
|
|
||||||
Les colonnes générées sont:
|
|
||||||
etudid
|
|
||||||
rang : rang indicatif (basé sur moy gen)
|
|
||||||
moy_gen : moy gen indicative
|
|
||||||
moy_ue_<ue_id>, ..., les moyennes d'UE
|
|
||||||
moy_res_<modimpl_id>_<ue_id>, ... les moyennes de ressources dans l'UE
|
|
||||||
moy_sae_<modimpl_id>_<ue_id>, ... les moyennes de SAE dans l'UE
|
|
||||||
|
|
||||||
On ajoute aussi des attributs:
|
|
||||||
- pour les lignes:
|
|
||||||
_css_row_class (inutilisé pour le monent)
|
|
||||||
_<column_id>_class classe css:
|
|
||||||
- la moyenne générale a la classe col_moy_gen
|
|
||||||
- les colonnes SAE ont la classe col_sae
|
|
||||||
- les colonnes Resources ont la classe col_res
|
|
||||||
- les colonnes d'UE ont la classe col_ue
|
|
||||||
- les colonnes de modules (SAE ou res.) d'une UE ont la classe mod_ue_<ue_id>
|
|
||||||
_<column_id>_order : clé de tri
|
|
||||||
"""
|
|
||||||
if convert_values:
|
|
||||||
fmt_note = scu.fmt_note
|
|
||||||
else:
|
|
||||||
fmt_note = lambda x: x
|
|
||||||
|
|
||||||
parcours = self.formsemestre.formation.get_parcours()
|
|
||||||
barre_moy = parcours.BARRE_MOY - scu.NOTES_TOLERANCE
|
|
||||||
barre_valid_ue = parcours.NOTES_BARRE_VALID_UE
|
|
||||||
barre_warning_ue = parcours.BARRE_UE_DISPLAY_WARNING
|
|
||||||
NO_NOTE = "-" # contenu des cellules sans notes
|
|
||||||
rows = []
|
|
||||||
# column_id : title
|
|
||||||
titles = {}
|
|
||||||
# les titres en footer: les mêmes, mais avec des bulles et liens:
|
|
||||||
titles_bot = {}
|
|
||||||
dict_nom_res = {} # cache uid : nomcomplet
|
|
||||||
|
|
||||||
def add_cell(
|
|
||||||
row: dict,
|
|
||||||
col_id: str,
|
|
||||||
title: str,
|
|
||||||
content: str,
|
|
||||||
classes: str = "",
|
|
||||||
idx: int = 100,
|
|
||||||
):
|
|
||||||
"Add a row to our table. classes is a list of css class names"
|
|
||||||
row[col_id] = content
|
|
||||||
if classes:
|
|
||||||
row[f"_{col_id}_class"] = classes + f" c{idx}"
|
|
||||||
if not col_id in titles:
|
|
||||||
titles[col_id] = title
|
|
||||||
titles[f"_{col_id}_col_order"] = idx
|
|
||||||
if classes:
|
|
||||||
titles[f"_{col_id}_class"] = classes
|
|
||||||
return idx + 1
|
|
||||||
|
|
||||||
etuds_inscriptions = self.formsemestre.etuds_inscriptions
|
|
||||||
ues = self.formsemestre.query_ues(with_sport=True) # avec bonus
|
|
||||||
ues_sans_bonus = [ue for ue in ues if ue.type != UE_SPORT]
|
|
||||||
modimpl_ids = set() # modimpl effectivement présents dans la table
|
|
||||||
for etudid in etuds_inscriptions:
|
|
||||||
idx = 0 # index de la colonne
|
|
||||||
etud = Identite.query.get(etudid)
|
|
||||||
row = {"etudid": etudid}
|
|
||||||
# --- Codes (seront cachés, mais exportés en excel)
|
|
||||||
idx = add_cell(row, "etudid", "etudid", etudid, "codes", idx)
|
|
||||||
idx = add_cell(
|
|
||||||
row, "code_nip", "code_nip", etud.code_nip or "", "codes", idx
|
|
||||||
)
|
|
||||||
# --- Rang
|
|
||||||
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}"
|
|
||||||
# --- Identité étudiant
|
|
||||||
idx = add_cell(
|
|
||||||
row, "civilite_str", "Civ.", etud.civilite_str, "identite_detail", idx
|
|
||||||
)
|
|
||||||
idx = add_cell(
|
|
||||||
row, "nom_disp", "Nom", etud.nom_disp(), "identite_detail", idx
|
|
||||||
)
|
|
||||||
row["_nom_disp_order"] = etud.sort_key
|
|
||||||
idx = add_cell(row, "prenom", "Prénom", etud.prenom, "identite_detail", idx)
|
|
||||||
idx = add_cell(
|
|
||||||
row, "nom_short", "Nom", etud.nom_short, "identite_court", idx
|
|
||||||
)
|
|
||||||
row["_nom_short_order"] = etud.sort_key
|
|
||||||
row["_nom_short_target"] = url_for(
|
|
||||||
"notes.formsemestre_bulletinetud",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
formsemestre_id=self.formsemestre.id,
|
|
||||||
etudid=etudid,
|
|
||||||
)
|
|
||||||
row["_nom_short_target_attrs"] = f'class="etudinfo" id="{etudid}"'
|
|
||||||
row["_nom_disp_target"] = row["_nom_short_target"]
|
|
||||||
row["_nom_disp_target_attrs"] = row["_nom_short_target_attrs"]
|
|
||||||
|
|
||||||
idx = 30 # début des colonnes de notes
|
|
||||||
# --- Moyenne générale
|
|
||||||
moy_gen = self.etud_moy_gen.get(etudid, False)
|
|
||||||
note_class = ""
|
|
||||||
if moy_gen is False:
|
|
||||||
moy_gen = NO_NOTE
|
|
||||||
elif isinstance(moy_gen, float) and moy_gen < barre_moy:
|
|
||||||
note_class = " moy_ue_warning" # en rouge
|
|
||||||
idx = add_cell(
|
|
||||||
row,
|
|
||||||
"moy_gen",
|
|
||||||
"Moy",
|
|
||||||
fmt_note(moy_gen),
|
|
||||||
"col_moy_gen" + note_class,
|
|
||||||
idx,
|
|
||||||
)
|
|
||||||
titles_bot["_moy_gen_target_attrs"] = (
|
|
||||||
'title="moyenne indicative"' if self.is_apc else ""
|
|
||||||
)
|
|
||||||
# --- Moyenne d'UE
|
|
||||||
nb_ues_validables, nb_ues_warning = 0, 0
|
|
||||||
for ue in ues_sans_bonus:
|
|
||||||
ue_status = self.get_etud_ue_status(etudid, ue.id)
|
|
||||||
if ue_status is not None:
|
|
||||||
col_id = f"moy_ue_{ue.id}"
|
|
||||||
val = ue_status["moy"]
|
|
||||||
note_class = ""
|
|
||||||
if isinstance(val, float):
|
|
||||||
if val < barre_moy:
|
|
||||||
note_class = " moy_inf"
|
|
||||||
elif val >= barre_valid_ue:
|
|
||||||
note_class = " moy_ue_valid"
|
|
||||||
nb_ues_validables += 1
|
|
||||||
if val < barre_warning_ue:
|
|
||||||
note_class = " moy_ue_warning" # notes très basses
|
|
||||||
nb_ues_warning += 1
|
|
||||||
idx = add_cell(
|
|
||||||
row,
|
|
||||||
col_id,
|
|
||||||
ue.acronyme,
|
|
||||||
fmt_note(val),
|
|
||||||
"col_ue" + note_class,
|
|
||||||
idx,
|
|
||||||
)
|
|
||||||
titles_bot[
|
|
||||||
f"_{col_id}_target_attrs"
|
|
||||||
] = f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """
|
|
||||||
if mode_jury:
|
|
||||||
# pas d'autre colonnes de résultats
|
|
||||||
continue
|
|
||||||
# Bonus (sport) dans cette UE ?
|
|
||||||
# Le bonus sport appliqué sur cette UE
|
|
||||||
if (self.bonus_ues is not None) and (ue.id in self.bonus_ues):
|
|
||||||
val = self.bonus_ues[ue.id][etud.id] or ""
|
|
||||||
val_fmt = val_fmt_html = fmt_note(val)
|
|
||||||
if val:
|
|
||||||
val_fmt_html = f'<span class="green-arrow-up"></span><span class="sp2l">{val_fmt}</span>'
|
|
||||||
idx = add_cell(
|
|
||||||
row,
|
|
||||||
f"bonus_ue_{ue.id}",
|
|
||||||
f"Bonus {ue.acronyme}",
|
|
||||||
val_fmt_html if allow_html else val_fmt,
|
|
||||||
"col_ue_bonus",
|
|
||||||
idx,
|
|
||||||
)
|
|
||||||
row[f"_bonus_ue_{ue.id}_xls"] = val_fmt
|
|
||||||
# Les moyennes des modules (ou ressources et SAÉs) dans cette UE
|
|
||||||
idx_malus = idx # place pour colonne malus à gauche des modules
|
|
||||||
idx += 1
|
|
||||||
for modimpl in self.modimpls_in_ue(ue, etudid, with_bonus=False):
|
|
||||||
if ue_status["is_capitalized"]:
|
|
||||||
val = "-c-"
|
|
||||||
else:
|
|
||||||
modimpl_results = self.modimpls_results.get(modimpl.id)
|
|
||||||
if modimpl_results: # pas bonus
|
|
||||||
if self.is_apc: # BUT
|
|
||||||
moys_vers_ue = modimpl_results.etuds_moy_module.get(
|
|
||||||
ue.id
|
|
||||||
)
|
|
||||||
val = (
|
|
||||||
moys_vers_ue.get(etudid, "?")
|
|
||||||
if moys_vers_ue is not None
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
else: # classique: Series indépendante de l'UE
|
|
||||||
val = modimpl_results.etuds_moy_module.get(
|
|
||||||
etudid, "?"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
val = ""
|
|
||||||
|
|
||||||
col_id = (
|
|
||||||
f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
|
|
||||||
)
|
|
||||||
val_fmt = val_fmt_html = fmt_note(val)
|
|
||||||
if convert_values and (
|
|
||||||
modimpl.module.module_type == scu.ModuleType.MALUS
|
|
||||||
):
|
|
||||||
val_fmt_html = (
|
|
||||||
(scu.EMO_RED_TRIANGLE_DOWN + val_fmt) if val else ""
|
|
||||||
)
|
|
||||||
idx = add_cell(
|
|
||||||
row,
|
|
||||||
col_id,
|
|
||||||
modimpl.module.code,
|
|
||||||
val_fmt_html,
|
|
||||||
# class col_res mod_ue_123
|
|
||||||
f"col_{modimpl.module.type_abbrv()} mod_ue_{ue.id}",
|
|
||||||
idx,
|
|
||||||
)
|
|
||||||
row[f"_{col_id}_xls"] = val_fmt
|
|
||||||
if modimpl.module.module_type == scu.ModuleType.MALUS:
|
|
||||||
titles[f"_{col_id}_col_order"] = idx_malus
|
|
||||||
titles_bot[f"_{col_id}_target"] = url_for(
|
|
||||||
"notes.moduleimpl_status",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
moduleimpl_id=modimpl.id,
|
|
||||||
)
|
|
||||||
nom_resp = dict_nom_res.get(modimpl.responsable_id)
|
|
||||||
if nom_resp is None:
|
|
||||||
user = User.query.get(modimpl.responsable_id)
|
|
||||||
nom_resp = user.get_nomcomplet() if user else ""
|
|
||||||
dict_nom_res[modimpl.responsable_id] = nom_resp
|
|
||||||
titles_bot[
|
|
||||||
f"_{col_id}_target_attrs"
|
|
||||||
] = f""" title="{modimpl.module.titre} ({nom_resp})" """
|
|
||||||
modimpl_ids.add(modimpl.id)
|
|
||||||
nb_ues_etud_parcours = len(self.etud_ues_ids(etudid))
|
|
||||||
ue_valid_txt = (
|
|
||||||
ue_valid_txt_html
|
|
||||||
) = f"{nb_ues_validables}/{nb_ues_etud_parcours}"
|
|
||||||
if nb_ues_warning:
|
|
||||||
ue_valid_txt_html += " " + scu.EMO_WARNING
|
|
||||||
add_cell(
|
|
||||||
row,
|
|
||||||
"ues_validables",
|
|
||||||
"UEs",
|
|
||||||
ue_valid_txt_html,
|
|
||||||
"col_ue col_ues_validables",
|
|
||||||
29, # juste avant moy. gen.
|
|
||||||
)
|
|
||||||
row["_ues_validables_xls"] = ue_valid_txt
|
|
||||||
if nb_ues_warning:
|
|
||||||
row["_ues_validables_class"] += " moy_ue_warning"
|
|
||||||
elif nb_ues_validables < len(ues_sans_bonus):
|
|
||||||
row["_ues_validables_class"] += " moy_inf"
|
|
||||||
row["_ues_validables_order"] = nb_ues_validables # pour tri
|
|
||||||
if mode_jury and self.validations:
|
|
||||||
if self.is_apc:
|
|
||||||
# formations BUT: pas de code semestre, concatene ceux des UE
|
|
||||||
dec_ues = self.validations.decisions_jury_ues.get(etudid)
|
|
||||||
if dec_ues:
|
|
||||||
jury_code_sem = ",".join(
|
|
||||||
[dec_ues[ue_id].get("code", "") for ue_id in dec_ues]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
jury_code_sem = ""
|
|
||||||
else:
|
|
||||||
# formations classiqes: code semestre
|
|
||||||
dec_sem = self.validations.decisions_jury.get(etudid)
|
|
||||||
jury_code_sem = dec_sem["code"] if dec_sem else ""
|
|
||||||
idx = add_cell(
|
|
||||||
row,
|
|
||||||
"jury_code_sem",
|
|
||||||
"Jury",
|
|
||||||
jury_code_sem,
|
|
||||||
"jury_code_sem",
|
|
||||||
1000,
|
|
||||||
)
|
|
||||||
idx = add_cell(
|
|
||||||
row,
|
|
||||||
"jury_link",
|
|
||||||
"",
|
|
||||||
f"""<a href="{url_for('notes.formsemestre_validation_etud_form',
|
|
||||||
scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre.id, etudid=etudid
|
|
||||||
)
|
|
||||||
}">{"saisir" if not jury_code_sem else "modifier"} décision</a>""",
|
|
||||||
"col_jury_link",
|
|
||||||
idx,
|
|
||||||
)
|
|
||||||
rows.append(row)
|
|
||||||
|
|
||||||
self.recap_add_partitions(rows, titles)
|
|
||||||
self._recap_add_admissions(rows, titles)
|
|
||||||
|
|
||||||
# tri par rang croissant
|
|
||||||
rows.sort(key=lambda e: e["_rang_order"])
|
|
||||||
|
|
||||||
# INFOS POUR FOOTER
|
|
||||||
bottom_infos = self._recap_bottom_infos(ues_sans_bonus, modimpl_ids, fmt_note)
|
|
||||||
if include_evaluations:
|
|
||||||
self._recap_add_evaluations(rows, titles, bottom_infos)
|
|
||||||
|
|
||||||
# Ajoute style "col_empty" aux colonnes de modules vides
|
|
||||||
for col_id in titles:
|
|
||||||
c_class = f"_{col_id}_class"
|
|
||||||
if "col_empty" in bottom_infos["moy"].get(c_class, ""):
|
|
||||||
for row in rows:
|
|
||||||
row[c_class] = row.get(c_class, "") + " col_empty"
|
|
||||||
titles[c_class] += " col_empty"
|
|
||||||
for row in bottom_infos.values():
|
|
||||||
row[c_class] = row.get(c_class, "") + " col_empty"
|
|
||||||
|
|
||||||
# --- TABLE FOOTER: ECTS, moyennes, min, max...
|
|
||||||
footer_rows = []
|
|
||||||
for (bottom_line, row) in bottom_infos.items():
|
|
||||||
# Cases vides à styler:
|
|
||||||
row["moy_gen"] = row.get("moy_gen", "")
|
|
||||||
row["_moy_gen_class"] = "col_moy_gen"
|
|
||||||
# titre de la ligne:
|
|
||||||
row["prenom"] = row["nom_short"] = (
|
|
||||||
row.get("_title", "") or bottom_line.capitalize()
|
|
||||||
)
|
|
||||||
row["_tr_class"] = bottom_line.lower() + (
|
|
||||||
(" " + row["_tr_class"]) if "_tr_class" in row else ""
|
|
||||||
)
|
|
||||||
footer_rows.append(row)
|
|
||||||
titles_bot.update(titles)
|
|
||||||
footer_rows.append(titles_bot)
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
return (rows, footer_rows, titles, column_ids)
|
|
||||||
|
|
||||||
def _recap_bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict:
|
|
||||||
"""Les informations à mettre en bas de la table: min, max, moy, ECTS"""
|
|
||||||
row_min, row_max, row_moy, row_coef, row_ects, row_apo = (
|
|
||||||
{"_tr_class": "bottom_info", "_title": "Min."},
|
|
||||||
{"_tr_class": "bottom_info"},
|
|
||||||
{"_tr_class": "bottom_info"},
|
|
||||||
{"_tr_class": "bottom_info"},
|
|
||||||
{"_tr_class": "bottom_info"},
|
|
||||||
{"_tr_class": "bottom_info", "_title": "Code Apogée"},
|
|
||||||
)
|
|
||||||
# --- ECTS
|
|
||||||
for ue in ues:
|
|
||||||
colid = f"moy_ue_{ue.id}"
|
|
||||||
row_ects[colid] = ue.ects
|
|
||||||
row_ects[f"_{colid}_class"] = "col_ue"
|
|
||||||
# style cases vides pour borders verticales
|
|
||||||
row_coef[colid] = ""
|
|
||||||
row_coef[f"_{colid}_class"] = "col_ue"
|
|
||||||
# row_apo[colid] = ue.code_apogee or ""
|
|
||||||
row_ects["moy_gen"] = sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT])
|
|
||||||
row_ects["_moy_gen_class"] = "col_moy_gen"
|
|
||||||
|
|
||||||
# --- MIN, MAX, MOY, APO
|
|
||||||
|
|
||||||
row_min["moy_gen"] = fmt_note(self.etud_moy_gen.min())
|
|
||||||
row_max["moy_gen"] = fmt_note(self.etud_moy_gen.max())
|
|
||||||
row_moy["moy_gen"] = fmt_note(self.etud_moy_gen.mean())
|
|
||||||
for ue in ues:
|
|
||||||
colid = f"moy_ue_{ue.id}"
|
|
||||||
row_min[colid] = fmt_note(self.etud_moy_ue[ue.id].min())
|
|
||||||
row_max[colid] = fmt_note(self.etud_moy_ue[ue.id].max())
|
|
||||||
row_moy[colid] = fmt_note(self.etud_moy_ue[ue.id].mean())
|
|
||||||
row_min[f"_{colid}_class"] = "col_ue"
|
|
||||||
row_max[f"_{colid}_class"] = "col_ue"
|
|
||||||
row_moy[f"_{colid}_class"] = "col_ue"
|
|
||||||
row_apo[colid] = ue.code_apogee or ""
|
|
||||||
|
|
||||||
for modimpl in self.formsemestre.modimpls_sorted:
|
|
||||||
if modimpl.id in modimpl_ids:
|
|
||||||
colid = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
|
|
||||||
if self.is_apc:
|
|
||||||
coef = self.modimpl_coefs_df[modimpl.id][ue.id]
|
|
||||||
else:
|
|
||||||
coef = modimpl.module.coefficient or 0
|
|
||||||
row_coef[colid] = fmt_note(coef)
|
|
||||||
notes = self.modimpl_notes(modimpl.id, ue.id)
|
|
||||||
if np.isnan(notes).all():
|
|
||||||
# aucune note valide
|
|
||||||
row_min[colid] = np.nan
|
|
||||||
row_max[colid] = np.nan
|
|
||||||
moy = np.nan
|
|
||||||
else:
|
|
||||||
row_min[colid] = fmt_note(np.nanmin(notes))
|
|
||||||
row_max[colid] = fmt_note(np.nanmax(notes))
|
|
||||||
moy = np.nanmean(notes)
|
|
||||||
row_moy[colid] = fmt_note(moy)
|
|
||||||
if np.isnan(moy):
|
|
||||||
# aucune note dans ce module
|
|
||||||
row_moy[f"_{colid}_class"] = "col_empty"
|
|
||||||
row_apo[colid] = modimpl.module.code_apogee or ""
|
|
||||||
|
|
||||||
return { # { key : row } avec key = min, max, moy, coef
|
|
||||||
"min": row_min,
|
|
||||||
"max": row_max,
|
|
||||||
"moy": row_moy,
|
|
||||||
"coef": row_coef,
|
|
||||||
"ects": row_ects,
|
|
||||||
"apo": row_apo,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _recap_etud_groups_infos(
|
|
||||||
self, etudid: int, row: dict, titles: dict
|
|
||||||
): # XXX non utilisé
|
|
||||||
"""Table recap: ajoute à row les colonnes sur les groupes pour cet etud"""
|
|
||||||
# dec = self.get_etud_decision_sem(etudid)
|
|
||||||
# if dec:
|
|
||||||
# codes_nb[dec["code"]] += 1
|
|
||||||
row_class = ""
|
|
||||||
etud_etat = self.get_etud_etat(etudid)
|
|
||||||
if etud_etat == DEM:
|
|
||||||
gr_name = "Dém."
|
|
||||||
row_class = "dem"
|
|
||||||
elif etud_etat == DEF:
|
|
||||||
gr_name = "Déf."
|
|
||||||
row_class = "def"
|
|
||||||
else:
|
|
||||||
# XXX probablement à revoir pour utiliser données cachées,
|
|
||||||
# via get_etud_groups_in_partition ou autre
|
|
||||||
group = sco_groups.get_etud_main_group(etudid, self.formsemestre.id)
|
|
||||||
gr_name = group["group_name"] or ""
|
|
||||||
row["group"] = gr_name
|
|
||||||
row["_group_class"] = "group"
|
|
||||||
if row_class:
|
|
||||||
row["_tr_class"] = " ".join([row.get("_tr_class", ""), row_class])
|
|
||||||
titles["group"] = "Gr"
|
|
||||||
|
|
||||||
def _recap_add_admissions(self, rows: list[dict], titles: dict):
|
|
||||||
"""Ajoute les colonnes "admission"
|
|
||||||
rows est une liste de dict avec une clé "etudid"
|
|
||||||
Les colonnes ont la classe css "admission"
|
|
||||||
"""
|
|
||||||
fields = {
|
|
||||||
"bac": "Bac",
|
|
||||||
"specialite": "Spécialité",
|
|
||||||
"type_admission": "Type Adm.",
|
|
||||||
"classement": "Rg. Adm.",
|
|
||||||
}
|
|
||||||
first = True
|
|
||||||
for i, cid in enumerate(fields):
|
|
||||||
titles[f"_{cid}_col_order"] = 10000 + i # tout à droite
|
|
||||||
if first:
|
|
||||||
titles[f"_{cid}_class"] = "admission admission_first"
|
|
||||||
first = False
|
|
||||||
else:
|
|
||||||
titles[f"_{cid}_class"] = "admission"
|
|
||||||
titles.update(fields)
|
|
||||||
for row in rows:
|
|
||||||
etud = Identite.query.get(row["etudid"])
|
|
||||||
admission = etud.admission.first()
|
|
||||||
first = True
|
|
||||||
for cid in fields:
|
|
||||||
row[cid] = getattr(admission, cid) or ""
|
|
||||||
if first:
|
|
||||||
row[f"_{cid}_class"] = "admission admission_first"
|
|
||||||
first = False
|
|
||||||
else:
|
|
||||||
row[f"_{cid}_class"] = "admission"
|
|
||||||
|
|
||||||
def recap_add_partitions(self, rows: list[dict], titles: dict, col_idx: int = None):
|
|
||||||
"""Ajoute les colonnes indiquant les groupes
|
|
||||||
rows est une liste de dict avec une clé "etudid"
|
|
||||||
Les colonnes ont la classe css "partition"
|
|
||||||
"""
|
|
||||||
partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
|
|
||||||
self.formsemestre.id
|
|
||||||
)
|
|
||||||
first_partition = True
|
|
||||||
col_order = 10 if col_idx is None else col_idx
|
|
||||||
for partition in partitions:
|
|
||||||
cid = f"part_{partition['partition_id']}"
|
|
||||||
rg_cid = cid + "_rg" # rang dans la partition
|
|
||||||
titles[cid] = partition["partition_name"]
|
|
||||||
if first_partition:
|
|
||||||
klass = "partition"
|
|
||||||
else:
|
|
||||||
klass = "partition partition_aux"
|
|
||||||
titles[f"_{cid}_class"] = klass
|
|
||||||
titles[f"_{cid}_col_order"] = col_order
|
|
||||||
titles[f"_{rg_cid}_col_order"] = col_order + 1
|
|
||||||
col_order += 2
|
|
||||||
if partition["bul_show_rank"]:
|
|
||||||
titles[rg_cid] = f"Rg {partition['partition_name']}"
|
|
||||||
titles[f"_{rg_cid}_class"] = "partition_rangs"
|
|
||||||
partition_etud_groups = partitions_etud_groups[partition["partition_id"]]
|
|
||||||
for row in rows:
|
|
||||||
group = None # group (dict) de l'étudiant dans cette partition
|
|
||||||
# dans NotesTableCompat, à revoir
|
|
||||||
etud_etat = self.get_etud_etat(row["etudid"])
|
|
||||||
if etud_etat == "D":
|
|
||||||
gr_name = "Dém."
|
|
||||||
row["_tr_class"] = "dem"
|
|
||||||
elif etud_etat == DEF:
|
|
||||||
gr_name = "Déf."
|
|
||||||
row["_tr_class"] = "def"
|
|
||||||
else:
|
|
||||||
group = partition_etud_groups.get(row["etudid"])
|
|
||||||
gr_name = group["group_name"] if group else ""
|
|
||||||
if gr_name:
|
|
||||||
row[cid] = gr_name
|
|
||||||
row[f"_{cid}_class"] = klass
|
|
||||||
# Rangs dans groupe
|
|
||||||
if (
|
|
||||||
partition["bul_show_rank"]
|
|
||||||
and (group is not None)
|
|
||||||
and (group["id"] in self.moy_gen_rangs_by_group)
|
|
||||||
):
|
|
||||||
rang = self.moy_gen_rangs_by_group[group["id"]][0]
|
|
||||||
row[rg_cid] = rang.get(row["etudid"], "")
|
|
||||||
|
|
||||||
first_partition = False
|
|
||||||
|
|
||||||
def _recap_add_evaluations(
|
|
||||||
self, rows: list[dict], titles: dict, bottom_infos: dict
|
|
||||||
):
|
|
||||||
"""Ajoute les colonnes avec les notes aux évaluations
|
|
||||||
rows est une liste de dict avec une clé "etudid"
|
|
||||||
Les colonnes ont la classe css "evaluation"
|
|
||||||
"""
|
|
||||||
# nouvelle ligne pour description évaluations:
|
|
||||||
bottom_infos["descr_evaluation"] = {
|
|
||||||
"_tr_class": "bottom_info",
|
|
||||||
"_title": "Description évaluation",
|
|
||||||
}
|
|
||||||
first_eval = True
|
|
||||||
index_col = 9000 # à droite
|
|
||||||
for modimpl in self.formsemestre.modimpls_sorted:
|
|
||||||
evals = self.modimpls_results[modimpl.id].get_evaluations_completes(modimpl)
|
|
||||||
eval_index = len(evals) - 1
|
|
||||||
inscrits = {i.etudid for i in modimpl.inscriptions}
|
|
||||||
first_eval_of_mod = True
|
|
||||||
for e in evals:
|
|
||||||
cid = f"eval_{e.id}"
|
|
||||||
titles[
|
|
||||||
cid
|
|
||||||
] = f'{modimpl.module.code} {eval_index} {e.jour.isoformat() if e.jour else ""}'
|
|
||||||
klass = "evaluation"
|
|
||||||
if first_eval:
|
|
||||||
klass += " first"
|
|
||||||
elif first_eval_of_mod:
|
|
||||||
klass += " first_of_mod"
|
|
||||||
titles[f"_{cid}_class"] = klass
|
|
||||||
first_eval_of_mod = first_eval = False
|
|
||||||
titles[f"_{cid}_col_order"] = index_col
|
|
||||||
index_col += 1
|
|
||||||
eval_index -= 1
|
|
||||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
|
|
||||||
e.evaluation_id
|
|
||||||
)
|
|
||||||
for row in rows:
|
|
||||||
etudid = row["etudid"]
|
|
||||||
if etudid in inscrits:
|
|
||||||
if etudid in notes_db:
|
|
||||||
val = notes_db[etudid]["value"]
|
|
||||||
else:
|
|
||||||
# Note manquante mais prise en compte immédiate: affiche ATT
|
|
||||||
val = scu.NOTES_ATTENTE
|
|
||||||
row[cid] = scu.fmt_note(val)
|
|
||||||
row[f"_{cid}_class"] = klass + {
|
|
||||||
"ABS": " abs",
|
|
||||||
"ATT": " att",
|
|
||||||
"EXC": " exc",
|
|
||||||
}.get(row[cid], "")
|
|
||||||
else:
|
|
||||||
row[cid] = "ni"
|
|
||||||
row[f"_{cid}_class"] = klass + " non_inscrit"
|
|
||||||
|
|
||||||
bottom_infos["coef"][cid] = e.coefficient
|
|
||||||
bottom_infos["min"][cid] = "0"
|
|
||||||
bottom_infos["max"][cid] = scu.fmt_note(e.note_max)
|
|
||||||
bottom_infos["descr_evaluation"][cid] = e.description or ""
|
|
||||||
bottom_infos["descr_evaluation"][f"_{cid}_target"] = url_for(
|
|
||||||
"notes.evaluation_listenotes",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
evaluation_id=e.id,
|
|
||||||
)
|
|
||||||
|
|
|
@ -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
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""Classe résultats pour compatibilité avec le code ScoDoc 7
|
"""Classe résultats pour compatibilité avec le code ScoDoc 7
|
||||||
"""
|
"""
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
from flask import flash, g, Markup, url_for
|
from flask import flash, g, Markup, url_for
|
||||||
|
|
||||||
|
@ -14,11 +15,8 @@ from app import log
|
||||||
from app.comp import moy_sem
|
from app.comp import moy_sem
|
||||||
from app.comp.aux_stats import StatsMoyenne
|
from app.comp.aux_stats import StatsMoyenne
|
||||||
from app.comp.res_common import ResultatsSemestre
|
from app.comp.res_common import ResultatsSemestre
|
||||||
from app.comp import res_sem
|
from app.models import Identite, FormSemestre, ModuleImpl, ScolarAutorisationInscription
|
||||||
from app.models import FormSemestre
|
from app.scodoc.codes_cursus import UE_SPORT, DEF
|
||||||
from app.models import Identite
|
|
||||||
from app.models import ModuleImpl
|
|
||||||
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
|
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
# Pour raccorder le code des anciens codes qui attendent une NoteTable
|
# Pour raccorder le code des anciens codes qui attendent une NoteTable
|
||||||
|
@ -26,7 +24,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).
|
||||||
"""
|
"""
|
||||||
|
@ -53,7 +51,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||||
self.moy_gen_rangs_by_group = {} # { group_id : (Series, Series) }
|
self.moy_gen_rangs_by_group = {} # { group_id : (Series, Series) }
|
||||||
self.ue_rangs_by_group = {} # { ue_id : {group_id : (Series, Series)}}
|
self.ue_rangs_by_group = {} # { ue_id : {group_id : (Series, Series)}}
|
||||||
self.expr_diagnostics = ""
|
self.expr_diagnostics = ""
|
||||||
self.parcours = self.formsemestre.formation.get_parcours()
|
self.parcours = self.formsemestre.formation.get_cursus()
|
||||||
self._modimpls_dict_by_ue = {} # local cache
|
self._modimpls_dict_by_ue = {} # local cache
|
||||||
|
|
||||||
def get_inscrits(self, include_demdef=True, order_by=False) -> list[Identite]:
|
def get_inscrits(self, include_demdef=True, order_by=False) -> list[Identite]:
|
||||||
|
@ -104,10 +102,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 }
|
||||||
"""
|
"""
|
||||||
|
@ -168,15 +165,24 @@ class NotesTableCompat(ResultatsSemestre):
|
||||||
moy_gen_rangs_by_group[group_id]
|
moy_gen_rangs_by_group[group_id]
|
||||||
ue_rangs_by_group[group_id]
|
ue_rangs_by_group[group_id]
|
||||||
"""
|
"""
|
||||||
|
mask_inscr = pd.Series(
|
||||||
|
[
|
||||||
|
self.formsemestre.etuds_inscriptions[etudid].etat == scu.INSCRIT
|
||||||
|
for etudid in self.etud_moy_gen.index
|
||||||
|
],
|
||||||
|
dtype=float,
|
||||||
|
index=self.etud_moy_gen.index,
|
||||||
|
)
|
||||||
|
etud_moy_gen_dem_zero = self.etud_moy_gen * mask_inscr
|
||||||
(
|
(
|
||||||
self.etud_moy_gen_ranks,
|
self.etud_moy_gen_ranks,
|
||||||
self.etud_moy_gen_ranks_int,
|
self.etud_moy_gen_ranks_int,
|
||||||
) = moy_sem.comp_ranks_series(self.etud_moy_gen)
|
) = moy_sem.comp_ranks_series(etud_moy_gen_dem_zero)
|
||||||
ues = self.formsemestre.query_ues()
|
ues = self.formsemestre.query_ues()
|
||||||
for ue in ues:
|
for ue in ues:
|
||||||
moy_ue = self.etud_moy_ue[ue.id]
|
moy_ue = self.etud_moy_ue[ue.id]
|
||||||
self.ue_rangs[ue.id] = (
|
self.ue_rangs[ue.id] = (
|
||||||
moy_sem.comp_ranks_series(moy_ue)[0], # juste en chaine
|
moy_sem.comp_ranks_series(moy_ue * mask_inscr)[0], # juste en chaine
|
||||||
int(moy_ue.count()),
|
int(moy_ue.count()),
|
||||||
)
|
)
|
||||||
# .count() -> nb of non NaN values
|
# .count() -> nb of non NaN values
|
||||||
|
@ -196,7 +202,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||||
)
|
)
|
||||||
# list() car pandas veut une sequence pour take()
|
# list() car pandas veut une sequence pour take()
|
||||||
# Rangs / moyenne générale:
|
# Rangs / moyenne générale:
|
||||||
group_moys_gen = self.etud_moy_gen[group_members]
|
group_moys_gen = etud_moy_gen_dem_zero[group_members]
|
||||||
self.moy_gen_rangs_by_group[group.id] = moy_sem.comp_ranks_series(
|
self.moy_gen_rangs_by_group[group.id] = moy_sem.comp_ranks_series(
|
||||||
group_moys_gen
|
group_moys_gen
|
||||||
)
|
)
|
||||||
|
@ -205,7 +211,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||||
group_moys_ue = self.etud_moy_ue[ue.id][group_members]
|
group_moys_ue = self.etud_moy_ue[ue.id][group_members]
|
||||||
self.ue_rangs_by_group.setdefault(ue.id, {})[
|
self.ue_rangs_by_group.setdefault(ue.id, {})[
|
||||||
group.id
|
group.id
|
||||||
] = moy_sem.comp_ranks_series(group_moys_ue)
|
] = moy_sem.comp_ranks_series(group_moys_ue * mask_inscr)
|
||||||
|
|
||||||
def get_etud_rang(self, etudid: int) -> str:
|
def get_etud_rang(self, etudid: int) -> str:
|
||||||
"""Le rang (classement) de l'étudiant dans le semestre.
|
"""Le rang (classement) de l'étudiant dans le semestre.
|
||||||
|
@ -272,10 +278,19 @@ class NotesTableCompat(ResultatsSemestre):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def etud_has_decision(self, etudid):
|
def etud_has_decision(self, etudid):
|
||||||
"""True s'il y a une décision de jury pour cet étudiant"""
|
"""True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
|
||||||
return self.get_etud_decision_ues(etudid) or self.get_etud_decision_sem(etudid)
|
prend aussi en compte les autorisations de passage.
|
||||||
|
Sous-classée en BUT pour les RCUEs et années.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
self.get_etud_decisions_ue(etudid)
|
||||||
|
or self.get_etud_decision_sem(etudid)
|
||||||
|
or ScolarAutorisationInscription.query.filter_by(
|
||||||
|
origin_formsemestre_id=self.formsemestre.id, etudid=etudid
|
||||||
|
).count()
|
||||||
|
)
|
||||||
|
|
||||||
def get_etud_decision_ues(self, etudid: int) -> dict:
|
def get_etud_decisions_ue(self, etudid: int) -> dict:
|
||||||
"""Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu.
|
"""Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu.
|
||||||
Ne tient pas compte des UE capitalisées.
|
Ne tient pas compte des UE capitalisées.
|
||||||
{ ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : "d/m/y", 'ects' : x }
|
{ ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : "d/m/y", 'ects' : x }
|
||||||
|
@ -284,16 +299,16 @@ class NotesTableCompat(ResultatsSemestre):
|
||||||
if self.get_etud_etat(etudid) == DEF:
|
if self.get_etud_etat(etudid) == DEF:
|
||||||
return {}
|
return {}
|
||||||
else:
|
else:
|
||||||
validations = self.load_validations()
|
validations = self.get_formsemestre_validations()
|
||||||
return validations.decisions_jury_ues.get(etudid, None)
|
return validations.decisions_jury_ues.get(etudid, None)
|
||||||
|
|
||||||
def get_etud_ects_valides(self, etudid: int, decisions_ues: dict = False) -> 0:
|
def get_etud_ects_valides(self, etudid: int, decisions_ues: dict = False) -> 0:
|
||||||
"""Le total des ECTS validés (et enregistrés) par l'étudiant dans ce semestre.
|
"""Le total des ECTS validés (et enregistrés) par l'étudiant dans ce semestre.
|
||||||
NB: avant jury, rien d'enregistré, donc zéro ECTS.
|
NB: avant jury, rien d'enregistré, donc zéro ECTS.
|
||||||
Optimisation: si decisions_ues est passé, l'utilise, sinon appelle get_etud_decision_ues()
|
Optimisation: si decisions_ues est passé, l'utilise, sinon appelle get_etud_decisions_ue()
|
||||||
"""
|
"""
|
||||||
if decisions_ues is False:
|
if decisions_ues is False:
|
||||||
decisions_ues = self.get_etud_decision_ues(etudid)
|
decisions_ues = self.get_etud_decisions_ue(etudid)
|
||||||
if not decisions_ues:
|
if not decisions_ues:
|
||||||
return 0.0
|
return 0.0
|
||||||
return sum([d.get("ects", 0.0) for d in decisions_ues.values()])
|
return sum([d.get("ects", 0.0) for d in decisions_ues.values()])
|
||||||
|
@ -311,7 +326,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||||
"compense_formsemestre_id": None,
|
"compense_formsemestre_id": None,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
validations = self.load_validations()
|
validations = self.get_formsemestre_validations()
|
||||||
return validations.decisions_jury.get(etudid, None)
|
return validations.decisions_jury.get(etudid, None)
|
||||||
|
|
||||||
def get_etud_mat_moy(self, matiere_id: int, etudid: int) -> str:
|
def get_etud_mat_moy(self, matiere_id: int, etudid: int) -> str:
|
||||||
|
|
|
@ -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):
|
||||||
|
@ -95,7 +96,7 @@ def permission_required(permission):
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def permission_required_compat_scodoc7(permission):
|
def permission_required_compat_scodoc7(permission): # XXX TODO A SUPPRIMER
|
||||||
"""Décorateur pour les fonctions utilisées comme API dans ScoDoc 7
|
"""Décorateur pour les fonctions utilisées comme API dans ScoDoc 7
|
||||||
Comme @permission_required mais autorise de passer directement
|
Comme @permission_required mais autorise de passer directement
|
||||||
les informations d'auth en paramètres:
|
les informations d'auth en paramètres:
|
||||||
|
@ -117,6 +118,10 @@ def permission_required_compat_scodoc7(permission):
|
||||||
else:
|
else:
|
||||||
abort(405) # method not allowed
|
abort(405) # method not allowed
|
||||||
if user_name and user_password:
|
if user_name and user_password:
|
||||||
|
# Ancienne API: va être supprimée courant mars 2023
|
||||||
|
current_app.logger.warning(
|
||||||
|
"using DEPRECATED ScoDoc7 authentication method !"
|
||||||
|
)
|
||||||
u = User.query.filter_by(user_name=user_name).first()
|
u = User.query.filter_by(user_name=user_name).first()
|
||||||
if u and u.check_password(user_password):
|
if u and u.check_password(user_password):
|
||||||
auth_ok = True
|
auth_ok = True
|
||||||
|
@ -180,19 +185,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
|
||||||
|
|
34
app/email.py
34
app/email.py
|
@ -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
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@ from flask import current_app, g
|
||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
|
|
||||||
from app import mail
|
from app import mail
|
||||||
|
from app.models.departements import Departement
|
||||||
|
from app.models.config import ScoDocSiteConfig
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
|
|
||||||
|
|
||||||
|
@ -56,6 +58,7 @@ def send_message(msg: Message):
|
||||||
In mail debug mode, addresses are discarded and all mails are sent to the
|
In mail debug mode, addresses are discarded and all mails are sent to the
|
||||||
specified debugging address.
|
specified debugging address.
|
||||||
"""
|
"""
|
||||||
|
email_test_mode_address = False
|
||||||
if hasattr(g, "scodoc_dept"):
|
if hasattr(g, "scodoc_dept"):
|
||||||
# on est dans un département, on peut accéder aux préférences
|
# on est dans un département, on peut accéder aux préférences
|
||||||
email_test_mode_address = sco_preferences.get_preference(
|
email_test_mode_address = sco_preferences.get_preference(
|
||||||
|
@ -81,6 +84,35 @@ Adresses d'origine:
|
||||||
+ msg.body
|
+ msg.body
|
||||||
)
|
)
|
||||||
|
|
||||||
|
current_app.logger.info(
|
||||||
|
f"""email sent to{' (mode test)' if email_test_mode_address else ''}: {msg.recipients}
|
||||||
|
from sender {msg.sender}
|
||||||
|
"""
|
||||||
|
)
|
||||||
Thread(
|
Thread(
|
||||||
target=send_async_email, args=(current_app._get_current_object(), msg)
|
target=send_async_email, args=(current_app._get_current_object(), msg)
|
||||||
).start()
|
).start()
|
||||||
|
|
||||||
|
|
||||||
|
def get_from_addr(dept_acronym: str = None):
|
||||||
|
"""L'adresse "from" à utiliser pour envoyer un mail
|
||||||
|
|
||||||
|
Si le departement est spécifié, ou si l'attribut `g.scodoc_dept`existe,
|
||||||
|
prend le `email_from_addr` des préférences de ce département si ce champ est non vide.
|
||||||
|
Sinon, utilise le paramètre global `email_from_addr`.
|
||||||
|
Sinon, la variable de config `SCODOC_MAIL_FROM`.
|
||||||
|
"""
|
||||||
|
dept_acronym = dept_acronym or getattr(g, "scodoc_dept", None)
|
||||||
|
if dept_acronym:
|
||||||
|
dept = Departement.query.filter_by(acronym=dept_acronym).first()
|
||||||
|
if dept:
|
||||||
|
from_addr = (
|
||||||
|
sco_preferences.get_preference("email_from_addr", dept_id=dept.id) or ""
|
||||||
|
).strip()
|
||||||
|
if from_addr:
|
||||||
|
return from_addr
|
||||||
|
return (
|
||||||
|
ScoDocSiteConfig.get("email_from_addr")
|
||||||
|
or current_app.config["SCODOC_MAIL_FROM"]
|
||||||
|
or "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
|
||||||
|
@ -216,7 +216,7 @@ def send_email_notifications_entreprise(subject: str, entreprise: Entreprise):
|
||||||
txt = "\n".join(txt)
|
txt = "\n".join(txt)
|
||||||
email.send_email(
|
email.send_email(
|
||||||
subject,
|
subject,
|
||||||
sco_preferences.get_preference("email_from_addr"),
|
email.get_from_addr(),
|
||||||
[EntreprisePreferences.get_email_notifications],
|
[EntreprisePreferences.get_email_notifications],
|
||||||
txt,
|
txt,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
63
app/forms/formsemestre/change_formation.py
Normal file
63
app/forms/formsemestre/change_formation.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# ScoDoc
|
||||||
|
#
|
||||||
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
Formulaire changement formation
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import RadioField, SubmitField, validators
|
||||||
|
|
||||||
|
from app.models import Formation
|
||||||
|
|
||||||
|
|
||||||
|
class FormSemestreChangeFormationForm(FlaskForm):
|
||||||
|
"Formulaire changement formation d'un formsemestre"
|
||||||
|
# consrtuit dynamiquement ci-dessous
|
||||||
|
|
||||||
|
|
||||||
|
def gen_formsemestre_change_formation_form(
|
||||||
|
formations: list[Formation],
|
||||||
|
) -> FormSemestreChangeFormationForm:
|
||||||
|
"Create our dynamical form"
|
||||||
|
# see https://wtforms.readthedocs.io/en/2.3.x/specific_problems/#dynamic-form-composition
|
||||||
|
class F(FormSemestreChangeFormationForm):
|
||||||
|
pass
|
||||||
|
|
||||||
|
setattr(
|
||||||
|
F,
|
||||||
|
"radio_but",
|
||||||
|
RadioField(
|
||||||
|
"Label",
|
||||||
|
choices=[
|
||||||
|
(formation.id, formation.get_titre_version())
|
||||||
|
for formation in formations
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setattr(F, "submit", SubmitField("Changer la formation"))
|
||||||
|
setattr(F, "cancel", SubmitField("Annuler"))
|
||||||
|
return F()
|
|
@ -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
|
||||||
|
@ -35,14 +35,14 @@ from wtforms.fields.simple import StringField
|
||||||
|
|
||||||
from app.models import SHORT_STR_LEN
|
from app.models import SHORT_STR_LEN
|
||||||
|
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc import codes_cursus
|
||||||
|
|
||||||
|
|
||||||
def _build_code_field(code):
|
def _build_code_field(code):
|
||||||
return StringField(
|
return StringField(
|
||||||
label=code,
|
label=code,
|
||||||
default=code,
|
default=code,
|
||||||
description=sco_codes_parcours.CODES_EXPL[code],
|
description=codes_cursus.CODES_EXPL[code],
|
||||||
validators=[
|
validators=[
|
||||||
validators.regexp(
|
validators.regexp(
|
||||||
r"^[A-Z0-9_]*$",
|
r"^[A-Z0-9_]*$",
|
||||||
|
@ -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")
|
||||||
|
|
78
app/forms/main/config_cas.py
Normal file
78
app/forms/main/config_cas.py
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
# -*- mode: python -*-
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# ScoDoc
|
||||||
|
#
|
||||||
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
Formulaire configuration CAS
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import BooleanField, SubmitField
|
||||||
|
from wtforms.fields.simple import FileField, StringField
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigCASForm(FlaskForm):
|
||||||
|
"Formulaire paramétrage CAS"
|
||||||
|
cas_enable = BooleanField("Activer le CAS")
|
||||||
|
cas_force = BooleanField(
|
||||||
|
"Forcer l'utilisation de CAS (tous les utilisateurs seront redirigés vers le CAS)"
|
||||||
|
)
|
||||||
|
|
||||||
|
cas_server = StringField(
|
||||||
|
label="URL du serveur CAS",
|
||||||
|
description="""url complète. Commence en général par <tt>https://</tt>.""",
|
||||||
|
)
|
||||||
|
cas_login_route = StringField(
|
||||||
|
label="Route du login CAS",
|
||||||
|
description="""ajouté à l'URL du serveur: exemple <tt>/cas</tt> (si commence par <tt>/</tt>, part de la racine)""",
|
||||||
|
default="/cas",
|
||||||
|
)
|
||||||
|
cas_logout_route = StringField(
|
||||||
|
label="Route du logout CAS",
|
||||||
|
description="""ajouté à l'URL du serveur: exemple <tt>/cas/logout</tt>""",
|
||||||
|
default="/cas/logout",
|
||||||
|
)
|
||||||
|
cas_validate_route = StringField(
|
||||||
|
label="Route de validation CAS",
|
||||||
|
description="""ajouté à l'URL du serveur: exemple <tt>/cas/serviceValidate</tt>""",
|
||||||
|
default="/cas/serviceValidate",
|
||||||
|
)
|
||||||
|
|
||||||
|
cas_attribute_id = StringField(
|
||||||
|
label="Attribut CAS utilisé comme id (laissez vide pour prendre l'id par défaut)",
|
||||||
|
description="""Le champs CAS qui sera considéré comme l'id unique des
|
||||||
|
comptes utilisateurs.""",
|
||||||
|
)
|
||||||
|
|
||||||
|
cas_ssl_verify = BooleanField("Vérification du certificat SSL")
|
||||||
|
cas_ssl_certificate_file = FileField(
|
||||||
|
label="Certificat (PEM)",
|
||||||
|
description="""Le contenu du certificat PEM
|
||||||
|
(commence typiquement par <tt>-----BEGIN CERTIFICATE-----</tt>)""",
|
||||||
|
)
|
||||||
|
|
||||||
|
submit = SubmitField("Valider")
|
||||||
|
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
|
@ -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
|
||||||
|
@ -148,6 +148,9 @@ class AddLogoForm(FlaskForm):
|
||||||
kwargs["meta"] = {"csrf": False}
|
kwargs["meta"] = {"csrf": False}
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def id(self):
|
||||||
|
return f"id=add_{self.dept_key.data}"
|
||||||
|
|
||||||
def validate_name(self, name):
|
def validate_name(self, name):
|
||||||
dept_id = dept_key_to_id(self.dept_key.data)
|
dept_id = dept_key_to_id(self.dept_key.data)
|
||||||
if dept_id == GLOBAL:
|
if dept_id == GLOBAL:
|
||||||
|
@ -171,7 +174,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()
|
||||||
|
@ -227,6 +230,10 @@ class LogoForm(FlaskForm):
|
||||||
self.description = "Se substitue au footer défini au niveau global"
|
self.description = "Se substitue au footer défini au niveau global"
|
||||||
self.titre = "Logo pied de page"
|
self.titre = "Logo pied de page"
|
||||||
|
|
||||||
|
def id(self):
|
||||||
|
idstring = f"{self.dept_key.data}_{self.logo_id.data}"
|
||||||
|
return f"id={idstring}"
|
||||||
|
|
||||||
def select_action(self):
|
def select_action(self):
|
||||||
from app.scodoc.sco_config_actions import LogoRename
|
from app.scodoc.sco_config_actions import LogoRename
|
||||||
from app.scodoc.sco_config_actions import LogoUpdate
|
from app.scodoc.sco_config_actions import LogoUpdate
|
||||||
|
@ -258,6 +265,9 @@ class DeptForm(FlaskForm):
|
||||||
kwargs["meta"] = {"csrf": False}
|
kwargs["meta"] = {"csrf": False}
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def id(self):
|
||||||
|
return f"id=DEPT_{self.dept_key.data}"
|
||||||
|
|
||||||
def is_local(self):
|
def is_local(self):
|
||||||
if self.dept_key.data == GLOBAL:
|
if self.dept_key.data == GLOBAL:
|
||||||
return None
|
return None
|
||||||
|
@ -434,7 +444,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
|
||||||
|
@ -31,8 +31,8 @@ Formulaires configuration Exports Apogée (codes)
|
||||||
|
|
||||||
from flask import flash, url_for, redirect, request, render_template
|
from flask import flash, url_for, redirect, request, render_template
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import BooleanField, SelectField, SubmitField
|
from wtforms import BooleanField, SelectField, StringField, SubmitField
|
||||||
|
from wtforms.validators import Email, Optional
|
||||||
import app
|
import app
|
||||||
from app.models import ScoDocSiteConfig
|
from app.models import ScoDocSiteConfig
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
@ -54,6 +54,28 @@ 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)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
email_from_addr = StringField(
|
||||||
|
label="Adresse source des mails",
|
||||||
|
description="""adresse email source (from) des mails émis par ScoDoc.
|
||||||
|
Attention: si ce champ peut aussi être défini dans chaque département.""",
|
||||||
|
validators=[Optional(), Email()],
|
||||||
|
)
|
||||||
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 +89,12 @@ 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(),
|
||||||
|
"email_from_addr": ScoDocSiteConfig.get("email_from_addr"),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
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 +121,28 @@ 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]
|
||||||
|
}"""
|
||||||
|
)
|
||||||
|
if ScoDocSiteConfig.set("email_from_addr", form_scodoc.data["email_from_addr"]):
|
||||||
|
flash("Adresse email origine enregistrée")
|
||||||
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
|
||||||
|
|
|
@ -9,6 +9,7 @@ CODE_STR_LEN = 16 # chaine pour les codes
|
||||||
SHORT_STR_LEN = 32 # courtes chaine, eg acronymes
|
SHORT_STR_LEN = 32 # courtes chaine, eg acronymes
|
||||||
APO_CODE_STR_LEN = 512 # nb de car max d'un code Apogée (il peut y en avoir plusieurs)
|
APO_CODE_STR_LEN = 512 # nb de car max d'un code Apogée (il peut y en avoir plusieurs)
|
||||||
GROUPNAME_STR_LEN = 64
|
GROUPNAME_STR_LEN = 64
|
||||||
|
USERNAME_STR_LEN = 64
|
||||||
|
|
||||||
convention = {
|
convention = {
|
||||||
"ix": "ix_%(column_0_label)s",
|
"ix": "ix_%(column_0_label)s",
|
||||||
|
@ -36,7 +37,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 +73,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())
|
||||||
|
@ -24,10 +26,8 @@ class Absence(db.Model):
|
||||||
# moduleimpid concerne (optionnel):
|
# moduleimpid concerne (optionnel):
|
||||||
moduleimpl_id = db.Column(
|
moduleimpl_id = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey("notes_moduleimpl.id"),
|
db.ForeignKey("notes_moduleimpl.id", ondelete="SET NULL"),
|
||||||
)
|
)
|
||||||
# XXX TODO: contrainte ajoutée: vérifier suppression du module
|
|
||||||
# (mettre à NULL sans supprimer)
|
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
data = {
|
data = {
|
||||||
|
|
336
app/models/assiduites.py
Normal file
336
app/models/assiduites.py
Normal file
|
@ -0,0 +1,336 @@
|
||||||
|
# -*- 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,
|
||||||
|
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, nullable=False)
|
||||||
|
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())
|
||||||
|
|
||||||
|
user_id = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("user.id", ondelete="SET NULL"),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
est_just = db.Column(db.Boolean, server_default="false", nullable=False)
|
||||||
|
|
||||||
|
def to_dict(self, format_api=True) -> dict:
|
||||||
|
"""Retourne la représentation json de l'assiduité"""
|
||||||
|
etat = self.etat
|
||||||
|
|
||||||
|
if format_api:
|
||||||
|
etat = EtatAssiduite.inverse().get(self.etat).name
|
||||||
|
data = {
|
||||||
|
"assiduite_id": self.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,
|
||||||
|
"user_id": self.user_id,
|
||||||
|
"est_just": self.est_just,
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
user_id: int = None,
|
||||||
|
est_just: bool = False,
|
||||||
|
) -> 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,
|
||||||
|
user_id=user_id,
|
||||||
|
est_just=est_just,
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
user_id=user_id,
|
||||||
|
est_just=est_just,
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
est_just: bool = False,
|
||||||
|
) -> 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,
|
||||||
|
est_just=est_just,
|
||||||
|
)
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
user_id = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("user.id", ondelete="SET NULL"),
|
||||||
|
nullable=True,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
"user_id": self.user_id,
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_justificatif(
|
||||||
|
cls,
|
||||||
|
etud: Identite,
|
||||||
|
date_debut: datetime,
|
||||||
|
date_fin: datetime,
|
||||||
|
etat: EtatJustificatif,
|
||||||
|
raison: str = None,
|
||||||
|
entry_date: datetime = None,
|
||||||
|
user_id: int = 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,
|
||||||
|
etudiant=etud,
|
||||||
|
raison=raison,
|
||||||
|
entry_date=entry_date,
|
||||||
|
user_id=user_id,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def compute_assiduites_justified(
|
||||||
|
justificatifs: Justificatif = Justificatif, reset: bool = False
|
||||||
|
) -> list[int]:
|
||||||
|
"""Calcule et modifie les champs "est_just" de chaque assiduité lié à l'étud
|
||||||
|
retourne la liste des assiduite_id justifiées
|
||||||
|
|
||||||
|
Si reset alors : met à false toutes les assiduités non justifiées par les justificatifs donnés
|
||||||
|
"""
|
||||||
|
|
||||||
|
list_assiduites_id: set[int] = set()
|
||||||
|
for justi in justificatifs:
|
||||||
|
assiduites: Assiduite = (
|
||||||
|
Assiduite.query.join(Justificatif, Assiduite.etudid == Justificatif.etudid)
|
||||||
|
.filter(Assiduite.etat != EtatAssiduite.PRESENT)
|
||||||
|
.filter(
|
||||||
|
Assiduite.date_debut <= justi.date_fin,
|
||||||
|
Assiduite.date_fin >= justi.date_debut,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for assi in assiduites:
|
||||||
|
assi.est_just = True
|
||||||
|
list_assiduites_id.add(assi.id)
|
||||||
|
db.session.add(assi)
|
||||||
|
|
||||||
|
if reset:
|
||||||
|
un_justified: Assiduite = (
|
||||||
|
Assiduite.query.filter(Assiduite.id.not_in(list_assiduites_id))
|
||||||
|
.join(Justificatif, Assiduite.etudid == Justificatif.etudid)
|
||||||
|
.filter(Assiduite.etat != EtatAssiduite.PRESENT)
|
||||||
|
)
|
||||||
|
for assi in un_justified:
|
||||||
|
assi.est_just = False
|
||||||
|
db.session.add(assi)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return
|
|
@ -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
|
||||||
|
@ -53,14 +53,18 @@ class XMLModel:
|
||||||
class ApcReferentielCompetences(db.Model, XMLModel):
|
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(
|
||||||
annexe = db.Column(db.Text())
|
db.Integer, db.ForeignKey("departement.id", ondelete="CASCADE"), index=True
|
||||||
specialite = db.Column(db.Text())
|
)
|
||||||
specialite_long = db.Column(db.Text())
|
annexe = db.Column(db.Text()) # '1', '22', ...
|
||||||
type_titre = db.Column(db.Text())
|
specialite = db.Column(db.Text()) # 'CJ', 'RT', 'INFO', ...
|
||||||
type_structure = db.Column(db.Text())
|
specialite_long = 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 +90,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 +114,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,14 +181,37 @@ 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):
|
||||||
"Compétence"
|
"Compétence"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
referentiel_id = db.Column(
|
referentiel_id = db.Column(
|
||||||
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
|
db.Integer,
|
||||||
|
db.ForeignKey("apc_referentiel_competences.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
)
|
)
|
||||||
# les compétences dans Orébut sont identifiées par leur id unique
|
# les compétences dans Orébut sont identifiées par leur id unique
|
||||||
# (mais id_orebut n'est pas unique car le même ref. pourra être chargé dans plusieurs depts)
|
# (mais id_orebut n'est pas unique car le même ref. pourra être chargé dans plusieurs depts)
|
||||||
|
@ -197,7 +247,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 +259,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:
|
||||||
|
@ -227,7 +280,9 @@ class ApcSituationPro(db.Model, XMLModel):
|
||||||
"Situation professionnelle"
|
"Situation professionnelle"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
competence_id = db.Column(
|
competence_id = db.Column(
|
||||||
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
|
db.Integer,
|
||||||
|
db.ForeignKey("apc_competence.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
)
|
)
|
||||||
libelle = db.Column(db.Text(), nullable=False)
|
libelle = db.Column(db.Text(), nullable=False)
|
||||||
# aucun attribut (le text devient le libellé)
|
# aucun attribut (le text devient le libellé)
|
||||||
|
@ -239,7 +294,9 @@ class ApcComposanteEssentielle(db.Model, XMLModel):
|
||||||
"Composante essentielle"
|
"Composante essentielle"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
competence_id = db.Column(
|
competence_id = db.Column(
|
||||||
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
|
db.Integer,
|
||||||
|
db.ForeignKey("apc_competence.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
)
|
)
|
||||||
libelle = db.Column(db.Text(), nullable=False)
|
libelle = db.Column(db.Text(), nullable=False)
|
||||||
|
|
||||||
|
@ -257,7 +314,9 @@ class ApcNiveau(db.Model, XMLModel):
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
competence_id = db.Column(
|
competence_id = db.Column(
|
||||||
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
|
db.Integer,
|
||||||
|
db.ForeignKey("apc_competence.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
)
|
)
|
||||||
libelle = db.Column(db.Text(), nullable=False)
|
libelle = db.Column(db.Text(), nullable=False)
|
||||||
annee = db.Column(db.Text(), nullable=False) # "BUT1", "BUT2", "BUT3"
|
annee = db.Column(db.Text(), nullable=False) # "BUT1", "BUT2", "BUT3"
|
||||||
|
@ -275,13 +334,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 +367,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(
|
||||||
|
@ -337,7 +397,7 @@ app_critiques_modules = db.Table(
|
||||||
),
|
),
|
||||||
db.Column(
|
db.Column(
|
||||||
"app_crit_id",
|
"app_crit_id",
|
||||||
db.ForeignKey("apc_app_critique.id"),
|
db.ForeignKey("apc_app_critique.id", ondelete="CASCADE"),
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -346,7 +406,9 @@ app_critiques_modules = db.Table(
|
||||||
class ApcAppCritique(db.Model, XMLModel):
|
class ApcAppCritique(db.Model, XMLModel):
|
||||||
"Apprentissage Critique BUT"
|
"Apprentissage Critique BUT"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
niveau_id = db.Column(db.Integer, db.ForeignKey("apc_niveau.id"), nullable=False)
|
niveau_id = db.Column(
|
||||||
|
db.Integer, db.ForeignKey("apc_niveau.id", ondelete="CASCADE"), nullable=False
|
||||||
|
)
|
||||||
code = db.Column(db.Text(), nullable=False, index=True)
|
code = db.Column(db.Text(), nullable=False, index=True)
|
||||||
libelle = db.Column(db.Text())
|
libelle = db.Column(db.Text())
|
||||||
|
|
||||||
|
@ -376,7 +438,9 @@ class ApcAppCritique(db.Model, XMLModel):
|
||||||
query = query.filter(ApcNiveau.competence == competence)
|
query = query.filter(ApcNiveau.competence == competence)
|
||||||
return query
|
return query
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self, with_code=False) -> dict:
|
||||||
|
if with_code:
|
||||||
|
return {"code": self.code, "libelle": self.libelle}
|
||||||
return {"libelle": self.libelle}
|
return {"libelle": self.libelle}
|
||||||
|
|
||||||
def get_label(self) -> str:
|
def get_label(self) -> str:
|
||||||
|
@ -393,7 +457,10 @@ class ApcAppCritique(db.Model, XMLModel):
|
||||||
parcours_modules = db.Table(
|
parcours_modules = db.Table(
|
||||||
"parcours_modules",
|
"parcours_modules",
|
||||||
db.Column(
|
db.Column(
|
||||||
"parcours_id", db.Integer, db.ForeignKey("apc_parcours.id"), primary_key=True
|
"parcours_id",
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("apc_parcours.id", ondelete="CASCADE"),
|
||||||
|
primary_key=True,
|
||||||
),
|
),
|
||||||
db.Column(
|
db.Column(
|
||||||
"module_id",
|
"module_id",
|
||||||
|
@ -407,7 +474,10 @@ parcours_modules = db.Table(
|
||||||
parcours_formsemestre = db.Table(
|
parcours_formsemestre = db.Table(
|
||||||
"parcours_formsemestre",
|
"parcours_formsemestre",
|
||||||
db.Column(
|
db.Column(
|
||||||
"parcours_id", db.Integer, db.ForeignKey("apc_parcours.id"), primary_key=True
|
"parcours_id",
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("apc_parcours.id", ondelete="CASCADE"),
|
||||||
|
primary_key=True,
|
||||||
),
|
),
|
||||||
db.Column(
|
db.Column(
|
||||||
"formsemestre_id",
|
"formsemestre_id",
|
||||||
|
@ -420,9 +490,12 @@ parcours_formsemestre = db.Table(
|
||||||
|
|
||||||
|
|
||||||
class ApcParcours(db.Model, XMLModel):
|
class ApcParcours(db.Model, XMLModel):
|
||||||
|
"Un parcours BUT"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
referentiel_id = db.Column(
|
referentiel_id = db.Column(
|
||||||
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
|
db.Integer,
|
||||||
|
db.ForeignKey("apc_referentiel_competences.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
)
|
)
|
||||||
numero = db.Column(db.Integer) # ordre de présentation
|
numero = db.Column(db.Integer) # ordre de présentation
|
||||||
code = db.Column(db.Text(), nullable=False)
|
code = db.Column(db.Text(), nullable=False)
|
||||||
|
@ -433,6 +506,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}>"
|
||||||
|
@ -450,11 +524,19 @@ 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)
|
||||||
parcours_id = db.Column(
|
parcours_id = db.Column(
|
||||||
db.Integer, db.ForeignKey("apc_parcours.id"), nullable=False
|
db.Integer, db.ForeignKey("apc_parcours.id", ondelete="CASCADE"), nullable=False
|
||||||
)
|
)
|
||||||
ordre = db.Column(db.Integer)
|
ordre = db.Column(db.Integer)
|
||||||
"numéro de l'année: 1, 2, 3"
|
"numéro de l'année: 1, 2, 3"
|
||||||
|
|
|
@ -2,20 +2,18 @@
|
||||||
|
|
||||||
"""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.scodoc import sco_codes_parcours as sco_codes
|
from app.models.ues import UniteEns
|
||||||
|
from app.scodoc import codes_cursus as sco_codes
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,7 +22,7 @@ class ApcValidationRCUE(db.Model):
|
||||||
|
|
||||||
aka "regroupements cohérents d'UE" dans le jargon BUT.
|
aka "regroupements cohérents d'UE" dans le jargon BUT.
|
||||||
|
|
||||||
le formsemestre est celui du semestre PAIR du niveau de compétence
|
Le formsemestre est celui du semestre PAIR du niveau de compétence
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__tablename__ = "apc_validation_rcue"
|
__tablename__ = "apc_validation_rcue"
|
||||||
|
@ -48,7 +46,9 @@ class ApcValidationRCUE(db.Model):
|
||||||
ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
|
ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
|
||||||
ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
|
ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
|
||||||
# optionnel, le parcours dans lequel se trouve la compétence:
|
# optionnel, le parcours dans lequel se trouve la compétence:
|
||||||
parcours_id = db.Column(db.Integer, db.ForeignKey("apc_parcours.id"), nullable=True)
|
parcours_id = db.Column(
|
||||||
|
db.Integer, db.ForeignKey("apc_parcours.id", ondelete="set null"), nullable=True
|
||||||
|
)
|
||||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
|
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
|
||||||
|
|
||||||
|
@ -59,13 +59,36 @@ class ApcValidationRCUE(db.Model):
|
||||||
parcour = db.relationship("ApcParcours")
|
parcour = db.relationship("ApcParcours")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} {self.id} {self.etud} {self.ue1}/{self.ue2}:{self.code!r}>"
|
return f"""<{self.__class__.__name__} {self.id} {self.etud} {
|
||||||
|
self.ue1}/{self.ue2}:{self.code!r}>"""
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {
|
||||||
|
self.code} enregistrée le {self.date.strftime("%d/%m/%Y")}"""
|
||||||
|
|
||||||
|
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()
|
||||||
|
@ -74,34 +97,41 @@ class ApcValidationRCUE(db.Model):
|
||||||
"niveau": None if niveau is None else niveau.to_dict_bul(),
|
"niveau": None if niveau is None else niveau.to_dict_bul(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def to_dict_codes(self) -> dict:
|
||||||
|
"Dict avec seulement les ids et la date - pour cache table jury"
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"code": self.code,
|
||||||
|
"date": self.date,
|
||||||
|
"etudid": self.etudid,
|
||||||
|
"niveau_id": self.niveau().id,
|
||||||
|
"formsemestre_id": self.formsemestre_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Attention: ce n'est pas un modèle mais une classe ordinaire:
|
# Attention: ce n'est pas un modèle mais une classe ordinaire:
|
||||||
class RegroupementCoherentUE:
|
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
|
||||||
|
@ -121,21 +151,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 = (
|
||||||
|
@ -145,7 +166,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,
|
||||||
|
@ -177,8 +205,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)
|
||||||
|
@ -214,62 +243,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.get_formsemestre(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):
|
||||||
|
@ -277,7 +306,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,
|
||||||
|
@ -299,7 +328,11 @@ 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):
|
||||||
|
return f"""décision sur année BUT{self.ordre} {self.annee_scolaire} : {self.code}"""
|
||||||
|
|
||||||
def to_dict_bul(self) -> dict:
|
def to_dict_bul(self) -> dict:
|
||||||
"dict pour bulletins"
|
"dict pour bulletins"
|
||||||
|
@ -333,7 +366,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"] = (
|
||||||
|
|
|
@ -4,15 +4,16 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import flash
|
from flask import flash
|
||||||
from app import db, log
|
from app import current_app, 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.codes_cursus 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,15 @@ 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,
|
||||||
|
# CAS
|
||||||
|
"cas_enable": bool,
|
||||||
|
"cas_server": str,
|
||||||
|
"cas_login_route": str,
|
||||||
|
"cas_logout_route": str,
|
||||||
|
"cas_validate_route": str,
|
||||||
|
"cas_attribute_id": str,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, name, value):
|
def __init__(self, name, value):
|
||||||
|
@ -166,7 +177,7 @@ class ScoDocSiteConfig(db.Model):
|
||||||
(starting with empty string to represent "no bonus function").
|
(starting with empty string to represent "no bonus function").
|
||||||
"""
|
"""
|
||||||
d = bonus_spo.get_bonus_class_dict()
|
d = bonus_spo.get_bonus_class_dict()
|
||||||
class_list = [(name, d[name].displayed_name) for name in d.keys()]
|
class_list = [(name, d[name].displayed_name) for name in d]
|
||||||
class_list.sort(key=lambda x: x[1].replace(" du ", " de "))
|
class_list.sort(key=lambda x: x[1].replace(" du ", " de "))
|
||||||
return [("", "")] + class_list
|
return [("", "")] + class_list
|
||||||
|
|
||||||
|
@ -200,13 +211,17 @@ class ScoDocSiteConfig(db.Model):
|
||||||
db.session.add(cfg)
|
db.session.add(cfg)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_cas_enabled(cls) -> bool:
|
||||||
|
"""True si on utilise le CAS"""
|
||||||
|
cfg = ScoDocSiteConfig.query.filter_by(name="cas_enable").first()
|
||||||
|
return cfg is not None and cfg.value
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_entreprises_enabled(cls) -> bool:
|
def is_entreprises_enabled(cls) -> bool:
|
||||||
"""True si on doit activer le module entreprise"""
|
"""True si on doit activer le module entreprise"""
|
||||||
cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
|
cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
|
||||||
if (cfg is None) or not cfg.value:
|
return cfg is not None and cfg.value
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def enable_entreprises(cls, enabled=True) -> bool:
|
def enable_entreprises(cls, enabled=True) -> bool:
|
||||||
|
@ -223,3 +238,99 @@ class ScoDocSiteConfig(db.Model):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, name: str, default: str = "") -> str:
|
||||||
|
"Get configuration param; empty string or specified default if unset"
|
||||||
|
cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
|
||||||
|
if cfg is None:
|
||||||
|
return default
|
||||||
|
return cfg.value or ""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set(cls, name: str, value: str) -> bool:
|
||||||
|
"Set parameter, returns True if change. Commit session."
|
||||||
|
value_str = str(value or "")
|
||||||
|
if (cls.get(name) or "") != value_str:
|
||||||
|
cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
|
||||||
|
if cfg is None:
|
||||||
|
cfg = ScoDocSiteConfig(name=name, value=value_str)
|
||||||
|
else:
|
||||||
|
cfg.value = str(value or "")
|
||||||
|
current_app.logger.info(
|
||||||
|
f"""ScoDocSiteConfig: recording {cfg.name}='{cfg.value[:32]}...'"""
|
||||||
|
)
|
||||||
|
db.session.add(cfg)
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
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,12 +2,15 @@
|
||||||
|
|
||||||
"""ScoDoc models : departements
|
"""ScoDoc models : departements
|
||||||
"""
|
"""
|
||||||
from typing import Any
|
import re
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
VALID_DEPT_EXP = re.compile(r"^[\w@\\\-\.]+$")
|
||||||
|
|
||||||
|
|
||||||
class Departement(db.Model):
|
class Departement(db.Model):
|
||||||
"""Un département ScoDoc"""
|
"""Un département ScoDoc"""
|
||||||
|
@ -39,7 +42,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,8 +50,28 @@ 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
|
||||||
|
def invalid_dept_acronym(cls, dept_acronym: str) -> bool:
|
||||||
|
"Check that dept_acronym is invalid"
|
||||||
|
return (
|
||||||
|
not dept_acronym
|
||||||
|
or (len(dept_acronym) >= SHORT_STR_LEN)
|
||||||
|
or not VALID_DEPT_EXP.match(dept_acronym)
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_acronym(cls, acronym):
|
def from_acronym(cls, acronym):
|
||||||
dept = cls.query.filter_by(acronym=acronym).first_or_404()
|
dept = cls.query.filter_by(acronym=acronym).first_or_404()
|
||||||
|
@ -59,6 +82,8 @@ def create_dept(acronym: str, visible=True) -> Departement:
|
||||||
"Create new departement"
|
"Create new departement"
|
||||||
from app.models import ScoPreference
|
from app.models import ScoPreference
|
||||||
|
|
||||||
|
if Departement.invalid_dept_acronym(acronym):
|
||||||
|
raise ScoValueError("acronyme departement invalide")
|
||||||
existing = Departement.query.filter_by(acronym=acronym).count()
|
existing = Departement.query.filter_by(acronym=acronym).count()
|
||||||
if existing:
|
if existing:
|
||||||
raise ScoValueError(f"acronyme {acronym} déjà existant")
|
raise ScoValueError(f"acronyme {acronym} déjà existant")
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
from flask import abort, has_request_context, url_for
|
from flask import abort, has_request_context, url_for
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
@ -27,6 +29,7 @@ class Identite(db.Model):
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
db.UniqueConstraint("dept_id", "code_nip"),
|
db.UniqueConstraint("dept_id", "code_nip"),
|
||||||
db.UniqueConstraint("dept_id", "code_ine"),
|
db.UniqueConstraint("dept_id", "code_ine"),
|
||||||
|
db.CheckConstraint("civilite IN ('M', 'F', 'X')"),
|
||||||
)
|
)
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
@ -36,10 +39,8 @@ class Identite(db.Model):
|
||||||
nom = db.Column(db.Text())
|
nom = db.Column(db.Text())
|
||||||
prenom = db.Column(db.Text())
|
prenom = db.Column(db.Text())
|
||||||
nom_usuel = db.Column(db.Text())
|
nom_usuel = db.Column(db.Text())
|
||||||
# optionnel (si present, affiché à la place du nom)
|
"optionnel (si present, affiché à la place du nom)"
|
||||||
civilite = db.Column(db.String(1), nullable=False)
|
civilite = db.Column(db.String(1), nullable=False)
|
||||||
__table_args__ = (db.CheckConstraint("civilite IN ('M', 'F', 'X')"),)
|
|
||||||
|
|
||||||
date_naissance = db.Column(db.Date)
|
date_naissance = db.Column(db.Date)
|
||||||
lieu_naissance = db.Column(db.Text())
|
lieu_naissance = db.Column(db.Text())
|
||||||
dept_naissance = db.Column(db.Text())
|
dept_naissance = db.Column(db.Text())
|
||||||
|
@ -58,6 +59,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 (
|
||||||
|
@ -65,13 +76,30 @@ class Identite(db.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_request(cls, etudid=None, code_nip=None):
|
def from_request(cls, etudid=None, code_nip=None) -> "Identite":
|
||||||
"""Étudiant à partir de l'etudid ou du code_nip, soit
|
"""Étudiant à partir de l'etudid ou du code_nip, soit
|
||||||
passés en argument soit retrouvés directement dans la requête web.
|
passés en argument soit retrouvés directement dans la requête web.
|
||||||
Erreur 404 si inexistant.
|
Erreur 404 si inexistant.
|
||||||
"""
|
"""
|
||||||
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 cls.query.filter_by(**args).first_or_404()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_etud(cls, etudid: int) -> "Identite":
|
||||||
|
"""Etudiant ou 404, cherche uniquement dans le département courant"""
|
||||||
|
if g.scodoc_dept:
|
||||||
|
return cls.query.filter_by(
|
||||||
|
id=etudid, dept_id=g.scodoc_dept_id
|
||||||
|
).first_or_404()
|
||||||
|
return cls.query.filter_by(id=etudid).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):
|
||||||
|
@ -142,9 +170,19 @@ class Identite(db.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_first_email(self, field="email") -> str:
|
def get_first_email(self, field="email") -> str:
|
||||||
"Le mail associé à la première adrese de l'étudiant, ou None"
|
"Le mail associé à la première adresse de l'étudiant, ou None"
|
||||||
return getattr(self.adresses[0], field) if self.adresses.count() > 0 else None
|
return getattr(self.adresses[0], field) if self.adresses.count() > 0 else None
|
||||||
|
|
||||||
|
def get_formsemestres(self) -> list:
|
||||||
|
"""Liste des formsemestres dans lesquels l'étudiant est (a été) inscrit,
|
||||||
|
triée par date_debut
|
||||||
|
"""
|
||||||
|
return sorted(
|
||||||
|
[ins.formsemestre for ins in self.formsemestre_inscriptions],
|
||||||
|
key=attrgetter("date_debut"),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
|
||||||
def to_dict_short(self) -> dict:
|
def to_dict_short(self) -> dict:
|
||||||
"""Les champs essentiels"""
|
"""Les champs essentiels"""
|
||||||
return {
|
return {
|
||||||
|
@ -169,6 +207,10 @@ class Identite(db.Model):
|
||||||
e["etudid"] = self.id
|
e["etudid"] = self.id
|
||||||
e["date_naissance"] = ndb.DateISOtoDMY(e["date_naissance"])
|
e["date_naissance"] = ndb.DateISOtoDMY(e["date_naissance"])
|
||||||
e["ne"] = self.e
|
e["ne"] = self.e
|
||||||
|
e["nomprenom"] = self.nomprenom
|
||||||
|
adresse = self.adresses.first()
|
||||||
|
if adresse:
|
||||||
|
e.update(adresse.to_dict())
|
||||||
return {k: e[k] or "" for k in e} # convert_null_outputs_to_empty
|
return {k: e[k] or "" for k in e} # convert_null_outputs_to_empty
|
||||||
|
|
||||||
def to_dict_bul(self, include_urls=True):
|
def to_dict_bul(self, include_urls=True):
|
||||||
|
@ -278,11 +320,18 @@ class Identite(db.Model):
|
||||||
inscription_courante = self.inscription_courante()
|
inscription_courante = self.inscription_courante()
|
||||||
if inscription_courante:
|
if inscription_courante:
|
||||||
titre_sem = inscription_courante.formsemestre.titre_mois()
|
titre_sem = inscription_courante.formsemestre.titre_mois()
|
||||||
|
if inscription_courante.etat == scu.DEMISSION:
|
||||||
|
inscr_txt = "Démission de"
|
||||||
|
elif inscription_courante.etat == scu.DEF:
|
||||||
|
inscr_txt = "Défaillant dans"
|
||||||
|
else:
|
||||||
|
inscr_txt = "Inscrit en"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"etat_in_cursem": inscription_courante.etat,
|
"etat_in_cursem": inscription_courante.etat,
|
||||||
"inscription_courante": inscription_courante,
|
"inscription_courante": inscription_courante,
|
||||||
"inscription": titre_sem,
|
"inscription": titre_sem,
|
||||||
"inscription_str": "Inscrit en " + titre_sem,
|
"inscription_str": inscr_txt + " " + titre_sem,
|
||||||
"situation": self.descr_situation_etud(),
|
"situation": self.descr_situation_etud(),
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
|
@ -311,7 +360,7 @@ class Identite(db.Model):
|
||||||
"situation": situation,
|
"situation": situation,
|
||||||
}
|
}
|
||||||
|
|
||||||
def inscription_etat(self, formsemestre_id):
|
def inscription_etat(self, formsemestre_id: int) -> str:
|
||||||
"""État de l'inscription de cet étudiant au semestre:
|
"""État de l'inscription de cet étudiant au semestre:
|
||||||
False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF
|
False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF
|
||||||
"""
|
"""
|
||||||
|
@ -394,14 +443,21 @@ class Identite(db.Model):
|
||||||
|
|
||||||
return situation
|
return situation
|
||||||
|
|
||||||
def etat_civil_pv(self, line_sep="\n") -> str:
|
def etat_civil_pv(self, with_paragraph=True, line_sep="\n") -> str:
|
||||||
"""Présentation, pour PV jury
|
"""Présentation, pour PV jury
|
||||||
M. Pierre Dupont
|
Si with_paragraph (défaut):
|
||||||
n° 12345678
|
M. Pierre Dupont
|
||||||
né(e) le 7/06/1974
|
n° 12345678
|
||||||
à Paris
|
né(e) le 7/06/1974
|
||||||
|
à Paris
|
||||||
|
Sinon:
|
||||||
|
M. Pierre Dupont
|
||||||
"""
|
"""
|
||||||
return f"""{self.nomprenom}{line_sep}n°{self.code_nip or ""}{line_sep}né{self.e} le {self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""}{line_sep}à {self.lieu_naissance or ""}"""
|
if with_paragraph:
|
||||||
|
return f"""{self.nomprenom}{line_sep}n°{self.code_nip or ""}{line_sep}né{self.e} le {
|
||||||
|
self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""}{
|
||||||
|
line_sep}à {self.lieu_naissance or ""}"""
|
||||||
|
return self.nomprenom
|
||||||
|
|
||||||
def photo_html(self, title=None, size="small") -> str:
|
def photo_html(self, title=None, size="small") -> str:
|
||||||
"""HTML img tag for the photo, either in small size (h90)
|
"""HTML img tag for the photo, either in small size (h90)
|
||||||
|
|
|
@ -5,12 +5,16 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
|
from app.models.etudiants import Identite
|
||||||
from app.models.moduleimpls import ModuleImpl
|
from app.models.moduleimpls import ModuleImpl
|
||||||
|
from app.models.notes import NotesNotes
|
||||||
from app.models.ues import UniteEns
|
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, ...)"""
|
||||||
|
@ -44,10 +48,12 @@ class Evaluation(db.Model):
|
||||||
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
|
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"""<Evaluation {self.id} {self.jour.isoformat() if self.jour else ''} "{self.description[:16] if self.description else ''}">"""
|
return f"""<Evaluation {self.id} {
|
||||||
|
self.jour.isoformat() if self.jour 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
|
||||||
|
@ -67,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)
|
||||||
|
@ -74,6 +108,29 @@ class Evaluation(db.Model):
|
||||||
if k != "_sa_instance_state" and k != "id" and k in data:
|
if k != "_sa_instance_state" and k != "id" and k in data:
|
||||||
setattr(self, k, data[k])
|
setattr(self, k, data[k])
|
||||||
|
|
||||||
|
def descr_heure(self) -> str:
|
||||||
|
"Description de la plage horaire pour affichages"
|
||||||
|
if self.heure_debut and (
|
||||||
|
not self.heure_fin or self.heure_fin == self.heure_debut
|
||||||
|
):
|
||||||
|
return f"""à {self.heure_debut.strftime("%Hh%M")}"""
|
||||||
|
elif self.heure_debut and self.heure_fin:
|
||||||
|
return f"""de {self.heure_debut.strftime("%Hh%M")} à {self.heure_fin.strftime("%Hh%M")}"""
|
||||||
|
else:
|
||||||
|
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
|
||||||
|
@ -87,6 +144,29 @@ class Evaluation(db.Model):
|
||||||
db.session.add(copy)
|
db.session.add(copy)
|
||||||
return copy
|
return copy
|
||||||
|
|
||||||
|
def set_default_poids(self) -> bool:
|
||||||
|
"""Initialize les poids bvers les UE à leurs valeurs par défaut
|
||||||
|
C'est à dire à 1 si le coef. module/UE est non nul, 0 sinon.
|
||||||
|
Les poids existants ne sont pas modifiés.
|
||||||
|
Return True if (uncommited) modification, False otherwise.
|
||||||
|
"""
|
||||||
|
ue_coef_dict = self.moduleimpl.module.get_ue_coef_dict()
|
||||||
|
sem_ues = self.moduleimpl.formsemestre.query_ues(with_sport=False).all()
|
||||||
|
modified = False
|
||||||
|
for ue in sem_ues:
|
||||||
|
existing_poids = EvaluationUEPoids.query.filter_by(
|
||||||
|
ue=ue, evaluation=self
|
||||||
|
).first()
|
||||||
|
if existing_poids is None:
|
||||||
|
coef_ue = ue_coef_dict.get(ue.id, 0.0) or 0.0
|
||||||
|
if coef_ue > 0:
|
||||||
|
poids = 1.0 # par défaut au départ
|
||||||
|
else:
|
||||||
|
poids = 0.0
|
||||||
|
self.set_ue_poids(ue, poids)
|
||||||
|
modified = True
|
||||||
|
return modified
|
||||||
|
|
||||||
def set_ue_poids(self, ue, poids: float) -> None:
|
def set_ue_poids(self, ue, poids: float) -> None:
|
||||||
"""Set poids évaluation vers cette UE"""
|
"""Set poids évaluation vers cette UE"""
|
||||||
self.update_ue_poids_dict({ue.id: poids})
|
self.update_ue_poids_dict({ue.id: poids})
|
||||||
|
@ -99,7 +179,7 @@ class Evaluation(db.Model):
|
||||||
for ue_id, poids in ue_poids_dict.items():
|
for ue_id, poids in ue_poids_dict.items():
|
||||||
ue = UniteEns.query.get(ue_id)
|
ue = UniteEns.query.get(ue_id)
|
||||||
L.append(EvaluationUEPoids(evaluation=self, ue=ue, poids=poids))
|
L.append(EvaluationUEPoids(evaluation=self, ue=ue, poids=poids))
|
||||||
self.ue_poids = L
|
self.ue_poids = L # backref # pylint:disable=attribute-defined-outside-init
|
||||||
self.moduleimpl.invalidate_evaluations_poids() # inval cache
|
self.moduleimpl.invalidate_evaluations_poids() # inval cache
|
||||||
|
|
||||||
def update_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
def update_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
||||||
|
@ -108,8 +188,18 @@ class Evaluation(db.Model):
|
||||||
current.update(ue_poids_dict)
|
current.update(ue_poids_dict)
|
||||||
self.set_ue_poids_dict(current)
|
self.set_ue_poids_dict(current)
|
||||||
|
|
||||||
def get_ue_poids_dict(self) -> dict:
|
def get_ue_poids_dict(self, sort=False) -> dict:
|
||||||
"""returns { ue_id : poids }"""
|
"""returns { ue_id : poids }
|
||||||
|
Si sort, trie par UE
|
||||||
|
"""
|
||||||
|
if sort:
|
||||||
|
return {
|
||||||
|
p.ue.id: p.poids
|
||||||
|
for p in sorted(
|
||||||
|
self.ue_poids, key=lambda p: (p.ue.numero or 0, p.ue.acronyme)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return {p.ue.id: p.poids for p in self.ue_poids}
|
return {p.ue.id: p.poids for p in self.ue_poids}
|
||||||
|
|
||||||
def get_ue_poids_str(self) -> str:
|
def get_ue_poids_str(self) -> str:
|
||||||
|
@ -130,6 +220,12 @@ class Evaluation(db.Model):
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_etud_note(self, etud: Identite) -> NotesNotes:
|
||||||
|
"""La note de l'étudiant, ou None si pas noté.
|
||||||
|
(nb: pas de cache, lent, ne pas utiliser pour des calculs)
|
||||||
|
"""
|
||||||
|
return NotesNotes.query.filter_by(etudid=etud.id, evaluation_id=self.id).first()
|
||||||
|
|
||||||
|
|
||||||
class EvaluationUEPoids(db.Model):
|
class EvaluationUEPoids(db.Model):
|
||||||
"""Poids des évaluations (BUT)
|
"""Poids des évaluations (BUT)
|
||||||
|
@ -164,7 +260,7 @@ class EvaluationUEPoids(db.Model):
|
||||||
|
|
||||||
|
|
||||||
# Fonction héritée de ScoDoc7 à refactorer
|
# Fonction héritée de ScoDoc7 à refactorer
|
||||||
def evaluation_enrich_dict(e):
|
def evaluation_enrich_dict(e: dict):
|
||||||
"""add or convert some fields in an evaluation dict"""
|
"""add or convert some fields in an evaluation dict"""
|
||||||
# For ScoDoc7 compat
|
# For ScoDoc7 compat
|
||||||
heure_debut_dt = e["heure_debut"] or datetime.time(
|
heure_debut_dt = e["heure_debut"] or datetime.time(
|
||||||
|
@ -173,7 +269,7 @@ def evaluation_enrich_dict(e):
|
||||||
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:
|
||||||
|
|
|
@ -13,7 +13,6 @@ from app import email
|
||||||
from app import log
|
from app import log
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app.models import SHORT_STR_LEN
|
from app.models import SHORT_STR_LEN
|
||||||
from app.models.formsemestre import FormSemestre
|
|
||||||
from app.models.moduleimpls import ModuleImpl
|
from app.models.moduleimpls import ModuleImpl
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
|
@ -170,10 +169,12 @@ class ScolarNews(db.Model):
|
||||||
log(f"news: {news}")
|
log(f"news: {news}")
|
||||||
news.notify_by_mail()
|
news.notify_by_mail()
|
||||||
|
|
||||||
def get_news_formsemestre(self) -> FormSemestre:
|
def get_news_formsemestre(self) -> "FormSemestre":
|
||||||
"""formsemestre concerné par la nouvelle
|
"""formsemestre concerné par la nouvelle
|
||||||
None si inexistant
|
None si inexistant
|
||||||
"""
|
"""
|
||||||
|
from app.models.formsemestre import FormSemestre
|
||||||
|
|
||||||
formsemestre_id = None
|
formsemestre_id = None
|
||||||
if self.type == self.NEWS_INSCR:
|
if self.type == self.NEWS_INSCR:
|
||||||
formsemestre_id = self.object
|
formsemestre_id = self.object
|
||||||
|
@ -232,8 +233,7 @@ class ScolarNews(db.Model):
|
||||||
txt = re.sub(r'<a.*?href\s*=\s*"(.*?)".*?>(.*?)</a>', r"\2: \1", txt)
|
txt = re.sub(r'<a.*?href\s*=\s*"(.*?)".*?>(.*?)</a>', r"\2: \1", txt)
|
||||||
|
|
||||||
subject = "[ScoDoc] " + self.NEWS_MAP.get(self.type, "?")
|
subject = "[ScoDoc] " + self.NEWS_MAP.get(self.type, "?")
|
||||||
sender = prefs["email_from_addr"]
|
sender = email.get_from_addr()
|
||||||
|
|
||||||
email.send_email(subject, sender, destinations, txt)
|
email.send_email(subject, sender, destinations, txt)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -17,9 +17,9 @@ from app.models.modules import Module
|
||||||
from app.models.moduleimpls import ModuleImpl
|
from app.models.moduleimpls import ModuleImpl
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc import codes_cursus
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_codes_parcours import UE_STANDARD
|
from app.scodoc.codes_cursus import UE_STANDARD
|
||||||
|
|
||||||
|
|
||||||
class Formation(db.Model):
|
class Formation(db.Model):
|
||||||
|
@ -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()"),
|
||||||
|
@ -47,7 +48,7 @@ class Formation(db.Model):
|
||||||
|
|
||||||
# Optionnel, pour les formations type BUT
|
# Optionnel, pour les formations type BUT
|
||||||
referentiel_competence_id = db.Column(
|
referentiel_competence_id = db.Column(
|
||||||
db.Integer, db.ForeignKey("apc_referentiel_competences.id")
|
db.Integer, db.ForeignKey("apc_referentiel_competences.id", ondelete="SET NULL")
|
||||||
)
|
)
|
||||||
ues = db.relationship("UniteEns", backref="formation", lazy="dynamic")
|
ues = db.relationship("UniteEns", backref="formation", lazy="dynamic")
|
||||||
formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation")
|
formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation")
|
||||||
|
@ -55,26 +56,41 @@ 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):
|
def to_dict(self, with_refcomp_attrs=False, with_departement=True):
|
||||||
|
"""As a dict.
|
||||||
|
Si with_refcomp_attrs, ajoute attributs permettant de retrouver le ref. de comp.
|
||||||
|
"""
|
||||||
e = dict(self.__dict__)
|
e = dict(self.__dict__)
|
||||||
e.pop("_sa_instance_state", None)
|
e.pop("_sa_instance_state", None)
|
||||||
e["departement"] = self.departement.to_dict()
|
if "referentiel_competence" in e:
|
||||||
# ScoDoc7 output_formators: (backward compat)
|
e.pop("referentiel_competence")
|
||||||
e["formation_id"] = self.id
|
e["code_specialite"] = e["code_specialite"] or ""
|
||||||
|
e["commentaire"] = e["commentaire"] or ""
|
||||||
|
if with_departement and self.departement:
|
||||||
|
e["departement"] = self.departement.to_dict()
|
||||||
|
else:
|
||||||
|
e.pop("departement", None)
|
||||||
|
e["formation_id"] = self.id # ScoDoc7 backward compat
|
||||||
|
if with_refcomp_attrs and self.referentiel_competence:
|
||||||
|
e["refcomp_version_orebut"] = self.referentiel_competence.version_orebut
|
||||||
|
e["refcomp_specialite"] = self.referentiel_competence.specialite
|
||||||
|
e["refcomp_type_titre"] = self.referentiel_competence.type_titre
|
||||||
|
|
||||||
return e
|
return e
|
||||||
|
|
||||||
def get_parcours(self):
|
def get_cursus(self) -> codes_cursus.TypeCursus:
|
||||||
"""get l'instance de TypeParcours de cette formation
|
"""get l'instance de TypeCursus de cette formation
|
||||||
(le TypeParcours définit le genre de formation, à ne pas confondre
|
(le TypeCursus définit le genre de formation, à ne pas confondre
|
||||||
avec les parcours du BUT).
|
avec les parcours du BUT).
|
||||||
"""
|
"""
|
||||||
return sco_codes_parcours.get_parcours_from_code(self.type_parcours)
|
return codes_cursus.get_cursus_from_code(self.type_parcours)
|
||||||
|
|
||||||
def get_titre_version(self) -> str:
|
def get_titre_version(self) -> str:
|
||||||
"""Titre avec version"""
|
"""Titre avec version"""
|
||||||
|
@ -82,7 +98,7 @@ class Formation(db.Model):
|
||||||
|
|
||||||
def is_apc(self):
|
def is_apc(self):
|
||||||
"True si formation APC avec SAE (BUT)"
|
"True si formation APC avec SAE (BUT)"
|
||||||
return self.get_parcours().APC_SAE
|
return self.get_cursus().APC_SAE
|
||||||
|
|
||||||
def get_module_coefs(self, semestre_idx: int = None):
|
def get_module_coefs(self, semestre_idx: int = None):
|
||||||
"""Les coefs des modules vers les UE (accès via cache)"""
|
"""Les coefs des modules vers les UE (accès via cache)"""
|
||||||
|
@ -101,9 +117,14 @@ class Formation(db.Model):
|
||||||
df_cache.ModuleCoefsCache.set(key, modules_coefficients)
|
df_cache.ModuleCoefsCache.set(key, modules_coefficients)
|
||||||
return modules_coefficients
|
return modules_coefficients
|
||||||
|
|
||||||
def has_locked_sems(self):
|
def has_locked_sems(self, semestre_idx: int = None):
|
||||||
"True if there is a locked formsemestre in this formation"
|
"""True if there is a locked formsemestre in this formation.
|
||||||
return len(self.formsemestres.filter_by(etat=False).all()) > 0
|
If semestre_idx is specified, check only this index.
|
||||||
|
"""
|
||||||
|
query = self.formsemestres.filter_by(etat=False)
|
||||||
|
if semestre_idx is not None:
|
||||||
|
query = query.filter_by(semestre_id=semestre_idx)
|
||||||
|
return len(query.all()) > 0
|
||||||
|
|
||||||
def invalidate_module_coefs(self, semestre_idx: int = None):
|
def invalidate_module_coefs(self, semestre_idx: int = None):
|
||||||
"""Invalide le cache des coefficients de modules.
|
"""Invalide le cache des coefficients de modules.
|
||||||
|
@ -194,12 +215,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,
|
||||||
|
@ -226,6 +252,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
|
||||||
|
@ -253,6 +294,6 @@ class Matiere(db.Model):
|
||||||
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
|
||||||
e["ue_id"] = self.id
|
|
||||||
e["numero"] = e["numero"] if e["numero"] else 0
|
e["numero"] = e["numero"] if e["numero"] else 0
|
||||||
|
e["ue_id"] = self.id
|
||||||
return e
|
return e
|
||||||
|
|
|
@ -1,45 +1,49 @@
|
||||||
# -*- 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
|
from flask_login import current_user
|
||||||
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.auth.models import User
|
||||||
from app.models import SHORT_STR_LEN
|
from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, 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
|
||||||
|
|
||||||
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.groups import GroupDescr, Partition
|
||||||
|
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
|
||||||
from app.models.modules import Module
|
from app.models.modules import Module
|
||||||
from app.models.moduleimpls import ModuleImpl
|
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
|
from app.models.validations import ScolarFormSemestreValidation
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc import codes_cursus, sco_preferences
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
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):
|
||||||
|
@ -54,51 +58,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(
|
||||||
|
@ -108,6 +119,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",
|
||||||
|
@ -145,7 +157,21 @@ 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()}>"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_formsemestre(cls, formsemestre_id: int) -> "FormSemestre":
|
||||||
|
""" "FormSemestre ou 404, cherche uniquement dans le département courant"""
|
||||||
|
if g.scodoc_dept:
|
||||||
|
return cls.query.filter_by(
|
||||||
|
id=formsemestre_id, dept_id=g.scodoc_dept_id
|
||||||
|
).first_or_404()
|
||||||
|
return cls.query.filter_by(id=formsemestre_id).first_or_404()
|
||||||
|
|
||||||
|
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).
|
||||||
|
@ -170,7 +196,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()
|
||||||
|
@ -197,9 +223,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
|
||||||
|
@ -219,7 +246,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:
|
||||||
|
@ -238,17 +266,41 @@ 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 flip_lock(self):
|
||||||
|
"""Flip etat (lock)"""
|
||||||
|
self.etat = not self.etat
|
||||||
|
db.session.add(self)
|
||||||
|
|
||||||
|
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_cursus().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,
|
||||||
|
@ -256,12 +308,15 @@ class FormSemestre(db.Model):
|
||||||
UniteEns.id == Module.ue_id,
|
UniteEns.id == Module.ue_id,
|
||||||
)
|
)
|
||||||
if not with_sport:
|
if not with_sport:
|
||||||
sem_ues = sem_ues.filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
|
sem_ues = sem_ues.filter(UniteEns.type != codes_cursus.UE_SPORT)
|
||||||
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)`.
|
||||||
|
@ -272,7 +327,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
|
||||||
|
@ -285,7 +346,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,
|
||||||
)
|
)
|
||||||
|
@ -324,7 +385,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
|
||||||
|
@ -338,7 +399,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
|
||||||
|
@ -351,29 +412,105 @@ 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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def est_terminal(self) -> bool:
|
||||||
|
"Vrai si dernier semestre de son cursus (ou formation mono-semestre)"
|
||||||
|
return (self.semestre_id < 0) or (
|
||||||
|
self.semestre_id == self.formation.get_cursus().NB_SEM
|
||||||
|
)
|
||||||
|
|
||||||
|
@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]:
|
||||||
|
@ -420,13 +557,35 @@ class FormSemestre(db.Model):
|
||||||
else:
|
else:
|
||||||
return ", ".join([u.get_nomcomplet() for u in self.responsables])
|
return ", ".join([u.get_nomcomplet() for u in self.responsables])
|
||||||
|
|
||||||
def est_responsable(self, user):
|
def est_responsable(self, user: User):
|
||||||
"True si l'user est l'un des responsables du semestre"
|
"True si l'user est l'un des responsables du semestre"
|
||||||
return user.id in [u.id for u in self.responsables]
|
return user.id in [u.id for u in self.responsables]
|
||||||
|
|
||||||
|
def est_chef_or_diretud(self, user: User = None):
|
||||||
|
"Vrai si utilisateur (par def. current) est admin, chef dept ou responsable du semestre"
|
||||||
|
user = user or current_user
|
||||||
|
return user.has_permission(Permission.ScoImplement) or self.est_responsable(
|
||||||
|
user
|
||||||
|
)
|
||||||
|
|
||||||
|
def can_edit_jury(self, user: User = None):
|
||||||
|
"""Vrai si utilisateur (par def. current) peut saisir decision de jury
|
||||||
|
dans ce semestre: vérifie permission et verrouillage.
|
||||||
|
"""
|
||||||
|
user = user or current_user
|
||||||
|
return self.etat and self.est_chef_or_diretud(user)
|
||||||
|
|
||||||
|
def can_edit_pv(self, user: User = None):
|
||||||
|
"Vrai si utilisateur (par def. current) peut editer un PV de jury de ce semestre"
|
||||||
|
user = user or current_user
|
||||||
|
# Autorise les secrétariats, repérés via la permission ScoEtudChangeAdr
|
||||||
|
return self.est_chef_or_diretud(user) or user.has_permission(
|
||||||
|
Permission.ScoEtudChangeAdr
|
||||||
|
)
|
||||||
|
|
||||||
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):
|
||||||
|
@ -459,7 +618,7 @@ class FormSemestre(db.Model):
|
||||||
if not imputation_dept:
|
if not imputation_dept:
|
||||||
imputation_dept = prefs["DeptName"]
|
imputation_dept = prefs["DeptName"]
|
||||||
imputation_dept = imputation_dept.upper()
|
imputation_dept = imputation_dept.upper()
|
||||||
parcours_name = self.formation.get_parcours().NAME
|
cursus_name = self.formation.get_cursus().NAME
|
||||||
modalite = self.modalite
|
modalite = self.modalite
|
||||||
# exception pour code Apprentissage:
|
# exception pour code Apprentissage:
|
||||||
modalite = (modalite or "").replace("FAP", "FA").replace("APP", "FA")
|
modalite = (modalite or "").replace("FAP", "FA").replace("APP", "FA")
|
||||||
|
@ -472,11 +631,13 @@ class FormSemestre(db.Model):
|
||||||
scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month)
|
scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month)
|
||||||
)
|
)
|
||||||
return scu.sanitize_string(
|
return scu.sanitize_string(
|
||||||
f"{imputation_dept}-{parcours_name}-{modalite}-{semestre_id}-{annee_sco}"
|
f"{imputation_dept}-{cursus_name}-{modalite}-{semestre_id}-{annee_sco}"
|
||||||
)
|
)
|
||||||
|
|
||||||
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}"
|
||||||
)
|
)
|
||||||
|
@ -484,10 +645,12 @@ class FormSemestre(db.Model):
|
||||||
titre_annee += "-" + str(self.date_fin.year)
|
titre_annee += "-" + str(self.date_fin.year)
|
||||||
return titre_annee
|
return titre_annee
|
||||||
|
|
||||||
def titre_formation(self):
|
def titre_formation(self, with_sem_idx=False):
|
||||||
"""Titre avec formation, court, pour passerelle: "BUT R&T"
|
"""Titre avec formation, court, pour passerelle: "BUT R&T"
|
||||||
(méthode de formsemestre car on pourrait ajouter le semestre, ou d'autres infos, à voir)
|
(méthode de formsemestre car on pourrait ajouter le semestre, ou d'autres infos, à voir)
|
||||||
"""
|
"""
|
||||||
|
if with_sem_idx and self.semestre_id > 0:
|
||||||
|
return f"{self.formation.acronyme} S{self.semestre_id}"
|
||||||
return self.formation.acronyme
|
return self.formation.acronyme
|
||||||
|
|
||||||
def titre_mois(self) -> str:
|
def titre_mois(self) -> str:
|
||||||
|
@ -502,9 +665,9 @@ class FormSemestre(db.Model):
|
||||||
|
|
||||||
def titre_num(self) -> str:
|
def titre_num(self) -> str:
|
||||||
"""Le titre et le semestre, ex ""DUT Informatique semestre 2"" """
|
"""Le titre et le semestre, ex ""DUT Informatique semestre 2"" """
|
||||||
if self.semestre_id == sco_codes_parcours.NO_SEMESTRE_ID:
|
if self.semestre_id == codes_cursus.NO_SEMESTRE_ID:
|
||||||
return self.titre
|
return self.titre
|
||||||
return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}"
|
return f"{self.titre} {self.formation.get_cursus().SESSION_NAME} {self.semestre_id}"
|
||||||
|
|
||||||
def sem_modalite(self) -> str:
|
def sem_modalite(self) -> str:
|
||||||
"""Le semestre et la modalité, ex "S2 FI" ou "S3 APP" """
|
"""Le semestre et la modalité, ex "S2 FI" ou "S3 APP" """
|
||||||
|
@ -582,14 +745,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:
|
||||||
|
@ -648,6 +840,71 @@ class FormSemestre(db.Model):
|
||||||
)
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
def etud_validations_description_html(self, etudid: int) -> str:
|
||||||
|
"""Description textuelle des validations de jury de cet étudiant dans ce semestre"""
|
||||||
|
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||||
|
|
||||||
|
vals_sem = ScolarFormSemestreValidation.query.filter_by(
|
||||||
|
etudid=etudid, formsemestre_id=self.id, ue_id=None
|
||||||
|
).all()
|
||||||
|
vals_ues = (
|
||||||
|
ScolarFormSemestreValidation.query.filter_by(
|
||||||
|
etudid=etudid, formsemestre_id=self.id
|
||||||
|
)
|
||||||
|
.join(UniteEns)
|
||||||
|
.order_by(UniteEns.numero)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
# Validations BUT:
|
||||||
|
vals_rcues = (
|
||||||
|
ApcValidationRCUE.query.filter_by(etudid=etudid, formsemestre_id=self.id)
|
||||||
|
.join(UniteEns, ApcValidationRCUE.ue1)
|
||||||
|
.order_by(UniteEns.numero)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
vals_annee = (
|
||||||
|
ApcValidationAnnee.query.filter_by(
|
||||||
|
etudid=etudid,
|
||||||
|
annee_scolaire=self.annee_scolaire(),
|
||||||
|
)
|
||||||
|
.join(ApcValidationAnnee.formsemestre)
|
||||||
|
.join(FormSemestre.formation)
|
||||||
|
.filter(Formation.formation_code == self.formation.formation_code)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
H = []
|
||||||
|
for vals in (vals_sem, vals_ues, vals_rcues, vals_annee):
|
||||||
|
if vals:
|
||||||
|
H.append(
|
||||||
|
f"""<ul><li>{"</li><li>".join(str(x) for x in vals)}</li></ul>"""
|
||||||
|
)
|
||||||
|
return "\n".join(H)
|
||||||
|
|
||||||
|
def etud_set_all_missing_notes(self, etud: Identite, value=None) -> int:
|
||||||
|
"""Met toutes les notes manquantes de cet étudiant dans ce semestre
|
||||||
|
(ie dans toutes les évaluations des modules auxquels il est inscrit et n'a pas de note)
|
||||||
|
à la valeur donnée par value, qui est en général "ATT", "ABS", "EXC".
|
||||||
|
"""
|
||||||
|
from app.scodoc import sco_saisie_notes
|
||||||
|
|
||||||
|
inscriptions = (
|
||||||
|
ModuleImplInscription.query.filter_by(etudid=etud.id)
|
||||||
|
.join(ModuleImpl)
|
||||||
|
.filter_by(formsemestre_id=self.id)
|
||||||
|
)
|
||||||
|
nb_recorded = 0
|
||||||
|
for inscription in inscriptions:
|
||||||
|
for evaluation in inscription.modimpl.evaluations:
|
||||||
|
if evaluation.get_etud_note(etud) is None:
|
||||||
|
if not sco_saisie_notes.do_evaluation_set_etud_note(
|
||||||
|
evaluation, etud, value
|
||||||
|
):
|
||||||
|
raise ScoValueError(
|
||||||
|
"erreur lors de l'enregistrement de la note"
|
||||||
|
)
|
||||||
|
nb_recorded += 1
|
||||||
|
return nb_recorded
|
||||||
|
|
||||||
|
|
||||||
# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
|
# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
|
||||||
notes_formsemestre_responsables = db.Table(
|
notes_formsemestre_responsables = db.Table(
|
||||||
|
@ -826,7 +1083,9 @@ class FormSemestreInscription(db.Model):
|
||||||
# Etape Apogée d'inscription (ajout 2020)
|
# Etape Apogée d'inscription (ajout 2020)
|
||||||
etape = db.Column(db.String(APO_CODE_STR_LEN))
|
etape = db.Column(db.String(APO_CODE_STR_LEN))
|
||||||
# Parcours (pour les BUT)
|
# Parcours (pour les BUT)
|
||||||
parcour_id = db.Column(db.Integer, db.ForeignKey("apc_parcours.id"), index=True)
|
parcour_id = db.Column(
|
||||||
|
db.Integer, db.ForeignKey("apc_parcours.id", ondelete="SET NULL"), index=True
|
||||||
|
)
|
||||||
parcour = db.relationship(ApcParcours)
|
parcour = db.relationship(ApcParcours)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
@ -846,8 +1105,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
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ class Partition(db.Model):
|
||||||
"""
|
"""
|
||||||
if not isinstance(partition_name, str):
|
if not isinstance(partition_name, str):
|
||||||
return False
|
return False
|
||||||
if not len(partition_name.strip()) > 0:
|
if not (0 < len(partition_name.strip()) < SHORT_STR_LEN):
|
||||||
return False
|
return False
|
||||||
if (not existing) and (
|
if (not existing) and (
|
||||||
partition_name in [p.partition_name for p in formsemestre.partitions]
|
partition_name in [p.partition_name for p in formsemestre.partitions]
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -146,7 +147,7 @@ class GroupDescr(db.Model):
|
||||||
"""
|
"""
|
||||||
if not isinstance(group_name, str):
|
if not isinstance(group_name, str):
|
||||||
return False
|
return False
|
||||||
if not default and not len(group_name.strip()) > 0:
|
if not default and not (0 < len(group_name.strip()) < GROUPNAME_STR_LEN):
|
||||||
return False
|
return False
|
||||||
if (not existing) and (group_name in [g.group_name for g in partition.groups]):
|
if (not existing) and (group_name in [g.group_name for g in partition.groups]):
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -5,10 +5,12 @@ import pandas as pd
|
||||||
import flask_sqlalchemy
|
import flask_sqlalchemy
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
|
from app.auth.models import User
|
||||||
from app.comp import df_cache
|
from app.comp import df_cache
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.modules import Module
|
from app.models.modules import Module
|
||||||
|
from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError
|
||||||
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,14 +22,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,11 +62,11 @@ 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.
|
||||||
"""
|
"""
|
||||||
if not self.module.formation.get_parcours().APC_SAE or (
|
if not self.module.formation.get_cursus().APC_SAE or (
|
||||||
self.module.module_type != scu.ModuleType.RESSOURCE
|
self.module.module_type != scu.ModuleType.RESSOURCE
|
||||||
and self.module.module_type != scu.ModuleType.SAE
|
and self.module.module_type != scu.ModuleType.SAE
|
||||||
):
|
):
|
||||||
|
@ -76,7 +76,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 +101,64 @@ class ModuleImpl(db.Model):
|
||||||
d.pop("module", None)
|
d.pop("module", None)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def can_change_ens_by(self, user: User, raise_exc=False) -> bool:
|
||||||
|
"""Check if user can modify module resp.
|
||||||
|
If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not.
|
||||||
|
= Admin, et dir des etud. (si option l'y autorise)
|
||||||
|
"""
|
||||||
|
if not self.formsemestre.etat:
|
||||||
|
if raise_exc:
|
||||||
|
raise ScoLockedSemError("Modification impossible: semestre verrouille")
|
||||||
|
return False
|
||||||
|
# -- check access
|
||||||
|
# admin ou resp. semestre avec flag resp_can_change_resp
|
||||||
|
if user.has_permission(Permission.ScoImplement):
|
||||||
|
return True
|
||||||
|
if (
|
||||||
|
user.id in [resp.id for resp in self.formsemestre.responsables]
|
||||||
|
) and self.formsemestre.resp_can_change_ens:
|
||||||
|
return True
|
||||||
|
if raise_exc:
|
||||||
|
raise AccessDenied(f"Modification impossible pour {user}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def can_change_ens_by(self, user: User, raise_exc=False) -> bool:
|
||||||
|
"""Check if user can modify module resp.
|
||||||
|
If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not.
|
||||||
|
= Admin, et dir des etud. (si option l'y autorise)
|
||||||
|
"""
|
||||||
|
if not self.formsemestre.etat:
|
||||||
|
if raise_exc:
|
||||||
|
raise ScoLockedSemError("Modification impossible: semestre verrouille")
|
||||||
|
return False
|
||||||
|
# -- check access
|
||||||
|
# admin ou resp. semestre avec flag resp_can_change_resp
|
||||||
|
if user.has_permission(Permission.ScoImplement):
|
||||||
|
return True
|
||||||
|
if (
|
||||||
|
user.id in [resp.id for resp in self.formsemestre.responsables]
|
||||||
|
) and self.formsemestre.resp_can_change_ens:
|
||||||
|
return True
|
||||||
|
if raise_exc:
|
||||||
|
raise AccessDenied(f"Modification impossible pour {user}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# 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(
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
"""ScoDoc 9 models : Modules
|
"""ScoDoc 9 models : Modules
|
||||||
"""
|
"""
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
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.codes_cursus import UE_SPORT
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,7 +39,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 +70,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
|
||||||
|
@ -141,6 +177,11 @@ class Module(db.Model):
|
||||||
ue_coef_dict = { ue_id : coef }
|
ue_coef_dict = { ue_id : coef }
|
||||||
Les coefs nuls (zéro) ne sont pas stockés: la relation est supprimée.
|
Les coefs nuls (zéro) ne sont pas stockés: la relation est supprimée.
|
||||||
"""
|
"""
|
||||||
|
if self.formation.has_locked_sems(self.ue.semestre_idx):
|
||||||
|
current_app.logguer.info(
|
||||||
|
f"set_ue_coef_dict: locked formation, ignoring request"
|
||||||
|
)
|
||||||
|
raise ScoValueError("Formation verrouillée")
|
||||||
changed = False
|
changed = False
|
||||||
for ue_id, coef in ue_coef_dict.items():
|
for ue_id, coef in ue_coef_dict.items():
|
||||||
# Existant ?
|
# Existant ?
|
||||||
|
@ -167,6 +208,11 @@ class Module(db.Model):
|
||||||
|
|
||||||
def update_ue_coef_dict(self, ue_coef_dict: dict):
|
def update_ue_coef_dict(self, ue_coef_dict: dict):
|
||||||
"""update coefs vers UE (ajoute aux existants)"""
|
"""update coefs vers UE (ajoute aux existants)"""
|
||||||
|
if self.formation.has_locked_sems(self.ue.semestre_idx):
|
||||||
|
current_app.logguer.info(
|
||||||
|
f"update_ue_coef_dict: locked formation, ignoring request"
|
||||||
|
)
|
||||||
|
raise ScoValueError("Formation verrouillée")
|
||||||
current = self.get_ue_coef_dict()
|
current = self.get_ue_coef_dict()
|
||||||
current.update(ue_coef_dict)
|
current.update(ue_coef_dict)
|
||||||
self.set_ue_coef_dict(current)
|
self.set_ue_coef_dict(current)
|
||||||
|
@ -175,8 +221,17 @@ class Module(db.Model):
|
||||||
"""returns { ue_id : coef }"""
|
"""returns { ue_id : coef }"""
|
||||||
return {p.ue.id: p.coef for p in self.ue_coefs}
|
return {p.ue.id: p.coef for p in self.ue_coefs}
|
||||||
|
|
||||||
|
def get_ue_coef_dict_acronyme(self):
|
||||||
|
"""returns { ue_acronyme : coef }"""
|
||||||
|
return {p.ue.acronyme: p.coef for p in self.ue_coefs}
|
||||||
|
|
||||||
def delete_ue_coef(self, ue):
|
def delete_ue_coef(self, ue):
|
||||||
"""delete coef"""
|
"""delete coef"""
|
||||||
|
if self.formation.has_locked_sems(self.ue.semestre_idx):
|
||||||
|
current_app.logguer.info(
|
||||||
|
f"delete_ue_coef: locked formation, ignoring request"
|
||||||
|
)
|
||||||
|
raise ScoValueError("Formation verrouillée")
|
||||||
ue_coef = ModuleUECoef.query.get((self.id, ue.id))
|
ue_coef = ModuleUECoef.query.get((self.id, ue.id))
|
||||||
if ue_coef:
|
if ue_coef:
|
||||||
db.session.delete(ue_coef)
|
db.session.delete(ue_coef)
|
||||||
|
@ -188,25 +243,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 +279,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)
|
||||||
|
|
|
@ -4,8 +4,6 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
|
|
||||||
import app.scodoc.notesdb as ndb
|
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
|
@ -53,6 +51,13 @@ class NotesNotes(db.Model):
|
||||||
d.pop("_sa_instance_state", None)
|
d.pop("_sa_instance_state", None)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
"pour debug"
|
||||||
|
from app.models.evaluations import Evaluation
|
||||||
|
|
||||||
|
return f"""<{self.__class__.__name__} {self.id} v={self.value} {self.date.isoformat()
|
||||||
|
} {Evaluation.query.get(self.evaluation_id) if self.evaluation_id else "X" }>"""
|
||||||
|
|
||||||
|
|
||||||
class NotesNotesLog(db.Model):
|
class NotesNotesLog(db.Model):
|
||||||
"""Historique des modifs sur notes (anciennes entrees de notes_notes)"""
|
"""Historique des modifs sur notes (anciennes entrees de notes_notes)"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -46,12 +51,26 @@ class UniteEns(db.Model):
|
||||||
color = db.Column(db.Text())
|
color = db.Column(db.Text())
|
||||||
|
|
||||||
# BUT
|
# BUT
|
||||||
niveau_competence_id = db.Column(db.Integer, db.ForeignKey("apc_niveau.id"))
|
niveau_competence_id = db.Column(
|
||||||
|
db.Integer, db.ForeignKey("apc_niveau.id", ondelete="SET NULL")
|
||||||
|
)
|
||||||
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", ondelete="SET NULL"), 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 +78,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 +115,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 +125,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 +183,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
|
||||||
|
@ -53,11 +54,21 @@ class ScolarFormSemestreValidation(db.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
ue = db.relationship("UniteEns", lazy="select", uselist=False)
|
ue = db.relationship("UniteEns", lazy="select", uselist=False)
|
||||||
|
formsemestre = db.relationship(
|
||||||
|
"FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id]
|
||||||
|
)
|
||||||
|
|
||||||
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):
|
||||||
|
if self.ue_id:
|
||||||
|
# Note: si l'objet vient d'être créé, ue_id peut exister mais pas ue !
|
||||||
|
return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id}: {self.code}"""
|
||||||
|
return f"""décision sur semestre {self.formsemestre.titre_mois()} du {self.event_date.strftime("%d/%m/%Y")}"""
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
|
"as a dict"
|
||||||
d = dict(self.__dict__)
|
d = dict(self.__dict__)
|
||||||
d.pop("_sa_instance_state", None)
|
d.pop("_sa_instance_state", None)
|
||||||
return d
|
return d
|
||||||
|
@ -83,7 +94,12 @@ 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"
|
||||||
d = dict(self.__dict__)
|
d = dict(self.__dict__)
|
||||||
d.pop("_sa_instance_state", None)
|
d.pop("_sa_instance_state", None)
|
||||||
return d
|
return d
|
||||||
|
@ -96,8 +112,7 @@ class ScolarAutorisationInscription(db.Model):
|
||||||
origin_formsemestre_id: int,
|
origin_formsemestre_id: int,
|
||||||
semestre_id: int,
|
semestre_id: int,
|
||||||
):
|
):
|
||||||
"""Enregistre une autorisation, remplace celle émanant du même semestre si elle existe."""
|
"""Ajoute une autorisation"""
|
||||||
cls.delete_autorisation_etud(etudid, origin_formsemestre_id)
|
|
||||||
autorisation = cls(
|
autorisation = cls(
|
||||||
etudid=etudid,
|
etudid=etudid,
|
||||||
formation_code=formation_code,
|
formation_code=formation_code,
|
||||||
|
@ -105,7 +120,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(
|
||||||
|
@ -113,16 +131,17 @@ class ScolarAutorisationInscription(db.Model):
|
||||||
etudid: int,
|
etudid: int,
|
||||||
origin_formsemestre_id: int,
|
origin_formsemestre_id: int,
|
||||||
):
|
):
|
||||||
"""Efface les autorisations de cette étudiant venant du sem. origine"""
|
"""Efface les autorisations de cet étudiant venant du sem. origine"""
|
||||||
autorisations = cls.query.filter_by(
|
autorisations = cls.query.filter_by(
|
||||||
etudid=etudid, origin_formsemestre_id=origin_formsemestre_id
|
etudid=etudid, origin_formsemestre_id=origin_formsemestre_id
|
||||||
)
|
)
|
||||||
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()
|
||||||
|
|
||||||
|
@ -140,11 +159,11 @@ class ScolarEvent(db.Model):
|
||||||
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
formsemestre_id = db.Column(
|
formsemestre_id = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey("notes_formsemestre.id"),
|
db.ForeignKey("notes_formsemestre.id", ondelete="SET NULL"),
|
||||||
)
|
)
|
||||||
ue_id = db.Column(
|
ue_id = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey("notes_ue.id"),
|
db.ForeignKey("notes_ue.id", ondelete="SET NULL"),
|
||||||
)
|
)
|
||||||
# 'CREATION', 'INSCRIPTION', 'DEMISSION',
|
# 'CREATION', 'INSCRIPTION', 'DEMISSION',
|
||||||
# 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM'
|
# 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM'
|
||||||
|
@ -156,8 +175,16 @@ class ScolarEvent(db.Model):
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey("notes_formsemestre.id"),
|
db.ForeignKey("notes_formsemestre.id"),
|
||||||
)
|
)
|
||||||
|
etud = db.relationship("Identite", lazy="select", backref="events", uselist=False)
|
||||||
|
formsemestre = db.relationship(
|
||||||
|
"FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id]
|
||||||
|
)
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
|
"as a dict"
|
||||||
d = dict(self.__dict__)
|
d = dict(self.__dict__)
|
||||||
d.pop("_sa_instance_state", None)
|
d.pop("_sa_instance_state", None)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.__class__.__name__}({self.event_type}, {self.event_date.isoformat()}, {self.formsemestre})"
|
||||||
|
|
|
@ -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
|
||||||
|
@ -253,7 +253,7 @@ def get_annotation_PE(etudid, tag_annotation_pe):
|
||||||
) # Suppression du tag d'annotation PE
|
) # Suppression du tag d'annotation PE
|
||||||
annotationPE = annotationPE.replace("\r", "") # Suppression des \r
|
annotationPE = annotationPE.replace("\r", "") # Suppression des \r
|
||||||
annotationPE = annotationPE.replace(
|
annotationPE = annotationPE.replace(
|
||||||
"<br/>", "\n\n"
|
"<br>", "\n\n"
|
||||||
) # Interprète les retours chariots html
|
) # Interprète les retours chariots html
|
||||||
return annotationPE
|
return annotationPE
|
||||||
return "" # pas d'annotations
|
return "" # pas d'annotations
|
||||||
|
|
|
@ -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,11 +48,11 @@ from zipfile import ZipFile
|
||||||
|
|
||||||
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 FormSemestre
|
from app.models import Formation, FormSemestre
|
||||||
|
|
||||||
from app.scodoc.gen_tables import GenTable, SeqGenTable
|
from app.scodoc.gen_tables import GenTable, SeqGenTable
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc import sco_codes_parcours # sco_codes_parcours.NEXT -> sem suivant
|
from app.scodoc import codes_cursus # codes_cursus.NEXT -> sem suivant
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
from app.pe import pe_tagtable
|
from app.pe import pe_tagtable
|
||||||
|
@ -65,10 +65,8 @@ def comp_nom_semestre_dans_parcours(sem):
|
||||||
"""Le nom a afficher pour titrer un semestre
|
"""Le nom a afficher pour titrer un semestre
|
||||||
par exemple: "semestre 2 FI 2015"
|
par exemple: "semestre 2 FI 2015"
|
||||||
"""
|
"""
|
||||||
from app.scodoc import sco_formations
|
formation: Formation = Formation.query.get_or_404(sem["formation_id"])
|
||||||
|
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
|
||||||
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
|
|
||||||
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
|
|
||||||
return "%s %s %s %s" % (
|
return "%s %s %s %s" % (
|
||||||
parcours.SESSION_NAME, # eg "semestre"
|
parcours.SESSION_NAME, # eg "semestre"
|
||||||
sem["semestre_id"], # eg 2
|
sem["semestre_id"], # eg 2
|
||||||
|
@ -457,10 +455,9 @@ class JuryPE(object):
|
||||||
|
|
||||||
reponse = False
|
reponse = False
|
||||||
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_code_cursus_etud(etud)
|
||||||
if (
|
if (
|
||||||
len(set(sco_codes_parcours.CODES_SEM_REO.keys()) & set(parcours.values()))
|
len(codes_cursus.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:
|
||||||
|
@ -529,14 +526,14 @@ class JuryPE(object):
|
||||||
from app.scodoc import sco_report
|
from app.scodoc import sco_report
|
||||||
|
|
||||||
etud = self.get_cache_etudInfo_d_un_etudiant(etudid)
|
etud = self.get_cache_etudInfo_d_un_etudiant(etudid)
|
||||||
(code, parcours) = sco_report.get_codeparcoursetud(
|
(code, parcours) = sco_report.get_code_cursus_etud(
|
||||||
etud
|
etud
|
||||||
) # description = '1234:A', parcours = {1:ADM, 2:NAR, ...}
|
) # description = '1234:A', parcours = {1:ADM, 2:NAR, ...}
|
||||||
sonDernierSemestreValide = max(
|
sonDernierSemestreValide = max(
|
||||||
[
|
[
|
||||||
int(cle)
|
int(cle)
|
||||||
for (cle, code) in parcours.items()
|
for (cle, code) in parcours.items()
|
||||||
if code in sco_codes_parcours.CODES_SEM_VALIDES
|
if code in codes_cursus.CODES_SEM_VALIDES
|
||||||
]
|
]
|
||||||
+ [0]
|
+ [0]
|
||||||
) # n° du dernier semestre valide, 0 sinon
|
) # n° du dernier semestre valide, 0 sinon
|
||||||
|
@ -563,9 +560,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 codes_cursus.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:
|
||||||
|
@ -1139,7 +1135,7 @@ class JuryPE(object):
|
||||||
# ------------------------------------------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------------------------------------------
|
||||||
def get_cache_notes_d_un_semestre(self, formsemestre_id: int) -> NotesTableCompat:
|
def get_cache_notes_d_un_semestre(self, formsemestre_id: int) -> NotesTableCompat:
|
||||||
"""Charge la table des notes d'un formsemestre"""
|
"""Charge la table des notes d'un formsemestre"""
|
||||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
return res_sem.load_formsemestre_results(formsemestre)
|
return res_sem.load_formsemestre_results(formsemestre)
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
|
@ -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
|
||||||
|
@ -41,11 +41,12 @@ from app.comp import res_sem
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import FormSemestre
|
from app.models import FormSemestre
|
||||||
from app.models.moduleimpls import ModuleImpl
|
from app.models.moduleimpls import ModuleImpl
|
||||||
|
|
||||||
from app.scodoc import sco_codes_parcours
|
|
||||||
from app.scodoc import sco_tag_module
|
|
||||||
from app.pe import pe_tagtable
|
from app.pe import pe_tagtable
|
||||||
|
|
||||||
|
from app.scodoc import codes_cursus
|
||||||
|
from app.scodoc import sco_tag_module
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
class SemestreTag(pe_tagtable.TableTag):
|
class SemestreTag(pe_tagtable.TableTag):
|
||||||
"""Un SemestreTag représente un tableau de notes (basé sur notesTable)
|
"""Un SemestreTag représente un tableau de notes (basé sur notesTable)
|
||||||
|
@ -103,7 +104,7 @@ class SemestreTag(pe_tagtable.TableTag):
|
||||||
self.inscrlist = [
|
self.inscrlist = [
|
||||||
etud
|
etud
|
||||||
for etud in self.nt.inscrlist
|
for etud in self.nt.inscrlist
|
||||||
if self.nt.get_etud_etat(etud["etudid"]) == "I"
|
if self.nt.get_etud_etat(etud["etudid"]) == scu.INSCRIT
|
||||||
]
|
]
|
||||||
self.identdict = {
|
self.identdict = {
|
||||||
etudid: ident
|
etudid: ident
|
||||||
|
@ -115,7 +116,7 @@ class SemestreTag(pe_tagtable.TableTag):
|
||||||
self.modimpls = [
|
self.modimpls = [
|
||||||
modimpl
|
modimpl
|
||||||
for modimpl in self.nt.formsemestre.modimpls_sorted
|
for modimpl in self.nt.formsemestre.modimpls_sorted
|
||||||
if modimpl.module.ue.type == sco_codes_parcours.UE_STANDARD
|
if modimpl.module.ue.type == codes_cursus.UE_STANDARD
|
||||||
] # la liste des modules (objet modimpl)
|
] # la liste des modules (objet modimpl)
|
||||||
self.somme_coeffs = sum(
|
self.somme_coeffs = sum(
|
||||||
[
|
[
|
||||||
|
@ -255,7 +256,7 @@ class SemestreTag(pe_tagtable.TableTag):
|
||||||
# Si le module ne fait pas partie des UE capitalisées
|
# Si le module ne fait pas partie des UE capitalisées
|
||||||
if modimpl.module.ue.id not in ue_capitalisees_id:
|
if modimpl.module.ue.id not in ue_capitalisees_id:
|
||||||
note = self.nt.get_etud_mod_moy(modimpl_id, etudid) # lecture de la note
|
note = self.nt.get_etud_mod_moy(modimpl_id, etudid) # lecture de la note
|
||||||
coeff = modimpl.module.coefficient # le coeff
|
coeff = modimpl.module.coefficient or 0.0 # le coeff (! non compatible BUT)
|
||||||
coeff_norm = (
|
coeff_norm = (
|
||||||
coeff / self.somme_coeffs if self.somme_coeffs != 0 else 0
|
coeff / self.somme_coeffs if self.somme_coeffs != 0 else 0
|
||||||
) # le coeff normalisé
|
) # le coeff normalisé
|
||||||
|
@ -276,7 +277,7 @@ class SemestreTag(pe_tagtable.TableTag):
|
||||||
fid_prec = fids_prec[0]
|
fid_prec = fids_prec[0]
|
||||||
# Lecture des notes de ce semestre
|
# Lecture des notes de ce semestre
|
||||||
# le tableau de note du semestre considéré:
|
# le tableau de note du semestre considéré:
|
||||||
formsemestre_prec = FormSemestre.query.get_or_404(fid_prec)
|
formsemestre_prec = FormSemestre.get_formsemestre(fid_prec)
|
||||||
nt_prec: NotesTableCompat = res_sem.load_formsemestre_results(
|
nt_prec: NotesTableCompat = res_sem.load_formsemestre_results(
|
||||||
formsemestre_prec
|
formsemestre_prec
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
@ -40,7 +40,7 @@ Created on Thu Sep 8 09:36:33 2016
|
||||||
import datetime
|
import datetime
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from app.scodoc import notes_table
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
class TableTag(object):
|
class TableTag(object):
|
||||||
|
@ -186,7 +186,7 @@ class TableTag(object):
|
||||||
if isinstance(col[0], float)
|
if isinstance(col[0], float)
|
||||||
else 0, # remplace les None et autres chaines par des zéros
|
else 0, # remplace les None et autres chaines par des zéros
|
||||||
) # triées
|
) # triées
|
||||||
self.rangs[tag] = notes_table.comp_ranks(lesMoyennesTriees) # les rangs
|
self.rangs[tag] = scu.comp_ranks(lesMoyennesTriees) # les rangs
|
||||||
|
|
||||||
# calcul des stats
|
# calcul des stats
|
||||||
self.comp_stats_d_un_tag(tag)
|
self.comp_stats_d_un_tag(tag)
|
||||||
|
|
|
@ -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
|
||||||
|
@ -55,7 +55,7 @@ def _pe_view_sem_recap_form(formsemestre_id):
|
||||||
<p class="help">
|
<p class="help">
|
||||||
Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de
|
Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de
|
||||||
poursuites d'études.
|
poursuites d'études.
|
||||||
<br/>
|
<br>
|
||||||
De nombreux aspects sont paramétrables:
|
De nombreux aspects sont paramétrables:
|
||||||
<a href="https://scodoc.org/AvisPoursuiteEtudes" target="_blank" rel="noopener">
|
<a href="https://scodoc.org/AvisPoursuiteEtudes" target="_blank" rel="noopener">
|
||||||
voir la documentation</a>.
|
voir la documentation</a>.
|
||||||
|
@ -65,7 +65,7 @@ def _pe_view_sem_recap_form(formsemestre_id):
|
||||||
<div class="pe_template_up">
|
<div class="pe_template_up">
|
||||||
Les templates sont généralement installés sur le serveur ou dans le
|
Les templates sont généralement installés sur le serveur ou dans le
|
||||||
paramétrage de ScoDoc.
|
paramétrage de ScoDoc.
|
||||||
<br/>
|
<br>
|
||||||
Au besoin, vous pouvez spécifier ici votre propre fichier de template
|
Au besoin, vous pouvez spécifier ici votre propre fichier de template
|
||||||
(<tt>un_avis.tex</tt>):
|
(<tt>un_avis.tex</tt>):
|
||||||
<div class="pe_template_upb">Template:
|
<div class="pe_template_upb">Template:
|
||||||
|
|
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("")
|
|
@ -10,6 +10,11 @@
|
||||||
"""
|
"""
|
||||||
import html
|
import html
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
import flask_wtf
|
||||||
|
import wtforms
|
||||||
|
from app import log
|
||||||
|
from app.scodoc.sco_exceptions import ScoInvalidCSRF
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
# re validant dd/mm/yyyy
|
# re validant dd/mm/yyyy
|
||||||
|
@ -22,7 +27,7 @@ def TrivialFormulator(
|
||||||
form_url,
|
form_url,
|
||||||
values,
|
values,
|
||||||
formdescription=(),
|
formdescription=(),
|
||||||
initvalues={},
|
initvalues=None,
|
||||||
method="post",
|
method="post",
|
||||||
enctype=None,
|
enctype=None,
|
||||||
submitlabel="OK",
|
submitlabel="OK",
|
||||||
|
@ -32,12 +37,15 @@ def TrivialFormulator(
|
||||||
cssclass="",
|
cssclass="",
|
||||||
cancelbutton=None,
|
cancelbutton=None,
|
||||||
submitbutton=True,
|
submitbutton=True,
|
||||||
submitbuttonattributes=[],
|
submitbuttonattributes=None,
|
||||||
top_buttons=False, # place buttons at top of form
|
top_buttons=False, # place buttons at top of form
|
||||||
bottom_buttons=True, # buttons after form
|
bottom_buttons=True, # buttons after form
|
||||||
html_foot_markup="",
|
html_foot_markup="",
|
||||||
readonly=False,
|
readonly=False,
|
||||||
is_submitted=False,
|
is_submitted=False,
|
||||||
|
title="",
|
||||||
|
after_table="",
|
||||||
|
before_table="{title}",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
form_url : URL for this form
|
form_url : URL for this form
|
||||||
|
@ -74,7 +82,8 @@ def TrivialFormulator(
|
||||||
HTML elements:
|
HTML elements:
|
||||||
input_type : 'text', 'textarea', 'password',
|
input_type : 'text', 'textarea', 'password',
|
||||||
'radio', 'menu', 'checkbox',
|
'radio', 'menu', 'checkbox',
|
||||||
'hidden', 'separator', 'file', 'date', 'datedmy' (avec validation),
|
'hidden', 'separator', 'table_separator',
|
||||||
|
'file', 'date', 'datedmy' (avec validation),
|
||||||
'boolcheckbox', 'text_suggest',
|
'boolcheckbox', 'text_suggest',
|
||||||
'color'
|
'color'
|
||||||
(default text)
|
(default text)
|
||||||
|
@ -95,7 +104,7 @@ def TrivialFormulator(
|
||||||
form_url,
|
form_url,
|
||||||
values,
|
values,
|
||||||
formdescription,
|
formdescription,
|
||||||
initvalues,
|
initvalues or {},
|
||||||
method,
|
method,
|
||||||
enctype,
|
enctype,
|
||||||
submitlabel,
|
submitlabel,
|
||||||
|
@ -105,12 +114,15 @@ def TrivialFormulator(
|
||||||
cssclass=cssclass,
|
cssclass=cssclass,
|
||||||
cancelbutton=cancelbutton,
|
cancelbutton=cancelbutton,
|
||||||
submitbutton=submitbutton,
|
submitbutton=submitbutton,
|
||||||
submitbuttonattributes=submitbuttonattributes,
|
submitbuttonattributes=submitbuttonattributes or [],
|
||||||
top_buttons=top_buttons,
|
top_buttons=top_buttons,
|
||||||
bottom_buttons=bottom_buttons,
|
bottom_buttons=bottom_buttons,
|
||||||
html_foot_markup=html_foot_markup,
|
html_foot_markup=html_foot_markup,
|
||||||
readonly=readonly,
|
readonly=readonly,
|
||||||
is_submitted=is_submitted,
|
is_submitted=is_submitted,
|
||||||
|
title=title,
|
||||||
|
after_table=after_table,
|
||||||
|
before_table=before_table,
|
||||||
)
|
)
|
||||||
form = t.getform()
|
form = t.getform()
|
||||||
if t.canceled():
|
if t.canceled():
|
||||||
|
@ -127,8 +139,8 @@ class TF(object):
|
||||||
self,
|
self,
|
||||||
form_url,
|
form_url,
|
||||||
values,
|
values,
|
||||||
formdescription=[],
|
formdescription=None,
|
||||||
initvalues={},
|
initvalues=None,
|
||||||
method="POST",
|
method="POST",
|
||||||
enctype=None,
|
enctype=None,
|
||||||
submitlabel="OK",
|
submitlabel="OK",
|
||||||
|
@ -138,17 +150,20 @@ class TF(object):
|
||||||
cssclass="",
|
cssclass="",
|
||||||
cancelbutton=None,
|
cancelbutton=None,
|
||||||
submitbutton=True,
|
submitbutton=True,
|
||||||
submitbuttonattributes=[],
|
submitbuttonattributes=None,
|
||||||
top_buttons=False, # place buttons at top of form
|
top_buttons=False, # place buttons at top of form
|
||||||
bottom_buttons=True, # buttons after form
|
bottom_buttons=True, # buttons after form
|
||||||
html_foot_markup="", # html snippet put at the end, just after the table
|
html_foot_markup="", # html snippet put at the end, just after the table
|
||||||
readonly=False,
|
readonly=False,
|
||||||
is_submitted=False,
|
is_submitted=False,
|
||||||
|
title="",
|
||||||
|
after_table="",
|
||||||
|
before_table="{title}",
|
||||||
):
|
):
|
||||||
self.form_url = form_url
|
self.form_url = form_url
|
||||||
self.values = values.copy()
|
self.values = values.copy()
|
||||||
self.formdescription = list(formdescription)
|
self.formdescription = list(formdescription or [])
|
||||||
self.initvalues = initvalues
|
self.initvalues = initvalues or {}
|
||||||
self.method = method
|
self.method = method
|
||||||
self.enctype = enctype
|
self.enctype = enctype
|
||||||
self.submitlabel = submitlabel
|
self.submitlabel = submitlabel
|
||||||
|
@ -161,10 +176,13 @@ class TF(object):
|
||||||
self.cssclass = cssclass
|
self.cssclass = cssclass
|
||||||
self.cancelbutton = cancelbutton
|
self.cancelbutton = cancelbutton
|
||||||
self.submitbutton = submitbutton
|
self.submitbutton = submitbutton
|
||||||
self.submitbuttonattributes = submitbuttonattributes
|
self.submitbuttonattributes = submitbuttonattributes or []
|
||||||
self.top_buttons = top_buttons
|
self.top_buttons = top_buttons
|
||||||
self.bottom_buttons = bottom_buttons
|
self.bottom_buttons = bottom_buttons
|
||||||
self.html_foot_markup = html_foot_markup
|
self.html_foot_markup = html_foot_markup
|
||||||
|
self.title = title
|
||||||
|
self.after_table = after_table
|
||||||
|
self.before_table = before_table
|
||||||
self.readonly = readonly
|
self.readonly = readonly
|
||||||
self.result = None
|
self.result = None
|
||||||
self.is_submitted = is_submitted
|
self.is_submitted = is_submitted
|
||||||
|
@ -176,11 +194,26 @@ class TF(object):
|
||||||
"true if form has been submitted"
|
"true if form has been submitted"
|
||||||
if self.is_submitted:
|
if self.is_submitted:
|
||||||
return True
|
return True
|
||||||
return self.values.get("%s_submitted" % self.formid, False)
|
form_submitted = self.values.get(f"{self.formid}_submitted", False)
|
||||||
|
if form_submitted:
|
||||||
|
self.check_csrf()
|
||||||
|
return form_submitted
|
||||||
|
|
||||||
|
def check_csrf(self):
|
||||||
|
"""check token for POST forms.
|
||||||
|
Raises ScoInvalidCSRF on failure.
|
||||||
|
"""
|
||||||
|
if self.method == "post":
|
||||||
|
token = self.values.get("csrf_token")
|
||||||
|
try:
|
||||||
|
flask_wtf.csrf.validate_csrf(token)
|
||||||
|
except wtforms.validators.ValidationError as exc:
|
||||||
|
log(f"Form.check_csrf: invalid CSRF token\n{exc.args}")
|
||||||
|
raise ScoInvalidCSRF() from exc
|
||||||
|
|
||||||
def canceled(self):
|
def canceled(self):
|
||||||
"true if form has been canceled"
|
"true if form has been canceled"
|
||||||
return self.values.get("%s_cancel" % self.formid, False)
|
return self.values.get(f"{self.formid}_cancel", False)
|
||||||
|
|
||||||
def getform(self):
|
def getform(self):
|
||||||
"return HTML form"
|
"return HTML form"
|
||||||
|
@ -357,12 +390,23 @@ class TF(object):
|
||||||
self.values[field] = True
|
self.values[field] = True
|
||||||
else:
|
else:
|
||||||
self.values[field] = False
|
self.values[field] = False
|
||||||
# open('/tmp/toto','a').write('checkvalues: val=%s (%s) values[%s] = %s\n' % (val, type(val), field, self.values[field]))
|
|
||||||
if descr.get("convert_numbers", False):
|
if descr.get("convert_numbers", False):
|
||||||
if typ[:3] == "int":
|
if typ[:3] == "int":
|
||||||
self.values[field] = int(self.values[field])
|
try:
|
||||||
|
self.values[field] = int(self.values[field])
|
||||||
|
except ValueError:
|
||||||
|
msg.append(
|
||||||
|
f"valeur invalide ({self.values[field]}) pour le champs {field}"
|
||||||
|
)
|
||||||
|
ok = False
|
||||||
elif typ == "float" or typ == "real":
|
elif typ == "float" or typ == "real":
|
||||||
self.values[field] = float(self.values[field].replace(",", "."))
|
try:
|
||||||
|
self.values[field] = float(self.values[field].replace(",", "."))
|
||||||
|
except ValueError:
|
||||||
|
msg.append(
|
||||||
|
f"valeur invalide ({self.values[field]}) pour le champs {field}"
|
||||||
|
)
|
||||||
|
ok = False
|
||||||
if ok:
|
if ok:
|
||||||
self.result = self.values
|
self.result = self.values
|
||||||
else:
|
else:
|
||||||
|
@ -423,9 +467,16 @@ class TF(object):
|
||||||
self.form_attrs,
|
self.form_attrs,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
R.append('<input type="hidden" name="%s_submitted" value="1">' % self.formid)
|
if self.method == "post":
|
||||||
|
R.append(
|
||||||
|
f"""<input type="hidden" name="csrf_token" value="{
|
||||||
|
flask_wtf.csrf.generate_csrf()
|
||||||
|
}">"""
|
||||||
|
)
|
||||||
|
R.append(f"""<input type="hidden" name="{self.formid}_submitted" value="1">""")
|
||||||
if self.top_buttons:
|
if self.top_buttons:
|
||||||
R.append(buttons_markup + "<p></p>")
|
R.append(buttons_markup + "<p></p>")
|
||||||
|
R.append(self.before_table.format(title=self.title))
|
||||||
R.append('<table class="tf">')
|
R.append('<table class="tf">')
|
||||||
for field, descr in self.formdescription:
|
for field, descr in self.formdescription:
|
||||||
if descr.get("readonly", False):
|
if descr.get("readonly", False):
|
||||||
|
@ -453,6 +504,16 @@ class TF(object):
|
||||||
etempl = separatortemplate
|
etempl = separatortemplate
|
||||||
R.append(etempl % {"label": title, "item_dom_attr": item_dom_attr})
|
R.append(etempl % {"label": title, "item_dom_attr": item_dom_attr})
|
||||||
continue
|
continue
|
||||||
|
elif input_type == "table_separator":
|
||||||
|
etempl = ""
|
||||||
|
# Table ouverte ?
|
||||||
|
if len([p for p in R if "<table" in p]) > len(
|
||||||
|
[p for p in R if "</table" in p]
|
||||||
|
):
|
||||||
|
R.append(f"""</table>{self.after_table}""")
|
||||||
|
R.append(
|
||||||
|
f"""{self.before_table.format(title=descr.get("title", ""))}<table class="tf">"""
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
etempl = itemtemplate
|
etempl = itemtemplate
|
||||||
lab = []
|
lab = []
|
||||||
|
@ -543,11 +604,8 @@ class TF(object):
|
||||||
disabled_items = descr.get("disabled_items", {})
|
disabled_items = descr.get("disabled_items", {})
|
||||||
if vertical:
|
if vertical:
|
||||||
lem.append("<table>")
|
lem.append("<table>")
|
||||||
for i in range(len(labels)):
|
for i in range(len(labels)): # pylint: disable=consider-using-enumerate
|
||||||
if input_type == "checkbox":
|
if input_type == "checkbox":
|
||||||
# from app.scodoc.sco_utils import log # debug only
|
|
||||||
# log('checkbox: values[%s] = "%s"' % (field,repr(values[field]) ))
|
|
||||||
# log("descr['allowed_values'][%s] = '%s'" % (i, repr(descr['allowed_values'][i])))
|
|
||||||
if (
|
if (
|
||||||
values[field]
|
values[field]
|
||||||
and descr["allowed_values"][i] in values[field]
|
and descr["allowed_values"][i] in values[field]
|
||||||
|
@ -563,7 +621,7 @@ class TF(object):
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
v = int(values[field])
|
v = int(values[field])
|
||||||
except:
|
except (ValueError, KeyError):
|
||||||
v = False
|
v = False
|
||||||
if v:
|
if v:
|
||||||
checked = 'checked="checked"'
|
checked = 'checked="checked"'
|
||||||
|
@ -613,7 +671,7 @@ class TF(object):
|
||||||
'<input type="hidden" name="%s" id="%s" value="%s" %s >'
|
'<input type="hidden" name="%s" id="%s" value="%s" %s >'
|
||||||
% (field, wid, values[field], attribs)
|
% (field, wid, values[field], attribs)
|
||||||
)
|
)
|
||||||
elif input_type == "separator":
|
elif (input_type == "separator") or (input_type == "table_separator"):
|
||||||
pass
|
pass
|
||||||
elif input_type == "file":
|
elif input_type == "file":
|
||||||
lem.append(
|
lem.append(
|
||||||
|
@ -644,13 +702,15 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
|
||||||
)
|
)
|
||||||
lem.append(('value="%(' + field + ')s" >') % values)
|
lem.append(('value="%(' + field + ')s" >') % values)
|
||||||
else:
|
else:
|
||||||
raise ValueError("unkown input_type for form (%s)!" % input_type)
|
raise ValueError(f"unkown input_type for form ({input_type})!")
|
||||||
explanation = descr.get("explanation", "")
|
explanation = descr.get("explanation", "")
|
||||||
if explanation:
|
if explanation:
|
||||||
lem.append('<span class="tf-explanation">%s</span>' % explanation)
|
lem.append(f"""<span class="tf-explanation">{explanation}</span>""")
|
||||||
comment = descr.get("comment", "")
|
comment = descr.get("comment", "")
|
||||||
if comment:
|
if comment:
|
||||||
lem.append('<br/><span class="tf-comment">%s</span>' % comment)
|
if (input_type != "checkbox") and (input_type != "boolcheckbox"):
|
||||||
|
lem.append("<br>")
|
||||||
|
lem.append(f"""<span class="tf-comment">{comment}</span>""")
|
||||||
R.append(
|
R.append(
|
||||||
etempl
|
etempl
|
||||||
% {
|
% {
|
||||||
|
@ -660,11 +720,11 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
R.append("</table>")
|
R.append("</table>")
|
||||||
|
R.append(self.after_table)
|
||||||
R.append(self.html_foot_markup)
|
R.append(self.html_foot_markup)
|
||||||
|
|
||||||
if self.bottom_buttons:
|
if self.bottom_buttons:
|
||||||
R.append("<br/>" + buttons_markup)
|
R.append("<br>" + buttons_markup)
|
||||||
|
|
||||||
if add_no_enter_js:
|
if add_no_enter_js:
|
||||||
R.append(
|
R.append(
|
||||||
|
@ -756,7 +816,7 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
|
||||||
|
|
||||||
if input_type == "separator": # separator
|
if input_type == "separator": # separator
|
||||||
R.append('<td colspan="2">%s' % title)
|
R.append('<td colspan="2">%s' % title)
|
||||||
else:
|
elif input_type != "table_separator":
|
||||||
R.append('<td class="tf-ro-fieldlabel%s">' % klass)
|
R.append('<td class="tf-ro-fieldlabel%s">' % klass)
|
||||||
R.append("%s</td>" % title)
|
R.append("%s</td>" % title)
|
||||||
R.append('<td class="tf-ro-field%s">' % klass)
|
R.append('<td class="tf-ro-field%s">' % klass)
|
||||||
|
@ -766,7 +826,7 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
|
||||||
elif input_type in ("radio", "menu", "checkbox", "boolcheckbox"):
|
elif input_type in ("radio", "menu", "checkbox", "boolcheckbox"):
|
||||||
if input_type == "boolcheckbox":
|
if input_type == "boolcheckbox":
|
||||||
labels = descr.get(
|
labels = descr.get(
|
||||||
"labels", descr.get("allowed_values", ["oui", "non"])
|
"labels", descr.get("allowed_values", ["non", "oui"])
|
||||||
)
|
)
|
||||||
_val = self.values[field]
|
_val = self.values[field]
|
||||||
if isinstance(_val, bool):
|
if isinstance(_val, bool):
|
||||||
|
@ -789,7 +849,11 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
|
||||||
R.append(
|
R.append(
|
||||||
'<div class="tf-ro-textarea">%s</div>' % html.escape(self.values[field])
|
'<div class="tf-ro-textarea">%s</div>' % html.escape(self.values[field])
|
||||||
)
|
)
|
||||||
elif input_type == "separator" or input_type == "hidden":
|
elif (
|
||||||
|
input_type == "separator"
|
||||||
|
or input_type == "hidden"
|
||||||
|
or input_type == "table_separator"
|
||||||
|
):
|
||||||
pass
|
pass
|
||||||
elif input_type == "file":
|
elif input_type == "file":
|
||||||
R.append("'%s'" % self.values[field])
|
R.append("'%s'" % self.values[field])
|
||||||
|
|
|
@ -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
|
||||||
|
@ -25,17 +25,20 @@
|
||||||
#
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""Semestres: Codes gestion parcours (constantes)
|
"""Semestres: Codes gestion cursus (constantes)
|
||||||
|
Attention: ne pas confondre avec les "parcours" du BUT.
|
||||||
|
Renommage des anciens "parcours" -> "cursus" effectué en 9.4.41
|
||||||
"""
|
"""
|
||||||
import collections
|
|
||||||
import enum
|
import enum
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
from app import log
|
from app import log
|
||||||
|
|
||||||
|
|
||||||
@enum.unique
|
@enum.unique
|
||||||
class CodesParcours(enum.IntEnum):
|
class CodesCursus(enum.IntEnum):
|
||||||
"""Codes numériques des parcours, enregistrés en base
|
"""Codes numériques des cursus (ex parcours), enregistrés en base
|
||||||
dans notes_formations.type_parcours
|
dans notes_formations.type_parcours
|
||||||
Ne pas modifier.
|
Ne pas modifier.
|
||||||
"""
|
"""
|
||||||
|
@ -77,7 +80,7 @@ UE_STANDARD = 0 # UE "fondamentale"
|
||||||
UE_SPORT = 1 # bonus "sport"
|
UE_SPORT = 1 # bonus "sport"
|
||||||
UE_STAGE_LP = 2 # ue "projet tuteuré et stage" dans les Lic. Pro.
|
UE_STAGE_LP = 2 # ue "projet tuteuré et stage" dans les Lic. Pro.
|
||||||
UE_STAGE_10 = 3 # ue "stage" avec moyenne requise > 10
|
UE_STAGE_10 = 3 # ue "stage" avec moyenne requise > 10
|
||||||
UE_ELECTIVE = 4 # UE "élective" dans certains parcours (UCAC?, ISCID)
|
UE_ELECTIVE = 4 # UE "élective" dans certains cursus (UCAC?, ISCID)
|
||||||
UE_PROFESSIONNELLE = 5 # UE "professionnelle" (ISCID, ...)
|
UE_PROFESSIONNELLE = 5 # UE "professionnelle" (ISCID, ...)
|
||||||
UE_OPTIONNELLE = 6 # UE non fondamentales (ILEPS, ...)
|
UE_OPTIONNELLE = 6 # UE non fondamentales (ILEPS, ...)
|
||||||
|
|
||||||
|
@ -120,6 +123,7 @@ ABL = "ABL"
|
||||||
ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé
|
ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé
|
||||||
ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10)
|
ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10)
|
||||||
ADJ = "ADJ" # admis par le jury
|
ADJ = "ADJ" # admis par le jury
|
||||||
|
ADJR = "ADJR" # UE admise car son RCUE est ADJ
|
||||||
ATT = "ATT" #
|
ATT = "ATT" #
|
||||||
ATJ = "ATJ" # pb assiduité: décision repoussée au semestre suivant
|
ATJ = "ATJ" # pb assiduité: décision repoussée au semestre suivant
|
||||||
ATB = "ATB"
|
ATB = "ATB"
|
||||||
|
@ -156,6 +160,7 @@ CODES_EXPL = {
|
||||||
ABL: "Année blanche",
|
ABL: "Année blanche",
|
||||||
ADC: "Validé par compensation",
|
ADC: "Validé par compensation",
|
||||||
ADJ: "Validé par le Jury",
|
ADJ: "Validé par le Jury",
|
||||||
|
ADJR: "UE validée car son RCUE est validé ADJ par le jury",
|
||||||
ADM: "Validé",
|
ADM: "Validé",
|
||||||
AJ: "Ajourné (ou UE/BC de BUT en attente pour problème de moyenne)",
|
AJ: "Ajourné (ou UE/BC de BUT en attente pour problème de moyenne)",
|
||||||
ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)",
|
ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)",
|
||||||
|
@ -183,16 +188,25 @@ CODES_EXPL = {
|
||||||
|
|
||||||
# Les codes de semestres:
|
# Les codes de semestres:
|
||||||
CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT}
|
CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT}
|
||||||
CODES_SEM_VALIDES = {ADM: True, ADC: True, ADJ: True} # semestre validé
|
CODES_SEM_VALIDES_DE_DROIT = {ADM, ADC}
|
||||||
CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente
|
CODES_SEM_VALIDES = CODES_SEM_VALIDES_DE_DROIT | {ADJ} # semestre validé
|
||||||
|
CODES_SEM_ATTENTES = {ATT, ATB, ATJ} # semestre en attente
|
||||||
|
|
||||||
CODES_SEM_REO = {NAR: 1} # reorientation
|
CODES_SEM_REO = {NAR} # reorientation
|
||||||
|
|
||||||
|
CODES_UE_VALIDES_DE_DROIT = {ADM, CMP} # validation "de droit"
|
||||||
|
CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR}
|
||||||
|
"UE validée"
|
||||||
|
CODES_UE_CAPITALISANTS = {ADM}
|
||||||
|
"UE capitalisée"
|
||||||
|
|
||||||
|
CODES_RCUE_VALIDES_DE_DROIT = {ADM, CMP}
|
||||||
|
CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ}
|
||||||
|
"Niveau RCUE validé"
|
||||||
|
|
||||||
CODES_UE_VALIDES = {ADM: True, CMP: True, ADJ: True} # UE validée
|
|
||||||
CODES_RCUE_VALIDES = CODES_UE_VALIDES # Niveau RCUE validé
|
|
||||||
# Pour le BUT:
|
# Pour le BUT:
|
||||||
|
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD}
|
||||||
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
|
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
|
||||||
CODES_RCUE = {ADM, AJ, CMP}
|
|
||||||
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
|
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
|
||||||
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
|
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
|
||||||
BUT_RCUE_SUFFISANT = 8.0 - NOTES_TOLERANCE
|
BUT_RCUE_SUFFISANT = 8.0 - NOTES_TOLERANCE
|
||||||
|
@ -203,21 +217,36 @@ BUT_CODES_PASSAGE = {
|
||||||
PAS1NCI,
|
PAS1NCI,
|
||||||
ATJ,
|
ATJ,
|
||||||
}
|
}
|
||||||
|
# les codes, du plus "défavorable" à l'étudiant au plus favorable:
|
||||||
|
# (valeur par défaut 0)
|
||||||
|
BUT_CODES_ORDERED = {
|
||||||
|
NAR: 0,
|
||||||
|
DEF: 0,
|
||||||
|
AJ: 10,
|
||||||
|
ATJ: 20,
|
||||||
|
CMP: 50,
|
||||||
|
ADC: 50,
|
||||||
|
PASD: 50,
|
||||||
|
PAS1NCI: 60,
|
||||||
|
ADJR: 90,
|
||||||
|
ADJ: 100,
|
||||||
|
ADM: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def code_semestre_validant(code: str) -> bool:
|
def code_semestre_validant(code: str) -> bool:
|
||||||
"Vrai si ce CODE entraine la validation du semestre"
|
"Vrai si ce CODE entraine la validation du semestre"
|
||||||
return CODES_SEM_VALIDES.get(code, False)
|
return code in CODES_SEM_VALIDES
|
||||||
|
|
||||||
|
|
||||||
def code_semestre_attente(code: str) -> bool:
|
def code_semestre_attente(code: str) -> bool:
|
||||||
"Vrai si ce CODE est un code d'attente (semestre validable plus tard par jury ou compensation)"
|
"Vrai si ce CODE est un code d'attente (semestre validable plus tard par jury ou compensation)"
|
||||||
return CODES_SEM_ATTENTES.get(code, False)
|
return code in CODES_SEM_ATTENTES
|
||||||
|
|
||||||
|
|
||||||
def code_ue_validant(code: str) -> bool:
|
def code_ue_validant(code: str) -> bool:
|
||||||
"Vrai si ce code d'UE est validant (ie attribue les ECTS)"
|
"Vrai si ce code d'UE est validant (ie attribue les ECTS)"
|
||||||
return CODES_UE_VALIDES.get(code, False)
|
return code in CODES_UE_VALIDES
|
||||||
|
|
||||||
|
|
||||||
DEVENIR_EXPL = {
|
DEVENIR_EXPL = {
|
||||||
|
@ -246,7 +275,7 @@ DEVENIRS_NEXT2 = {NEXT_OR_NEXT2: 1, NEXT2: 1}
|
||||||
|
|
||||||
NO_SEMESTRE_ID = -1 # code semestre si pas de semestres
|
NO_SEMESTRE_ID = -1 # code semestre si pas de semestres
|
||||||
|
|
||||||
# Règles gestion parcours
|
# Règles gestion cursus
|
||||||
class DUTRule(object):
|
class DUTRule(object):
|
||||||
def __init__(self, rule_id, premise, conclusion):
|
def __init__(self, rule_id, premise, conclusion):
|
||||||
self.rule_id = rule_id
|
self.rule_id = rule_id
|
||||||
|
@ -268,12 +297,12 @@ class DUTRule(object):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
# Types de parcours
|
# Types de cursus
|
||||||
DEFAULT_TYPE_PARCOURS = 100 # pour le menu de creation nouvelle formation
|
DEFAULT_TYPE_CURSUS = 100 # pour le menu de creation nouvelle formation
|
||||||
|
|
||||||
|
|
||||||
class TypeParcours(object):
|
class TypeCursus:
|
||||||
TYPE_PARCOURS = None # id, utilisé par notes_formation.type_parcours
|
TYPE_CURSUS = None # id, utilisé par notes_formation.type_parcours
|
||||||
NAME = None # required
|
NAME = None # required
|
||||||
NB_SEM = 1 # Nombre de semestres
|
NB_SEM = 1 # Nombre de semestres
|
||||||
COMPENSATION_UE = True # inutilisé
|
COMPENSATION_UE = True # inutilisé
|
||||||
|
@ -287,9 +316,9 @@ class TypeParcours(object):
|
||||||
SESSION_NAME = "semestre"
|
SESSION_NAME = "semestre"
|
||||||
SESSION_NAME_A = "du "
|
SESSION_NAME_A = "du "
|
||||||
SESSION_ABBRV = "S" # S1, S2, ...
|
SESSION_ABBRV = "S" # S1, S2, ...
|
||||||
UNUSED_CODES = set() # Ensemble des codes jury non autorisés dans ce parcours
|
UNUSED_CODES = set() # Ensemble des codes jury non autorisés dans ce cursus
|
||||||
UE_IS_MODULE = False # 1 seul module par UE (si plusieurs modules, etudiants censéments inscrits à un seul d'entre eux)
|
UE_IS_MODULE = False # 1 seul module par UE (si plusieurs modules, etudiants censéments inscrits à un seul d'entre eux)
|
||||||
ECTS_ONLY = False # Parcours avec progression basée uniquement sur les ECTS
|
ECTS_ONLY = False # Cursus avec progression basée uniquement sur les ECTS
|
||||||
ALLOWED_UE_TYPES = list(
|
ALLOWED_UE_TYPES = list(
|
||||||
UE_TYPE_NAME.keys()
|
UE_TYPE_NAME.keys()
|
||||||
) # par defaut, autorise tous les types d'UE
|
) # par defaut, autorise tous les types d'UE
|
||||||
|
@ -335,18 +364,18 @@ class TypeParcours(object):
|
||||||
return False, """<b>%d UE sous la barre</b>""" % n
|
return False, """<b>%d UE sous la barre</b>""" % n
|
||||||
|
|
||||||
|
|
||||||
# Parcours définis (instances de sous-classes de TypeParcours):
|
# Cursus définis (instances de sous-classes de TypeCursus):
|
||||||
TYPES_PARCOURS = collections.OrderedDict() # type : Parcours
|
SCO_CURSUS: dict[int, TypeCursus] = {} # type : Cursus
|
||||||
|
|
||||||
|
|
||||||
def register_parcours(Parcours):
|
def register_cursus(cursus: TypeCursus):
|
||||||
TYPES_PARCOURS[int(Parcours.TYPE_PARCOURS)] = Parcours
|
SCO_CURSUS[int(cursus.TYPE_CURSUS)] = cursus
|
||||||
|
|
||||||
|
|
||||||
class ParcoursBUT(TypeParcours):
|
class CursusBUT(TypeCursus):
|
||||||
"""BUT Bachelor Universitaire de Technologie"""
|
"""BUT Bachelor Universitaire de Technologie"""
|
||||||
|
|
||||||
TYPE_PARCOURS = 700
|
TYPE_CURSUS = 700
|
||||||
NAME = "BUT"
|
NAME = "BUT"
|
||||||
NB_SEM = 6
|
NB_SEM = 6
|
||||||
COMPENSATION_UE = False
|
COMPENSATION_UE = False
|
||||||
|
@ -355,63 +384,63 @@ class ParcoursBUT(TypeParcours):
|
||||||
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT]
|
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT]
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursBUT())
|
register_cursus(CursusBUT())
|
||||||
|
|
||||||
|
|
||||||
class ParcoursDUT(TypeParcours):
|
class CursusDUT(TypeCursus):
|
||||||
"""DUT selon l'arrêté d'août 2005"""
|
"""DUT selon l'arrêté d'août 2005"""
|
||||||
|
|
||||||
TYPE_PARCOURS = 100
|
TYPE_CURSUS = 100
|
||||||
NAME = "DUT"
|
NAME = "DUT"
|
||||||
NB_SEM = 4
|
NB_SEM = 4
|
||||||
COMPENSATION_UE = True
|
COMPENSATION_UE = True
|
||||||
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT]
|
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT]
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursDUT())
|
register_cursus(CursusDUT())
|
||||||
|
|
||||||
|
|
||||||
class ParcoursDUT4(ParcoursDUT):
|
class CursusDUT4(CursusDUT):
|
||||||
"""DUT (en 4 semestres sans compensations)"""
|
"""DUT (en 4 semestres sans compensations)"""
|
||||||
|
|
||||||
TYPE_PARCOURS = 110
|
TYPE_CURSUS = 110
|
||||||
NAME = "DUT4"
|
NAME = "DUT4"
|
||||||
COMPENSATION_UE = False
|
COMPENSATION_UE = False
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursDUT4())
|
register_cursus(CursusDUT4())
|
||||||
|
|
||||||
|
|
||||||
class ParcoursDUTMono(TypeParcours):
|
class CursusDUTMono(TypeCursus):
|
||||||
"""DUT en un an (FC, Années spéciales)"""
|
"""DUT en un an (FC, Années spéciales)"""
|
||||||
|
|
||||||
TYPE_PARCOURS = 120
|
TYPE_CURSUS = 120
|
||||||
NAME = "DUT"
|
NAME = "DUT"
|
||||||
NB_SEM = 1
|
NB_SEM = 1
|
||||||
COMPENSATION_UE = False
|
COMPENSATION_UE = False
|
||||||
UNUSED_CODES = set((ADC, ATT, ATB))
|
UNUSED_CODES = set((ADC, ATT, ATB))
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursDUTMono())
|
register_cursus(CursusDUTMono())
|
||||||
|
|
||||||
|
|
||||||
class ParcoursDUT2(ParcoursDUT):
|
class CursusDUT2(CursusDUT):
|
||||||
"""DUT en deux semestres (par ex.: années spéciales semestrialisées)"""
|
"""DUT en deux semestres (par ex.: années spéciales semestrialisées)"""
|
||||||
|
|
||||||
TYPE_PARCOURS = CodesParcours.DUT2
|
TYPE_CURSUS = CodesCursus.DUT2
|
||||||
NAME = "DUT2"
|
NAME = "DUT2"
|
||||||
NB_SEM = 2
|
NB_SEM = 2
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursDUT2())
|
register_cursus(CursusDUT2())
|
||||||
|
|
||||||
|
|
||||||
class ParcoursLP(TypeParcours):
|
class CursusLP(TypeCursus):
|
||||||
"""Licence Pro (en un "semestre")
|
"""Licence Pro (en un "semestre")
|
||||||
(pour anciennes LP. Après 2014, préférer ParcoursLP2014)
|
(pour anciennes LP. Après 2014, préférer CursusLP2014)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
TYPE_PARCOURS = CodesParcours.LP
|
TYPE_CURSUS = CodesCursus.LP
|
||||||
NAME = "LP"
|
NAME = "LP"
|
||||||
NB_SEM = 1
|
NB_SEM = 1
|
||||||
COMPENSATION_UE = False
|
COMPENSATION_UE = False
|
||||||
|
@ -422,35 +451,35 @@ class ParcoursLP(TypeParcours):
|
||||||
UNUSED_CODES = set((ADC, ATT, ATB))
|
UNUSED_CODES = set((ADC, ATT, ATB))
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursLP())
|
register_cursus(CursusLP())
|
||||||
|
|
||||||
|
|
||||||
class ParcoursLP2sem(ParcoursLP):
|
class CursusLP2sem(CursusLP):
|
||||||
"""Licence Pro (en deux "semestres")"""
|
"""Licence Pro (en deux "semestres")"""
|
||||||
|
|
||||||
TYPE_PARCOURS = CodesParcours.LP2sem
|
TYPE_CURSUS = CodesCursus.LP2sem
|
||||||
NAME = "LP2sem"
|
NAME = "LP2sem"
|
||||||
NB_SEM = 2
|
NB_SEM = 2
|
||||||
COMPENSATION_UE = True
|
COMPENSATION_UE = True
|
||||||
UNUSED_CODES = set((ADC,)) # autorise les codes ATT et ATB, mais pas ADC.
|
UNUSED_CODES = set((ADC,)) # autorise les codes ATT et ATB, mais pas ADC.
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursLP2sem())
|
register_cursus(CursusLP2sem())
|
||||||
|
|
||||||
|
|
||||||
class ParcoursLP2semEvry(ParcoursLP):
|
class CursusLP2semEvry(CursusLP):
|
||||||
"""Licence Pro (en deux "semestres", U. Evry)"""
|
"""Licence Pro (en deux "semestres", U. Evry)"""
|
||||||
|
|
||||||
TYPE_PARCOURS = CodesParcours.LP2semEvry
|
TYPE_CURSUS = CodesCursus.LP2semEvry
|
||||||
NAME = "LP2semEvry"
|
NAME = "LP2semEvry"
|
||||||
NB_SEM = 2
|
NB_SEM = 2
|
||||||
COMPENSATION_UE = True
|
COMPENSATION_UE = True
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursLP2semEvry())
|
register_cursus(CursusLP2semEvry())
|
||||||
|
|
||||||
|
|
||||||
class ParcoursLP2014(TypeParcours):
|
class CursusLP2014(TypeCursus):
|
||||||
"""Licence Pro (en un "semestre"), selon arrêté du 22/01/2014"""
|
"""Licence Pro (en un "semestre"), selon arrêté du 22/01/2014"""
|
||||||
|
|
||||||
# Note: texte de référence
|
# Note: texte de référence
|
||||||
|
@ -467,7 +496,7 @@ class ParcoursLP2014(TypeParcours):
|
||||||
# l'établissement d'un coefficient qui peut varier dans un rapport de 1 à 3. ", etc ne sont _pas_
|
# l'établissement d'un coefficient qui peut varier dans un rapport de 1 à 3. ", etc ne sont _pas_
|
||||||
# vérifiés par ScoDoc)
|
# vérifiés par ScoDoc)
|
||||||
|
|
||||||
TYPE_PARCOURS = CodesParcours.LP2014
|
TYPE_CURSUS = CodesCursus.LP2014
|
||||||
NAME = "LP2014"
|
NAME = "LP2014"
|
||||||
NB_SEM = 1
|
NB_SEM = 1
|
||||||
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT, UE_STAGE_LP]
|
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT, UE_STAGE_LP]
|
||||||
|
@ -487,7 +516,7 @@ class ParcoursLP2014(TypeParcours):
|
||||||
(ue_status["moy"], ue_status["coef_ue"])
|
(ue_status["moy"], ue_status["coef_ue"])
|
||||||
for ue_status in ues_status
|
for ue_status in ues_status
|
||||||
if ue_status["ue"]["type"] == UE_STAGE_LP
|
if ue_status["ue"]["type"] == UE_STAGE_LP
|
||||||
and type(ue_status["moy"]) == float
|
and np.issubdtype(type(ue_status["moy"]), np.floating)
|
||||||
]
|
]
|
||||||
# Moyenne des moyennes:
|
# Moyenne des moyennes:
|
||||||
sum_coef = sum(x[1] for x in mc_stages_proj)
|
sum_coef = sum(x[1] for x in mc_stages_proj)
|
||||||
|
@ -505,74 +534,74 @@ class ParcoursLP2014(TypeParcours):
|
||||||
return True, "" # pas de coef, condition ok
|
return True, "" # pas de coef, condition ok
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursLP2014())
|
register_cursus(CursusLP2014())
|
||||||
|
|
||||||
|
|
||||||
class ParcoursLP2sem2014(ParcoursLP):
|
class CursusLP2sem2014(CursusLP):
|
||||||
"""Licence Pro (en deux "semestres", selon arrêté du 22/01/2014)"""
|
"""Licence Pro (en deux "semestres", selon arrêté du 22/01/2014)"""
|
||||||
|
|
||||||
TYPE_PARCOURS = CodesParcours.LP2sem2014
|
TYPE_CURSUS = CodesCursus.LP2sem2014
|
||||||
NAME = "LP2014_2sem"
|
NAME = "LP2014_2sem"
|
||||||
NB_SEM = 2
|
NB_SEM = 2
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursLP2sem2014())
|
register_cursus(CursusLP2sem2014())
|
||||||
|
|
||||||
|
|
||||||
# Masters: M2 en deux semestres
|
# Masters: M2 en deux semestres
|
||||||
class ParcoursM2(TypeParcours):
|
class CursusM2(TypeCursus):
|
||||||
"""Master 2 (en deux "semestres")"""
|
"""Master 2 (en deux "semestres")"""
|
||||||
|
|
||||||
TYPE_PARCOURS = CodesParcours.M2
|
TYPE_CURSUS = CodesCursus.M2
|
||||||
NAME = "M2sem"
|
NAME = "M2sem"
|
||||||
NB_SEM = 2
|
NB_SEM = 2
|
||||||
COMPENSATION_UE = True
|
COMPENSATION_UE = True
|
||||||
UNUSED_CODES = set((ATT, ATB))
|
UNUSED_CODES = set((ATT, ATB))
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursM2())
|
register_cursus(CursusM2())
|
||||||
|
|
||||||
|
|
||||||
class ParcoursM2noncomp(ParcoursM2):
|
class CursusM2noncomp(CursusM2):
|
||||||
"""Master 2 (en deux "semestres") sans compensation"""
|
"""Master 2 (en deux "semestres") sans compensation"""
|
||||||
|
|
||||||
TYPE_PARCOURS = CodesParcours.M2noncomp
|
TYPE_CURSUS = CodesCursus.M2noncomp
|
||||||
NAME = "M2noncomp"
|
NAME = "M2noncomp"
|
||||||
COMPENSATION_UE = False
|
COMPENSATION_UE = False
|
||||||
UNUSED_CODES = set((ADC, ATT, ATB))
|
UNUSED_CODES = set((ADC, ATT, ATB))
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursM2noncomp())
|
register_cursus(CursusM2noncomp())
|
||||||
|
|
||||||
|
|
||||||
class ParcoursMono(TypeParcours):
|
class CursusMono(TypeCursus):
|
||||||
"""Formation générique en une session"""
|
"""Formation générique en une session"""
|
||||||
|
|
||||||
TYPE_PARCOURS = CodesParcours.Mono
|
TYPE_CURSUS = CodesCursus.Mono
|
||||||
NAME = "Mono"
|
NAME = "Mono"
|
||||||
NB_SEM = 1
|
NB_SEM = 1
|
||||||
COMPENSATION_UE = False
|
COMPENSATION_UE = False
|
||||||
UNUSED_CODES = set((ADC, ATT, ATB))
|
UNUSED_CODES = set((ADC, ATT, ATB))
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursMono())
|
register_cursus(CursusMono())
|
||||||
|
|
||||||
|
|
||||||
class ParcoursLegacy(TypeParcours):
|
class CursusLegacy(TypeCursus):
|
||||||
"""DUT (ancien ScoDoc, ne plus utiliser)"""
|
"""DUT (ancien ScoDoc, ne plus utiliser)"""
|
||||||
|
|
||||||
TYPE_PARCOURS = CodesParcours.Legacy
|
TYPE_CURSUS = CodesCursus.Legacy
|
||||||
NAME = "DUT"
|
NAME = "DUT"
|
||||||
NB_SEM = 4
|
NB_SEM = 4
|
||||||
COMPENSATION_UE = None # backward compat: defini dans formsemestre
|
COMPENSATION_UE = None # backward compat: defini dans formsemestre
|
||||||
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT]
|
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT]
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursLegacy())
|
register_cursus(CursusLegacy())
|
||||||
|
|
||||||
|
|
||||||
class ParcoursISCID(TypeParcours):
|
class CursusISCID(TypeCursus):
|
||||||
"""Superclasse pour les parcours de l'ISCID"""
|
"""Superclasse pour les cursus de l'ISCID"""
|
||||||
|
|
||||||
# SESSION_NAME = "année"
|
# SESSION_NAME = "année"
|
||||||
# SESSION_NAME_A = "de l'"
|
# SESSION_NAME_A = "de l'"
|
||||||
|
@ -591,32 +620,32 @@ class ParcoursISCID(TypeParcours):
|
||||||
ECTS_PROF_DIPL = 0 # crédits professionnels requis pour obtenir le diplôme
|
ECTS_PROF_DIPL = 0 # crédits professionnels requis pour obtenir le diplôme
|
||||||
|
|
||||||
|
|
||||||
class ParcoursBachelorISCID6(ParcoursISCID):
|
class CursusBachelorISCID6(CursusISCID):
|
||||||
"""ISCID: Bachelor en 3 ans (6 sem.)"""
|
"""ISCID: Bachelor en 3 ans (6 sem.)"""
|
||||||
|
|
||||||
NAME = "ParcoursBachelorISCID6"
|
NAME = "CursusBachelorISCID6"
|
||||||
TYPE_PARCOURS = CodesParcours.ISCID6
|
TYPE_CURSUS = CodesCursus.ISCID6
|
||||||
NAME = ""
|
NAME = ""
|
||||||
NB_SEM = 6
|
NB_SEM = 6
|
||||||
ECTS_PROF_DIPL = 8 # crédits professionnels requis pour obtenir le diplôme
|
ECTS_PROF_DIPL = 8 # crédits professionnels requis pour obtenir le diplôme
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursBachelorISCID6())
|
register_cursus(CursusBachelorISCID6())
|
||||||
|
|
||||||
|
|
||||||
class ParcoursMasterISCID4(ParcoursISCID):
|
class CursusMasterISCID4(CursusISCID):
|
||||||
"ISCID: Master en 2 ans (4 sem.)"
|
"ISCID: Master en 2 ans (4 sem.)"
|
||||||
TYPE_PARCOURS = CodesParcours.ISCID4
|
TYPE_CURSUS = CodesCursus.ISCID4
|
||||||
NAME = "ParcoursMasterISCID4"
|
NAME = "CursusMasterISCID4"
|
||||||
NB_SEM = 4
|
NB_SEM = 4
|
||||||
ECTS_PROF_DIPL = 15 # crédits professionnels requis pour obtenir le diplôme
|
ECTS_PROF_DIPL = 15 # crédits professionnels requis pour obtenir le diplôme
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursMasterISCID4())
|
register_cursus(CursusMasterISCID4())
|
||||||
|
|
||||||
|
|
||||||
class ParcoursILEPS(TypeParcours):
|
class CursusILEPS(TypeCursus):
|
||||||
"""Superclasse pour les parcours de l'ILEPS"""
|
"""Superclasse pour les cursus de l'ILEPS"""
|
||||||
|
|
||||||
# SESSION_NAME = "année"
|
# SESSION_NAME = "année"
|
||||||
# SESSION_NAME_A = "de l'"
|
# SESSION_NAME_A = "de l'"
|
||||||
|
@ -632,18 +661,18 @@ class ParcoursILEPS(TypeParcours):
|
||||||
BARRE_UE_DEFAULT = 0.0 # pas de barre sur les autres UE
|
BARRE_UE_DEFAULT = 0.0 # pas de barre sur les autres UE
|
||||||
|
|
||||||
|
|
||||||
class ParcoursLicenceILEPS6(ParcoursILEPS):
|
class CursusLicenceILEPS6(CursusILEPS):
|
||||||
"""ILEPS: Licence 6 semestres"""
|
"""ILEPS: Licence 6 semestres"""
|
||||||
|
|
||||||
TYPE_PARCOURS = 1010
|
TYPE_CURSUS = 1010
|
||||||
NAME = "LicenceILEPS6"
|
NAME = "LicenceILEPS6"
|
||||||
NB_SEM = 6
|
NB_SEM = 6
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursLicenceILEPS6())
|
register_cursus(CursusLicenceILEPS6())
|
||||||
|
|
||||||
|
|
||||||
class ParcoursUCAC(TypeParcours):
|
class CursusUCAC(TypeCursus):
|
||||||
"""Règles de validation UCAC"""
|
"""Règles de validation UCAC"""
|
||||||
|
|
||||||
SESSION_NAME = "année"
|
SESSION_NAME = "année"
|
||||||
|
@ -657,79 +686,79 @@ class ParcoursUCAC(TypeParcours):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ParcoursLicenceUCAC3(ParcoursUCAC):
|
class CursusLicenceUCAC3(CursusUCAC):
|
||||||
"""UCAC: Licence en 3 sessions d'un an"""
|
"""UCAC: Licence en 3 sessions d'un an"""
|
||||||
|
|
||||||
TYPE_PARCOURS = CodesParcours.LicenceUCAC3
|
TYPE_CURSUS = CodesCursus.LicenceUCAC3
|
||||||
NAME = "Licence UCAC en 3 sessions d'un an"
|
NAME = "Licence UCAC en 3 sessions d'un an"
|
||||||
NB_SEM = 3
|
NB_SEM = 3
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursLicenceUCAC3())
|
register_cursus(CursusLicenceUCAC3())
|
||||||
|
|
||||||
|
|
||||||
class ParcoursMasterUCAC2(ParcoursUCAC):
|
class CursusMasterUCAC2(CursusUCAC):
|
||||||
"""UCAC: Master en 2 sessions d'un an"""
|
"""UCAC: Master en 2 sessions d'un an"""
|
||||||
|
|
||||||
TYPE_PARCOURS = CodesParcours.MasterUCAC2
|
TYPE_CURSUS = CodesCursus.MasterUCAC2
|
||||||
NAME = "Master UCAC en 2 sessions d'un an"
|
NAME = "Master UCAC en 2 sessions d'un an"
|
||||||
NB_SEM = 2
|
NB_SEM = 2
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursMasterUCAC2())
|
register_cursus(CursusMasterUCAC2())
|
||||||
|
|
||||||
|
|
||||||
class ParcoursMonoUCAC(ParcoursUCAC):
|
class CursusMonoUCAC(CursusUCAC):
|
||||||
"""UCAC: Formation en 1 session de durée variable"""
|
"""UCAC: Formation en 1 session de durée variable"""
|
||||||
|
|
||||||
TYPE_PARCOURS = CodesParcours.MonoUCAC
|
TYPE_CURSUS = CodesCursus.MonoUCAC
|
||||||
NAME = "Formation UCAC en 1 session de durée variable"
|
NAME = "Formation UCAC en 1 session de durée variable"
|
||||||
NB_SEM = 1
|
NB_SEM = 1
|
||||||
UNUSED_CODES = set((ADC, ATT, ATB))
|
UNUSED_CODES = set((ADC, ATT, ATB))
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursMonoUCAC())
|
register_cursus(CursusMonoUCAC())
|
||||||
|
|
||||||
|
|
||||||
class Parcours6Sem(TypeParcours):
|
class Cursus6Sem(TypeCursus):
|
||||||
"""Parcours générique en 6 semestres"""
|
"""Cursus générique en 6 semestres"""
|
||||||
|
|
||||||
TYPE_PARCOURS = CodesParcours.GEN_6_SEM
|
TYPE_CURSUS = CodesCursus.GEN_6_SEM
|
||||||
NAME = "Formation en 6 semestres"
|
NAME = "Formation en 6 semestres"
|
||||||
NB_SEM = 6
|
NB_SEM = 6
|
||||||
COMPENSATION_UE = True
|
COMPENSATION_UE = True
|
||||||
|
|
||||||
|
|
||||||
register_parcours(Parcours6Sem())
|
register_cursus(Cursus6Sem())
|
||||||
|
|
||||||
# # En cours d'implémentation:
|
# # En cours d'implémentation:
|
||||||
# class ParcoursLicenceLMD(TypeParcours):
|
# class CursusLicenceLMD(TypeCursus):
|
||||||
# """Licence standard en 6 semestres dans le LMD"""
|
# """Licence standard en 6 semestres dans le LMD"""
|
||||||
# TYPE_PARCOURS = 401
|
# TYPE_CURSUS = 401
|
||||||
# NAME = "Licence LMD"
|
# NAME = "Licence LMD"
|
||||||
# NB_SEM = 6
|
# NB_SEM = 6
|
||||||
# COMPENSATION_UE = True
|
# COMPENSATION_UE = True
|
||||||
|
|
||||||
# register_parcours(ParcoursLicenceLMD())
|
# register_cursus(CursusLicenceLMD())
|
||||||
|
|
||||||
|
|
||||||
class ParcoursMasterLMD(TypeParcours):
|
class CursusMasterLMD(TypeCursus):
|
||||||
"""Master générique en 4 semestres dans le LMD"""
|
"""Master générique en 4 semestres dans le LMD"""
|
||||||
|
|
||||||
TYPE_PARCOURS = CodesParcours.MasterLMD
|
TYPE_CURSUS = CodesCursus.MasterLMD
|
||||||
NAME = "Master LMD"
|
NAME = "Master LMD"
|
||||||
NB_SEM = 4
|
NB_SEM = 4
|
||||||
COMPENSATION_UE = True # variabale inutilisée
|
COMPENSATION_UE = True # variabale inutilisée
|
||||||
UNUSED_CODES = set((ADC, ATT, ATB))
|
UNUSED_CODES = set((ADC, ATT, ATB))
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursMasterLMD())
|
register_cursus(CursusMasterLMD())
|
||||||
|
|
||||||
|
|
||||||
class ParcoursMasterIG(ParcoursMasterLMD):
|
class CursusMasterIG(CursusMasterLMD):
|
||||||
"""Master de l'Institut Galilée (U. Paris 13) en 4 semestres (LMD)"""
|
"""Master de l'Institut Galilée (U. Paris 13) en 4 semestres (LMD)"""
|
||||||
|
|
||||||
TYPE_PARCOURS = CodesParcours.MasterIG
|
TYPE_CURSUS = CodesCursus.MasterIG
|
||||||
NAME = "Master IG P13"
|
NAME = "Master IG P13"
|
||||||
BARRE_MOY = 10.0
|
BARRE_MOY = 10.0
|
||||||
NOTES_BARRE_VALID_UE_TH = 10.0 # seuil pour valider UE
|
NOTES_BARRE_VALID_UE_TH = 10.0 # seuil pour valider UE
|
||||||
|
@ -739,7 +768,7 @@ class ParcoursMasterIG(ParcoursMasterLMD):
|
||||||
BARRE_MOY_UE_STAGE = 10.0
|
BARRE_MOY_UE_STAGE = 10.0
|
||||||
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT, UE_STAGE_10]
|
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT, UE_STAGE_10]
|
||||||
|
|
||||||
def check_barre_ues(self, ues_status): # inspire de la fonction de ParcoursLP2014
|
def check_barre_ues(self, ues_status): # inspire de la fonction de CursusLP2014
|
||||||
"""True si la ou les conditions sur les UE sont valides
|
"""True si la ou les conditions sur les UE sont valides
|
||||||
moyenne d'UE > 7, ou > 10 si UE de stage
|
moyenne d'UE > 7, ou > 10 si UE de stage
|
||||||
"""
|
"""
|
||||||
|
@ -778,10 +807,10 @@ class ParcoursMasterIG(ParcoursMasterLMD):
|
||||||
return True, "" # pas de coef, condition ok
|
return True, "" # pas de coef, condition ok
|
||||||
|
|
||||||
|
|
||||||
register_parcours(ParcoursMasterIG())
|
register_cursus(CursusMasterIG())
|
||||||
|
|
||||||
|
|
||||||
# Ajouter ici vos parcours, le TYPE_PARCOURS devant être unique au monde
|
# Ajouter ici vos cursus, le TYPE_CURSUS devant être unique au monde
|
||||||
# (avisez sur la liste de diffusion)
|
# (avisez sur la liste de diffusion)
|
||||||
|
|
||||||
|
|
||||||
|
@ -789,16 +818,17 @@ register_parcours(ParcoursMasterIG())
|
||||||
|
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
_tp = list(TYPES_PARCOURS.items())
|
_tp = list(SCO_CURSUS.items())
|
||||||
_tp.sort(key=lambda x: x[1].__doc__) # sort by intitulé
|
_tp.sort(key=lambda x: x[1].__doc__) # sort by intitulé
|
||||||
FORMATION_PARCOURS_DESCRS = [p[1].__doc__ for p in _tp] # intitulés (eg pour menu)
|
FORMATION_CURSUS_DESCRS = [p[1].__doc__ for p in _tp] # intitulés (eg pour menu)
|
||||||
FORMATION_PARCOURS_TYPES = [p[0] for p in _tp] # codes numeriques (TYPE_PARCOURS)
|
FORMATION_CURSUS_TYPES = [p[0] for p in _tp] # codes numeriques (TYPE_CURSUS)
|
||||||
|
|
||||||
|
|
||||||
def get_parcours_from_code(code_parcours):
|
def get_cursus_from_code(code_cursus: int) -> TypeCursus:
|
||||||
parcours = TYPES_PARCOURS.get(code_parcours)
|
"renvoie le cursus de code indiqué"
|
||||||
if parcours is None:
|
cursus = SCO_CURSUS.get(code_cursus)
|
||||||
log(f"Warning: invalid code_parcours: {code_parcours}")
|
if cursus is None:
|
||||||
|
log(f"Warning: invalid code_cursus: {code_cursus}")
|
||||||
# default to legacy
|
# default to legacy
|
||||||
parcours = TYPES_PARCOURS.get(0)
|
cursus = SCO_CURSUS.get(0)
|
||||||
return parcours
|
return cursus
|
|
@ -4,7 +4,7 @@
|
||||||
#
|
#
|
||||||
# Command: ./csv2rules.py misc/parcoursDUT.csv
|
# Command: ./csv2rules.py misc/parcoursDUT.csv
|
||||||
#
|
#
|
||||||
from app.scodoc.sco_codes_parcours import (
|
from app.scodoc.codes_cursus import (
|
||||||
DUTRule,
|
DUTRule,
|
||||||
ADC,
|
ADC,
|
||||||
ADJ,
|
ADJ,
|
||||||
|
|
|
@ -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
|
||||||
|
@ -251,7 +251,7 @@ def sco_header(
|
||||||
#gtrcontent {{
|
#gtrcontent {{
|
||||||
margin-left: {params["margin_left"]};
|
margin-left: {params["margin_left"]};
|
||||||
height: 100%%;
|
height: 100%%;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 16px;
|
||||||
}}
|
}}
|
||||||
</style>
|
</style>
|
||||||
"""
|
"""
|
||||||
|
@ -274,21 +274,11 @@ 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))
|
||||||
|
|
||||||
# Avertissement si mot de passe à changer
|
|
||||||
if user_check:
|
|
||||||
if current_user.passwd_temp:
|
|
||||||
H.append(
|
|
||||||
f"""<div class="passwd_warn">
|
|
||||||
Attention !<br/>
|
|
||||||
Vous avez reçu un mot de passe temporaire.<br/>
|
|
||||||
Vous devez le changer: <a href="{scu.UsersURL}/form_change_password?user_name={current_user.user_name}">cliquez ici</a>
|
|
||||||
</div>"""
|
|
||||||
)
|
|
||||||
#
|
#
|
||||||
if head_message:
|
if head_message:
|
||||||
H.append('<div class="head_message">' + html.escape(head_message) + "</div>")
|
H.append('<div class="head_message">' + html.escape(head_message) + "</div>")
|
||||||
|
|
|
@ -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,26 +48,26 @@ def sidebar_common():
|
||||||
url_for("users.user_info_page",
|
url_for("users.user_info_page",
|
||||||
scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
|
scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
|
||||||
}">{current_user.user_name}</a>
|
}">{current_user.user_name}</a>
|
||||||
<br/><a id="deconnectlink" href="{url_for("auth.logout")}">déconnexion</a>
|
<br><a id="deconnectlink" href="{url_for("auth.logout")}">déconnexion</a>
|
||||||
</div>
|
</div>
|
||||||
{sidebar_dept()}
|
{sidebar_dept()}
|
||||||
<h2 class="insidebar">Scolarité</h2>
|
<h2 class="insidebar">Scolarité</h2>
|
||||||
<a href="{scu.ScoURL()}" class="sidebar">Semestres</a> <br/>
|
<a href="{scu.ScoURL()}" class="sidebar">Semestres</a> <br>
|
||||||
<a href="{scu.NotesURL()}" class="sidebar">Programmes</a> <br/>
|
<a href="{scu.NotesURL()}" class="sidebar">Programmes</a> <br>
|
||||||
<a href="{scu.AbsencesURL()}" class="sidebar">Absences</a> <br/>
|
<a href="{scu.AbsencesURL()}" class="sidebar">Absences</a> <br>
|
||||||
"""
|
"""
|
||||||
]
|
]
|
||||||
if current_user.has_permission(
|
if current_user.has_permission(
|
||||||
Permission.ScoUsersAdmin
|
Permission.ScoUsersAdmin
|
||||||
) or current_user.has_permission(Permission.ScoUsersView):
|
) or current_user.has_permission(Permission.ScoUsersView):
|
||||||
H.append(
|
H.append(
|
||||||
f"""<a href="{scu.UsersURL()}" class="sidebar">Utilisateurs</a> <br/>"""
|
f"""<a href="{scu.UsersURL()}" class="sidebar">Utilisateurs</a> <br>"""
|
||||||
)
|
)
|
||||||
|
|
||||||
if current_user.has_permission(Permission.ScoChangePreferences):
|
if current_user.has_permission(Permission.ScoChangePreferences):
|
||||||
H.append(
|
H.append(
|
||||||
f"""<a href="{url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)}"
|
f"""<a href="{url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)}"
|
||||||
class="sidebar">Paramétrage</a> <br/>"""
|
class="sidebar">Paramétrage</a> <br>"""
|
||||||
)
|
)
|
||||||
|
|
||||||
return "".join(H)
|
return "".join(H)
|
||||||
|
@ -84,7 +84,7 @@ def sidebar(etudid: int = None):
|
||||||
H = [
|
H = [
|
||||||
f"""<div class="sidebar">
|
f"""<div class="sidebar">
|
||||||
{ sidebar_common() }
|
{ sidebar_common() }
|
||||||
<div class="box-chercheetud">Chercher étudiant:<br/>
|
<div class="box-chercheetud">Chercher étudiant:<br>
|
||||||
<form method="get" id="form-chercheetud"
|
<form method="get" id="form-chercheetud"
|
||||||
action="{url_for('scolar.search_etud_in_dept', scodoc_dept=g.scodoc_dept) }">
|
action="{url_for('scolar.search_etud_in_dept', scodoc_dept=g.scodoc_dept) }">
|
||||||
<div><input type="text" size="12" class="in-expnom" name="expnom" spellcheck="false"></input></div>
|
<div><input type="text" size="12" class="in-expnom" name="expnom" spellcheck="false"></input></div>
|
||||||
|
@ -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(
|
||||||
|
@ -121,7 +120,7 @@ def sidebar(etudid: int = None):
|
||||||
nbabsnj = nbabs - nbabsjust
|
nbabsnj = nbabs - nbabsjust
|
||||||
H.append(
|
H.append(
|
||||||
f"""<span title="absences du { cur_sem["date_debut"] } au { cur_sem["date_fin"] }">(1/2 j.)
|
f"""<span title="absences du { cur_sem["date_debut"] } au { cur_sem["date_fin"] }">(1/2 j.)
|
||||||
<br/>{ nbabsjust } J., { nbabsnj } N.J.</span>"""
|
<br>{ nbabsjust } J., { nbabsnj } N.J.</span>"""
|
||||||
)
|
)
|
||||||
H.append("<ul>")
|
H.append("<ul>")
|
||||||
if current_user.has_permission(Permission.ScoAbsChange):
|
if current_user.has_permission(Permission.ScoAbsChange):
|
||||||
|
@ -150,7 +149,7 @@ def sidebar(etudid: int = None):
|
||||||
# Logo
|
# Logo
|
||||||
H.append(
|
H.append(
|
||||||
f"""<div class="logo-insidebar">
|
f"""<div class="logo-insidebar">
|
||||||
<div class="sidebar-bottom"><a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }" class="sidebar">À propos</a><br/>
|
<div class="sidebar-bottom"><a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }" class="sidebar">À propos</a><br>
|
||||||
<a href="{ scu.SCO_USER_MANUAL }" target="_blank" class="sidebar">Aide</a>
|
<a href="{ scu.SCO_USER_MANUAL }" target="_blank" class="sidebar">Aide</a>
|
||||||
</div></div>
|
</div></div>
|
||||||
<div class="logo-logo">
|
<div class="logo-logo">
|
||||||
|
@ -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' :
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user