Compare commits

..

597 Commits

Author SHA1 Message Date
1cf3f283dc reset password 2022-01-18 16:02:07 +01:00
f7db75e1a2 Fix: exception si import notes sur etuds non inscrit 2022-01-17 22:33:27 +01:00
831d14cf7d 9.0.25 2022-01-17 00:18:08 +01:00
b8b3185901 Traite #276 2022-01-17 00:06:21 +01:00
da5445baa8 Améliore message erreur import étudiants via excel 2022-01-15 14:59:59 +01:00
69d494c02c Fix: evaluation_listenotes si aucun inscrit 2022-01-14 11:18:31 +01:00
fa99cbf3d0 Fix #269. Utilisateurs avec logins numériques. 2022-01-13 23:30:25 +01:00
b6cedbd6b6 introducing ScoPDFFormatError for friendlier PDF errors 2022-01-13 22:36:40 +01:00
55bd15a67b Fix: script migration si tables entreprises ScoDoc 7 incohérentes 2022-01-13 21:36:56 +01:00
ec108a4454 geb_tables: meilleur msg erreur si template pdf invalide 2022-01-13 21:13:09 +01:00
a5c0619102 Bulletin BUT: reprise du f5d0074 de la passerelle Scodoc_Notes 2022-01-11 23:16:59 +01:00
6f0b03242d modif exc. handling 2022-01-11 22:44:03 +01:00
af12191cc4 change exc sur can_change_groups 2022-01-10 23:43:12 +01:00
126f719f7a Index INE et NIP 2022-01-10 15:15:26 +01:00
b2893a3371 Améliore validation des dates et des ids 2022-01-10 12:00:02 +01:00
00b6d19c0c hotfix: enlève décision jury bul. BUT 2022-01-10 00:02:34 +01:00
58a6d16d12 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc 2022-01-09 21:49:21 +01:00
0a1264051c Bulletin BUT: n'affiche que les modules auxquels l'étudiant est inscrit 2022-01-09 21:48:58 +01:00
f7dbff782f Merge pull request 'Relevé : décision jury + flèches' (#268) from lehmann/ScoDoc-Front:master into master
Reviewed-on: https://scodoc.org/git/ScoDoc/ScoDoc/pulls/268
2022-01-09 12:03:47 +01:00
acdd037483 Relevé : décision jury + flèches 2022-01-09 11:36:15 +01:00
68dec8e1f8 Fix: anciens bulletins XML du BUT des démissionnaires 2022-01-09 10:11:50 +01:00
9172282451 backport fix formations 2022-01-08 20:07:13 +01:00
24bfb8a13d 9.1.20 avec des cerises 2022-01-08 19:53:17 +01:00
fbae5d268f Fix: exports bul. xml quand non publiés 2022-01-08 15:27:40 +01:00
f2e9fbb8cd Fix: edition de formations migrées (non affichages de certains modules) 2022-01-08 14:01:16 +01:00
0c57aa83ca version 9.1.19 2022-01-07 18:44:42 +01:00
8b2edca257 Fix: passage d'un semestre à l'autre 2022-01-07 18:38:32 +01:00
0ceb1c8046 Fix: flag publication bulletins XML BUT compat 2022-01-07 18:09:45 +01:00
b1ab7a4df9 Fix: notes des évaluations BUT en XML compt sur 20 2022-01-07 18:05:27 +01:00
d68f81b4ea Fix exports Apogée (pb liés passage à Python 3) 2022-01-07 17:58:34 +01:00
4140f11b7b Fonction bonus_sport lr (La Rochelle) 2022-01-07 15:21:30 +01:00
d1ff47727b Fix: bug si user_name ne contient que des chiffres 2022-01-07 15:19:34 +01:00
20c8f22c7b cosmétique accueil 2022-01-05 21:44:22 +01:00
d6c6a08828 Fix: 2 bugs sur formulaire création/modif utilisateurs 2022-01-05 21:11:57 +01:00
d622b313b0 Merge branch 'jmplace-gestion_scodoc-data.old' 2022-01-05 16:46:09 +01:00
db9acb67dd détails bash 2022-01-05 16:45:55 +01:00
8a415984c6 Merge branch 'gestion_scodoc-data.old' of https://scodoc.org/git/jmplace/ScoDoc-Lille into jmplace-gestion_scodoc-data.old 2022-01-05 16:14:59 +01:00
6157e54a5f affichage nom complet du dept. sur la page accueil 2022-01-05 16:06:56 +01:00
Jean-Marie PLACE
efe997fe55 check scodoc-data.old ; correction bug --keep-env 2022-01-05 06:10:25 +01:00
ff948cb98d new users default dept in form 2022-01-05 01:04:07 +01:00
4b63fe81e4 fix: het sort 2022-01-05 01:03:25 +01:00
5895e5c33c edition formations / tests unitaires ok 2022-01-04 23:05:37 +01:00
e3535aa4da check_moduleimpl_conformity: modif exc incohérence 2022-01-04 19:46:35 +01:00
e1adf93bf0 Fix: edition des users sans dept 2022-01-04 19:32:58 +01:00
be2227f8a3 L'admin peut cacher/montrer des départements 2022-01-04 18:10:14 +01:00
9b9b2f270b Fix: flag publication bulletins 2022-01-04 17:49:13 +01:00
6fe77988a0 Fix: liste des indices de semestres 2022-01-04 17:44:56 +01:00
a1bb957eaf meilleure gestion des suppressions d'objets dans l'édition des formations 2022-01-04 17:33:02 +01:00
f2e21e0cc2 default module type name 2022-01-04 15:03:38 +01:00
e838321e0c 9.1.17 2022-01-04 12:26:08 +01:00
46c64ba78b comments 2022-01-03 12:33:27 +01:00
dc004de8ef update copyright 2022-01-01 14:49:42 +01:00
8bd9cf8956 upgrade python components 2022-01-01 13:05:17 +01:00
8ff524bf5f Merge pull request 'ajoute test_logos' (#249) from jmplace/ScoDoc-Lille:tests_api_logos into master
Reviewed-on: https://scodoc.org/git/ScoDoc/ScoDoc/pulls/249

config_test_logos.py pourra être renommé config_test_api.py.
2021-12-31 17:32:32 +01:00
Jean-Marie PLACE
9f3f2b33e3 ajoute test_logos 2021-12-31 11:59:13 +01:00
9b5b4777e2 Messages erreurs quand saisie champs trop longs 2021-12-29 19:30:49 +01:00
9cd31e66f0 Edition/créeation utilisateurs: choix département selon les permissions 2021-12-29 11:26:54 +01:00
2d2b2b2f39 Form création dept + déplace form logos 2021-12-23 16:03:30 +01:00
429820b786 9.1.15 2021-12-23 00:18:05 +01:00
e7d2094f0b Merge branch 'lehmann-master' 2021-12-23 00:17:27 +01:00
253e42d9f3 Merge branch 'master' of https://scodoc.org/git/lehmann/ScoDoc-Front into lehmann-master 2021-12-23 00:16:00 +01:00
d12db96389 typo 2021-12-22 14:31:44 +01:00
9eb2c2462b liens marge gauche 2021-12-22 13:13:01 +01:00
799245b265 version 9.1.13 2021-12-22 10:32:35 +01:00
d86bb3e9b7 le fichier oublié en 9.1.13 2021-12-22 10:23:17 +01:00
5422124d68 Merge branch 'jmplace-api_logos' 2021-12-22 00:36:24 +01:00
8db9a027cb API logos / reorganise code 2021-12-22 00:35:58 +01:00
69fc831ef3 Merge branch 'api_logos' of https://scodoc.org/git/jmplace/ScoDoc-Lille into jmplace-api_logos 2021-12-21 23:16:47 +01:00
3631719f54 fix #238 2021-12-21 23:05:49 +01:00
6baec61e0e Bulletin : Liens vers plus bas + lien vers fiche étudiant 2021-12-21 22:22:55 +01:00
Jean-Marie PLACE
12f646547e api list logos 2021-12-21 21:11:18 +01:00
be3a9078e2 Merge pull request 'Bul. json: URL fiche etud' (#3) from ScoDoc/ScoDoc:master into master
Reviewed-on: https://scodoc.org/git/lehmann/ScoDoc-Front/pulls/3
2021-12-21 16:05:38 +01:00
6dbba98097 Bul. json: URL fiche etud 2021-12-21 15:33:21 +01:00
e0edde3f46 Merge pull request 'master' (#2) from ScoDoc/ScoDoc:master into master
Reviewed-on: https://scodoc.org/git/lehmann/ScoDoc-Front/pulls/2
2021-12-21 15:18:31 +01:00
2ab7cef447 Merge branch 'lehmann-master' 2021-12-21 15:13:49 +01:00
ce2ae9340c cache codes INE/NIP dans bulletin scodoc 2021-12-21 15:10:49 +01:00
5e31d3372a interversion INE/NIP dans bulletins 2021-12-21 15:10:20 +01:00
271d7fba05 Merge branch 'master' of https://scodoc.org/git/lehmann/ScoDoc-Front into lehmann-master 2021-12-21 14:45:59 +01:00
fdee02d726 Reverse ine / nio 2021-12-21 13:56:19 +01:00
06ca136384 Exemple API avec contrôle accès manuel 2021-12-21 00:10:51 +01:00
58cacb67b8 commande user-role 2021-12-21 00:04:42 +01:00
f1c43a5bb8 petit refactoring (photos) 2021-12-20 22:53:09 +01:00
8532ab5134 JWT token size 2021-12-20 22:50:14 +01:00
8059e1622f Renomme qq modèles (CamelCase sur FormSemestre) 2021-12-20 20:38:21 +01:00
f263354f88 Fermeture tickets #203 #204 #205 #217 2021-12-20 15:39:43 +01:00
19c736c894 Merge pull request 'master' (#1) from ScoDoc/ScoDoc:master into master
Reviewed-on: https://scodoc.org/git/lehmann/ScoDoc-Front/pulls/1
2021-12-19 18:36:06 +01:00
ac5c433f5a Affichage poids sur tableau bord module + fix #222 2021-12-19 11:08:03 +01:00
c882a96556 Import formations XML: anciennes ou BUT 2021-12-18 17:39:03 +01:00
3d8fe461b9 ajout titres pages (liste evals, bul.) 2021-12-18 12:16:49 +01:00
6d7645a599 Fix: ordre étudiants dans df evals (bis) 2021-12-18 12:15:40 +01:00
43a47f3416 Merge branch 'lehmann-master' 2021-12-18 09:48:20 +01:00
1aa822c906 couleur SAE 2021-12-18 09:47:45 +01:00
562888eff6 Merge branch 'master' of https://scodoc.org/git/lehmann/ScoDoc-Front into lehmann-master 2021-12-18 09:35:56 +01:00
3114189e4c Amélioration relevé 2021-12-18 03:36:00 +01:00
ec61fced73 Amérlioration relevés 2021-12-18 03:32:18 +01:00
3346eded05 Relevé version Web Component 2021-12-18 02:30:47 +01:00
0b03b0c188 TEST 2021-12-18 01:43:34 +01:00
6596e70eec Affichage des notes d'évaluation dans tableau de bord module (prise en compte des (de)inscriptions). 2021-12-17 23:50:34 +01:00
5e65e75a3b tolere module_type NULL (bug Orléans/Chimie ?) 2021-12-17 23:05:59 +01:00
4511951255 Fix: suppression toutes notes évaluations 2021-12-17 22:53:34 +01:00
593317a50a Export recapcomplet json: format BUT 2021-12-17 21:59:28 +01:00
71615613f1 Fix: bulletins oldjson des dém. 2021-12-17 21:36:34 +01:00
SebL68
339af76a33 Update scodoc.py 2021-12-17 16:34:35 +01:00
ff1e503d30 ue_move renvoie au semestre courant 2021-12-17 14:30:38 +01:00
b709b9c2f1 Fix: ordre des étudiants 2021-12-17 14:24:00 +01:00
87bcfc801a Fix: objects_renumber 2021-12-17 13:42:39 +01:00
8c95edf01c Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc 2021-12-17 00:39:58 +01:00
1a7310950b oups 2021-12-17 00:39:45 +01:00
eb5a1c727c oups 2021-12-17 00:39:06 +01:00
b153c9ad9e Bulletins BUT JSON: décisions de jury et situation 2021-12-17 00:25:45 +01:00
78c7c1a763 Clonage semestre avec poids des évaluation. Closes #221 2021-12-16 22:54:24 +01:00
40b31602d2 initialise numéros partitions pour tri (#225) 2021-12-16 21:42:23 +01:00
5f409f0267 Fix #230: notes d'éval / 20 dans bul json 2021-12-16 21:10:20 +01:00
3ec5cef3c0 Merge pull request 'execute migrate_logos as scodoc (not root)' (#224) from jmplace/ScoDoc-Lille:fix_migrate_logo_as_root into master
Reviewed-on: https://scodoc.org/git/ScoDoc/ScoDoc/pulls/224
2021-12-16 16:28:41 +01:00
d40d82aeb7 Edition formation BUT: affiche UE de rattachement et force cohérence semestre module/UE 2021-12-16 16:27:35 +01:00
bf14f8ed34 curl -> wget for build_release 2021-12-16 16:26:23 +01:00
d89d1be041 Fix: evaluation completes avec démissionnaires. Fixes #227 2021-12-16 14:40:24 +01:00
ef8e9b4ef0 Bulletin démissionnaires: closes #226 2021-12-16 12:41:37 +01:00
e06cf82db8 Modification bulletins BUT: plus de moyennes indicatives de module 2021-12-16 00:03:24 +01:00
1b2573d130 Fix: calcul cube sans les démissionnaires. 2021-12-15 23:29:18 +01:00
051e0d24e2 execute migrate_logos as scodoc (not root) 2021-12-15 23:03:03 +01:00
79e2c9476b WIP: fix rapide pour eviter plante quand démissionnaires 2021-12-15 08:12:47 +01:00
25a441f7f2 WIP: moyennes modules, evals, bulletins. 2021-12-14 23:03:59 +01:00
4d6d7ad168 nan dans les moyennes de modules (UE non évaluées) 2021-12-14 14:33:49 +01:00
0d75d64e85 grr covention formattage JS à définir 2021-12-14 14:33:10 +01:00
af5c79d95d Fix: formsemestre_import_etud_admission / dates naissances manquantes 2021-12-14 12:47:59 +01:00
683b760341 Merge pull request 'fix afffichage bulletin si date naissance ansente' (#218) from jmplace/ScoDoc-Lille:fix_no_birthdate into master
Reviewed-on: https://scodoc.org/git/ScoDoc/ScoDoc/pulls/218
2021-12-14 12:42:28 +01:00
56d9681c87 fix afffichage bulletin si date naissance ansente 2021-12-14 11:39:12 +01:00
8bf5a0ab3e Fix: migration scodoc7 2021-12-14 10:31:33 +01:00
83f8f2ddbc fix: plante recap si aucun etudiant 2021-12-14 09:30:47 +01:00
ae42db06da invalidation cache Redis lors des mises à jour 2021-12-14 07:48:22 +01:00
69431b4de0 9.1.3 2021-12-13 23:45:28 +01:00
5e492dc145 Fixes: cascades et invalidations de caches 2021-12-13 23:44:13 +01:00
862ffb89a1 publication API ListeAbsEtud compat ScoDoc7 2021-12-13 22:15:42 +01:00
66f39b8f16 shell: ajoute np et pd 2021-12-13 19:05:51 +01:00
1e7a509879 Fix: edit module si pas de numero 2021-12-13 19:05:31 +01:00
44237c648b Fix: bug check conformité si pas de poids 2021-12-13 19:04:35 +01:00
79c4b33bee Fix: export zip photos 2021-12-13 11:10:53 +01:00
2244f29c33 Fix: operator priority 2021-12-13 10:20:16 +01:00
28f266fac6 navigation édition formation 2021-12-12 23:03:36 +01:00
b319d9c0d3 Gestion logos (contrib @jmp) 2021-12-12 22:40:02 +01:00
13d0e462cc msg erreur si upload fichier invalide 2021-12-12 22:27:06 +01:00
4aa4ab316c petits détails (messages) 2021-12-12 22:11:11 +01:00
5c90448656 Fix: erreur si titre module null 2021-12-12 21:49:03 +01:00
cf939587b1 affiche nom ens. sur table list ops 2021-12-12 21:48:26 +01:00
4a5509694f Merge pull request 'Mise à jour de 'app/scodoc/sco_undo_notes.py'' (#208) from pascal.bouron/ScoDoc_Lyon:master into master
Reviewed-on: https://scodoc.org/git/ScoDoc/ScoDoc/pulls/208
2021-12-12 21:30:58 +01:00
861a7da8a1 Mise à jour de 'app/scodoc/sco_undo_notes.py'
Remplacement uid par user_name
Ajout colonne prenom (étudiant) pour lever ambiguité lorsqu'il y a des homonymes
2021-12-12 21:11:18 +01:00
10941b6ef4 Merge pull request 'livrable_logos' (#200) from jmplace/ScoDoc-Lille:livrable_logos into PNBUT
Reviewed-on: https://scodoc.org/git/ScoDoc/ScoDoc/pulls/200
2021-12-12 17:00:52 +01:00
3eda56e89c Fix: read non-utf8 archives index 2021-12-12 16:53:52 +01:00
848a168f02 cosmetic 2021-12-12 16:53:14 +01:00
567d95b61d Fix: association à une nouvelle version de programme: rattachement des poids 2021-12-12 16:24:50 +01:00
d6ebb55b95 support old json export for BUT, using format=oldjson 2021-12-12 15:55:04 +01:00
cf996e46d0 Accès bulletins BUt par code_nip 2021-12-12 13:00:30 +01:00
abc7fb3378 présentation UE/modules en mode BUT 2021-12-12 12:32:21 +01:00
44d2ffb240 black + lien saisie note 2021-12-12 12:31:51 +01:00
711ca9c220 calcul et affiche les rangs / moy gen BUT 2021-12-12 10:17:02 +01:00
80b8956af5 Bulletins JS: sans option bul_show_all_evals + liens vers évaluations. 2021-12-12 08:44:05 +01:00
77be33d046 Fix #74 2021-12-12 08:40:25 +01:00
547e5f989d fix css 2021-12-11 21:50:17 +01:00
f94d40d316 greffe APC sur tableau recap. 2021-12-11 20:27:58 +01:00
fea2078201 Enregistrement des coefs quand on quitte le formulaire 2021-12-11 18:52:43 +01:00
1e96d72ab1 PN: édition UE de rattachement des modules 2021-12-11 18:37:13 +01:00
235ca69a82 essai avec last_modified 2021-12-11 17:55:08 +01:00
4f8f327cb6 disable cache for logos objects 2021-12-11 17:20:34 +01:00
8a5c4b5ced mise a jour tests + images de test 2021-12-11 17:05:38 +01:00
aa78346a06 Bulletins XML (ancienne structure) avec résultats BUT 2021-12-11 16:46:15 +01:00
23f1dc4ed2 remove toolbar 2021-12-11 16:07:25 +01:00
2920c6f131 temp. le 19/11 12h 2021-12-11 15:25:42 +01:00
483c22678a build logo form (header & footer) 2021-12-11 14:55:46 +01:00
d8091b4efb strip config_logo from scolar 2021-12-11 14:55:45 +01:00
a6c95b013b ajout localize-logo flask command 2021-12-11 14:55:45 +01:00
51506c6d6f migration des logos_dept scodoc7 -> scodoc9 2021-12-11 14:55:42 +01:00
df3439351d ajout tests logos 2021-12-11 14:53:12 +01:00
b336a1c1a2 ajout URL de récupéraions des logos (deprecates /ScoDoc/logo_header et /ScoDoc/logo_footer) 2021-12-11 14:53:12 +01:00
29c9982afc adaptation du template d'affichage des images (maintenant miniatures) 2021-12-11 14:53:12 +01:00
b3e1659049 adaptation du code de traitement des balises <logo...> des éditions paramétrées 2021-12-11 14:53:12 +01:00
f219b8d003 adaptation de code de traitement des templates pdf 2021-12-11 14:53:12 +01:00
ecd637fb39 adaptation du code d'édition des pv 2021-12-11 14:53:12 +01:00
d7e34b8ce2 adaptation de la sauvegarde des fichiers logos poursuites d'études 2021-12-11 14:53:11 +01:00
5e461f7dd6 Ecriture des fonctions d'accés aux logos (et aux images) 2021-12-11 14:53:11 +01:00
915d4059a7 fichier oublié dans le commit précédent 2021-12-11 12:39:53 +01:00
5c2c97cfb3 Calcul moyenne générale indicative 2021-12-11 12:10:05 +01:00
46d9316984 qq commentaires 2021-12-11 10:57:06 +01:00
49db8c3c71 Fix: affichage notes eval ABS 2021-12-11 10:56:40 +01:00
c2c1c5b5f2 Pour une poignée d'octets 2021-12-11 09:58:04 +01:00
2cca1e9bbd fix Module.set_ue_coef_dict 2021-12-10 16:45:36 +01:00
272de740e1 Fix: édition coefs semestres, retour au semestre sélectionné. 2021-12-10 15:51:43 +01:00
2f9b2a5a2d affichage coefs UE dans bulles tableau bord semestre + qq typos 2021-12-10 15:27:52 +01:00
e5324e214c ignore UE et matières dans tri modules BUT (tableau bord sem.) 2021-12-10 15:03:29 +01:00
517ee8bd2d Désactive tri des JSON 2021-12-10 09:27:47 +01:00
c332f1cee7 ordre des UE (et coefs) 2021-12-10 01:55:13 +01:00
0c49cfaf78 ajout numero UE 2021-12-10 01:54:11 +01:00
acebc8ab08 page infos UE + qq corrections 2021-12-10 00:54:57 +01:00
8ce6c907f3 PN: ordre édition des UE 2021-12-09 12:07:58 +01:00
23d908b932 PN: ordre autres modules 2021-12-09 11:56:10 +01:00
478688fe49 Ordre des modules et UE (edition PN et poids) 2021-12-09 11:52:46 +01:00
aeb0d67f38 affichage non conformité 2021-12-08 23:43:07 +01:00
ab1898b185 initialisation poids formulaire prenant en compte les coefs module 2021-12-08 23:42:19 +01:00
857c3007a5 sépare modèles UE et Module 2021-12-08 22:47:29 +01:00
11b3f64319 Calcul moyenne UE BUT progressif (admet modules sans notes) 2021-12-08 21:49:13 +01:00
270d03057f commence à saboter les couleurs de Seb. :-) 2021-12-08 15:11:57 +01:00
45f4fe3e12 cosmetic 2021-12-08 15:11:05 +01:00
36b432839a Calcul moyennes modules avec inscriptions partielles, ATT, EXC, ABS, mode immédiat. (+ tests unit.) 2021-12-08 14:13:18 +01:00
9927368680 Merge pull request 'fix_ine_bug dans prepajury' (#195) from jmplace/ScoDoc-Lille:fix_ine_prepajury into PNBUT
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/195
2021-12-07 20:56:46 +01:00
f40817dbf5 fix_ine_bug dans prepajury 2021-12-07 10:15:14 +01:00
5b7adf16ec fix: cas but avec modules exotiques + tableau bord si pas de coefs 2021-12-06 23:33:41 +01:00
7d5160bb83 bordereau (contrib Pascal B.). 2021-12-06 23:15:55 +01:00
902df8f886 version 2021-12-06 22:57:52 +01:00
f2db115fdf Merge pull request 'Bordereau de signatures' (#194) from pascal.bouron/ScoDoc_Lyon:master into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/194
2021-12-06 22:56:01 +01:00
6cc76037a6 bul: lien vers page module 2021-12-06 22:37:49 +01:00
fa4dcb3169 intégration bulletin dans template std 2021-12-06 21:19:40 +01:00
3b30a67b17 intégration v0 bulletin BUT par Seb. L. 2021-12-06 20:58:39 +01:00
065fc460cd sco_evaluations
Gestion du non-affichage de l'heure lorsqu'aucune date n'est fixée pour l'évaluation

sco_liste_notes
Generation de bordereaux de signatures pour les évaluations
- nouveau type d'export pdf (nommé "bordereau")
- ajout colonne signatures
- modification de la page evaluation_listenotes  pour intégrer le nouveau lien
2021-12-06 14:04:03 +01:00
177f23b48c json: formatte notes en txt 2021-12-06 10:57:10 +01:00
20470484d1 recap UE: filtre modules avec coef nul 2021-12-06 09:21:15 +01:00
e4b9a48ebf branchement export sae/ressources bulletins (oubliés précedemment) 2021-12-05 23:56:22 +01:00
fd80ee2452 bul json: options affichage 2021-12-05 21:54:04 +01:00
3ba30f6250 Génération bulletin BUT json 2021-12-05 20:21:51 +01:00
1a673862aa poids évals (graph Seb.) 2021-12-04 22:48:32 +01:00
507ff12642 fix 2021-12-04 22:41:06 +01:00
24440f457b vis. et suppr. des ref. comp. 2021-12-04 22:20:08 +01:00
f555122989 flashed messages 2021-12-04 22:19:20 +01:00
d79f376b24 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into PNBUT 2021-12-04 21:31:56 +01:00
f105e1f331 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc 2021-12-04 21:18:12 +01:00
3aa115e7a2 9.0.68 2021-12-04 21:18:00 +01:00
5fc0363265 comment 2021-12-04 21:12:38 +01:00
cd694a956d Merge pull request 'Suppression de (%(jour)s) lorsque l'évaluation n'a pas de date' (#192) from pascal.bouron/ScoDoc_Lyon:master into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/192
2021-12-04 21:09:28 +01:00
2e1ec1c5ea Templates jinja2 pour pages ScoDoc 2021-12-04 21:06:32 +01:00
b1bc8b3f41 préparatifs/refactoring 2021-12-04 21:04:09 +01:00
540a956fae Suppression de (%(jour)s) lorsque l'évaluation n'a pas de date 2021-12-03 21:17:30 +01:00
c455f6261f liste refcomps + routes 2021-12-03 15:46:46 +01:00
958539977a modif export json (retours Seb) 2021-12-03 15:44:57 +01:00
6627a9c6b2 traitement exceptions imports xml 2021-12-03 14:13:49 +01:00
ec93a8cdbc Merge pull request 'soften xml formation element' (#191) from jmplace/ScoDoc-Lille:fix_import_xml into PNBUT
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/191
2021-12-03 11:26:17 +01:00
0a000afba4 soften xml formation element 2021-12-03 11:15:35 +01:00
4d857a1567 chargement/association ref. comp. BUT 2021-12-03 11:03:33 +01:00
e88e280994 Import ref. comp. (fichier oublié) 2021-12-02 21:43:48 +01:00
9ee0acd60b Affichage des coefs de modules (avec Sébastien) 2021-12-02 21:16:14 +01:00
d435f3b835 export ref. comp. en json 2021-12-02 17:36:32 +01:00
d2b69c2f73 Import ref. Compétences BUT (Orébut) 2021-12-02 12:08:03 +01:00
440e9157b4 Import/export xml des formations (format ScoDoc) avec coefs UE 2021-11-30 11:07:17 +01:00
5d8dad3711 Fix: codes parcours entiers 2021-11-30 10:55:44 +01:00
83f522f08c Fix: cascade on ue coefs 2021-11-30 10:54:12 +01:00
47e752c95c Cache coefs et poids. Check conformité PN. 2021-11-29 22:18:37 +01:00
8647203f43 Calcul moyennes BUT: prise en compte des inscriptions aux modules optionnels. 2021-11-29 00:01:39 +01:00
fa896b77ab cast matrice des inscriptions aux modules en booleen 2021-11-29 00:00:44 +01:00
09eb73be4a WIP: calcul moyennes UE BUT 2021-11-28 16:31:33 +01:00
be8925e163 charegment matrice inscriptions modules 2021-11-28 16:27:05 +01:00
7819d382e4 tests pour moyennes UE BUT 2021-11-28 16:26:04 +01:00
47c1a75bb0 modeles 2021-11-28 10:13:18 +01:00
baa5286dae Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc 2021-11-27 19:05:10 +01:00
8fc16dfeda Fix: cascades sur itemsuivi 2021-11-27 19:04:30 +01:00
21fa7112c8 param checking (exceptions pour utilisateurs) 2021-11-27 18:55:47 +01:00
480af81e0d check user params (old ids) 2021-11-27 18:44:32 +01:00
5f868fd27c filtre UE sport 2021-11-26 18:13:37 +01:00
5a3c25e67f Optimize compute_module_moy 2021-11-26 17:26:34 +01:00
d05ce6a93b Merge pull request 'Mise à jour de 'tools/configure-scodoc9.sh'' (#187) from pascal.bouron/ScoDoc:master into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/187
Fix #186
2021-11-24 21:35:09 +01:00
f47cd46abc Mise à jour de 'tools/configure-scodoc9.sh'
Correction de bug du à  '
2021-11-24 21:26:34 +01:00
83ba9cf186 Calculs moyennes modules BUT: tests unitaires complets, corrections. 2021-11-23 21:54:54 +01:00
daa06651d5 Fix: export tables csv: force conversion des chaines 2021-11-23 15:10:34 +01:00
1b7a28ac8d robustifie script migration (post Colmar) 2021-11-23 12:31:03 +01:00
502f6a9277 Modif script migration pour facilier reprise sur erreur 2021-11-22 22:39:05 +01:00
4f90404c3a Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into PNBUT 2021-11-22 00:32:53 +01:00
a83ab8f684 WIP: calcul des moyennes de modules BUT 2021-11-22 00:31:53 +01:00
5e2e36cfab Modifie script d'upgrade pour éviter les 'kept back' 2021-11-21 23:58:18 +01:00
6c747b1f0e removed added deb dependency to unblock updates 2021-11-21 23:40:34 +01:00
adbbd51cf1 version 2021-11-20 19:38:19 +01:00
ea8598d411 version bump (9.0.63) 2021-11-20 19:30:15 +01:00
3a0a2382c8 refactoring 2021-11-20 17:53:21 +01:00
504b12cadb evaluation_listenotes: petite optim 2021-11-20 17:21:51 +01:00
780a117fbd WIP: chargement notes et calcul moy module 2021-11-20 16:35:09 +01:00
042d5080b2 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into PNBUT 2021-11-19 15:28:31 +01:00
08d4258a49 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc 2021-11-19 15:20:28 +01:00
bfd60ffce4 Fix: menu édition groupes (contrib. C. Martin) 2021-11-19 15:19:54 +01:00
84f25817d1 Fix edit evaluation non APC 2021-11-18 23:54:50 +01:00
e706407bcb Ajoute relations: inscriptions etuds 2021-11-18 23:53:57 +01:00
01ea6286ee importe modeles pour faciliter essais interactifs 2021-11-18 23:52:06 +01:00
b28ffdd7a8 WIP: Editions formations 2021-11-18 22:46:18 +01:00
b2c98af293 WIP: Edition formations 2021-11-18 11:29:38 +01:00
e3219a6b0c WIP: Edition formations 2021-11-18 00:24:56 +01:00
8f4299e880 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into PNBUT 2021-11-17 10:29:18 +01:00
58a7508043 WIP: PN BUT 2021-11-17 10:28:51 +01:00
fc6da6c976 Merge pull request 'Fix debian package depends' (#185) from jmartin/ScoDoc:master into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/185

Elle l'est si on installe suivant la doc (https://scodoc.org/GuideInstallDebian11/) en cochant "standard system utilities", je suppose.

Mais en effet c'est mieux d'indiquer la dépendance explicitement !
2021-11-16 10:22:48 +01:00
e26dfd7042 Merge pull request 'Mise à jour de 'tools/configure-scodoc9.sh'' (#184) from pascal.bouron/ScoDoc:master into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/184
2021-11-16 08:58:23 +01:00
673ac2fdc9 Mise à jour de 'README.md'
Correction des commandes flask
2021-11-16 06:47:35 +01:00
51430f82bd Merge branch 'master' into master 2021-11-16 06:14:12 +01:00
8dc98c64f1 Mise à jour de 'tools/configure-scodoc9.sh' 2021-11-16 06:07:40 +01:00
7ebd6f31dc Merge pull request 'Mise à jour de 'app/templates/sidebar_dept.html'' (#183) from pascal.bouron/ScoDoc:master into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/183
2021-11-15 19:43:04 +01:00
5d9513315b Merge branch 'master' into master 2021-11-15 16:24:07 +01:00
9d8b2d5071 Mise à jour de 'app/templates/sidebar_dept.html'
Le lien personnalisable (URL de l'"intranet" du département) ne fonctionnait pas.
Il y avait un espace de trop dans le nom de la variable

testé : OK
2021-11-15 16:20:22 +01:00
jeromemartin
9e8da925a5 Fix debian package depends 2021-11-15 16:00:14 +01:00
52fa49d7f6 Merge pull request 'Mise à jour de 'app/scodoc/sco_undo_notes.py'' (#182) from pascal.bouron/ScoDoc:master into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/182
2021-11-15 10:58:00 +01:00
4d9d5293c1 Mise à jour de 'app/scodoc/sco_undo_notes.py'
Correction du SELECT pour inclure code_nip
2021-11-15 10:49:16 +01:00
14ab816bee index semestre dans les UE 2021-11-14 18:09:20 +01:00
4182fd494c Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into PNBUT 2021-11-13 18:25:02 +01:00
5b534abf5f Parcours Licence ILEPS 2021-11-13 18:15:15 +01:00
f6b2297bd3 Edition coefs (nuls) + tests 2021-11-13 12:09:30 +01:00
1488689bfb améliore editeur coef (enter) 2021-11-13 12:07:22 +01:00
459e75db89 details cosmétiques 2021-11-13 09:43:13 +01:00
477c2efac9 Nouvelle version table_editor (via Seb. L.) 2021-11-13 09:11:00 +01:00
c0dd83fadb reorganisation des modeles 2021-11-13 08:25:51 +01:00
89e7250f4a - Coef evaluations (modèles).
- Refactoring.
- Changement des noms des classes (modèles) des formations.
- Début intégration calculs BUT.
- Requiert numpy et pandas.
2021-11-12 22:17:46 +01:00
d71b399c3d Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into PNBUT 2021-11-12 11:40:31 +01:00
47d728376c code cosmetic 2021-11-12 11:31:50 +01:00
ce4115eeef closes #178 (colonne NIP dans liste saisie notes) 2021-11-12 11:30:59 +01:00
0e6513e339 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into PNBUT 2021-11-12 11:01:59 +01:00
12c0af8bb6 Merge pull request 'fix_groups_missing_from_prepajury.xlsx' (#181) from jmplace/ScoDoc-Lille:fix_missing_groups_from_prepajury into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/181
2021-11-12 11:00:07 +01:00
c23a5abb6c fix_groups_missing_from_prepajury.xlsx 2021-11-12 09:25:47 +01:00
090391300a add dependence on cracklib-runtime 2021-11-10 12:03:49 +01:00
139eb8171a Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into PNBUT
update
2021-11-10 09:38:08 +01:00
ddf3d73c92 enhance check xls upload notes 2021-11-10 09:37:45 +01:00
94325cef2c Mise à jour, re-ordonne db migrations 2021-11-09 22:39:04 +01:00
03cfcf3298 version bump 2021-11-09 22:29:20 +01:00
0573b259d9 re-autorise API evaluation_listenotes en mode ScoDoc7 2021-11-09 22:12:11 +01:00
a4e4c39797 Fix: suppression semestres avec notifications absences 2021-11-09 11:52:41 +01:00
ade1b4445c Tolère parcours inconnus (pour dev avec bases du futur:) 2021-11-09 11:47:48 +01:00
7f2e87e9d0 Fix: exceptions dans formules utilisateurs 2021-11-09 11:45:51 +01:00
3bb9a5cb76 WIP: distinction SAE/ressources, poids de evals 2021-11-08 19:44:25 +01:00
981f2c0e46 SignaleAbsenceGrSemestre : cosmetic 2021-11-07 10:00:50 +01:00
ae525fd267 fix #12 2021-11-06 18:41:57 +01:00
a25ebe9a46 fix #22 2021-11-06 18:34:01 +01:00
ac09c104e9 fix route in import_infos_admissions 2021-11-06 17:58:11 +01:00
50115337b1 fix #175 2021-11-06 17:24:11 +01:00
afb94cb011 enhance path management 2021-11-06 17:23:53 +01:00
f6dfa912d7 fix #159 2021-11-06 16:35:21 +01:00
5f19931d63 fix err suggestion utilisateur si on entrait un nombre 2021-11-06 11:04:47 +01:00
bd5c4d8243 NA0 -> NA 2021-11-06 10:58:56 +01:00
2f72401ba1 modification code NA0 => NA 2021-11-03 16:32:19 +01:00
f534e9757f Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into PNBUT 2021-11-03 16:25:48 +01:00
4bf983dbe4 Add unit test: test_notes_modules_att_dem (scenario "lyonnais") 2021-11-03 16:15:39 +01:00
23a59357e9 Fix: ue_set_internal => cache invalidation 2021-11-03 16:11:58 +01:00
81720facff Fix: ListeAbsEtud arg 2021-11-03 14:12:27 +01:00
518b9c049c update 2021-11-03 00:44:23 +01:00
1c719b5c7c Fix regression: groups_view argument 2021-11-03 00:27:50 +01:00
ddcc518807 Memoize (cache) user_info 2021-11-02 23:42:46 +01:00
5002afade1 Accélère accès aux préférences 2021-11-02 15:49:12 +01:00
39a9f353d2 update 2021-11-01 17:02:24 +01:00
7589d4cc34 API ScoDoc 7: autorise POSTs, ajoute groups_view, script exemple/test 2021-11-01 16:59:56 +01:00
01a84f3b12 tests unitaires calculs moyennes modules et UE 2021-11-01 16:12:53 +01:00
9f9cb6cca2 migrate_scodoc7_dept_archives (ajout du s au nom de la fonction) 2021-11-01 15:21:38 +01:00
d8e1c428b0 migrate_scodoc7_dept_archives (ajout du s au nom de la fonction) 2021-11-01 15:16:51 +01:00
4fc31d8b47 Unit test: moyenne module 2021-10-30 23:27:27 +02:00
e46c6a410f Merge pull request 'ameliore_create_change_user' (#177) from jmplace/ScoDoc-Lille:ameliore_create_change_user into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/177
2021-10-30 12:33:22 +02:00
68ac7c293a mail from current user rather than admin 2021-10-30 12:09:35 +02:00
8f1e465280 show current user name while getting old_password 2021-10-30 12:06:55 +02:00
c248def7f2 refactoring Mode 2021-10-30 12:03:21 +02:00
2b91fd78df ajout mention \'message automatique\' 2021-10-30 12:01:17 +02:00
b1aa36b136 version 9.0.57 2021-10-28 00:54:26 +02:00
d2f41b6a21 API scodoc7, exemple/test usage, progres sur l'API scodoc9 2021-10-28 00:52:23 +02:00
db937ca7c5 Merge pull request 'fix: bcc as list in Message build' (#176) from jmplace/ScoDoc-Lille:fix_bcc into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/176
2021-10-26 22:27:29 +02:00
461f14631b fix: bcc as list in Message build 2021-10-26 19:27:51 +02:00
668210aaef Fix: suppression de modules avec tags (cascade manquante) 2021-10-26 10:22:55 +02:00
0da60384a1 Modification authentification ScoDoc7 API POST 2021-10-26 00:13:42 +02:00
c29199eff4 Fix: get_etud_dept recherche par INE 2021-10-25 15:51:11 +02:00
5268ea4f13 Detecte et supprime doublons dans les préférences 2021-10-25 15:49:56 +02:00
IDK
1be2ba1498 maj 2021-10-24 18:35:10 +02:00
66d443944a Fix: partition_rename error message 2021-10-24 18:28:01 +02:00
ad0cd6236c AddBilletAbsence: autorise POST pour anciens clients PHP 2021-10-24 12:01:42 +02:00
e8ce1e303e formation_export: n'exporte plus les UE externes 2021-10-24 11:43:53 +02:00
2fe9e5ec39 Sépare les UE externes dans la pae édition programme 2021-10-22 23:09:15 +02:00
c49aecaa2f Fix regression on ue_list 2021-10-21 06:32:03 +02:00
0f67ee33ae Fix etud_info xml/json quote 2021-10-20 23:18:00 +02:00
280f6cf1c1 Fix etud_info xml quote 2021-10-20 22:34:06 +02:00
92de66f734 Modif liens sidebar. Closes #53 2021-10-20 21:58:01 +02:00
f73e720de1 Fix: suppression de notes par un enseignant non privilégié 2021-10-20 19:11:26 +02:00
0c913dacdc Fix: ordre des partitions 2021-10-20 17:41:38 +02:00
66dbec86bf Add cli: photos-import-files 2021-10-20 16:47:41 +02:00
e56a97eaf6 Aligne max upload de Flask et nginx (16Mo) 2021-10-19 15:57:28 +02:00
IDK
3878d68b38 Utilise NA pour les notes manquants (et plus NA0, ...) 2021-10-19 15:52:02 +02:00
IDK
e249f45ce9 maj 2021-10-17 23:24:18 +02:00
54ed09ed08 renomme: ue_list, matiere_list 2021-10-17 23:19:26 +02:00
565055b4e5 Merge pull request 'restore date formatting at the right place' (#171) from jmplace/ScoDoc-Lille:oops_miss_date_export into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/171
2021-10-17 22:04:51 +02:00
63d73c9ecd Merge pull request 'traduction/adaptation messages par défaut ; strip email' (#170) from jmplace/ScoDoc-Lille:change_password_retouches into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/170
2021-10-17 22:04:30 +02:00
d69a6c283f restore date formatting at the right place 2021-10-17 20:20:31 +02:00
264c0d7d9e traduction/adaptation messages par défaut ; strip email 2021-10-17 12:15:24 +02:00
29ec51c001 Merge branch 'jmplace-change_email' 2021-10-17 11:20:27 +02:00
a909a307c0 Améliorer page infos utilisateurs 2021-10-17 11:19:01 +02:00
c96b114b08 Merge branch 'change_email' of https://scodoc.org/git/jmplace/ScoDoc-Lille into jmplace-change_email 2021-10-17 08:47:04 +02:00
390118226d modification du formulaire de changement de mot de passe personnel 2021-10-16 23:22:03 +02:00
bb7ed682c0 Fixes #158 2021-10-16 19:31:14 +02:00
256e89605b rename some old methods 2021-10-16 19:20:36 +02:00
2ca91fc4e9 added some relations 2021-10-16 19:19:07 +02:00
c56a4257bd Merge pull request 'fix true_false' (#168) from jmplace/ScoDoc-Lille:remise_fix_True_False into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/168
2021-10-16 14:34:38 +02:00
3c38ef4cc0 fix true_false 2021-10-16 14:30:35 +02:00
c658c7675e Merge pull request 'fix_timezone_bug' (#166) from jmplace/ScoDoc-Lille:fix_timezone_bug into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/166
2021-10-16 14:04:29 +02:00
7cc9f6d1f4 clear timezone for datetime object 2021-10-16 10:25:40 +02:00
c05e763900 Merge pull request 'ajout vérification date d'expiration' (#165) from jmplace/ScoDoc-Lille:controle_date_exp_anterieure into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/165
2021-10-16 10:19:14 +02:00
4ce50927b0 clear timezone for datetime values 2021-10-16 10:10:35 +02:00
177a891236 ajout vérification date d'expiration 2021-10-16 07:10:55 +02:00
9c50b58d5f amélioration formulaires creation/edition utilisateurs 2021-10-15 19:17:40 +02:00
c2de33f7f5 Merge pull request 'create_user_plus' (#164) from jmplace/ScoDoc-Lille:create_user_plus into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/164
2021-10-15 17:44:33 +02:00
c68633bf5b typo 2021-10-15 15:34:10 +02:00
45d20789dd wip: avant tests 2021-10-15 15:16:39 +02:00
93a23ff112 fix: partition_name when numeric 2021-10-15 14:31:11 +02:00
e8e3423193 Un peu de nettoyage de d'optimisation (gain ~ 30-40% sur calcul NT). 2021-10-15 14:00:51 +02:00
e243fe6bb0 installmgr url 2021-10-14 11:01:29 +02:00
46269fcebe Corrige mail envoi mot de passe utilisateur 2021-10-13 21:49:55 +02:00
a539061c1f Merge pull request 'permet de lever certaines vérifications lors de l import' (#161) from jmplace/ScoDoc-Lille:import_users_release_some_checks into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/161
2021-10-13 21:31:41 +02:00
9694ba61c4 Evite les erreurs de formulaires POST quand l'utilisateur s'est déconnecté dans un autre onglet 2021-10-13 21:00:03 +02:00
9c528bec7f permet de lever certaines vérifications lors de l import 2021-10-13 16:32:43 +02:00
1b8186e69b améliore gestion erreur saisies de notes 2021-10-13 15:56:24 +02:00
f0d641a31e Merge pull request 'complements_email_check_from_9.0.52' (#160) from jmplace/ScoDoc-Lille:complements_email_check_from_9.0.52 into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/160
2021-10-13 15:35:45 +02:00
feb57c2ac6 redirection vers all_depts apres création ; blackify 2021-10-13 15:27:19 +02:00
071c15af79 complements_import_users_from_9.0.52 2021-10-13 15:03:41 +02:00
dc26d1edea Modif mail import user. A compléter suivant la PR de JMP 2021-10-13 10:33:20 +02:00
6e1bc9665d renamed some group mgt methods 2021-10-12 16:05:50 +02:00
c1d13d6089 Python 3: n'utilise plus six. Utilise systématiquement with avec open. 2021-10-11 22:22:42 +02:00
IDK
aed2d6ce10 maj 2021-10-11 16:26:22 +02:00
3c5b721a3a oops: fichier oublié : fix #147 2021-10-11 16:05:23 +02:00
165220e2f1 fix #147 2021-10-11 12:29:33 +02:00
17cfd7ad79 page erreur 403 2021-10-10 21:09:53 +02:00
179442aa69 blackify 2021-10-10 21:09:27 +02:00
e6e1835cca Merge pull request 'check mail address' (#156) from jmplace/ScoDoc-Lille:email_check into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/156
2021-10-10 21:05:04 +02:00
fdd7af6a8a Améliore page erreur 403 (permission refusée) 2021-10-10 21:03:18 +02:00
7d5eff4f82 refactor check_modif_user 2021-10-10 10:52:06 +02:00
76bc957373 check mail address 2021-10-10 09:26:46 +02:00
d980c6794a Constrainte d'unicité sur les inscriptions aux modules 2021-10-09 20:20:44 +02:00
cd6fd10abf close #146 2021-10-09 19:48:55 +02:00
3e45762382 evite erreur dans log au lancement hors context requete 2021-10-07 23:33:24 +02:00
085ef05e01 Fix AddBilletAbsence 2021-10-07 23:21:30 +02:00
7dda35d37e fix #153 (about link in sidebar) 2021-10-07 23:07:53 +02:00
b38ee4ea25 Fix: create user avec date exp. 2021-10-07 23:00:02 +02:00
47f1497e5e Merge pull request 'complement sauvegarde/restauration' (#152) from jmplace/ScoDoc-Lille:sauv-restore into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/152
2021-10-07 22:30:13 +02:00
8667bd58ba Merge pull request 'fix filename' (#151) from jmplace/ScoDoc-Lille:fix_filename_import_notes into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/151
2021-10-07 22:28:30 +02:00
1f688e2cd5 log exc: ajoute erreur (sur la page) et mail admin 2021-10-07 22:26:43 +02:00
52e837dc81 améliore msg err sur imports etudiants excel 2021-10-07 22:24:53 +02:00
190304043d Fix: codes sems sur page accueil 2021-10-07 22:23:49 +02:00
9015780eb7 complement sauvegarde/restauration 2021-10-06 13:53:09 +02:00
19586559ba fix filename 2021-10-06 13:46:29 +02:00
5ac5f5eb19 Fixes #146 2021-10-05 23:43:32 +02:00
ef6a6d6ec2 Merge pull request 'autorise les chaînes JJ/MM/AAAA comme date' (#148) from jmplace/ScoDoc-Lille:fix_date_import into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/148
2021-10-05 15:24:17 +02:00
1fe814a674 Merge pull request 'fix excel export with datetime.date values (formsemestre_description with evals)' (#145) from jmplace/ScoDoc-Lille:fix_date_xls into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/145
2021-10-05 12:35:39 +02:00
8ab9a67fa6 autorise les chaînes JJ/MM/AAAA comme date 2021-10-05 12:09:20 +02:00
bf83d8475a fix excel export with datetime.date values (formsemestre_description with evals) 2021-10-05 05:38:55 +02:00
79b8520034 redirige les url /ScoDoc/DEPT vers /ScoDoc/DEPT/Scolarite 2021-10-04 22:30:57 +02:00
54f0b87d39 Dialogue affection groupe: template jinja2, début d'optimisation de XMLgetGroupsInPartition 2021-10-04 22:05:05 +02:00
51bd6ba141 added profiling command 2021-10-04 22:03:11 +02:00
3e1136a077 Fix: desinscription des modules d'une UE 2021-10-04 15:17:03 +02:00
0ab9a281a9 Fix regression: API billetin par code_nip 2021-10-04 15:09:19 +02:00
e32d7b1b4e Améliore message d'erreur si logo manquant dans un PDF 2021-10-04 00:22:44 +02:00
f59308b863 enregistre date dernière connexion. + fix liste users 2021-10-03 18:19:51 +02:00
dd8a07ef64 n'interroge plus le service portail qand on demande les listes avec codes 2021-10-03 17:31:03 +02:00
bc112efd76 fix typo / formsemestre_edit_uecoefs 2021-10-01 23:52:36 +02:00
1781548b66 fix: page user si pas de nom ni de prenom 2021-10-01 23:48:11 +02:00
1c927cb541 Merge pull request 'ameliioration placement (adapte la taille du select au contenu)' (#140) from jmplace/ScoDoc-Lille:placement_amelioration into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/140
2021-09-30 17:51:07 +02:00
814a8dbc24 Améliore qq msg d'erreur + lien formsemestre_status vers listes 2021-09-30 17:50:37 +02:00
4a3e37d371 ameliioration placement (adapte la taille du select au contenu) 2021-09-30 17:11:03 +02:00
a447c6e5f9 Fix regression: validations UE quand semestre validé 2021-09-30 14:35:21 +02:00
8463d368a1 Fix: report_debouche_date 2021-09-30 09:37:18 +02:00
1f125d3a1d fix: import etudiants hors semestre 2021-09-29 20:08:18 +02:00
51fec2d301 Change le type de bulletin par défaut pour les nouveaux départements. Le type "exemple" n'est proposé qu'en dev. 2021-09-29 14:47:43 +02:00
8bfa936361 fix delete etud 2021-09-29 14:15:12 +02:00
1c27ec7dc2 branche pour PN BUT 2021-09-29 10:36:57 +02:00
dffb369bb0 form create user si pas de roles 2021-09-29 10:27:49 +02:00
656c8a9f22 Fix: edition user sans dept 2021-09-28 16:20:15 +02:00
5dfdf4265e autorise chiffres dans user_name 2021-09-28 16:10:27 +02:00
36c22a7ca7 Liste billest d'absences restreinte au département 2021-09-28 09:59:50 +02:00
4728e77a7b Fix: formulaires 2021-09-28 09:14:04 +02:00
f79003186a cosmetic 2021-09-28 07:28:16 +02:00
11ef8857e2 fix urls 2021-09-28 07:27:55 +02:00
f5529ec4a6 dump fichier en erreur pour debug 2021-09-28 07:22:23 +02:00
550a7888bf enhance exception logging 2021-09-27 22:58:05 +02:00
a8198f889a améliore dialogue inscription/passage 2021-09-27 22:54:58 +02:00
651f111839 ignore inscriptions répétées 2021-09-27 22:54:23 +02:00
76af0eb166 +x 2021-09-27 22:52:26 +02:00
bf57f2bfa5 remet nom auteurs annotations étudiants 2021-09-27 17:33:44 +02:00
c7aba95015 fixes 2021-09-27 17:18:43 +02:00
f012fe6fcf FIX regression / REQUEST+formulaires / + passage étudiants 2021-09-27 16:42:14 +02:00
d577066911 Fix regressions (post ablation REQUEST) 2021-09-27 14:54:52 +02:00
b1fa9b8ef8 small fixes 2021-09-27 13:43:11 +02:00
2a1c541fbd pylintrc with flask plugins 2021-09-27 12:14:36 +02:00
1b89010b45 Merge pull request 'Grand nettoyage: élimination des REQUEST héritées de Zope.' (#138) from no-request into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/138
2021-09-27 10:33:36 +02:00
59e1fdc15e Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc 2021-09-27 10:29:11 +02:00
4429ffd3c8 Merge pull request 'with_pe_v2' (#137) from jmplace/ScoDoc-Lille:with_pe_v2 into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/137
2021-09-27 10:27:45 +02:00
057832c309 Grand nettoyage: élimination des REQUEST héritées de Zope. 2021-09-27 10:20:10 +02:00
bb47e89e97 lustrage (blak & pylint) ; test sur autres export excel: ok 2021-09-27 07:46:35 +02:00
ccd1a0daba refactor templates to zip 2021-09-27 07:31:59 +02:00
230c7d488e fonctionnel - a améliorer 2021-09-27 07:16:30 +02:00
9ee7dec202 deal with binary files (zip) 2021-09-26 23:27:54 +02:00
18d6324488 ré-écriture ue_sharing_code avec SQLAlchemy 2021-09-26 11:28:13 +02:00
16b3701815 avant repare imports 2021-09-26 10:01:20 +02:00
5ea4e74117 wip poursuite d etudes 2021-09-26 09:52:55 +02:00
ce31d3148d Sépare PE dans package 'pe' et le désactive en production. 2021-09-25 23:56:17 +02:00
fa5539fd75 Améliore script import users ScoDoc7 2021-09-25 22:42:44 +02:00
ddf4bf788f Fix: synchro Apogée quand étudiants ajoutés manuellement 2021-09-25 17:33:59 +02:00
14d533b38a lien inscription ailleurs sur page etud 2021-09-25 15:12:13 +02:00
671ef6a7fa Fix urgent: regression sur XMLgetAbsEtud 2021-09-25 12:35:26 +02:00
edc6da3005 applique upgrade alembic dans le script de restoration 2021-09-25 12:33:37 +02:00
b015cf3f88 Merge pull request 'enable xml/json result as file' (#136) from jmplace/ScoDoc-Lille:export_json_xml_as_files into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/136
2021-09-25 10:46:43 +02:00
a2c16207cb Ordre boites export Apogee + fix export excel 2021-09-25 10:43:06 +02:00
00dbd25b42 enable xml/json result as file 2021-09-25 09:53:31 +02:00
4e59b9597b Merge pull request 'fix suffix processing' (#135) from jmplace/ScoDoc-Lille:fix_suffix into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/135
2021-09-25 09:21:08 +02:00
f1660e12e1 version 2021-09-25 09:13:56 +02:00
cb03cc962c Scripts save/restore 2021-09-25 09:13:39 +02:00
81df68b491 fix suffix processing 2021-09-25 09:07:05 +02:00
1741e75f72 formation_delete sans REQUESt 2021-09-24 20:35:50 +02:00
c41726c4a8 Fix: association nouvelle version 2021-09-24 20:20:45 +02:00
7879c176dd Bonus Béziers (à valider) 2021-09-24 20:19:20 +02:00
75f43bbdde élimination des qq REQUESTS et correction publication XML/XLS/JSON/... 2021-09-24 16:32:49 +02:00
0a50edc9f0 removed some useless REQUESTs 2021-09-24 12:10:53 +02:00
373feece76 fix: POST suppr. photo 2021-09-24 11:12:49 +02:00
6d1ffb122b Fix: billets d'absences (POST) 2021-09-24 01:07:00 +02:00
92c401f17c Fix #133. Image photo: enleve canal alpha avant conversion en jpeg. 2021-09-24 00:54:59 +02:00
36c7358eed Améliore message d'erreur si upload image invalide 2021-09-24 00:47:06 +02:00
9c5408f503 Fix: export tables en XML si id de colonnes numeriques 2021-09-24 00:33:18 +02:00
2add3e12cc Fix: lien validation jury 2021-09-24 00:28:09 +02:00
d0ab9dc66a Fix: remove obsolete field "debouche" from scodoc7 editor 2021-09-24 00:19:19 +02:00
beeca54a94 Fix: liens dans table liste absences 2021-09-24 00:11:56 +02:00
73cf9a6f4d Fix: evaluations à dates vides (tri) 2021-09-24 00:04:28 +02:00
fede1ae7af Merge pull request 'fix_group_selector' (#134) from jmplace/ScoDoc-Lille:fix_group_selector into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/134
2021-09-23 23:51:11 +02:00
845152afdd put back one empty line 2021-09-23 14:44:28 +02:00
a4d091fa2d retrait du cast int(retreive_formsemestre_from_request(...)) réalisée par la fonction 2021-09-23 14:36:47 +02:00
ffa7e07cd3 Merge branch 'master' into fix_group_selector 2021-09-23 13:44:30 +02:00
865192bc0d type hint 2021-09-23 08:15:54 +02:00
b56f205e89 fix group_selector / info_etu popup 2021-09-23 07:34:42 +02:00
eded2fffe9 enhance scu.send_file, fix some API bugs. 2021-09-21 22:19:08 +02:00
13f1539282 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc 2021-09-21 16:51:07 +02:00
ae51e4c17a Merge pull request 'gentil message sur erreur d import esxcel' (#131) from jmplace/ScoDoc-Lille:import_excel_errmsg into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/131
2021-09-21 16:50:45 +02:00
7214627994 Merge pull request 'transmit multiple occurence of an argument into a list' (#130) from jmplace/ScoDoc-Lille:fix_multivalued_args into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/130
2021-09-21 16:49:41 +02:00
6cc1b60da4 Adaptation des tests unitaires aux vues de l'API (qui renvoient des réponses Flask). 2021-09-21 16:34:58 +02:00
4297d36dad gentil message sur erreur d import esxcel 2021-09-21 16:28:26 +02:00
2999199b19 La config test (tests unitaire) utilise SCODOC_TEST_DATABASE_URI et non plus SCODOC_DATABASE_URI 2021-09-21 15:55:09 +02:00
f516ccdfe7 retire arguments REQUEST de sendResult et sendPDFFile 2021-09-21 15:53:33 +02:00
2c97349acf améliore affichage exceptions (ex: erreur inscription étudiants) 2021-09-21 15:02:22 +02:00
5dfc64a62d sco_debouche APi views return empty OK responses / removed useless REQUEST 2021-09-21 13:36:56 +02:00
9dd8198c7b transmit multiple occurence of an argument into a list 2021-09-21 06:48:54 +02:00
f18a9c7559 fix: requete billets absences 2021-09-20 15:55:48 +02:00
985c6df3b6 empeche noms de feuilles excel invalides 2021-09-20 15:54:38 +02:00
286e9cdc2f version 9.0.32 2021-09-19 21:39:34 +02:00
0381576750 modif contrainte sur formations 2021-09-19 21:31:35 +02:00
7a0a04bdb3 fix install 2021-09-19 20:44:26 +02:00
35f23995aa fix retrait semestre d'un export Apo 2021-09-19 16:19:02 +02:00
29221666a4 Merge pull request 'patch_placement' (#127) from jmplace/ScoDoc-Lille:patch_placement into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/127
2021-09-19 16:14:58 +02:00
d7e6a7d714 ameliore assistance 2021-09-19 16:13:57 +02:00
179be1baa0 fix rang 1 twice in excel map 2021-09-19 09:21:37 +02:00
a5ed9b815f fix plantage si un seul groupe ; correction indentation for radio button in wtf_forms (css) 2021-09-19 09:11:11 +02:00
13c027fc19 Adapte assistance à ScoDoc 9 2021-09-18 22:00:10 +02:00
31505e1330 détails feuille placement 2021-09-18 14:21:15 +02:00
9a9dc4a483 Fix conflicts 2021-09-18 14:03:36 +02:00
11ba73d264 Merge pull request 'scodoc9_placement_PR' (#126) from jmplace/ScoDoc-Lille:scodoc9_placement_PR into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/126
2021-09-18 13:43:11 +02:00
7daa49f2aa Elimine les attributs de ZREQUEST, sauf forms. 2021-09-18 13:42:19 +02:00
f7961a135a finalisation 2021-09-18 11:08:04 +02:00
c955870e1e recheck 2021-09-18 10:51:19 +02:00
80f5536de5 Merge remote-tracking branch 'origin/master' into scodoc9_placement_PR
# Conflicts:
#	app/scodoc/sco_groups_view.py
#	app/scodoc/sco_placement.py
2021-09-18 10:39:53 +02:00
2519d08e40 remove breakpoints() + blackify 2021-09-18 10:11:46 +02:00
987800c30e Remplace REQUEST.URL par accès à Flask request global object 2021-09-18 10:10:02 +02:00
2a72fb881b replace send_excel_file by scu.send_file 2021-09-18 01:30:47 +02:00
87ecd09f0e fix a regression ; eliminate send_from_flask 2021-09-17 16:01:01 +02:00
6e7a104fb0 pylint corrections (suite) 2021-09-17 12:31:43 +02:00
b03eee12a1 pylint corrections 2021-09-17 10:59:26 +02:00
44117fb0e2 blackify + suppress cr-at-eol 2021-09-17 10:26:20 +02:00
42ef9f795f Merge remote-tracking branch 'origin/master' into temp
# Conflicts:
#	app/scodoc/sco_placement.py
2021-09-17 10:12:16 +02:00
bd2e0ccde5 Archives: utilise dept_id et non acronyme 2021-09-17 10:02:27 +02:00
5f0f437f2e Merge remote-tracking branch 'jmplace/temp' into temp
# Conflicts:
#	app/scodoc/sco_placement.py
2021-09-17 09:49:00 +02:00
b6cc251c94 minor changes 2021-09-17 09:44:47 +02:00
5f6c434497 minor refactor 2021-09-17 09:20:04 +02:00
45352d9248 Script migration: vérification existence departements dans base cible 2021-09-17 09:15:12 +02:00
b3225e07f7 avant tests 2021-09-17 08:22:54 +02:00
0ef822cfd8 déplacement du template placement 2021-09-17 05:54:11 +02:00
a23ae38014 integration master 2021-09-17 05:03:34 +02:00
7d59b52018 version bump 2021-09-16 22:33:15 +02:00
80238545f3 oups: fichiers oubliés. 2021-09-16 22:31:47 +02:00
72e075530c Pour la transition BUT: bloquage du calcul des moyennes 2021-09-16 22:24:08 +02:00
91cc421ef8 Post-migration des archives 2021-09-16 21:42:45 +02:00
8b6a569a31 Classe ReverseProxied WSGI pour ré-écriture des URL http/https 2021-09-16 16:05:37 +02:00
c8949e870f code cleaning 2021-09-16 14:58:56 +02:00
30481e4729 ajout doc et modif config SCODOC_DATABASE_URI 2021-09-16 11:46:36 +02:00
085aff657a fix: preference par departement 2021-09-16 11:45:39 +02:00
3666f8b1ec Améliore sidebar avec template jinja 2021-09-16 10:09:17 +02:00
bec7deb581 Ajout perm. export Apo aux rôles Secr et Admin 2021-09-16 09:13:05 +02:00
6dbbcde454 unifie fichiers moodlecsv 2021-09-16 08:17:26 +02:00
9578c789dc Fix sort groupe (#py3) 2021-09-16 00:16:50 +02:00
0fedb7771c Améliore téléchargement fichiers (REQUEST => send_file) 2021-09-16 00:15:29 +02:00
6dba8933c4 fix selection groupe saisie note 2021-09-15 23:02:11 +02:00
5efc493542 Escape html read-only values 2021-09-15 22:31:16 +02:00
a34dd656be Change html header: xhtml -> html5 2021-09-15 22:13:04 +02:00
2ec2be4234 fix link 2021-09-15 20:24:44 +02:00
49609fa657 harmless typo in migration script 2021-09-15 19:20:41 +02:00
8a16216d4b fixes: lien params seulement pour admin, type passage étudiants, log sources ips 2021-09-15 15:19:08 +02:00
96f457260f version 9.0.25 2021-09-15 00:55:26 +02:00
0f9b52bc9b fix sco_inscr_passage 2021-09-15 00:54:18 +02:00
83174f2f5e typo (synchro apo) 2021-09-15 00:40:19 +02:00
3fbda90a2f Better logging. New log for exceptions: /opt/scodoc-data/log/scodoc_exc.log 2021-09-15 00:33:30 +02:00
de206674d9 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc 2021-09-14 23:11:14 +02:00
b06f37b18e Merge pull request 'fix export excel en neutralisant les formules comme chaine' (#124) from jmplace/ScoDoc-Lille:fix_formula into master
Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/124
2021-09-14 23:10:47 +02:00
3496cc7beb fix envoi mail etud change 2021-09-14 12:21:22 +02:00
01c264c3c7 add copy to .gitignore 2021-09-13 07:17:18 +02:00
c44aa808df preparation envoi fichier 2021-09-13 07:16:37 +02:00
c8872bd220 excel file returned 2021-09-12 09:31:07 +02:00
7f63ab222b before refactoring 2021-09-12 07:04:05 +02:00
ed07e42222 placement fait 2021-09-11 19:37:32 +02:00
35768e9241 placement fait 2021-09-11 19:35:30 +02:00
050e54de3e placement fait 2021-09-11 18:33:55 +02:00
37484b7fc9 Merge branch 'master' into clean 2021-09-11 10:21:54 +02:00
f828134ea2 complements scodoc.css pour formulaire 2021-09-11 10:04:52 +02:00
a4d0205cc7 Merge remote-tracking branch 'origin/master' into clean 2021-09-05 14:52:58 +02:00
770ccb4d6e fin fusion 2021-09-05 14:50:35 +02:00
295 changed files with 21389 additions and 8467 deletions

1
.gitignore vendored
View File

@ -170,3 +170,4 @@ Thumbs.db
*.code-workspace
copy

View File

@ -1,9 +1,7 @@
# ScoDoc - Gestion de la scolarité - Version ScoDoc 9
(c) Emmanuel Viennet 1999 - 2021 (voir LICENCE.txt)
VERSION EXPERIMENTALE - NE PAS DEPLOYER - TESTS EN COURS
(c) Emmanuel Viennet 1999 - 2022 (voir LICENCE.txt)
Installation: voir instructions à jour sur <https://scodoc.org/GuideInstallDebian11>
@ -11,8 +9,6 @@ Documentation utilisateur: <https://scodoc.org>
## Version ScoDoc 9
N'utiliser que pour les développements et tests.
La version ScoDoc 9 est basée sur Flask (au lieu de Zope) et sur
**python 3.9+**.
@ -22,15 +18,12 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
### État actuel (27 août 21)
### État actuel (4 dec 21)
- Tests en cours, notamment système d'installation et de migration.
- 9.0 (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf:
- ancien module "Entreprises" (obsolète)
**Fonctionnalités non intégrées:**
- feuille "placement" (en cours)
- ancien module "Entreprises" (obsolete)
- 9.1 (branche "PNBUT") est la version de développement.
### Lignes de commandes
@ -46,7 +39,7 @@ les fichiers locaux (archives, photos, configurations, logs) sous
postgresql et la configuration du système Linux.
### Fichiers locaux
Sous `/opt/scodoc-data`, fichiers et répertoires appartienant à l'utilisateur `scodoc`.
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`.
@ -93,11 +86,22 @@ exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
### Tests unitaires
Les tests unitaires utilisent normalement la base postgresql `SCODOC_TEST`.
Avant le premier lancement, créer cette base ainsi:
./tools/create_database.sh SCODOC_TEST
export FLASK_ENV=test
flask db upgrade
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
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
scripts de tests:
Lancer au préalable:
flask sco-delete-dept TEST00 && flask sco-create-dept TEST00
flask delete-dept TEST00 && flask create-dept TEST00
Puis dérouler les tests unitaires:
@ -110,13 +114,16 @@ Ou avec couverture (`pip install pytest-cov`)
#### 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 étudianst et semestres quand on développe.
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 utilisée par les tests:
Il suffit de positionner une variable d'environnement indiquant la BD
utilisée par les tests:
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
puis de les lancer normalement, par exemple:
(si elle n'existe pas, voir plus loin pour la créer) puis de les lancer
normalement, par exemple:
pytest tests/unit/test_sco_basic.py
@ -132,12 +139,13 @@ base de données (tous les départements, et les utilisateurs) avant de commence
On utilise SQLAlchemy avec Alembic et Flask-Migrate.
flask db migrate -m "ScoDoc 9.0.x: ..." # ajuster le message !
flask db migrate -m "message explicatif....."
flask db upgrade
Ne pas oublier de commiter les migrations (`git add migrations` ...).
Ne pas oublier de d'ajouter le script de migration à git (`git add migrations/...`).
Mémo pour développeurs: séquence re-création d'une base:
**Mémo**: séquence re-création d'une base (vérifiez votre `.env`
ou variables d'environnement pour interroger la bonne base !).
dropdb SCODOC_DEV
tools/create_database.sh SCODOC_DEV # créé base SQL
@ -152,7 +160,25 @@ Si la base utilisée pour les dev n'est plus en phase avec les scripts de
migration, utiliser les commandes `flask db history`et `flask db stamp`pour se
positionner à la bonne étape.
# Paquet debian 11
### Profiling
Sur une machine de DEV, lancer
flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data
le fichier `.prof` sera alors écrit dans `/opt/scodoc-data` (on peut aussi utiliser `/tmp`).
Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bien:
pip install snakeviz
puis
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
# Paquet Debian 11
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
important est `postinst`qui se charge de configurer le système (install ou

View File

@ -1,6 +1,7 @@
# -*- coding: UTF-8 -*
# pylint: disable=invalid-name
import datetime
import os
import socket
import sys
@ -17,14 +18,19 @@ from flask import render_template
from flask.logging import default_handler
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_login import LoginManager, current_user
from flask_mail import Mail
from flask_bootstrap import Bootstrap
from flask_moment import Moment
from flask_caching import Cache
import sqlalchemy
from app.scodoc.sco_exceptions import ScoValueError, APIInvalidParams
from app.scodoc.sco_exceptions import (
AccessDenied,
ScoGenError,
ScoValueError,
APIInvalidParams,
)
from config import DevConfig
import sco_version
@ -50,10 +56,21 @@ def handle_sco_value_error(exc):
return render_template("sco_value_error.html", exc=exc), 404
def handle_access_denied(exc):
return render_template("error_access_denied.html", exc=exc), 403
def internal_server_error(e):
"""Bugs scodoc, erreurs 500"""
# note that we set the 500 status explicitly
return render_template("error_500.html", SCOVERSION=sco_version.SCOVERSION), 500
return (
render_template(
"error_500.html",
SCOVERSION=sco_version.SCOVERSION,
date=datetime.datetime.now().isoformat(),
),
500,
)
def handle_invalid_usage(error):
@ -82,7 +99,7 @@ def postgresql_server_error(e):
return render_raw_html("error_503.html", SCOVERSION=sco_version.SCOVERSION), 503
class RequestFormatter(logging.Formatter):
class LogRequestFormatter(logging.Formatter):
"""Ajoute URL et remote_addr for logging"""
def format(self, record):
@ -92,10 +109,46 @@ class RequestFormatter(logging.Formatter):
else:
record.url = None
record.remote_addr = None
record.sco_user = current_user
if has_request_context():
record.sco_admin_mail = current_app.config["SCODOC_ADMIN_MAIL"]
else:
record.sco_admin_mail = "(pas de requête)"
return super().format(record)
class LogExceptionFormatter(logging.Formatter):
"""Formatteur pour les exceptions: ajoute détails"""
def format(self, record):
if has_request_context():
record.url = request.url
record.remote_addr = request.environ.get(
"HTTP_X_FORWARDED_FOR", request.remote_addr
)
record.http_referrer = request.referrer
record.http_method = request.method
if request.method == "GET":
record.http_params = str(request.args)
else:
# rep = reprlib.Repr() # abbrège
record.http_params = str(request.form)[:2048]
else:
record.url = None
record.remote_addr = None
record.http_referrer = None
record.http_method = None
record.http_params = None
record.sco_user = current_user
if has_request_context():
record.sco_admin_mail = current_app.config["SCODOC_ADMIN_MAIL"]
else:
record.sco_admin_mail = "(pas de requête)"
return super().format(record)
class ScoSMTPHandler(SMTPHandler):
def getSubject(self, record: logging.LogRecord) -> str:
stack_summary = traceback.extract_tb(record.exc_info[2])
@ -105,8 +158,24 @@ class ScoSMTPHandler(SMTPHandler):
return subject
class ReverseProxied(object):
"""Adaptateur wsgi qui nous permet d'avoir toutes les URL calculées en https
sauf quand on est en dev.
La variable HTTP_X_FORWARDED_PROTO est positionnée par notre config nginx"""
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
scheme = environ.get("HTTP_X_FORWARDED_PROTO")
if scheme:
environ["wsgi.url_scheme"] = scheme # ou forcer à https ici ?
return self.app(environ, start_response)
def create_app(config_class=DevConfig):
app = Flask(__name__, static_url_path="/ScoDoc/static", static_folder="static")
app.wsgi_app = ReverseProxied(app.wsgi_app)
app.logger.setLevel(logging.DEBUG)
app.config.from_object(config_class)
@ -119,7 +188,10 @@ def create_app(config_class=DevConfig):
cache.init_app(app)
sco_cache.CACHE = cache
app.register_error_handler(ScoGenError, handle_sco_value_error)
app.register_error_handler(ScoValueError, handle_sco_value_error)
app.register_error_handler(AccessDenied, handle_access_denied)
app.register_error_handler(500, internal_server_error)
app.register_error_handler(503, postgresql_server_error)
app.register_error_handler(APIInvalidParams, handle_invalid_usage)
@ -148,9 +220,18 @@ def create_app(config_class=DevConfig):
absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences"
)
app.register_blueprint(api_bp, url_prefix="/ScoDoc/api")
scodoc_exc_formatter = RequestFormatter(
"[%(asctime)s] %(remote_addr)s requested %(url)s\n"
"%(levelname)s in %(module)s: %(message)s"
scodoc_log_formatter = LogRequestFormatter(
"[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n"
"%(levelname)s: %(message)s"
)
# les champs additionnels sont définis dans LogRequestFormatter
scodoc_exc_formatter = LogExceptionFormatter(
"[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n"
"%(levelname)s: %(message)s\n"
"Referrer: %(http_referrer)s\n"
"Method: %(http_method)s\n"
"Params: %(http_params)s\n"
"Admin mail: %(sco_admin_mail)s\n"
)
if not app.testing:
if not app.debug:
@ -179,7 +260,7 @@ def create_app(config_class=DevConfig):
app.logger.addHandler(mail_handler)
else:
# Pour logs en DEV uniquement:
default_handler.setFormatter(scodoc_exc_formatter)
default_handler.setFormatter(scodoc_log_formatter)
# Config logs pour DEV et PRODUCTION
# Configuration des logs (actifs aussi en mode development)
@ -188,9 +269,17 @@ def create_app(config_class=DevConfig):
file_handler = WatchedFileHandler(
app.config["SCODOC_LOG_FILE"], encoding="utf-8"
)
file_handler.setFormatter(scodoc_exc_formatter)
file_handler.setFormatter(scodoc_log_formatter)
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
# Log pour les erreurs (exceptions) uniquement:
# usually /opt/scodoc-data/log/scodoc_exc.log
file_handler = WatchedFileHandler(
app.config["SCODOC_ERR_FILE"], encoding="utf-8"
)
file_handler.setFormatter(scodoc_exc_formatter)
file_handler.setLevel(logging.ERROR)
app.logger.addHandler(file_handler)
# app.logger.setLevel(logging.INFO)
app.logger.info(f"{sco_version.SCONAME} {sco_version.SCOVERSION} startup")
@ -199,15 +288,19 @@ def create_app(config_class=DevConfig):
)
# ---- INITIALISATION SPECIFIQUES A SCODOC
from app.scodoc import sco_bulletins_generator
from app.scodoc.sco_bulletins_example import BulletinGeneratorExample
from app.scodoc.sco_bulletins_legacy import BulletinGeneratorLegacy
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
from app.scodoc.sco_bulletins_ucac import BulletinGeneratorUCAC
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorExample)
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorLegacy)
# l'ordre est important, le premeir sera le "défaut" pour les nouveaux départements.
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandard)
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorLegacy)
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorUCAC)
if app.testing or app.debug:
from app.scodoc.sco_bulletins_example import BulletinGeneratorExample
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorExample)
return app
@ -225,6 +318,8 @@ def set_sco_dept(scodoc_dept: str):
g.scodoc_dept_id = dept.id # l'id
if not hasattr(g, "db_conn"):
ndb.open_db_connection()
if not hasattr(g, "stored_get_formsemestre"):
g.stored_get_formsemestre = {}
def user_db_init():
@ -263,7 +358,7 @@ def sco_db_insert_constants():
current_app.logger.info("Init Sco db")
# Modalités:
models.NotesFormModalite.insert_modalites()
models.FormationModalite.insert_modalites()
def initialize_scodoc_database(erase=False, create_all=False):

View File

@ -2,7 +2,25 @@
"""
from flask import Blueprint
from flask import request
bp = Blueprint("api", __name__)
def requested_format(default_format="json", allowed_formats=None):
"""Extract required format from query string.
* default value is json. A list of allowed formats may be provided
(['json'] considered if not provided).
* if the required format is not in allowed list, returns None.
NB: if json in not in allowed_formats, format specification is mandatory.
"""
format_type = request.args.get("format", default_format)
if format_type in (allowed_formats or ["json"]):
return format_type
return None
from app.api import tokens
from app.api import sco_api
from app.api import logos

View File

@ -1,6 +1,7 @@
# -*- coding: UTF-8 -*
# Authentication code borrowed from Miguel Grinberg's Mega Tutorial
# (see https://github.com/miguelgrinberg/microblog)
# and modified for ScoDoc
# Under The MIT License (MIT)
@ -23,6 +24,7 @@
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from flask import g
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
from app.auth.models import User
from app.api.errors import error_response
@ -33,8 +35,9 @@ token_auth = HTTPTokenAuth()
@basic_auth.verify_password
def verify_password(username, password):
user = User.query.filter_by(username=username).first()
user = User.query.filter_by(user_name=username).first()
if user and user.check_password(password):
g.current_user = user
return user
@ -45,9 +48,30 @@ def basic_auth_error(status):
@token_auth.verify_token
def verify_token(token):
return User.check_token(token) if token else None
user = User.check_token(token) if token else None
g.current_user = user
return user
@token_auth.error_handler
def token_auth_error(status):
return error_response(status)
@token_auth.get_user_roles
def get_user_roles(user):
return user.roles
# def token_permission_required(permission):
# def decorator(f):
# @wraps(f)
# def decorated_function(*args, **kwargs):
# scodoc_dept = getattr(g, "scodoc_dept", None)
# if not current_user.has_permission(permission, scodoc_dept):
# abort(403)
# return f(*args, **kwargs)
# return login_required(decorated_function)
# return decorator

View File

@ -20,7 +20,9 @@
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.from flask import jsonify
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from flask import jsonify
from werkzeug.http import HTTP_STATUS_CODES

97
app/api/logos.py Normal file
View File

@ -0,0 +1,97 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 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
#
##############################################################################
"""API: gestion des logos
Contrib @jmp
"""
from datetime import datetime
from flask import jsonify, g, send_file
from app.api import bp
from app.api import requested_format
from app.api.auth import token_auth
from app.api.errors import error_response
from app.models import Departement
from app.scodoc.sco_logos import list_logos, find_logo
from app.scodoc.sco_permissions import Permission
@bp.route("/logos", methods=["GET"])
@token_auth.login_required
def api_get_glob_logos():
if not g.current_user.has_permission(Permission.ScoSuperAdmin, None):
return error_response(401, message="accès interdit")
required_format = requested_format() # json only
if required_format is None:
return error_response(400, "Illegal format")
logos = list_logos()[None]
return jsonify(list(logos.keys()))
@bp.route("/logos/<string:logoname>", methods=["GET"])
@token_auth.login_required
def api_get_glob_logo(logoname):
if not g.current_user.has_permission(Permission.ScoSuperAdmin, None):
return error_response(401, message="accès interdit")
logo = find_logo(logoname=logoname)
if logo is None:
return error_response(404, message="logo not found")
logo.select()
return send_file(
logo.filepath,
mimetype=f"image/{logo.suffix}",
last_modified=datetime.now(),
)
@bp.route("/departements/<string:departement>/logos", methods=["GET"])
@token_auth.login_required
def api_get_local_logos(departement):
dept_id = Departement.from_acronym(departement).id
if not g.current_user.has_permission(Permission.ScoChangePreferences, departement):
return error_response(401, message="accès interdit")
logos = list_logos().get(dept_id, dict())
return jsonify(list(logos.keys()))
@bp.route("/departements/<string:departement>/logos/<string:logoname>", methods=["GET"])
@token_auth.login_required
def api_get_local_logo(departement, logoname):
# format = requested_format("jpg", ['png', 'jpg']) XXX ?
dept_id = Departement.from_acronym(departement).id
if not g.current_user.has_permission(Permission.ScoChangePreferences, departement):
return error_response(401, message="accès interdit")
logo = find_logo(logoname=logoname, dept_id=dept_id)
if logo is None:
return error_response(404, message="logo not found")
logo.select()
return send_file(
logo.filepath,
mimetype=f"image/{logo.suffix}",
last_modified=datetime.now(),
)

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -29,7 +29,7 @@
"""
# PAS ENCORE IMPLEMENTEE, juste un essai
# Pour P. Bouron, il faudrait en priorité l'équivalent de
# Scolarite/Notes/do_moduleimpl_withmodule_list
# Scolarite/Notes/moduleimpl_withmodule_list (alias scodoc7 do_moduleimpl_withmodule_list)
# Scolarite/Notes/evaluation_create
# Scolarite/Notes/evaluation_delete
# Scolarite/Notes/formation_list
@ -38,19 +38,43 @@
# Scolarite/Notes/groups_view
# Scolarite/Notes/moduleimpl_status
# Scolarite/setGroups
from datetime import datetime
from flask import jsonify, request, url_for, abort
from app import db
from app.api import bp
from flask import jsonify, request, g, send_file
from sqlalchemy.sql import func
from app import db, log
from app.api import bp, requested_format
from app.api.auth import token_auth
from app.api.errors import bad_request
from app.api.errors import error_response
from app import models
from app.models import FormSemestre, FormSemestreInscription, Identite
from app.scodoc.sco_permissions import Permission
@bp.route("/ScoDoc/api/list_depts", methods=["GET"])
@bp.route("list_depts", methods=["GET"])
@token_auth.login_required
def list_depts():
depts = models.Departement.query.filter_by(visible=True).all()
data = {"items": [d.to_dict() for d in depts]}
data = [d.to_dict() for d in depts]
return jsonify(data)
@bp.route("/etudiants/courant", methods=["GET"])
@token_auth.login_required
def etudiants():
"""Liste de tous les étudiants actuellement inscrits à un semestre
en cours.
"""
# Vérification de l'accès: permission Observateir sur tous les départements
# (c'est un exemple à compléter)
if not g.current_user.has_permission(Permission.ScoObservateur, None):
return error_response(401, message="accès interdit")
query = db.session.query(Identite).filter(
FormSemestreInscription.formsemestre_id == FormSemestre.id,
FormSemestreInscription.etudid == Identite.id,
FormSemestre.date_debut <= func.now(),
FormSemestre.date_fin >= func.now(),
)
return jsonify([e.to_dict_bul(include_urls=False) for e in query])

View File

@ -8,7 +8,7 @@ TODO: à revoir complètement pour reprendre ZScoUsers et les pages d'authentifi
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from app.auth.models import User
from app.auth.models import User, is_valid_password
_ = lambda x: x # sans babel
@ -43,8 +43,11 @@ class UserCreationForm(FlaskForm):
class ResetPasswordRequestForm(FlaskForm):
email = StringField(_l("Email"), validators=[DataRequired(), Email()])
submit = SubmitField(_l("Request Password Reset"))
email = StringField(
_l("Adresse email associée à votre compte ScoDoc:"),
validators=[DataRequired(), Email()],
)
submit = SubmitField(_l("Envoyer"))
class ResetPasswordForm(FlaskForm):
@ -52,7 +55,11 @@ class ResetPasswordForm(FlaskForm):
password2 = PasswordField(
_l("Répéter"), validators=[DataRequired(), EqualTo("password")]
)
submit = SubmitField(_l("Request Password Reset"))
submit = SubmitField(_l("Valider ce mot de passe"))
def validate_password(self, password):
if not is_valid_password(password.data):
raise ValidationError(f"Mot de passe trop simple, recommencez")
class DeactivateUserForm(FlaskForm):

View File

@ -10,7 +10,8 @@ import re
from time import time
from typing import Optional
from flask import current_app, url_for, g
import cracklib # pylint: disable=import-error
from flask import current_app, g
from flask_login import UserMixin, AnonymousUserMixin
from werkzeug.security import generate_password_hash, check_password_hash
@ -18,14 +19,32 @@ from werkzeug.security import generate_password_hash, check_password_hash
import jwt
from app import db, login
from app.models import Departement
from app.models import SHORT_STR_LEN
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS
import app.scodoc.sco_utils as scu
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):
"""Check password.
returns True if OK.
"""
if (
hasattr(scu.CONFIG, "MIN_PASSWORD_LENGTH")
and scu.CONFIG.MIN_PASSWORD_LENGTH > 0
and len(cleartxt) < scu.CONFIG.MIN_PASSWORD_LENGTH
):
return False # invalid: too short
try:
_ = cracklib.FascistCheck(cleartxt)
return True
except ValueError:
return False
class User(UserMixin, db.Model):
@ -37,7 +56,7 @@ class User(UserMixin, db.Model):
nom = db.Column(db.String(64))
prenom = db.Column(db.String(64))
dept = db.Column(db.String(32), index=True)
dept = db.Column(db.String(SHORT_STR_LEN), index=True)
active = db.Column(db.Boolean, default=True, index=True)
password_hash = db.Column(db.String(128))
@ -47,12 +66,19 @@ class User(UserMixin, db.Model):
date_created = db.Column(db.DateTime, default=datetime.utcnow)
date_expiration = db.Column(db.DateTime, default=None)
passwd_temp = db.Column(db.Boolean, default=False)
token = db.Column(db.String(32), index=True, unique=True)
token = db.Column(db.Text(), index=True, unique=True)
token_expiration = db.Column(db.DateTime)
roles = db.relationship("Role", secondary="user_role", viewonly=True)
Permission = Permission
_departement = db.relationship(
"Departement",
foreign_keys=[Departement.acronym],
primaryjoin=(dept == Departement.acronym),
lazy="dynamic",
)
def __init__(self, **kwargs):
self.roles = []
self.user_roles = []
@ -86,6 +112,7 @@ class User(UserMixin, db.Model):
self.password_hash = generate_password_hash(password)
else:
self.password_hash = None
self.passwd_temp = False
def check_password(self, password):
"""Check given password vs current one.
@ -110,6 +137,7 @@ class User(UserMixin, db.Model):
return check_password_hash(self.password_hash, password)
def get_reset_password_token(self, expires_in=600):
"Un token pour réinitialiser son mot de passe"
return jwt.encode(
{"reset_password": self.id, "exp": time() + expires_in},
current_app.config["SECRET_KEY"],
@ -118,15 +146,17 @@ class User(UserMixin, db.Model):
@staticmethod
def verify_reset_password_token(token):
"Vérification du token de reéinitialisation du mot de passe"
try:
id = jwt.decode(
user_id = jwt.decode(
token, current_app.config["SECRET_KEY"], algorithms=["HS256"]
)["reset_password"]
except:
return
return User.query.get(id)
return User.query.get(user_id)
def to_dict(self, include_email=True):
"""l'utilisateur comme un dict, avec des champs supplémentaires"""
data = {
"date_expiration": self.date_expiration.isoformat() + "Z"
if self.date_expiration
@ -177,8 +207,9 @@ class User(UserMixin, db.Model):
if "roles_string" in data:
self.user_roles = []
for r_d in data["roles_string"].split(","):
role, dept = UserRole.role_dept_from_string(r_d)
self.add_role(role, dept)
if r_d:
role, dept = UserRole.role_dept_from_string(r_d)
self.add_role(role, dept)
def get_token(self, expires_in=3600):
now = datetime.utcnow()
@ -194,11 +225,20 @@ class User(UserMixin, db.Model):
@staticmethod
def check_token(token):
"""Retreive user for given token, chek token's validity
and returns the user object.
"""
user = User.query.filter_by(token=token).first()
if user is None or user.token_expiration < datetime.utcnow():
return None
return user
def get_dept_id(self) -> int:
"returns user's department id, or None"
if self.dept:
return self._departement.first().id
return None
# Permissions management:
def has_permission(self, perm: int, dept=False):
"""Check if user has permission `perm` in given `dept`.
@ -250,7 +290,7 @@ class User(UserMixin, db.Model):
"""string repr. of user's roles (with depts)
e.g. "Ens_RT, Ens_Info, Secr_CJ"
"""
return ",".join(f"{r.role.name}_{r.dept or ''}" for r in self.user_roles)
return ",".join(f"{r.role.name or ''}_{r.dept or ''}" for r in self.user_roles)
def is_administrator(self):
"True if i'm an active SuperAdmin"
@ -329,7 +369,7 @@ class Role(db.Model):
"""Roles for ScoDoc"""
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
name = db.Column(db.String(64), unique=True) # TODO: , nullable=False))
default = db.Column(db.Boolean, default=False, index=True)
permissions = db.Column(db.BigInteger) # 64 bits
users = db.relationship("User", secondary="user_role", viewonly=True)
@ -388,7 +428,7 @@ class UserRole(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
role_id = db.Column(db.Integer, db.ForeignKey("role.id"))
dept = db.Column(db.String(64)) # dept acronym
dept = db.Column(db.String(64)) # dept acronym ou NULL
user = db.relationship(
User, backref=db.backref("user_roles", cascade="all, delete-orphan")
)
@ -407,6 +447,9 @@ class UserRole(db.Model):
"""
fields = role_dept.split("_", 1) # maxsplit=1, le dept peut contenir un "_"
if len(fields) != 2:
current_app.logger.warning(
f"role_dept_from_string: Invalid role_dept '{role_dept}'"
)
raise ScoValueError("Invalid role_dept")
role_name, dept = fields
if dept == "":
@ -418,7 +461,7 @@ class UserRole(db.Model):
def get_super_admin():
"""L'utilisateur admin (où le premier, s'il y en a plusieurs).
"""L'utilisateur admin (ou le premier, s'il y en a plusieurs).
Utilisé par les tests unitaires et le script de migration.
"""
admin_role = Role.query.filter_by(name="SuperAdmin").first()
@ -433,5 +476,5 @@ def get_super_admin():
@login.user_loader
def load_user(id):
return User.query.get(int(id))
def load_user(uid):
return User.query.get(int(uid))

View File

@ -46,7 +46,10 @@ def login():
if not next_page or url_parse(next_page).netloc != "":
next_page = url_for("scodoc.index")
return redirect(next_page)
return render_template("auth/login.html", title=_("Sign In"), form=form)
message = request.args.get("message", "")
return render_template(
"auth/login.html", title=_("Sign In"), form=form, message=message
)
@bp.route("/logout")
@ -95,7 +98,9 @@ def reset_password_request():
current_app.logger.info(
"reset_password_request: for unkown user '{}'".format(form.email.data)
)
flash(_("Voir les instructions envoyées par mail"))
flash(
_("Voir les instructions envoyées par mail (pensez à regarder vos spams)")
)
return redirect(url_for("auth.login"))
return render_template(
"auth/reset_password_request.html", title=_("Reset Password"), form=form
@ -113,6 +118,6 @@ def reset_password(token):
if form.validate_on_submit():
user.set_password(form.password.data)
db.session.commit()
flash(_("Your password has been reset."))
flash(_("Votre mot de passe a été changé."))
return redirect(url_for("auth.login"))
return render_template("auth/reset_password.html", form=form)
return render_template("auth/reset_password.html", form=form, user=user)

424
app/but/bulletin_but.py Normal file
View File

@ -0,0 +1,424 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
from collections import defaultdict
import datetime
from flask import url_for, g
import numpy as np
import pandas as pd
from app import db
from app.comp import moy_ue, moy_sem, inscr_mod
from app.models import ModuleImpl
from app.scodoc import sco_utils as scu
from app.scodoc.sco_cache import ResultatsSemestreBUTCache
from app.scodoc import sco_bulletins_json
from app.scodoc import sco_preferences
from app.scodoc.sco_utils import jsnan, fmt_note
class ResultatsSemestreBUT:
"""Structure légère pour stocker les résultats du semestre et
générer les bulletins.
__init__ : charge depuis le cache ou calcule
"""
_cached_attrs = (
"sem_cube",
"modimpl_inscr_df",
"modimpl_coefs_df",
"etud_moy_ue",
"modimpls_evals_poids",
"modimpls_evals_notes",
"etud_moy_gen",
"etud_moy_gen_ranks",
"modimpls_evaluations_complete",
)
def __init__(self, formsemestre):
self.formsemestre = formsemestre
self.ues = formsemestre.query_ues().all()
self.modimpls = formsemestre.modimpls.all()
self.etuds = self.formsemestre.get_inscrits(include_dem=False)
self.etud_index = {e.id: idx for idx, e in enumerate(self.etuds)}
self.saes = [
m for m in self.modimpls if m.module.module_type == scu.ModuleType.SAE
]
self.ressources = [
m for m in self.modimpls if m.module.module_type == scu.ModuleType.RESSOURCE
]
if not self.load_cached():
self.compute()
self.store()
def load_cached(self) -> bool:
"Load cached dataframes, returns False si pas en cache"
data = ResultatsSemestreBUTCache.get(self.formsemestre.id)
if not data:
return False
for attr in self._cached_attrs:
setattr(self, attr, data[attr])
return True
def store(self):
"Cache our dataframes"
ResultatsSemestreBUTCache.set(
self.formsemestre.id,
{attr: getattr(self, attr) for attr in self._cached_attrs},
)
def compute(self):
"Charge les notes et inscriptions et calcule toutes les moyennes"
(
self.sem_cube,
self.modimpls_evals_poids,
self.modimpls_evals_notes,
modimpls_evaluations,
self.modimpls_evaluations_complete,
) = moy_ue.notes_sem_load_cube(self.formsemestre)
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
self.formsemestre, ues=self.ues, modimpls=self.modimpls
)
# l'idx de la colonne du mod modimpl.id est
# modimpl_coefs_df.columns.get_loc(modimpl.id)
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
self.etud_moy_ue = moy_ue.compute_ue_moys(
self.sem_cube,
self.etuds,
self.modimpls,
self.ues,
self.modimpl_inscr_df,
self.modimpl_coefs_df,
)
self.etud_moy_gen = moy_sem.compute_sem_moys(
self.etud_moy_ue, self.modimpl_coefs_df
)
self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen)
def etud_ue_mod_results(self, etud, ue, modimpls) -> dict:
"dict synthèse résultats dans l'UE pour les modules indiqués"
d = {}
etud_idx = self.etud_index[etud.id]
ue_idx = self.modimpl_coefs_df.index.get_loc(ue.id)
etud_moy_module = self.sem_cube[etud_idx] # module x UE
for mi in modimpls:
if self.modimpl_inscr_df[str(mi.id)][etud.id]: # si inscrit
coef = self.modimpl_coefs_df[mi.id][ue.id]
if coef > 0:
d[mi.module.code] = {
"id": mi.id,
"coef": coef,
"moyenne": fmt_note(
etud_moy_module[
self.modimpl_coefs_df.columns.get_loc(mi.id)
][ue_idx]
),
}
return d
def etud_ue_results(self, etud, ue):
"dict synthèse résultats UE"
d = {
"id": ue.id,
"numero": ue.numero,
"ECTS": {
"acquis": 0, # XXX TODO voir jury
"total": ue.ects,
},
"competence": None, # XXX TODO lien avec référentiel
"moyenne": {
"value": fmt_note(self.etud_moy_ue[ue.id][etud.id]),
"min": fmt_note(self.etud_moy_ue[ue.id].min()),
"max": fmt_note(self.etud_moy_ue[ue.id].max()),
"moy": fmt_note(self.etud_moy_ue[ue.id].mean()),
},
"bonus": None, # XXX TODO
"malus": None, # XXX TODO voir ce qui est ici
"capitalise": None, # "AAAA-MM-JJ" TODO
"ressources": self.etud_ue_mod_results(etud, ue, self.ressources),
"saes": self.etud_ue_mod_results(etud, ue, self.saes),
}
return d
def etud_mods_results(self, etud, modimpls) -> dict:
"""dict synthèse résultats des modules indiqués,
avec évaluations de chacun."""
d = {}
# etud_idx = self.etud_index[etud.id]
for mi in modimpls:
# mod_idx = self.modimpl_coefs_df.columns.get_loc(mi.id)
# # moyennes indicatives (moyennes de moyennes d'UE)
# try:
# moyennes_etuds = np.nan_to_num(
# np.nanmean(self.sem_cube[:, mod_idx, :], axis=1),
# copy=False,
# )
# except RuntimeWarning: # all nans in np.nanmean (sur certains etuds sans notes valides)
# pass
# try:
# moy_indicative_mod = np.nanmean(self.sem_cube[etud_idx, mod_idx])
# except RuntimeWarning: # all nans in np.nanmean
# pass
if self.modimpl_inscr_df[str(mi.id)][etud.id]: # si inscrit
d[mi.module.code] = {
"id": mi.id,
"titre": mi.module.titre,
"code_apogee": mi.module.code_apogee,
"url": url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=mi.id,
),
"moyenne": {
# # moyenne indicative de module: moyenne des UE, ignorant celles sans notes (nan)
# "value": fmt_note(moy_indicative_mod),
# "min": fmt_note(moyennes_etuds.min()),
# "max": fmt_note(moyennes_etuds.max()),
# "moy": fmt_note(moyennes_etuds.mean()),
},
"evaluations": [
self.etud_eval_results(etud, e)
for eidx, e in enumerate(mi.evaluations)
if e.visibulletin
and self.modimpls_evaluations_complete[mi.id][eidx]
],
}
return d
def etud_eval_results(self, etud, e) -> dict:
"dict resultats d'un étudiant à une évaluation"
eval_notes = self.modimpls_evals_notes[e.moduleimpl_id][e.id] # pd.Series
notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
d = {
"id": e.id,
"description": e.description,
"date": e.jour.isoformat() if e.jour else None,
"heure_debut": e.heure_debut.strftime("%H:%M") if e.heure_debut else None,
"heure_fin": e.heure_fin.strftime("%H:%M") if e.heure_debut else None,
"coef": e.coefficient,
"poids": {p.ue.acronyme: p.poids for p in e.ue_poids},
"note": {
"value": fmt_note(
self.modimpls_evals_notes[e.moduleimpl_id][e.id][etud.id],
note_max=e.note_max,
),
"min": fmt_note(notes_ok.min()),
"max": fmt_note(notes_ok.max()),
"moy": fmt_note(notes_ok.mean()),
},
"url": url_for(
"notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept,
evaluation_id=e.id,
),
}
return d
def bulletin_etud(self, etud, formsemestre) -> dict:
"""Le bulletin de l'étudiant dans ce semestre"""
etat_inscription = etud.etat_inscription(formsemestre.id)
d = {
"version": "0",
"type": "BUT",
"date": datetime.datetime.utcnow().isoformat() + "Z",
"etudiant": etud.to_dict_bul(),
"formation": {
"id": formsemestre.formation.id,
"acronyme": formsemestre.formation.acronyme,
"titre_officiel": formsemestre.formation.titre_officiel,
"titre": formsemestre.formation.titre,
},
"formsemestre_id": formsemestre.id,
"etat_inscription": etat_inscription,
"options": bulletin_option_affichage(formsemestre),
}
semestre_infos = {
"etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo],
"date_debut": formsemestre.date_debut.isoformat(),
"date_fin": formsemestre.date_fin.isoformat(),
"annee_universitaire": self.formsemestre.annee_scolaire_str(),
"inscription": "TODO-MM-JJ", # XXX TODO
"numero": formsemestre.semestre_id,
"groupes": [], # XXX TODO
"absences": { # XXX TODO
"injustifie": 1,
"total": 33,
},
}
semestre_infos.update(
sco_bulletins_json.dict_decision_jury(etud.id, formsemestre.id)
)
if etat_inscription == scu.INSCRIT:
semestre_infos.update(
{
"notes": { # moyenne des moyennes générales du semestre
"value": fmt_note(self.etud_moy_gen[etud.id]),
"min": fmt_note(self.etud_moy_gen.min()),
"moy": fmt_note(self.etud_moy_gen.mean()),
"max": fmt_note(self.etud_moy_gen.max()),
},
"rang": { # classement wrt moyenne général, indicatif
"value": self.etud_moy_gen_ranks[etud.id],
"total": len(self.etuds),
},
},
)
d.update(
{
"ressources": self.etud_mods_results(etud, self.ressources),
"saes": self.etud_mods_results(etud, self.saes),
"ues": {
ue.acronyme: self.etud_ue_results(etud, ue) for ue in self.ues
},
"semestre": semestre_infos,
},
)
else:
semestre_infos.update(
{
"notes": {
"value": "DEM",
"min": "",
"moy": "",
"max": "",
},
"rang": {"value": "DEM", "total": len(self.etuds)},
}
)
d.update(
{
"semestre": semestre_infos,
"ressources": {},
"saes": {},
"ues": {},
}
)
return d
def bulletin_option_affichage(formsemestre):
"dict avec les options d'affichages (préférences) pour ce semestre"
prefs = sco_preferences.SemPreferences(formsemestre.id)
fields = (
"bul_show_abs",
"bul_show_abs_modules",
"bul_show_ects",
"bul_show_codemodules",
"bul_show_matieres",
"bul_show_rangs",
"bul_show_ue_rangs",
"bul_show_mod_rangs",
"bul_show_moypromo",
"bul_show_minmax",
"bul_show_minmax_mod",
"bul_show_minmax_eval",
"bul_show_coef",
"bul_show_ue_cap_details",
"bul_show_ue_cap_current",
"bul_show_temporary",
"bul_temporary_txt",
"bul_show_uevalid",
"bul_show_date_inscr",
)
# on enlève le "bul_" de la clé:
return {field[4:]: prefs[field] for field in fields}
# Pour raccorder le code des anciens bulletins qui attendent une NoteTable
class APCNotesTableCompat:
"""Implementation partielle de NotesTable pour les formations APC
Accès aux notes et rangs.
"""
def __init__(self, formsemestre):
self.results = ResultatsSemestreBUT(formsemestre)
nb_etuds = len(self.results.etuds)
self.rangs = self.results.etud_moy_gen_ranks
self.moy_min = self.results.etud_moy_gen.min()
self.moy_max = self.results.etud_moy_gen.max()
self.moy_moy = self.results.etud_moy_gen.mean()
self.bonus = defaultdict(lambda: 0.0) # XXX
self.ue_rangs = {
u.id: (defaultdict(lambda: 0.0), nb_etuds) for u in self.results.ues
}
self.mod_rangs = {
m.id: (defaultdict(lambda: 0), nb_etuds) for m in self.results.modimpls
}
def get_ues(self):
ues = []
for ue in self.results.ues:
d = ue.to_dict()
d.update(
{
"max": self.results.etud_moy_ue[ue.id].max(),
"min": self.results.etud_moy_ue[ue.id].min(),
"moy": self.results.etud_moy_ue[ue.id].mean(),
"nb_moy": len(self.results.etud_moy_ue),
}
)
ues.append(d)
return ues
def get_modimpls(self):
return [m.to_dict() for m in self.results.modimpls]
def get_etud_moy_gen(self, etudid):
return self.results.etud_moy_gen[etudid]
def get_moduleimpls_attente(self):
return [] # XXX TODO
def get_etud_rang(self, etudid):
return self.rangs[etudid]
def get_etud_rang_group(self, etudid, group_id):
return (None, 0) # XXX unimplemented TODO
def get_etud_ue_status(self, etudid, ue_id):
return {
"cur_moy_ue": self.results.etud_moy_ue[ue_id][etudid],
"is_capitalized": False, # XXX TODO
}
def get_etud_mod_moy(self, moduleimpl_id, etudid):
mod_idx = self.results.modimpl_coefs_df.columns.get_loc(moduleimpl_id)
etud_idx = self.results.etud_index[etudid]
# moyenne sur les UE:
self.results.sem_cube[etud_idx, mod_idx].mean()
def get_mod_stats(self, moduleimpl_id):
return {
"moy": "-",
"max": "-",
"min": "-",
"nb_notes": "-",
"nb_missing": "-",
"nb_valid_evals": "-",
}
def get_evals_in_mod(self, moduleimpl_id):
mi = ModuleImpl.query.get(moduleimpl_id)
evals_results = []
for e in mi.evaluations:
d = e.to_dict()
d["heure_debut"] = e.heure_debut # datetime.time
d["heure_fin"] = e.heure_fin
d["jour"] = e.jour # datetime
d["notes"] = {
etud.id: {
"etudid": etud.id,
"value": self.results.modimpls_evals_notes[e.moduleimpl_id][e.id][
etud.id
],
}
for etud in self.results.etuds
}
evals_results.append(d)
return evals_results

View File

@ -0,0 +1,334 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 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
#
##############################################################################
"""Génération du bulletin en format XML / compatibilité ScoDoc 7
=> exporte quelques résultats BUT dans le format des anciens bulletins XML ScoDoc 7
afin d'avoir un affichage acceptable sur les ENT anciens.
Les plate-formes modernes utilisent uniquement la version JSON (but/bulletin_but.py)
"""
import datetime
from xml.etree import ElementTree
from xml.etree.ElementTree import Element
from app import log
from app.but import bulletin_but
from app.models import FormSemestre, Identite
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc import sco_abs
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_ue
from app.scodoc import sco_etud
from app.scodoc import sco_photos
from app.scodoc import sco_preferences
from app.scodoc import sco_xml
def bulletin_but_xml_compat(
formsemestre_id,
etudid,
doc=None, # XML document
force_publishing=False,
xml_nodate=False,
xml_with_decisions=False, # inclue les decisions même si non publiées
version="long",
) -> str:
"""Bulletin XML au format ScoDoc 7, avec informations "BUT" """
from app.scodoc import sco_bulletins
log(
"bulletin_but_xml_compat( formsemestre_id=%s, etudid=%s )"
% (formsemestre_id, etudid)
)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
etud = Identite.query.get_or_404(etudid)
results = bulletin_but.ResultatsSemestreBUT(formsemestre)
nb_inscrits = len(results.etuds)
etat_inscription = etud.etat_inscription(formsemestre.id)
if (not formsemestre.bul_hide_xml) or force_publishing:
published = 1
else:
published = 0
if xml_nodate:
docdate = ""
else:
docdate = datetime.datetime.now().isoformat()
el = {
"etudid": str(etudid),
"formsemestre_id": str(formsemestre_id),
"date": docdate,
"publie": str(published),
}
if formsemestre.etapes:
el["etape_apo"] = formsemestre.etapes[0].etape_apo or ""
n = 2
for et in formsemestre.etapes[1:]:
el["etape_apo" + str(n)] = et.etape_apo or ""
n += 1
x = Element("bulletinetud", **el)
if doc:
is_appending = True
doc.append(x)
else:
is_appending = False
doc = x
# Infos sur l'etudiant
doc.append(
Element(
"etudiant",
etudid=str(etudid),
code_nip=etud.code_nip or "",
code_ine=etud.code_ine or "",
nom=scu.quote_xml_attr(etud.nom),
prenom=scu.quote_xml_attr(etud.prenom),
civilite=scu.quote_xml_attr(etud.civilite_str()),
sexe=scu.quote_xml_attr(etud.civilite_str()), # compat
photo_url=scu.quote_xml_attr(sco_photos.get_etud_photo_url(etud.id)),
email=scu.quote_xml_attr(etud.get_first_email() or ""),
emailperso=scu.quote_xml_attr(etud.get_first_email("emailperso") or ""),
)
)
# Disponible pour publication ?
if not published:
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(
scu.SCO_ENCODING
) # stop !
if etat_inscription == scu.INSCRIT:
# Moyenne générale:
doc.append(
Element(
"note",
value=scu.fmt_note(results.etud_moy_gen[etud.id]),
min=scu.fmt_note(results.etud_moy_gen.min()),
max=scu.fmt_note(results.etud_moy_gen.max()),
moy=scu.fmt_note(results.etud_moy_gen.mean()), # moyenne des moy. gen.
)
)
rang = 0 # XXX TODO rang de l'étduiant selon la moy gen indicative
bonus = 0 # XXX TODO valeur du bonus sport
doc.append(Element("rang", value=str(rang), ninscrits=str(nb_inscrits)))
# XXX TODO: ajouter "rang_group" : rangs dans les partitions
doc.append(Element("note_max", value="20")) # notes toujours sur 20
doc.append(Element("bonus_sport_culture", value=str(bonus)))
# Liste les UE / modules /evals
for ue in results.ues:
rang_ue = 0 # XXX TODO rang de l'étudiant dans cette UE
nb_inscrits_ue = (
nb_inscrits # approx: compliqué de définir le "nb d'inscrit à une UE"
)
x_ue = Element(
"ue",
id=str(ue.id),
numero=scu.quote_xml_attr(ue.numero),
acronyme=scu.quote_xml_attr(ue.acronyme or ""),
titre=scu.quote_xml_attr(ue.titre or ""),
code_apogee=scu.quote_xml_attr(ue.code_apogee or ""),
)
doc.append(x_ue)
if ue.type != sco_codes_parcours.UE_SPORT:
v = results.etud_moy_ue[ue.id][etud.id]
else:
v = 0 # XXX TODO valeur bonus sport pour cet étudiant
x_ue.append(
Element(
"note",
value=scu.fmt_note(v),
min=scu.fmt_note(results.etud_moy_ue[ue.id].min()),
max=scu.fmt_note(results.etud_moy_ue[ue.id].max()),
)
)
x_ue.append(Element("ects", value=str(ue.ects if ue.ects else 0)))
x_ue.append(Element("rang", value=str(rang_ue)))
x_ue.append(Element("effectif", value=str(nb_inscrits_ue)))
# Liste les modules rattachés à cette UE
for modimpl in results.modimpls:
# Liste ici uniquement les modules rattachés à cette UE
if modimpl.module.ue.id == ue.id:
mod_moy = scu.fmt_note(results.etud_moy_ue[ue.id][etud.id])
coef = results.modimpl_coefs_df[modimpl.id][ue.id]
x_mod = Element(
"module",
id=str(modimpl.id),
code=str(modimpl.module.code or ""),
coefficient=str(coef),
numero=str(modimpl.module.numero or 0),
titre=scu.quote_xml_attr(modimpl.module.titre or ""),
abbrev=scu.quote_xml_attr(modimpl.module.abbrev or ""),
code_apogee=scu.quote_xml_attr(
modimpl.module.code_apogee or ""
),
)
x_ue.append(x_mod)
x_mod.append(
Element(
"note",
value=mod_moy,
min=scu.fmt_note(results.etud_moy_ue[ue.id].min()),
max=scu.fmt_note(results.etud_moy_ue[ue.id].max()),
moy=scu.fmt_note(results.etud_moy_ue[ue.id].mean()),
)
)
# XXX TODO rangs et effectifs
# --- notes de chaque eval:
if version != "short":
for e in modimpl.evaluations:
if e.visibulletin or version == "long":
x_eval = Element(
"evaluation",
jour=e.jour.isoformat() if e.jour else "",
heure_debut=e.heure_debut.isoformat()
if e.heure_debut
else "",
heure_fin=e.heure_fin.isoformat()
if e.heure_debut
else "",
coefficient=str(e.coefficient),
# pas les poids en XML compat
evaluation_type=str(e.evaluation_type),
description=scu.quote_xml_attr(e.description),
# notes envoyées sur 20, ceci juste pour garder trace:
note_max_origin=str(e.note_max),
)
x_mod.append(x_eval)
x_eval.append(
Element(
"note",
value=scu.fmt_note(
results.modimpls_evals_notes[
e.moduleimpl_id
][e.id][etud.id],
note_max=e.note_max,
),
)
)
# XXX TODO: Evaluations incomplètes ou futures: XXX
# XXX TODO UE capitalisee (listee seulement si meilleure que l'UE courante)
# --- Absences
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
# -------- LA SUITE EST COPIEE SANS MODIF DE sco_bulletins_xml.py ---------
# TODO : refactoring
# --- Decision Jury
if (
sco_preferences.get_preference("bul_show_decision", formsemestre_id)
or xml_with_decisions
):
infos, dpv = sco_bulletins.etud_descr_situation_semestre(
etudid,
formsemestre_id,
format="xml",
show_uevalid=sco_preferences.get_preference(
"bul_show_uevalid", formsemestre_id
),
)
x_situation = Element("situation")
x_situation.text = scu.quote_xml_attr(infos["situation"])
doc.append(x_situation)
if dpv:
decision = dpv["decisions"][0]
etat = decision["etat"]
if decision["decision_sem"]:
code = decision["decision_sem"]["code"] or ""
else:
code = ""
if (
decision["decision_sem"]
and "compense_formsemestre_id" in decision["decision_sem"]
):
doc.append(
Element(
"decision",
code=code,
etat=str(etat),
compense_formsemestre_id=str(
decision["decision_sem"]["compense_formsemestre_id"] or ""
),
)
)
else:
doc.append(Element("decision", code=code, etat=str(etat)))
if decision[
"decisions_ue"
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
for ue_id in decision["decisions_ue"].keys():
ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
doc.append(
Element(
"decision_ue",
ue_id=str(ue["ue_id"]),
numero=scu.quote_xml_attr(ue["numero"]),
acronyme=scu.quote_xml_attr(ue["acronyme"]),
titre=scu.quote_xml_attr(ue["titre"]),
code=decision["decisions_ue"][ue_id]["code"],
)
)
for aut in decision["autorisations"]:
doc.append(
Element(
"autorisation_inscription", semestre_id=str(aut["semestre_id"])
)
)
else:
doc.append(Element("decision", code="", etat="DEM"))
# --- Appreciations
cnx = ndb.GetDBConnexion()
apprecs = sco_etud.appreciations_list(
cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
)
for appr in apprecs:
x_appr = Element(
"appreciation",
date=ndb.DateDMYtoISO(appr["date"]),
)
x_appr.text = scu.quote_xml_attr(appr["comment"])
doc.append(x_appr)
if is_appending:
return None
else:
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING)
"""
formsemestre_id=718
etudid=12496
from app.but.bulletin_but import *
mapp.set_sco_dept("RT")
sem = FormSemestre.query.get(formsemestre_id)
r = ResultatsSemestreBUT(sem)
"""

View File

@ -0,0 +1,35 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""ScoDoc 9 : Formulaires / référentiel de compétence
"""
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed, FileRequired
from wtforms import SelectField, SubmitField
class FormationRefCompForm(FlaskForm):
referentiel_competence = SelectField("Référentiels déjà chargés")
submit = SubmitField("Valider")
cancel = SubmitField("Annuler")
class RefCompLoadForm(FlaskForm):
upload = FileField(
label="Sélectionner un fichier XML Orébut",
validators=[
FileRequired(),
FileAllowed(
[
"xml",
],
"Fichier XML Orébut seulement",
),
],
)
submit = SubmitField("Valider")
cancel = SubmitField("Annuler")

126
app/but/import_refcomp.py Normal file
View File

@ -0,0 +1,126 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
from xml.etree import ElementTree
from typing import TextIO
from app import db
from app.models.but_refcomp import (
ApcReferentielCompetences,
ApcCompetence,
ApcSituationPro,
ApcAppCritique,
ApcComposanteEssentielle,
ApcNiveau,
ApcParcours,
ApcAnneeParcours,
ApcParcoursNiveauCompetence,
)
from app.scodoc.sco_exceptions import ScoFormatError
def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
"""Importation XML Orébut
peut lever TypeError ou ScoFormatError
Résultat: instance de ApcReferentielCompetences
"""
try:
root = ElementTree.XML(xml_data)
except ElementTree.ParseError:
raise ScoFormatError("fichier XML Orébut invalide")
if root.tag != "referentiel_competence":
raise ScoFormatError("élément racine 'referentiel_competence' manquant")
args = ApcReferentielCompetences.attr_from_xml(root.attrib)
args["dept_id"] = dept_id
args["scodoc_orig_filename"] = orig_filename
ref = ApcReferentielCompetences(**args)
db.session.add(ref)
competences = root.find("competences")
if not competences:
raise ScoFormatError("élément 'competences' manquant")
for competence in competences.findall("competence"):
c = ApcCompetence(**ApcCompetence.attr_from_xml(competence.attrib))
ref.competences.append(c)
# --- SITUATIONS
situations = competence.find("situations")
for situation in situations:
libelle = "".join(situation.itertext()).strip()
s = ApcSituationPro(competence_id=c.id, libelle=libelle)
c.situations.append(s)
# --- COMPOSANTES ESSENTIELLES
composantes = competence.find("composantes_essentielles")
for composante in composantes:
libelle = "".join(composante.itertext()).strip()
ce = ApcComposanteEssentielle(libelle=libelle)
c.composantes_essentielles.append(ce)
# --- NIVEAUX (années)
niveaux = competence.find("niveaux")
for niveau in niveaux:
niv = ApcNiveau(**ApcNiveau.attr_from_xml(niveau.attrib))
c.niveaux.append(niv)
acs = niveau.find("acs")
for ac in acs:
libelle = "".join(ac.itertext()).strip()
code = ac.attrib["code"]
niv.app_critiques.append(ApcAppCritique(code=code, libelle=libelle))
# --- PARCOURS
parcours = root.find("parcours")
if not parcours:
raise ScoFormatError("élément 'parcours' manquant")
for parcour in parcours.findall("parcour"):
parc = ApcParcours(**ApcParcours.attr_from_xml(parcour.attrib))
ref.parcours.append(parc)
for annee in parcour.findall("annee"):
a = ApcAnneeParcours(**ApcAnneeParcours.attr_from_xml(annee.attrib))
parc.annees.append(a)
for competence in annee.findall("competence"):
nom = competence.attrib["nom"]
niveau = int(competence.attrib["niveau"])
# Retrouve la competence
comp = ref.competences.filter_by(titre=nom).all()
if len(comp) == 0:
raise ScoFormatError(f"competence {nom} référencée mais on définie")
elif len(comp) > 1:
raise ScoFormatError(f"competence {nom} ambigüe")
ass = ApcParcoursNiveauCompetence(
niveau=niveau, annee_parcours=a, competence=comp[0]
)
db.session.add(ass)
db.session.commit()
return ref
"""
xmlfile = open("but-RT-refcomp-30112021.xml")
tree = ElementTree.parse(xmlfile)
# get root element
root = tree.getroot()
assert root.tag == "referentiel_competence"
ref = ApcReferentielCompetences(**ApcReferentielCompetences.attr_from_xml(root.attrib))
competences = root.find("competences")
if not competences:
raise ScoFormatError("élément 'competences' manquant")
competence = competences.findall("competence")[0] # XXX
from app.but.import_refcomp import *
dept_id = models.Departement.query.first().id
data = open("tests/data/but-RT-refcomp-exemple.xml").read()
ref = orebut_import_refcomp(data, dept_id)
#------
from app.but.import_refcomp import *
ref = ApcReferentielCompetences.query.first()
p = ApcParcours(code="PARC", libelle="Parcours Test")
ref.parcours.append(p)
annee = ApcAnneeParcours(numero=1)
p.annees.append(annee)
annee.competences
c = ref.competences.filter_by(titre="Administrer").first()
annee.competences.append(c)
"""

49
app/comp/df_cache.py Normal file
View File

@ -0,0 +1,49 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 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
#
##############################################################################
"""caches pour tables APC
"""
from app.scodoc import sco_cache
class ModuleCoefsCache(sco_cache.ScoDocCache):
"""Cache for module coefs
Clé: formation_id.semestre_idx
Valeur: DataFrame (df_load_module_coefs)
"""
prefix = "MCO"
class EvaluationsPoidsCache(sco_cache.ScoDocCache):
"""Cache for poids evals
Clé: moduleimpl_id
Valeur: DataFrame (df_load_evaluations_poids)
"""
prefix = "EPC"

70
app/comp/inscr_mod.py Normal file
View File

@ -0,0 +1,70 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
"""Matrices d'inscription aux modules d'un semestre
"""
import numpy as np
import pandas as pd
from app import db
from app import models
#
# Le chargement des inscriptions est long: matrice nb_module x nb_etuds
# sur test debug 116 etuds, 18 modules, on est autour de 250ms.
# On a testé trois approches, ci-dessous (et retenu la 1ere)
#
def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame:
"""Charge la matrice des inscriptions aux modules du semestre
rows: etudid
columns: moduleimpl_id (en chaîne)
value: bool (0/1 inscrit ou pas)
"""
# méthode la moins lente: une requete par module, merge les dataframes
moduleimpl_ids = [m.id for m in formsemestre.modimpls]
etudids = [i.etudid for i in formsemestre.get_inscrits(include_dem=False)]
df = pd.DataFrame(index=etudids, dtype=int)
for moduleimpl_id in moduleimpl_ids:
ins_df = pd.read_sql_query(
"""SELECT etudid, 1 AS "%(moduleimpl_id)s"
FROM notes_moduleimpl_inscription
WHERE moduleimpl_id=%(moduleimpl_id)s""",
db.engine,
params={"moduleimpl_id": moduleimpl_id},
index_col="etudid",
dtype=int,
)
df = df.merge(ins_df, how="left", left_index=True, right_index=True)
# les colonnes de df sont en float (Nan) quand il n'y a
# aucun inscrit au module.
df.fillna(0, inplace=True) # les non-inscrits
return df.astype(bool) # x100 25.5s 15s 17s
# chrono avec timeit:
# timeit.timeit('x = df_load_module_inscr_v0(696)', number=100, globals=globals())
def df_load_modimpl_inscr_v0(formsemestre):
# methode 0, pur SQL Alchemy, 1.5 à 2 fois plus lente
moduleimpl_ids = [m.id for m in formsemestre.modimpls]
etudids = [i.etudid for i in formsemestre.inscriptions]
df = pd.DataFrame(False, columns=moduleimpl_ids, index=etudids, dtype=bool)
for modimpl in formsemestre.modimpls:
ins_mod = df[modimpl.id]
for inscr in modimpl.inscriptions:
ins_mod[inscr.etudid] = True
return df # x100 30.7s 46s 32s
def df_load_modimpl_inscr_v2(formsemestre):
moduleimpl_ids = [m.id for m in formsemestre.modimpls]
etudids = [i.etudid for i in formsemestre.inscriptions]
df = pd.DataFrame(False, columns=moduleimpl_ids, index=etudids, dtype=bool)
cursor = db.engine.execute(
"select moduleimpl_id, etudid from notes_moduleimpl_inscription i, notes_moduleimpl m where i.moduleimpl_id = m.id and m.formsemestre_id = %(formsemestre_id)s",
{"formsemestre_id": formsemestre.id},
)
for moduleimpl_id, etudid in cursor:
df[moduleimpl_id][etudid] = True
return df # x100 44s, 31s, 29s, 28s

243
app/comp/moy_mod.py Normal file
View File

@ -0,0 +1,243 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 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
#
##############################################################################
"""Fonctions de calcul des moyennes de modules (modules, ressources ou SAÉ)
Rappel: pour éviter les confusions, on appelera *poids* les coefficients d'une
évaluation dans un module, et *coefficients* ceux utilisés pour le calcul de la
moyenne générale d'une UE.
"""
import numpy as np
import pandas as pd
from pandas.core.frame import DataFrame
from app import db
from app import log
from app import models
from app.models import ModuleImpl, Evaluation, EvaluationUEPoids
from app.scodoc import sco_utils as scu
def df_load_evaluations_poids(
moduleimpl_id: int, default_poids=1.0
) -> tuple[pd.DataFrame, list]:
"""Charge poids des évaluations d'un module et retourne un dataframe
rows = evaluations, columns = UE, value = poids (float).
Les valeurs manquantes (évaluations sans coef vers des UE) sont
remplies par default_poids.
Résultat: (evals_poids, liste de UE du semestre)
"""
modimpl = ModuleImpl.query.get(moduleimpl_id)
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
ues = modimpl.formsemestre.query_ues(with_sport=False).all()
ue_ids = [ue.id for ue in ues]
evaluation_ids = [evaluation.id for evaluation in evaluations]
df = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
for eval_poids in EvaluationUEPoids.query.join(
EvaluationUEPoids.evaluation
).filter_by(moduleimpl_id=moduleimpl_id):
df[eval_poids.ue_id][eval_poids.evaluation_id] = eval_poids.poids
if default_poids is not None:
df.fillna(value=default_poids, inplace=True)
return df, ues
def check_moduleimpl_conformity(
moduleimpl, evals_poids: pd.DataFrame, modules_coefficients: pd.DataFrame
) -> bool:
"""Vérifie que les évaluations de ce moduleimpl sont bien conformes
au PN.
Un module est dit *conforme* si et seulement si la somme des poids de ses
évaluations vers une UE de coefficient non nul est non nulle.
"""
nb_evals, nb_ues = evals_poids.shape
if nb_evals == 0:
return True # modules vides conformes
if nb_ues == 0:
return False # situation absurde (pas d'UE)
if len(modules_coefficients) != nb_ues:
# bug ?
log(
"check_moduleimpl_conformity: nb ue incoherent (moduleimpl.id={moduleimpl.id})"
)
return False
module_evals_poids = evals_poids.transpose().sum(axis=1).to_numpy() != 0
check = all(
(modules_coefficients[moduleimpl.module.id].to_numpy() != 0)
== module_evals_poids
)
return check
def df_load_modimpl_notes(moduleimpl_id: int) -> tuple:
"""Construit un dataframe avec toutes les notes de toutes les évaluations du module.
colonnes: le nom de la colonne est l'evaluation_id (int)
index (lignes): etudid (int)
Résultat: (evals_notes, liste de évaluations du moduleimpl,
liste de booleens indiquant si l'évaluation est "complete")
L'ensemble des étudiants est celui des inscrits au SEMESTRE.
Les notes renvoyées sont "brutes" (séries de floats) et peuvent prendre les valeurs:
note : float (valeur enregistrée brute, non normalisée sur 20)
pas de note: NaN (rien en bd, ou étudiant non inscrit au module)
absent: NOTES_ABSENCE (NULL en bd)
excusé: NOTES_NEUTRALISE (voir sco_utils)
attente: NOTES_ATTENTE
L'évaluation "complete" (prise en compte dans les calculs) si:
- soit tous les étudiants inscrits au module ont des notes
- soit elle a été déclarée "à prise ne compte immédiate" (publish_incomplete)
N'utilise pas de cache ScoDoc.
"""
# L'index du dataframe est la liste des étudiants inscrits au semestre,
# sans les démissionnaires
etudids = [
e.etudid
for e in ModuleImpl.query.get(moduleimpl_id).formsemestre.get_inscrits(
include_dem=False
)
]
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
# --- Calcul nombre d'inscrits pour détermnier si évaluation "complete":
if evaluations:
# on prend les inscrits au module ET au semestre (donc sans démissionnaires)
inscrits_module = {
ins.etud.id for ins in evaluations[0].moduleimpl.inscriptions
}.intersection(etudids)
nb_inscrits_module = len(inscrits_module)
else:
nb_inscrits_module = 0
# empty df with all students:
evals_notes = pd.DataFrame(index=etudids, dtype=float)
evaluations_completes = []
for evaluation in evaluations:
eval_df = pd.read_sql_query(
"""SELECT n.etudid, n.value AS "%(evaluation_id)s"
FROM notes_notes n, notes_moduleimpl_inscription i
WHERE evaluation_id=%(evaluation_id)s
AND n.etudid = i.etudid
AND i.moduleimpl_id = %(moduleimpl_id)s
ORDER BY n.etudid
""",
db.engine,
params={
"evaluation_id": evaluation.id,
"moduleimpl_id": evaluation.moduleimpl.id,
},
index_col="etudid",
)
eval_df[str(evaluation.id)] = pd.to_numeric(eval_df[str(evaluation.id)])
# is_complete ssi tous les inscrits (non dem) au semestre ont une note
is_complete = (
len(set(eval_df.index).intersection(etudids)) == nb_inscrits_module
) or evaluation.publish_incomplete
evaluations_completes.append(is_complete)
# NULL en base => ABS (= -999)
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
# Ce merge met à NULL les élements non présents
# (notes non saisies ou etuds non inscrits au module):
evals_notes = evals_notes.merge(
eval_df, how="left", left_index=True, right_index=True
)
# Force columns names to integers (evaluation ids)
evals_notes.columns = pd.Int64Index(
[int(x) for x in evals_notes.columns], dtype="int64"
)
return evals_notes, evaluations, evaluations_completes
def compute_module_moy(
evals_notes_df: pd.DataFrame,
evals_poids_df: pd.DataFrame,
evaluations: list,
evaluations_completes: list,
) -> pd.DataFrame:
"""Calcule les moyennes des étudiants dans ce module
- evals_notes : DataFrame, colonnes: EVALS, Lignes: etudid
valeur: notes brutes, float ou NOTES_ATTENTE, NOTES_NEUTRALISE,
NOTES_ABSENCE.
Les NaN désignent les notes manquantes (non saisies).
- evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
- evaluations: séquence d'évaluations (utilisées pour le coef et
le barème)
- evaluations_completes: séquence de booléens indiquant les
évals à prendre en compte.
Résultat: DataFrame, colonnes UE, lignes etud
= la note de l'étudiant dans chaque UE pour ce module.
ou NaN si les évaluations (dans lesquelles l'étudiant à des notes)
ne donnent pas de coef vers cette UE.
"""
nb_etuds, nb_evals = evals_notes_df.shape
nb_ues = evals_poids_df.shape[1]
assert evals_poids_df.shape[0] == nb_evals # compat notes/poids
if nb_etuds == 0:
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
# Coefficients des évaluations, met à zéro ceux des évals incomplètes:
evals_coefs = (
np.array(
[e.coefficient for e in evaluations],
dtype=float,
)
* evaluations_completes
).reshape(-1, 1)
evals_poids = evals_poids_df.values * evals_coefs
# -> evals_poids shape : (nb_evals, nb_ues)
assert evals_poids.shape == (nb_evals, nb_ues)
# Remplace les notes ATT, EXC, ABS, NaN par zéro et mets les notes sur 20:
evals_notes = np.where(
evals_notes_df.values > scu.NOTES_ABSENCE, evals_notes_df.values, 0.0
) / [e.note_max / 20.0 for e in evaluations]
# Les poids des évals pour les étudiant: là où il a des notes non neutralisées
# (ABS n'est pas neutralisée, mais ATTENTE et NEUTRALISE oui)
# Note: les NaN sont remplacés par des 0 dans evals_notes
# et dans dans evals_poids_etuds
# (rappel: la comparaison est toujours false face à un NaN)
# shape: (nb_etuds, nb_evals, nb_ues)
poids_stacked = np.stack([evals_poids] * nb_etuds)
evals_poids_etuds = np.where(
np.stack([evals_notes_df.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
poids_stacked,
0,
)
# Calcule la moyenne pondérée sur les notes disponibles:
evals_notes_stacked = np.stack([evals_notes] * nb_ues, axis=2)
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etuds_moy_module = np.sum(
evals_poids_etuds * evals_notes_stacked, axis=1
) / np.sum(evals_poids_etuds, axis=1)
etuds_moy_module_df = pd.DataFrame(
etuds_moy_module, index=evals_notes_df.index, columns=evals_poids_df.columns
)
return etuds_moy_module_df

79
app/comp/moy_sem.py Normal file
View File

@ -0,0 +1,79 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 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
#
##############################################################################
"""Fonctions de calcul des moyennes de semestre (indicatives dans le BUT)
"""
import numpy as np
import pandas as pd
def compute_sem_moys(etud_moy_ue_df, modimpl_coefs_df):
"""Calcule la moyenne générale indicative
= moyenne des moyennes d'UE, pondérée par la somme de leurs coefs
etud_moy_ue_df: DataFrame, colonnes ue_id, lignes etudid
modimpl_coefs_df: DataFrame, colonnes moduleimpl_id, lignes UE
Result: panda Series, index etudid, valeur float (moyenne générale)
"""
moy_gen = (etud_moy_ue_df * modimpl_coefs_df.values.sum(axis=1)).sum(
axis=1
) / modimpl_coefs_df.values.sum()
return moy_gen
def comp_ranks_series(notes: pd.Series):
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur numérique)
en tenant compte des ex-aequos
Le resultat est: { etudid : rang } rang est une chaine decrivant le rang
"""
notes = notes.sort_values(ascending=False) # Serie, tri par ordre décroissant
rangs = pd.Series(index=notes.index, dtype=str) # le rang est une chaîne
N = len(notes)
nb_ex = 0 # nb d'ex-aequo consécutifs en cours
notes_i = notes.iat
for i, etudid in enumerate(notes.index):
# test ex-aequo
if i < (N - 1):
next = notes_i[i + 1]
else:
next = None
val = notes_i[i]
if nb_ex:
srang = "%d ex" % (i + 1 - nb_ex)
if val == next:
nb_ex += 1
else:
nb_ex = 0
else:
if val == next:
srang = "%d ex" % (i + 1 - nb_ex)
nb_ex = 1
else:
srang = "%d" % (i + 1)
rangs[etudid] = srang
return rangs

236
app/comp/moy_ue.py Normal file
View File

@ -0,0 +1,236 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 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
#
##############################################################################
"""Fonctions de calcul des moyennes d'UE
"""
import numpy as np
import pandas as pd
from app import db
from app import models
from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef
from app.comp import moy_mod
from app.models.formsemestre import FormSemestre
from app.scodoc import sco_codes_parcours
def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.DataFrame:
"""Charge les coefs des modules de la formation pour le semestre indiqué.
Ces coefs lient les modules à chaque UE.
Résultat: (module_coefs_df, ues, modules)
DataFrame rows = UEs, columns = modules, value = coef.
Considère toutes les UE (sauf sport) et modules du semestre.
Les coefs non définis (pas en base) sont mis à zéro.
Si semestre_idx None, prend toutes les UE de la formation.
"""
ues = (
UniteEns.query.filter_by(formation_id=formation_id)
.filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
.order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme)
)
modules = Module.query.filter_by(formation_id=formation_id).order_by(
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
)
if semestre_idx is not None:
ues = ues.filter_by(semestre_idx=semestre_idx)
modules = modules.filter_by(semestre_id=semestre_idx)
ues = ues.all()
modules = modules.all()
ue_ids = [ue.id for ue in ues]
module_ids = [module.id for module in modules]
module_coefs_df = pd.DataFrame(columns=module_ids, index=ue_ids, dtype=float)
query = (
db.session.query(ModuleUECoef)
.filter(UniteEns.formation_id == formation_id)
.filter(ModuleUECoef.ue_id == UniteEns.id)
)
if semestre_idx is not None:
query = query.filter(UniteEns.semestre_idx == semestre_idx)
for mod_coef in query:
module_coefs_df[mod_coef.module_id][mod_coef.ue_id] = mod_coef.coef
module_coefs_df.fillna(value=0, inplace=True)
return module_coefs_df, ues, modules
def df_load_modimpl_coefs(
formsemestre: models.FormSemestre, ues=None, modimpls=None
) -> pd.DataFrame:
"""Charge les coefs des modules du formsemestre indiqué.
Comme df_load_module_coefs mais prend seulement les UE
et modules du formsemestre.
Si ues et modimpls sont None, prend tous ceux du formsemestre.
Résultat: (module_coefs_df, ues, modules)
DataFrame rows = UEs, columns = modimpl, value = coef.
"""
if ues is None:
ues = formsemestre.query_ues().all()
ue_ids = [x.id for x in ues]
if modimpls is None:
modimpls = formsemestre.modimpls.all()
modimpl_ids = [x.id for x in modimpls]
mod2impl = {m.module.id: m.id for m in modimpls}
modimpl_coefs_df = pd.DataFrame(columns=modimpl_ids, index=ue_ids, dtype=float)
mod_coefs = (
db.session.query(ModuleUECoef)
.filter(ModuleUECoef.module_id == ModuleImpl.module_id)
.filter(ModuleImpl.formsemestre_id == formsemestre.id)
)
for mod_coef in mod_coefs:
modimpl_coefs_df[mod2impl[mod_coef.module_id]][mod_coef.ue_id] = mod_coef.coef
modimpl_coefs_df.fillna(value=0, inplace=True)
return modimpl_coefs_df, ues, modimpls
def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
"""Réuni les notes moyennes des modules du semestre en un "cube"
modimpls_notes : liste des moyennes de module
(DataFrames rendus par compute_module_moy, (etud x UE))
Resultat: ndarray (etud x module x UE)
"""
assert len(modimpls_notes)
modimpls_notes_arr = [df.values for df in modimpls_notes]
modimpls_notes = np.stack(modimpls_notes_arr)
# passe de (mod x etud x ue) à (etud x mod x UE)
return modimpls_notes.swapaxes(0, 1)
def notes_sem_load_cube(formsemestre):
"""Calcule le cube des notes du semestre
(charge toutes les notes, calcule les moyenne des modules
et assemble le cube)
Resultat:
sem_cube : ndarray (etuds x modimpls x UEs)
modimpls_evals_poids dict { modimpl.id : evals_poids }
modimpls_evals_notes dict { modimpl.id : evals_notes }
modimpls_evaluations dict { modimpl.id : liste des évaluations }
modimpls_evaluations_complete: {modimpl_id : liste de booleens (complete/non)}
"""
modimpls_evals_poids = {}
modimpls_evals_notes = {}
modimpls_evaluations = {}
modimpls_evaluations_complete = {}
modimpls_notes = []
for modimpl in formsemestre.modimpls:
evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes(
modimpl.id
)
evals_poids, ues = moy_mod.df_load_evaluations_poids(modimpl.id)
etuds_moy_module = moy_mod.compute_module_moy(
evals_notes, evals_poids, evaluations, evaluations_completes
)
modimpls_evals_poids[modimpl.id] = evals_poids
modimpls_evals_notes[modimpl.id] = evals_notes
modimpls_evaluations[modimpl.id] = evaluations
modimpls_evaluations_complete[modimpl.id] = evaluations_completes
modimpls_notes.append(etuds_moy_module)
if len(modimpls_notes):
cube = notes_sem_assemble_cube(modimpls_notes)
else:
nb_etuds = formsemestre.etuds.count()
cube = np.zeros((nb_etuds, 0, 0), dtype=float)
return (
cube,
modimpls_evals_poids,
modimpls_evals_notes,
modimpls_evaluations,
modimpls_evaluations_complete,
)
def compute_ue_moys(
sem_cube: np.array,
etuds: list,
modimpls: list,
ues: list,
modimpl_inscr_df: pd.DataFrame,
modimpl_coefs_df: pd.DataFrame,
) -> pd.DataFrame:
"""Calcul de la moyenne d'UE
La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR
NI non inscrit à (au moins un) module de cette UE
NA pas de notes disponibles
ERR erreur dans une formule utilisateur. [XXX pas encore gérées ici]
sem_cube: notes moyennes aux modules
ndarray (etuds x modimpls x UEs)
(floats avec des NaN)
etuds : lites des étudiants (dim. 0 du cube)
modimpls : liste des modules à considérer (dim. 1 du cube)
ues : liste des UE (dim. 2 du cube)
module_inscr_df: matrice d'inscription du semestre (etud x modimpl)
module_coefs_df: matrice coefficients (UE x modimpl)
Resultat: DataFrame columns UE, rows etudid
"""
nb_etuds, nb_modules, nb_ues = sem_cube.shape
assert len(modimpls) == nb_modules
if nb_modules == 0 or nb_etuds == 0:
return pd.DataFrame(
index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
)
assert len(etuds) == nb_etuds
assert len(ues) == nb_ues
assert modimpl_inscr_df.shape[0] == nb_etuds
assert modimpl_inscr_df.shape[1] == nb_modules
assert modimpl_coefs_df.shape[0] == nb_ues
assert modimpl_coefs_df.shape[1] == nb_modules
modimpl_inscr = modimpl_inscr_df.values
modimpl_coefs = modimpl_coefs_df.values
# Duplique les inscriptions sur les UEs:
modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues, axis=2)
# Enlève les NaN du numérateur:
# si on veut prendre en compte les modules avec notes neutralisées ?
sem_cube_no_nan = np.nan_to_num(sem_cube, nan=0.0)
# Ne prend pas en compte les notes des étudiants non inscrits au module:
# Annule les notes:
sem_cube_inscrits = np.where(modimpl_inscr_stacked, sem_cube_no_nan, 0.0)
# Annule les coefs des modules où l'étudiant n'est pas inscrit:
modimpl_coefs_etuds = np.where(
modimpl_inscr_stacked, np.stack([modimpl_coefs.T] * nb_etuds), 0.0
)
# Annule les coefs des modules NaN
modimpl_coefs_etuds_no_nan = np.where(np.isnan(sem_cube), 0.0, modimpl_coefs_etuds)
#
# Version vectorisée
#
etud_moy_ue = np.sum(
modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
return pd.DataFrame(
etud_moy_ue, index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
)

View File

@ -10,16 +10,15 @@ import logging
import werkzeug
from werkzeug.exceptions import BadRequest
import flask
from flask import g
from flask import abort, current_app
from flask import request
from flask import g, current_app, request
from flask import abort, url_for, redirect
from flask_login import current_user
from flask_login import login_required
from flask import current_app
import flask_login
import app
from app.auth.models import User
import app.scodoc.sco_utils as scu
class ZUser(object):
@ -39,69 +38,6 @@ class ZUser(object):
raise NotImplementedError()
class ZRequest(object):
"Emulating Zope 2 REQUEST"
def __init__(self):
if current_app.config["DEBUG"]:
self.URL = request.base_url
self.BASE0 = request.url_root
else:
self.URL = request.base_url.replace("http://", "https://")
self.BASE0 = request.url_root.replace("http://", "https://")
self.URL0 = self.URL
# query_string is bytes:
self.QUERY_STRING = request.query_string.decode("utf-8")
self.REQUEST_METHOD = request.method
self.AUTHENTICATED_USER = current_user
self.REMOTE_ADDR = request.remote_addr
if request.method == "POST":
# request.form is a werkzeug.datastructures.ImmutableMultiDict
# must copy to get a mutable version (needed by TrivialFormulator)
self.form = request.form.copy()
if request.files:
# Add files in form:
self.form.update(request.files)
for k in request.form:
if k.endswith(":list"):
self.form[k[:-5]] = request.form.getlist(k)
elif request.method == "GET":
self.form = {}
for k in request.args:
# current_app.logger.debug("%s\t%s" % (k, request.args.getlist(k)))
if k.endswith(":list"):
self.form[k[:-5]] = request.args.getlist(k)
else:
self.form[k] = request.args[k]
# current_app.logger.info("ZRequest.form=%s" % str(self.form))
self.RESPONSE = ZResponse()
def __str__(self):
return """REQUEST
URL={r.URL}
QUERY_STRING={r.QUERY_STRING}
REQUEST_METHOD={r.REQUEST_METHOD}
AUTHENTICATED_USER={r.AUTHENTICATED_USER}
form={r.form}
""".format(
r=self
)
class ZResponse(object):
"Emulating Zope 2 RESPONSE"
def __init__(self):
self.headers = {}
def redirect(self, url):
# current_app.logger.debug("ZResponse redirect to:" + str(url))
return flask.redirect(url) # http 302
def setHeader(self, header, value):
self.headers[header.lower()] = value
def scodoc(func):
"""Décorateur pour toutes les fonctions ScoDoc
Affecte le département à g
@ -114,6 +50,25 @@ def scodoc(func):
@wraps(func)
def scodoc_function(*args, **kwargs):
# print("@scodoc")
# interdit les POST si pas loggué
if (
request.method == "POST"
and not current_user.is_authenticated
and not request.form.get(
"__ac_password"
) # exception pour compat API ScoDoc7
):
current_app.logger.info(
"POST by non authenticated user (request.form=%s)",
str(request.form)[:2048],
)
return redirect(
url_for(
"auth.login",
message="La page a expiré. Identifiez-vous et recommencez l'opération",
)
)
if "scodoc_dept" in kwargs:
dept_acronym = kwargs["scodoc_dept"]
# current_app.logger.info("setting dept to " + dept_acronym)
@ -123,6 +78,7 @@ def scodoc(func):
# current_app.logger.info("setting dept to None")
g.scodoc_dept = None
g.scodoc_dept_id = -1 # invalide
return func(*args, **kwargs)
return scodoc_function
@ -132,7 +88,6 @@ def permission_required(permission):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# current_app.logger.info("PERMISSION; kwargs=%s" % str(kwargs))
scodoc_dept = getattr(g, "scodoc_dept", None)
if not current_user.has_permission(permission, scodoc_dept):
abort(403)
@ -144,7 +99,7 @@ def permission_required(permission):
def permission_required_compat_scodoc7(permission):
"""Décorateur pour les fonctions utilisée 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
les informations d'auth en paramètres:
__ac_name, __ac_password
@ -153,8 +108,8 @@ def permission_required_compat_scodoc7(permission):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# current_app.logger.warning("PERMISSION; kwargs=%s" % str(kwargs))
# cherche les paramètre d'auth:
# print("@permission_required_compat_scodoc7")
auth_ok = False
if request.method == "GET":
user_name = request.args.get("__ac_name")
@ -169,7 +124,6 @@ def permission_required_compat_scodoc7(permission):
if u and u.check_password(user_password):
auth_ok = True
flask_login.login_user(u)
# reprend le chemin classique:
scodoc_dept = getattr(g, "scodoc_dept", None)
@ -193,7 +147,6 @@ def admin_required(f):
def scodoc7func(func):
"""Décorateur pour intégrer les fonctions Zope 2 de ScoDoc 7.
Ajoute l'argument REQUEST s'il est dans la signature de la fonction.
Les paramètres de la query string deviennent des (keywords) paramètres de la fonction.
"""
@ -206,19 +159,21 @@ def scodoc7func(func):
1. via a Flask route ("top level call")
2. or be called directly from Python.
If called via a route, this decorator setups a REQUEST object (emulating Zope2 REQUEST)
"""
# print("@scodoc7func")
# Détermine si on est appelé via une route ("toplevel")
# ou par un appel de fonction python normal.
top_level = not hasattr(g, "zrequest")
top_level = not hasattr(g, "scodoc7_decorated")
if not top_level:
# ne "redécore" pas
return func(*args, **kwargs)
g.scodoc7_decorated = True
# --- Emulate Zope's REQUEST
REQUEST = ZRequest()
g.zrequest = REQUEST
req_args = REQUEST.form # args from query string (get) or form (post)
# --- Add positional arguments
# REQUEST = ZRequest()
# g.zrequest = REQUEST
# args from query string (get) or form (post)
req_args = scu.get_request_args()
## --- Add positional arguments
pos_arg_values = []
argspec = inspect.getfullargspec(func)
# current_app.logger.info("argspec=%s" % str(argspec))
@ -227,10 +182,12 @@ def scodoc7func(func):
arg_names = argspec.args[:-nb_default_args]
else:
arg_names = argspec.args
for arg_name in arg_names:
if arg_name == "REQUEST": # special case
pos_arg_values.append(REQUEST)
for arg_name in arg_names: # pour chaque arg de la fonction vue
if arg_name == "REQUEST": # ne devrait plus arriver !
# debug check, TODO remove after tests
raise ValueError("invalid REQUEST parameter !")
else:
# peut produire une KeyError s'il manque un argument attendu:
v = req_args[arg_name]
# try to convert all arguments to INTEGERS
# necessary for db ids and boolean values
@ -244,9 +201,9 @@ def scodoc7func(func):
# Add keyword arguments
if nb_default_args:
for arg_name in argspec.args[-nb_default_args:]:
if arg_name == "REQUEST": # special case
kwargs[arg_name] = REQUEST
elif arg_name in req_args:
# if arg_name == "REQUEST": # special case
# kwargs[arg_name] = REQUEST
if arg_name in req_args:
# set argument kw optionnel
v = req_args[arg_name]
# try to convert all arguments to INTEGERS
@ -270,13 +227,13 @@ def scodoc7func(func):
# Build response, adding collected http headers:
headers = []
kw = {"response": value, "status": 200}
if g.zrequest:
headers = g.zrequest.RESPONSE.headers
if not headers:
# no customized header, speedup:
return value
if "content-type" in headers:
kw["mimetype"] = headers["content-type"]
# if g.zrequest:
# headers = g.zrequest.RESPONSE.headers
# if not headers:
# # no customized header, speedup:
# return value
# if "content-type" in headers:
# kw["mimetype"] = headers["content-type"]
r = flask.Response(**kw)
for h in headers:
r.headers[h] = headers[h]

View File

@ -0,0 +1,403 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# ScoDoc
#
# Copyright (c) 1999 - 2022 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
#
##############################################################################
"""
Formulaires configuration logos
Contrib @jmp, dec 21
"""
import re
from flask import flash, url_for, redirect, render_template
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from wtforms import SelectField, SubmitField, FormField, validators, FieldList
from wtforms.fields.simple import StringField, HiddenField
from app import AccessDenied
from app.models import Departement
from app.models import ScoDocSiteConfig
from app.scodoc import sco_logos, html_sco_header
from app.scodoc import sco_utils as scu
from app.scodoc.sco_config_actions import (
LogoDelete,
LogoUpdate,
LogoInsert,
BonusSportUpdate,
)
from flask_login import current_user
from app.scodoc.sco_logos import find_logo
JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + []
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
# class ItemForm(FlaskForm):
# """Unused Generic class to document common behavior for classes
# * ScoConfigurationForm
# * DeptForm
# * LogoForm
# Some or all of these implements:
# * Composite design pattern (ScoConfigurationForm and DeptForm)
# - a FieldList(FormField(ItemForm))
# - FieldListItem are created by browsing the model
# - index dictionnary to provide direct access to a SubItemForm
# - the direct access method (get_form)
# * have some information added to be displayed
# - information are collected from a model object
# Common methods:
# * build(model) (not for LogoForm who has no child)
# for each child:
# * create en entry in the FieldList for each subitem found
# * update self.index
# * fill_in additional information into the form
# * recursively calls build for each chid
# some spécific information may be added after standard processing
# (typically header/footer description)
# * preview(data)
# check the data from a post and build a list of operations that has to be done.
# for a two phase process:
# * phase 1 (list all opérations)
# * phase 2 (may be confirmation and execure)
# - if no op found: return to the form with a message 'Aucune modification trouvée'
# - only one operation found: execute and go to main page
# - more than 1 operation found. asked form confirmation (and execution if confirmed)
#
# Someday we'll have time to refactor as abstract classes but Abstract FieldList makes this
# a bit complicated
# """
# Terminology:
# dept_id : identifies a dept in modele (= list_logos()). None designates globals logos
# dept_key : identifies a dept in this form only (..index[dept_key], and fields 'dept_key').
# 'GLOBAL' designates globals logos (we need a string value to set up HiddenField
GLOBAL = "_"
def dept_id_to_key(dept_id):
if dept_id is None:
return GLOBAL
return dept_id
def dept_key_to_id(dept_key):
if dept_key == GLOBAL:
return None
return dept_key
class AddLogoForm(FlaskForm):
"""Formulaire permettant l'ajout d'un logo (dans un département)"""
dept_key = HiddenField()
name = StringField(
label="Nom",
validators=[
validators.regexp(
r"^[a-zA-Z0-9-]*$",
re.IGNORECASE,
"Ne doit comporter que lettres, chiffres ou -",
),
validators.Length(
max=20, message="Un nom ne doit pas dépasser 20 caractères"
),
validators.DataRequired("Nom de logo requis (alphanumériques ou '-')"),
],
)
upload = FileField(
label="Sélectionner l'image",
validators=[
FileAllowed(
scu.LOGOS_IMAGES_ALLOWED_TYPES,
f"n'accepte que les fichiers image {', '.join(scu.LOGOS_IMAGES_ALLOWED_TYPES)}",
),
validators.DataRequired("Fichier image manquant"),
],
)
do_insert = SubmitField("ajouter une image")
def __init__(self, *args, **kwargs):
kwargs["meta"] = {"csrf": False}
super().__init__(*args, **kwargs)
def validate_name(self, name):
dept_id = dept_key_to_id(self.dept_key.data)
if dept_id == GLOBAL:
dept_id = None
if find_logo(logoname=name.data, dept_id=dept_id) is not None:
raise validators.ValidationError("Un logo de même nom existe déjà")
def select_action(self):
if self.data["do_insert"]:
if self.validate():
return LogoInsert.build_action(self.data)
return None
class LogoForm(FlaskForm):
"""Embed both presentation of a logo (cf. template file configuration.html)
and all its data and UI action (change, delete)"""
dept_key = HiddenField()
logo_id = HiddenField()
upload = FileField(
label="Remplacer l'image",
validators=[
FileAllowed(
scu.LOGOS_IMAGES_ALLOWED_TYPES,
f"n'accepte que les fichiers image {', '.join(scu.LOGOS_IMAGES_ALLOWED_TYPES)}",
)
],
)
do_delete = SubmitField("Supprimer l'image")
def __init__(self, *args, **kwargs):
kwargs["meta"] = {"csrf": False}
super().__init__(*args, **kwargs)
self.logo = find_logo(
logoname=self.logo_id.data, dept_id=dept_key_to_id(self.dept_key.data)
).select()
self.description = None
self.titre = None
self.can_delete = True
if self.dept_key.data == GLOBAL:
if self.logo_id.data == "header":
self.can_delete = False
self.description = ""
self.titre = "Logo en-tête"
if self.logo_id.data == "footer":
self.can_delete = False
self.titre = "Logo pied de page"
self.description = ""
else:
if self.logo_id.data == "header":
self.description = "Se substitue au header défini au niveau global"
self.titre = "Logo en-tête"
if self.logo_id.data == "footer":
self.description = "Se substitue au footer défini au niveau global"
self.titre = "Logo pied de page"
def select_action(self):
if self.do_delete.data and self.can_delete:
return LogoDelete.build_action(self.data)
if self.upload.data and self.validate():
return LogoUpdate.build_action(self.data)
return None
class DeptForm(FlaskForm):
dept_key = HiddenField()
dept_name = HiddenField()
add_logo = FormField(AddLogoForm)
logos = FieldList(FormField(LogoForm))
def __init__(self, *args, **kwargs):
kwargs["meta"] = {"csrf": False}
super().__init__(*args, **kwargs)
def is_local(self):
if self.dept_key.data == GLOBAL:
return None
return True
def select_action(self):
action = self.add_logo.form.select_action()
if action:
return action
for logo_entry in self.logos.entries:
logo_form = logo_entry.form
action = logo_form.select_action()
if action:
return action
return None
def get_form(self, logoname=None):
"""Retourne le formulaire associé à un logo. None si pas trouvé"""
if logoname is None: # recherche de département
return self
return self.index.get(logoname, None)
def _make_dept_id_name():
"""Cette section assure que tous les départements sont traités (y compris ceux qu'ont pas de logo au départ)
et détermine l'ordre d'affichage des DeptForm (GLOBAL d'abord, puis par ordre alpha de nom de département)
-> [ (None, None), (dept_id, dept_name)... ]"""
depts = [(None, GLOBAL)]
for dept in (
Departement.query.filter_by(visible=True).order_by(Departement.acronym).all()
):
depts.append((dept.id, dept.acronym))
return depts
def _ordered_logos(modele):
"""sort logoname alphabetically but header and footer moved at start. (since there is no space in logoname)"""
def sort(name):
if name == "header":
return " 0"
if name == "footer":
return " 1"
return name
order = sorted(modele.keys(), key=sort)
return order
def _make_dept_data(dept_id, dept_name, modele):
dept_key = dept_id_to_key(dept_id)
data = {
"dept_key": dept_key,
"dept_name": dept_name,
"add_logo": {"dept_key": dept_key},
}
logos = []
if modele is not None:
for name in _ordered_logos(modele):
logos.append({"dept_key": dept_key, "logo_id": name})
data["logos"] = logos
return data
def _make_depts_data(modele):
data = []
for dept_id, dept_name in _make_dept_id_name():
data.append(
_make_dept_data(
dept_id=dept_id, dept_name=dept_name, modele=modele.get(dept_id, None)
)
)
return data
def _make_data(bonus_sport, modele):
data = {
"bonus_sport_func_name": bonus_sport,
"depts": _make_depts_data(modele=modele),
}
return data
class ScoDocConfigurationForm(FlaskForm):
"Panneau de configuration général"
bonus_sport_func_name = SelectField(
label="Fonction de calcul des bonus sport&culture",
choices=[
(x, x if x else "Aucune")
for x in ScoDocSiteConfig.get_bonus_sport_func_names()
],
)
depts = FieldList(FormField(DeptForm))
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# def _set_global_logos_infos(self):
# "specific processing for globals items"
# global_header = self.get_form(logoname="header")
# global_header.description = (
# "image placée en haut de certains documents documents PDF."
# )
# global_header.titre = "Logo en-tête"
# global_header.can_delete = False
# global_footer = self.get_form(logoname="footer")
# global_footer.description = (
# "image placée en pied de page de certains documents documents PDF."
# )
# global_footer.titre = "Logo pied de page"
# global_footer.can_delete = False
# def _build_dept(self, dept_id, dept_name, modele):
# dept_key = dept_id or GLOBAL
# data = {"dept_key": dept_key}
# entry = self.depts.append_entry(data)
# entry.form.build(dept_name, modele.get(dept_id, {}))
# self.index[str(dept_key)] = entry.form
# def build(self, modele):
# "Build the Form hierachy (DeptForm, LogoForm) and add extra data (from modele)"
# # if entries already initialized (POST). keep subforms
# self.index = {}
# # create entries in FieldList (one entry per dept
# for dept_id, dept_name in self.dept_id_name:
# self._build_dept(dept_id=dept_id, dept_name=dept_name, modele=modele)
# self._set_global_logos_infos()
def get_form(self, dept_key=GLOBAL, logoname=None):
"""Retourne un formulaire:
* pour un département (get_form(dept_id)) ou à un logo (get_form(dept_id, logname))
* propre à un département (get_form(dept_id, logoname) ou global (get_form(logoname))
retourne None si le formulaire cherché ne peut être trouvé
"""
dept_form = self.index.get(dept_key, None)
if dept_form is None: # département non trouvé
return None
return dept_form.get_form(logoname)
def select_action(self):
if (
self.data["bonus_sport_func_name"]
!= ScoDocSiteConfig.get_bonus_sport_func_name()
):
return BonusSportUpdate(self.data)
for dept_entry in self.depts:
dept_form = dept_entry.form
action = dept_form.select_action()
if action:
return action
return None
def configuration():
"""Panneau de configuration général"""
auth_name = str(current_user)
if not current_user.is_administrator():
raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name)
form = ScoDocConfigurationForm(
data=_make_data(
bonus_sport=ScoDocSiteConfig.get_bonus_sport_func_name(),
modele=sco_logos.list_logos(),
)
)
if form.is_submitted():
action = form.select_action()
if action:
action.execute()
flash(action.message)
return redirect(
url_for(
"scodoc.configuration",
)
)
return render_template(
"configuration.html",
scodoc_dept=None,
title="Configuration ScoDoc",
form=form,
)

View File

@ -0,0 +1,63 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# ScoDoc
#
# Copyright (c) 1999 - 2022 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
#
##############################################################################
"""
Formulaires création département
"""
from flask import flash, url_for, redirect, render_template
from flask_wtf import FlaskForm
from wtforms import SubmitField, validators
from wtforms.fields.simple import StringField, BooleanField
from app.models import SHORT_STR_LEN
class CreateDeptForm(FlaskForm):
"""Formulaire permettant l'ajout d'un département"""
acronym = StringField(
label="Acronyme",
validators=[
validators.regexp(
r"^[a-zA-Z0-9_\-]*$",
message="Ne doit comporter que lettres, chiffres ou -",
),
validators.Length(
max=SHORT_STR_LEN,
message=f"L'acronyme ne doit pas dépasser {SHORT_STR_LEN} caractères",
),
validators.DataRequired("acronyme du département requis"),
],
)
# description = StringField(label="Description")
visible = BooleanField(
"Visible sur page d'accueil",
default=True,
)
submit = SubmitField("Valider")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})

View File

@ -30,37 +30,44 @@ from app.models.etudiants import (
EtudAnnotation,
)
from app.models.events import Scolog, ScolarNews
from app.models.formations import (
NotesFormation,
NotesUE,
NotesMatiere,
NotesModule,
NotesTag,
notes_modules_tags,
)
from app.models.formations import Formation, Matiere
from app.models.modules import Module, ModuleUECoef, NotesTag, notes_modules_tags
from app.models.ues import UniteEns
from app.models.formsemestre import (
FormSemestre,
NotesFormsemestreEtape,
NotesFormModalite,
NotesFormsemestreUECoef,
NotesFormsemestreUEComputationExpr,
NotesFormsemestreCustomMenu,
NotesFormsemestreInscription,
FormSemestreEtape,
FormationModalite,
FormSemestreUECoef,
FormSemestreUEComputationExpr,
FormSemestreCustomMenu,
FormSemestreInscription,
notes_formsemestre_responsables,
NotesModuleImpl,
notes_modules_enseignants,
NotesModuleImplInscription,
NotesEvaluation,
NotesSemSet,
notes_semset_formsemestre,
)
from app.models.moduleimpls import (
ModuleImpl,
notes_modules_enseignants,
ModuleImplInscription,
)
from app.models.evaluations import (
Evaluation,
EvaluationUEPoids,
)
from app.models.groups import Partition, GroupDescr, group_membership
from app.models.notes import (
ScolarEvent,
ScolarFormsemestreValidation,
ScolarFormSemestreValidation,
ScolarAutorisationInscription,
NotesAppreciations,
BulAppreciations,
NotesNotes,
NotesNotesLog,
)
from app.models.preferences import ScoPreference, ScoDocSiteConfig
from app.models.but_refcomp import (
ApcReferentielCompetences,
ApcCompetence,
ApcSituationPro,
ApcAppCritique,
)

View File

@ -4,9 +4,6 @@
"""
from app import db
from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN
class Absence(db.Model):
@ -49,7 +46,7 @@ class AbsenceNotification(db.Model):
nbabsjust = db.Column(db.Integer)
formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
db.ForeignKey("notes_formsemestre.id", ondelete="CASCADE"),
)
@ -73,3 +70,17 @@ class BilletAbsence(db.Model):
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
# true si l'absence _pourrait_ etre justifiée
justified = db.Column(db.Boolean(), default=False, server_default="false")
def to_dict(self):
data = {
"id": self.id,
"billet_id": self.id,
"etudid": self.etudid,
"abs_begin": self.abs_begin,
"abs_end": self.abs_begin,
"description": self.description,
"etat": self.etat,
"entry_date": self.entry_date,
"justified": self.justified,
}
return data

19
app/models/but_pn.py Normal file
View File

@ -0,0 +1,19 @@
"""ScoDoc 9 models : Formation BUT 2021
"""
from enum import unique
from typing import Any
from app import db
from app.scodoc.sco_utils import ModuleType
class APCFormation(db.Model):
"""Formation par compétence"""
id = db.Column(db.Integer, primary_key=True)
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
specialite = db.Column(db.Text(), nullable=False) # "RT"
specialite_long = db.Column(
db.Text(), nullable=False
) # "Réseaux et télécommunications"

287
app/models/but_refcomp.py Normal file
View File

@ -0,0 +1,287 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""ScoDoc 9 models : Référentiel Compétence BUT 2021
"""
from datetime import datetime
from enum import unique
from typing import Any
from app import db
from app.scodoc.sco_utils import ModuleType
class XMLModel:
_xml_attribs = {} # to be overloaded
id = "_"
@classmethod
def attr_from_xml(cls, args: dict) -> dict:
"""dict with attributes imported from Orébut XML
and renamed for our models.
The mapping is specified by the _xml_attribs
attribute in each model class.
"""
return {cls._xml_attribs.get(k, k): v for (k, v) in args.items()}
def __repr__(self):
return f'<{self.__class__.__name__} {self.id} "{self.titre if hasattr(self, "titre") else ""}">'
class ApcReferentielCompetences(db.Model, XMLModel):
id = db.Column(db.Integer, primary_key=True)
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
specialite = db.Column(db.Text())
specialite_long = db.Column(db.Text())
type_titre = db.Column(db.Text())
_xml_attribs = { # Orébut xml attrib : attribute
"type": "type_titre",
}
# ScoDoc specific fields:
scodoc_date_loaded = db.Column(db.DateTime, default=datetime.utcnow)
scodoc_orig_filename = db.Column(db.Text())
# Relations:
competences = db.relationship(
"ApcCompetence",
backref="referentiel",
lazy="dynamic",
cascade="all, delete-orphan",
)
parcours = db.relationship(
"ApcParcours",
backref="referentiel",
lazy="dynamic",
cascade="all, delete-orphan",
)
formations = db.relationship("Formation", backref="referentiel_competence")
def to_dict(self):
"""Représentation complète du ref. de comp.
comme un dict.
"""
return {
"dept_id": self.dept_id,
"specialite": self.specialite,
"specialite_long": self.specialite_long,
"type_titre": self.type_titre,
"scodoc_date_loaded": self.scodoc_date_loaded.isoformat() + "Z"
if self.scodoc_date_loaded
else "",
"scodoc_orig_filename": self.scodoc_orig_filename,
"competences": {x.titre: x.to_dict() for x in self.competences},
"parcours": {x.code: x.to_dict() for x in self.parcours},
}
class ApcCompetence(db.Model, XMLModel):
__table_args__ = (
# les compétences dans Orébut sont identifiées par leur "titre"
# unique au sein d'un référentiel:
db.UniqueConstraint(
"referentiel_id", "titre", name="apc_competence_referentiel_id_titre_key"
),
)
id = db.Column(db.Integer, primary_key=True)
referentiel_id = db.Column(
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
)
titre = db.Column(db.Text(), nullable=False, index=True)
titre_long = db.Column(db.Text())
couleur = db.Column(db.Text())
numero = db.Column(db.Integer) # ordre de présentation
_xml_attribs = { # xml_attrib : attribute
"name": "titre",
"libelle_long": "titre_long",
}
situations = db.relationship(
"ApcSituationPro",
backref="competence",
lazy="dynamic",
cascade="all, delete-orphan",
)
composantes_essentielles = db.relationship(
"ApcComposanteEssentielle",
backref="competence",
lazy="dynamic",
cascade="all, delete-orphan",
)
niveaux = db.relationship(
"ApcNiveau",
backref="competence",
lazy="dynamic",
cascade="all, delete-orphan",
)
def to_dict(self):
return {
"titre": self.titre,
"titre_long": self.titre_long,
"couleur": self.couleur,
"numero": self.numero,
"situations": [x.to_dict() for x in self.situations],
"composantes_essentielles": [
x.to_dict() for x in self.composantes_essentielles
],
"niveaux": {x.annee: x.to_dict() for x in self.niveaux},
}
class ApcSituationPro(db.Model, XMLModel):
id = db.Column(db.Integer, primary_key=True)
competence_id = db.Column(
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
)
libelle = db.Column(db.Text(), nullable=False)
# aucun attribut (le text devient le libellé)
def to_dict(self):
return {"libelle": self.libelle}
class ApcComposanteEssentielle(db.Model, XMLModel):
id = db.Column(db.Integer, primary_key=True)
competence_id = db.Column(
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
)
libelle = db.Column(db.Text(), nullable=False)
def to_dict(self):
return {"libelle": self.libelle}
class ApcNiveau(db.Model, XMLModel):
id = db.Column(db.Integer, primary_key=True)
competence_id = db.Column(
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
)
libelle = db.Column(db.Text(), nullable=False)
annee = db.Column(db.Text(), nullable=False) # "BUT2"
# L'ordre est l'année d'apparition de ce niveau
ordre = db.Column(db.Integer, nullable=False) # 1, 2, 3
app_critiques = db.relationship(
"ApcAppCritique",
backref="niveau",
lazy="dynamic",
cascade="all, delete-orphan",
)
def to_dict(self):
return {
"libelle": self.libelle,
"annee": self.annee,
"ordre": self.ordre,
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques},
}
class ApcAppCritique(db.Model, XMLModel):
"Apprentissage Critique BUT"
id = db.Column(db.Integer, primary_key=True)
niveau_id = db.Column(db.Integer, db.ForeignKey("apc_niveau.id"), nullable=False)
code = db.Column(db.Text(), nullable=False, index=True)
libelle = db.Column(db.Text())
modules = db.relationship(
"Module",
secondary="apc_modules_acs",
lazy="dynamic",
backref=db.backref("app_critiques", lazy="dynamic"),
)
def to_dict(self):
return {"libelle": self.libelle}
def get_label(self):
return self.code + " - " + self.titre
def __repr__(self):
return "<AppCritique {}>".format(self.code)
def get_saes(self):
"""Liste des SAE associées"""
return [m for m in self.modules if m.module_type == ModuleType.SAE]
ApcAppCritiqueModules = db.Table(
"apc_modules_acs",
db.Column("module_id", db.ForeignKey("notes_modules.id")),
db.Column("app_crit_id", db.ForeignKey("apc_app_critique.id")),
)
class ApcParcours(db.Model, XMLModel):
id = db.Column(db.Integer, primary_key=True)
referentiel_id = db.Column(
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
)
numero = db.Column(db.Integer) # ordre de présentation
code = db.Column(db.Text(), nullable=False)
libelle = db.Column(db.Text(), nullable=False)
annees = db.relationship(
"ApcAnneeParcours",
backref="parcours",
lazy="dynamic",
cascade="all, delete-orphan",
)
def to_dict(self):
return {
"code": self.code,
"numero": self.numero,
"libelle": self.libelle,
"annees": {x.ordre: x.to_dict() for x in self.annees},
}
class ApcAnneeParcours(db.Model, XMLModel):
id = db.Column(db.Integer, primary_key=True)
parcours_id = db.Column(
db.Integer, db.ForeignKey("apc_parcours.id"), nullable=False
)
ordre = db.Column(db.Integer)
def to_dict(self):
return {
"ordre": self.ordre,
"competences": {
x.competence.titre: {"niveau": x.niveau}
for x in self.niveaux_competences
},
}
class ApcParcoursNiveauCompetence(db.Model):
"""Association entre année de parcours et compétence.
Le "niveau" de la compétence est donné ici
(convention Orébut)
"""
competence_id = db.Column(
db.Integer,
db.ForeignKey("apc_competence.id", ondelete="CASCADE"),
primary_key=True,
)
annee_parcours_id = db.Column(
db.Integer,
db.ForeignKey("apc_annee_parcours.id", ondelete="CASCADE"),
primary_key=True,
)
niveau = db.Column(db.Integer, nullable=False) # 1, 2, 3
competence = db.relationship(
ApcCompetence,
backref=db.backref(
"annee_parcours",
passive_deletes=True,
cascade="save-update, merge, delete, delete-orphan",
),
)
annee_parcours = db.relationship(
ApcAnneeParcours,
backref=db.backref(
"niveaux_competences",
passive_deletes=True,
cascade="save-update, merge, delete, delete-orphan",
),
)

View File

@ -12,8 +12,10 @@ class Departement(db.Model):
"""Un département ScoDoc"""
id = db.Column(db.Integer, primary_key=True)
acronym = db.Column(db.String(SHORT_STR_LEN), nullable=False, index=True)
description = db.Column(db.Text())
acronym = db.Column(
db.String(SHORT_STR_LEN), nullable=False, index=True
) # ne change jamais, voir la pref. DeptName
description = db.Column(db.Text()) # pas utilisé par ScoDoc : voir DeptFullName
date_creation = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
visible = db.Column(
db.Boolean(), nullable=False, default=True, server_default="true"
@ -21,9 +23,7 @@ class Departement(db.Model):
entreprises = db.relationship("Entreprise", lazy="dynamic", backref="departement")
etudiants = db.relationship("Identite", lazy="dynamic", backref="departement")
formations = db.relationship(
"NotesFormation", lazy="dynamic", backref="departement"
)
formations = db.relationship("Formation", lazy="dynamic", backref="departement")
formsemestres = db.relationship(
"FormSemestre", lazy="dynamic", backref="departement"
)
@ -33,7 +33,7 @@ class Departement(db.Model):
semsets = db.relationship("NotesSemSet", lazy="dynamic", backref="departement")
def __repr__(self):
return f"<Departement {self.acronym}>"
return f"<{self.__class__.__name__}(id={self.id}, acronym='{self.acronym}')>"
def to_dict(self):
data = {
@ -44,3 +44,20 @@ class Departement(db.Model):
"date_creation": self.date_creation,
}
return data
@classmethod
def from_acronym(cls, acronym):
dept = cls.query.filter_by(acronym=acronym).first_or_404()
return dept
def create_dept(acronym: str, visible=True) -> Departement:
"Create new departement"
from app.models import ScoPreference
departement = Departement(acronym=acronym, visible=visible)
p1 = ScoPreference(name="DeptName", value=acronym, departement=departement)
db.session.add(p1)
db.session.add(departement)
db.session.commit()
return departement

View File

@ -4,9 +4,6 @@
"""
from app import db
from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN
class Entreprise(db.Model):

View File

@ -4,10 +4,10 @@
et données rattachées (adresses, annotations, ...)
"""
from flask import g, url_for
from app import db
from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN
from app import models
class Identite(db.Model):
@ -38,10 +38,83 @@ class Identite(db.Model):
boursier = db.Column(db.Boolean()) # True si boursier ('O' en ScoDoc7)
photo_filename = db.Column(db.Text())
# Codes INE et NIP pas unique car le meme etud peut etre ds plusieurs dept
code_nip = db.Column(db.Text())
code_ine = db.Column(db.Text())
code_nip = db.Column(db.Text(), index=True)
code_ine = db.Column(db.Text(), index=True)
# Ancien id ScoDoc7 pour les migrations de bases anciennes
# ne pas utiliser après migrate_scodoc7_dept_archives
scodoc7_id = db.Column(db.Text(), nullable=True)
#
adresses = db.relationship("Adresse", lazy="dynamic", backref="etud")
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
def __repr__(self):
return f"<Etud {self.id} {self.nom} {self.prenom}>"
def civilite_str(self):
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
personnes ne souhaitant pas d'affichage).
"""
return {"M": "M.", "F": "Mme", "X": ""}[self.civilite]
def nom_disp(self):
"nom à afficher"
if self.nom_usuel:
return (
(self.nom_usuel + " (" + self.nom + ")") if self.nom else self.nom_usuel
)
else:
return self.nom
def get_first_email(self, field="email") -> str:
"le mail associé à la première adrese de l'étudiant, ou None"
return self.adresses[0].email or None if self.adresses.count() > 0 else None
def to_dict_bul(self, include_urls=True):
"""Infos exportées dans les bulletins"""
from app.scodoc import sco_photos
d = {
"civilite": self.civilite,
"code_ine": self.code_ine,
"code_nip": self.code_nip,
"date_naissance": self.date_naissance.isoformat()
if self.date_naissance
else None,
"email": self.get_first_email(),
"emailperso": self.get_first_email("emailperso"),
"etudid": self.id,
"nom": self.nom_disp(),
"prenom": self.prenom,
}
if include_urls:
d["fiche_url"] = url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=self.id
)
d["photo_url"] = (sco_photos.get_etud_photo_url(self.id),)
return d
def inscription_courante(self):
"""La première inscription à un formsemestre _actuellement_ en cours.
None s'il n'y en a pas (ou plus, ou pas encore).
"""
r = [
ins
for ins in self.formsemestre_inscriptions
if ins.formsemestre.est_courant()
]
return r[0] if r else None
def etat_inscription(self, formsemestre_id):
"""etat de l'inscription de cet étudiant au semestre:
False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF
"""
# voir si ce n'est pas trop lent:
ins = models.FormSemestreInscription.query.filter_by(
etudid=self.id, formsemestre_id=formsemestre_id
).first()
if ins:
return ins.etat
return False
class Adresse(db.Model):
@ -146,10 +219,13 @@ class ItemSuiviTag(db.Model):
# Association tag <-> module
itemsuivi_tags_assoc = db.Table(
"itemsuivi_tags_assoc",
db.Column("tag_id", db.Integer, db.ForeignKey("itemsuivi_tags.id")),
db.Column("itemsuivi_id", db.Integer, db.ForeignKey("itemsuivi.id")),
db.Column(
"tag_id", db.Integer, db.ForeignKey("itemsuivi_tags.id", ondelete="CASCADE")
),
db.Column(
"itemsuivi_id", db.Integer, db.ForeignKey("itemsuivi.id", ondelete="CASCADE")
),
)
# ON DELETE CASCADE ?
class EtudAnnotation(db.Model):

138
app/models/evaluations.py Normal file
View File

@ -0,0 +1,138 @@
# -*- coding: UTF-8 -*
"""ScoDoc models: evaluations
"""
from app import db
from app.models import UniteEns
import app.scodoc.notesdb as ndb
from app.scodoc import sco_evaluation_db
class Evaluation(db.Model):
"""Evaluation (contrôle, examen, ...)"""
__tablename__ = "notes_evaluation"
id = db.Column(db.Integer, primary_key=True)
evaluation_id = db.synonym("id")
moduleimpl_id = db.Column(
db.Integer, db.ForeignKey("notes_moduleimpl.id"), index=True
)
jour = db.Column(db.Date)
heure_debut = db.Column(db.Time)
heure_fin = db.Column(db.Time)
description = db.Column(db.Text)
note_max = db.Column(db.Float)
coefficient = db.Column(db.Float)
visibulletin = db.Column(
db.Boolean, nullable=False, default=True, server_default="true"
)
publish_incomplete = db.Column(
db.Boolean, nullable=False, default=False, server_default="false"
)
# type d'evaluation: 0 normale, 1 rattrapage, 2 "2eme session"
evaluation_type = db.Column(
db.Integer, nullable=False, default=0, server_default="0"
)
# ordre de presentation (par défaut, le plus petit numero
# est la plus ancienne eval):
numero = db.Column(db.Integer)
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
def __repr__(self):
return f"""<Evaluation {self.id} {self.jour.isoformat() if self.jour else ''} "{self.description[:16] if self.description else ''}">"""
def to_dict(self):
e = dict(self.__dict__)
e.pop("_sa_instance_state", None)
# ScoDoc7 output_formators
e["evaluation_id"] = self.id
e["jour"] = ndb.DateISOtoDMY(e["jour"])
e["numero"] = ndb.int_null_is_zero(e["numero"])
return sco_evaluation_db.evaluation_enrich_dict(e)
def from_dict(self, data):
"""Set evaluation attributes from given dict values."""
sco_evaluation_db._check_evaluation_args(data)
for k in self.__dict__.keys():
if k != "_sa_instance_state" and k != "id" and k in data:
setattr(self, k, data[k])
def clone(self, not_copying=()):
"""Clone, not copying the given attrs
Attention: la copie n'a pas d'id avant le prochain commit
"""
d = dict(self.__dict__)
d.pop("id") # get rid of id
d.pop("_sa_instance_state") # get rid of SQLAlchemy special attr
for k in not_copying:
d.pop(k)
copy = self.__class__(**d)
db.session.add(copy)
return copy
def set_ue_poids(self, ue, poids: float) -> None:
"""Set poids évaluation vers cette UE"""
self.update_ue_poids_dict({ue.id: poids})
def set_ue_poids_dict(self, ue_poids_dict: dict) -> None:
"""set poids vers les UE (remplace existants)
ue_poids_dict = { ue_id : poids }
"""
L = []
for ue_id, poids in ue_poids_dict.items():
ue = UniteEns.query.get(ue_id)
L.append(EvaluationUEPoids(evaluation=self, ue=ue, poids=poids))
self.ue_poids = L
self.moduleimpl.invalidate_evaluations_poids() # inval cache
def update_ue_poids_dict(self, ue_poids_dict: dict) -> None:
"""update poids vers UE (ajoute aux existants)"""
current = self.get_ue_poids_dict()
current.update(ue_poids_dict)
self.set_ue_poids_dict(current)
def get_ue_poids_dict(self) -> dict:
"""returns { ue_id : poids }"""
return {p.ue.id: p.poids for p in self.ue_poids}
def get_ue_poids_str(self) -> str:
"""string describing poids, for excel cells and pdfs
Note: si les poids ne sont pas initialisés (poids par défaut),
ils ne sont pas affichés.
"""
return ", ".join([f"{p.ue.acronyme}: {p.poids}" for p in self.ue_poids])
class EvaluationUEPoids(db.Model):
"""Poids des évaluations (BUT)
association many to many
"""
evaluation_id = db.Column(
db.Integer,
db.ForeignKey("notes_evaluation.id", ondelete="CASCADE"),
primary_key=True,
)
ue_id = db.Column(
db.Integer,
db.ForeignKey("notes_ue.id", ondelete="CASCADE"),
primary_key=True,
)
poids = db.Column(
db.Float,
nullable=False,
)
evaluation = db.relationship(
Evaluation,
backref=db.backref("ue_poids", cascade="all, delete-orphan"),
)
ue = db.relationship(
UniteEns,
backref=db.backref("evaluation_ue_poids", cascade="all, delete-orphan"),
)
def __repr__(self):
return f"<EvaluationUEPoids {self.evaluation} {self.ue} poids={self.poids}>"

View File

@ -4,9 +4,7 @@
"""
from app import db
from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN
class Scolog(db.Model):

View File

@ -1,17 +1,22 @@
"""ScoDoc8 models : Formations (hors BUT)
"""ScoDoc 9 models : Formations
"""
from typing import Any
from app import db
from app.models import APO_CODE_STR_LEN
from app.comp import df_cache
from app.models import SHORT_STR_LEN
from app.models.modules import Module
from app.models.ues import UniteEns
from app.scodoc import notesdb as ndb
from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_utils as scu
class NotesFormation(db.Model):
class Formation(db.Model):
"""Programme pédagogique d'une formation"""
__tablename__ = "notes_formations"
__table_args__ = (db.UniqueConstraint("acronyme", "titre", "version"),)
__table_args__ = (db.UniqueConstraint("dept_id", "acronyme", "titre", "version"),)
id = db.Column(db.Integer, primary_key=True)
formation_id = db.synonym("id")
@ -30,39 +35,117 @@ class NotesFormation(db.Model):
type_parcours = db.Column(db.Integer, default=0, server_default="0")
code_specialite = db.Column(db.String(SHORT_STR_LEN))
formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation")
class NotesUE(db.Model):
"""Unité d'Enseignement"""
__tablename__ = "notes_ue"
id = db.Column(db.Integer, primary_key=True)
ue_id = db.synonym("id")
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
acronyme = db.Column(db.Text(), nullable=False)
numero = db.Column(db.Integer) # ordre de présentation
titre = db.Column(db.Text())
# Type d'UE: 0 normal ("fondamentale"), 1 "sport", 2 "projet et stage (LP)",
# 4 "élective"
type = db.Column(db.Integer, default=0, server_default="0")
# Les UE sont "compatibles" (pour la capitalisation) ssi elles ont ^m code
# note: la fonction SQL notes_newid_ucod doit être créée à part
ue_code = db.Column(
db.String(SHORT_STR_LEN),
server_default=db.text("notes_newid_ucod()"),
nullable=False,
# Optionnel, pour les formations type BUT
referentiel_competence_id = db.Column(
db.Integer, db.ForeignKey("apc_referentiel_competences.id")
)
ects = db.Column(db.Float) # nombre de credits ECTS
is_external = db.Column(db.Boolean(), default=False, server_default="false")
# id de l'element pedagogique Apogee correspondant:
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
# coef UE, utilise seulement si l'option use_ue_coefs est activée:
coefficient = db.Column(db.Float)
ues = db.relationship("UniteEns", backref="formation", lazy="dynamic")
formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation")
ues = db.relationship("UniteEns", lazy="dynamic", backref="formation")
modules = db.relationship("Module", lazy="dynamic", backref="formation")
def __repr__(self):
return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme}')>"
def to_dict(self):
e = dict(self.__dict__)
e.pop("_sa_instance_state", None)
# ScoDoc7 output_formators: (backward compat)
e["formation_id"] = self.id
return e
def get_parcours(self):
"""get l'instance de TypeParcours de cette formation"""
return sco_codes_parcours.get_parcours_from_code(self.type_parcours)
def is_apc(self):
"True si formation APC avec SAE (BUT)"
return self.get_parcours().APC_SAE
def get_module_coefs(self, semestre_idx: int = None):
"""Les coefs des modules vers les UE (accès via cache)"""
from app.comp import moy_ue
if semestre_idx is None:
key = f"{self.id}"
else:
key = f"{self.id}.{semestre_idx}"
modules_coefficients = df_cache.ModuleCoefsCache.get(key)
if modules_coefficients is None:
modules_coefficients, _, _ = moy_ue.df_load_module_coefs(
self.id, semestre_idx
)
df_cache.ModuleCoefsCache.set(key, modules_coefficients)
return modules_coefficients
def has_locked_sems(self):
"True if there is a locked formsemestre in this formation"
return len(self.formsemestres.filter_by(etat=False).all()) > 0
def invalidate_module_coefs(self, semestre_idx: int = None):
"""Invalide les coefficients de modules cachés.
Si semestre_idx est None, invalide tous les semestres,
sinon invalide le semestre indiqué et le cache de la formation.
"""
if semestre_idx is None:
keys = {f"{self.id}.{m.semestre_id}" for m in self.modules}
else:
keys = f"{self.id}.{semestre_idx}"
df_cache.ModuleCoefsCache.delete_many(keys | {f"{self.id}"})
sco_cache.invalidate_formsemestre()
def invalidate_cached_sems(self):
for sem in self.formsemestres:
sco_cache.invalidate_formsemestre(formsemestre_id=sem.id)
def sanitize_old_formation(self) -> None:
"""
Corrige si nécessaire certains champs issus d'anciennes versions de ScoDoc:
- affecte à chaque module de cette formation le semestre de son UE de rattachement,
si elle en a une.
- si le module_type n'est pas renseigné, le met à STANDARD.
Devrait être appelé lorsqu'on change le type de formation vers le BUT, et aussi
lorsqu'on change le semestre d'une UE BUT.
Utile pour la migration des anciennes formations vers le BUT.
En cas de changement, invalide les caches coefs/poids.
"""
if not self.is_apc():
return
change = False
for mod in self.modules:
# --- Indices de semestres:
if (
mod.ue.semestre_idx is not None
and mod.ue.semestre_idx > 0
and mod.semestre_id != mod.ue.semestre_idx
):
mod.semestre_id = mod.ue.semestre_idx
db.session.add(mod)
change = True
# --- Types de modules
if mod.module_type is None:
mod.module_type = scu.ModuleType.STANDARD
db.session.add(mod)
change = True
# --- Numéros de modules
if Module.query.filter_by(formation_id=self.id, numero=None).count() > 0:
scu.objects_renumber(db, self.modules.all())
# --- Types d'UE (avant de rendre le type non nullable)
ues_sans_type = UniteEns.query.filter_by(formation_id=self.id, type=None)
if ues_sans_type.count() > 0:
for ue in ues_sans_type:
ue.type = 0
db.session.add(ue)
db.session.commit()
if change:
self.invalidate_module_coefs()
class NotesMatiere(db.Model):
class Matiere(db.Model):
"""Matières: regroupe les modules d'une UE
La matière a peu d'utilité en dehors de la présentation des modules
d'une UE.
@ -77,50 +160,4 @@ class NotesMatiere(db.Model):
titre = db.Column(db.Text())
numero = db.Column(db.Integer) # ordre de présentation
class NotesModule(db.Model):
"""Module"""
__tablename__ = "notes_modules"
id = db.Column(db.Integer, primary_key=True)
module_id = db.synonym("id")
titre = db.Column(db.Text())
abbrev = db.Column(db.Text()) # nom court
# certains départements ont des codes infiniment longs: donc Text !
code = db.Column(db.Text(), nullable=False)
heures_cours = db.Column(db.Float)
heures_td = db.Column(db.Float)
heures_tp = db.Column(db.Float)
coefficient = db.Column(db.Float) # coef PPN
ects = db.Column(db.Float) # Crédits ECTS
ue_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), index=True)
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
matiere_id = db.Column(db.Integer, db.ForeignKey("notes_matieres.id"))
# pas un id mais le numéro du semestre: 1, 2, ...
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
numero = db.Column(db.Integer) # ordre de présentation
# id de l'element pedagogique Apogee correspondant:
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
module_type = db.Column(db.Integer) # NULL ou 0:defaut, 1: malus (NOTES_MALUS)
class NotesTag(db.Model):
"""Tag sur un module"""
__tablename__ = "notes_tags"
__table_args__ = (db.UniqueConstraint("title", "dept_id"),)
id = db.Column(db.Integer, primary_key=True)
tag_id = db.synonym("id")
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
title = db.Column(db.Text(), nullable=False)
# Association tag <-> module
notes_modules_tags = db.Table(
"notes_modules_tags",
db.Column("tag_id", db.Integer, db.ForeignKey("notes_tags.id")),
db.Column("module_id", db.Integer, db.ForeignKey("notes_modules.id")),
)
modules = db.relationship("Module", lazy="dynamic", backref="matiere")

View File

@ -1,19 +1,29 @@
# -*- coding: UTF-8 -*
"""ScoDoc models
"""ScoDoc models: formsemestre
"""
from typing import Any
import datetime
import flask_sqlalchemy
from app import db
from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN
from app.models import UniteEns
import app.scodoc.sco_utils as scu
from app.models.ues import UniteEns
from app.models.modules import Module
from app.models.moduleimpls import ModuleImpl
from app.models.etudiants import Identite
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_preferences
from app.scodoc.sco_vdi import ApoEtapeVDI
class FormSemestre(db.Model):
"""Mise en oeuvre d'un semestre de formation
was notes_formsemestre
"""
"""Mise en oeuvre d'un semestre de formation"""
__tablename__ = "notes_formsemestre"
@ -32,7 +42,7 @@ class FormSemestre(db.Model):
) # False si verrouillé
modalite = db.Column(
db.String(SHORT_STR_LEN), db.ForeignKey("notes_form_modalites.modalite")
)
) # "FI", "FAP", "FC", ...
# gestion compensation sem DUT:
gestion_compensation = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
@ -41,6 +51,10 @@ class FormSemestre(db.Model):
bul_hide_xml = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
)
# Bloque le calcul des moyennes (générale et d'UE)
block_moyennes = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
)
# semestres decales (pour gestion jurys):
gestion_semestrielle = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
@ -66,16 +80,174 @@ class FormSemestre(db.Model):
# code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'
elt_annee_apo = db.Column(db.Text())
# Relations:
etapes = db.relationship(
"NotesFormsemestreEtape", cascade="all,delete", backref="notes_formsemestre"
"FormSemestreEtape", cascade="all,delete", backref="formsemestre"
)
modimpls = db.relationship("ModuleImpl", backref="formsemestre", lazy="dynamic")
etuds = db.relationship(
"Identite",
secondary="notes_formsemestre_inscription",
viewonly=True,
lazy="dynamic",
)
responsables = db.relationship(
"User",
secondary="notes_formsemestre_responsables",
lazy=True,
backref=db.backref("formsemestres", lazy=True),
)
# Ancien id ScoDoc7 pour les migrations de bases anciennes
# ne pas utiliser après migrate_scodoc7_dept_archives
scodoc7_id = db.Column(db.Text(), nullable=True)
def __init__(self, **kwargs):
super(FormSemestre, self).__init__(**kwargs)
if self.modalite is None:
self.modalite = NotesFormModalite.DEFAULT_MODALITE
self.modalite = FormationModalite.DEFAULT_MODALITE
def to_dict(self):
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
# ScoDoc7 output_formators: (backward compat)
d["formsemestre_id"] = self.id
d["date_debut"] = (
self.date_debut.strftime("%d/%m/%Y") if self.date_debut else ""
)
d["date_fin"] = self.date_fin.strftime("%d/%m/%Y") if self.date_fin else ""
d["responsables"] = [u.id for u in self.responsables]
return d
def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery:
"""UE des modules de ce semestre, triées par numéro.
- Formations classiques: les UEs auxquelles appartiennent
les modules mis en place dans ce semestre.
- Formations APC / BUT: les UEs de la formation qui ont
le même numéro de semestre que ce formsemestre.
"""
if self.formation.get_parcours().APC_SAE:
sem_ues = UniteEns.query.filter_by(
formation=self.formation, semestre_idx=self.semestre_id
)
else:
sem_ues = db.session.query(UniteEns).filter(
ModuleImpl.formsemestre_id == self.id,
Module.id == ModuleImpl.module_id,
UniteEns.id == Module.ue_id,
)
if not with_sport:
sem_ues = sem_ues.filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
return sem_ues.order_by(UniteEns.numero)
def est_courant(self) -> bool:
"""Vrai si la date actuelle (now) est dans le semestre
(les dates de début et fin sont incluses)
"""
today = datetime.date.today()
return (self.date_debut <= today) and (today <= self.date_fin)
def est_decale(self):
"""Vrai si semestre "décalé"
c'est à dire semestres impairs commençant entre janvier et juin
et les pairs entre juillet et decembre
"""
if self.semestre_id <= 0:
return False # formations sans semestres
return (self.semestre_id % 2 and self.date_debut.month <= 6) or (
not self.semestre_id % 2 and self.date_debut.month > 6
)
def etapes_apo_str(self) -> str:
"""Chaine décrivant les étapes de ce semestre
ex: "V1RT, V1RT3, V1RT4"
"""
if not self.etapes:
return ""
return ", ".join([str(x.etape_apo) for x in self.etapes])
def responsables_str(self, abbrev_prenom=True) -> str:
"""chaîne "J. Dupond, X. Martin"
ou "Jacques Dupond, Xavier Martin"
"""
if not self.responsables:
return ""
if abbrev_prenom:
return ", ".join([u.get_prenomnom() for u in self.responsables])
else:
return ", ".join([u.get_nomcomplet() for u in self.responsables])
def annee_scolaire_str(self):
"2021 - 2022"
return scu.annee_scolaire_repr(self.date_debut.year, self.date_debut.month)
def session_id(self) -> str:
"""identifiant externe de semestre de formation
Exemple: RT-DUT-FI-S1-ANNEE
DEPT-TYPE-MODALITE+-S?|SPECIALITE
TYPE=DUT|LP*|M*
MODALITE=FC|FI|FA (si plusieurs, en inverse alpha)
SPECIALITE=[A-Z]+ EON,ASSUR, ... (si pas Sn ou SnD)
ANNEE=annee universitaire de debut (exemple: un S2 de 2013-2014 sera S2-2013)
"""
imputation_dept = sco_preferences.get_preference("ImputationDept", self.id)
if not imputation_dept:
imputation_dept = sco_preferences.get_preference("DeptName")
imputation_dept = imputation_dept.upper()
parcours_name = self.formation.get_parcours().NAME
modalite = self.modalite
# exception pour code Apprentissage:
modalite = (modalite or "").replace("FAP", "FA").replace("APP", "FA")
if self.semestre_id > 0:
decale = "D" if self.est_decale() else ""
semestre_id = f"S{self.semestre_id}{decale}"
else:
semestre_id = self.formation.code_specialite or ""
annee_sco = str(
scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month)
)
return scu.sanitize_string(
"-".join((imputation_dept, parcours_name, modalite, semestre_id, annee_sco))
)
def titre_mois(self) -> str:
"""Le titre et les dates du semestre, pour affichage dans des listes
Ex: "BUT QLIO (PN 2022) semestre 1 FI (Sept 2022 - Jan 2023)"
"""
return f"""{self.titre_num()} {self.modalite or ''} ({
scu.MONTH_NAMES_ABBREV[self.date_debut.month-1]} {
self.date_debut.year} - {
scu.MONTH_NAMES_ABBREV[self.date_fin.month -1]} {
self.date_fin.year})"""
def titre_num(self) -> str:
"""Le titre est le semestre, ex ""DUT Informatique semestre 2"" """
if self.semestre_id == sco_codes_parcours.NO_SEMESTRE_ID:
return self.titre
return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}"
def get_abs_count(self, etudid):
"""Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs non justifiées, nb abs justifiées)
Utilise un cache.
"""
from app.scodoc import sco_abs
return sco_abs.get_abs_count_in_interval(
etudid, self.date_debut.isoformat(), self.date_fin.isoformat()
)
def get_inscrits(self, include_dem=False) -> list:
"""Liste des étudiants inscrits à ce semestre
Si all, tous les étudiants, avec les démissionnaires.
"""
if include_dem:
return [ins.etud for ins in self.inscriptions]
else:
return [ins.etud for ins in self.inscriptions if ins.etat == scu.INSCRIT]
# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
@ -90,7 +262,7 @@ notes_formsemestre_responsables = db.Table(
)
class NotesFormsemestreEtape(db.Model):
class FormSemestreEtape(db.Model):
"""Étape Apogée associées au semestre"""
__tablename__ = "notes_formsemestre_etapes"
@ -99,10 +271,16 @@ class NotesFormsemestreEtape(db.Model):
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
)
etape_apo = db.Column(db.String(APO_CODE_STR_LEN))
etape_apo = db.Column(db.String(APO_CODE_STR_LEN), index=True)
def __repr__(self):
return f"<Etape {self.id} apo={self.etape_apo}>"
def as_apovdi(self):
return ApoEtapeVDI(self.etape_apo)
class NotesFormModalite(db.Model):
class FormationModalite(db.Model):
"""Modalités de formation, utilisées pour la présentation
(grouper les semestres, générer des codes, etc.)
"""
@ -129,7 +307,7 @@ class NotesFormModalite(db.Model):
numero = 0
try:
for (code, titre) in (
(NotesFormModalite.DEFAULT_MODALITE, "Formation Initiale"),
(FormationModalite.DEFAULT_MODALITE, "Formation Initiale"),
("FAP", "Apprentissage"),
("FC", "Formation Continue"),
("DEC", "Formation Décalées"),
@ -140,9 +318,9 @@ class NotesFormModalite(db.Model):
("EXT", "Extérieur"),
("OTHER", "Autres formations"),
):
modalite = NotesFormModalite.query.filter_by(modalite=code).first()
modalite = FormationModalite.query.filter_by(modalite=code).first()
if modalite is None:
modalite = NotesFormModalite(
modalite = FormationModalite(
modalite=code, titre=titre, numero=numero
)
db.session.add(modalite)
@ -153,7 +331,7 @@ class NotesFormModalite(db.Model):
raise
class NotesFormsemestreUECoef(db.Model):
class FormSemestreUECoef(db.Model):
"""Coef des UE capitalisees arrivant dans ce semestre"""
__tablename__ = "notes_formsemestre_uecoef"
@ -172,7 +350,7 @@ class NotesFormsemestreUECoef(db.Model):
coefficient = db.Column(db.Float, nullable=False)
class NotesFormsemestreUEComputationExpr(db.Model):
class FormSemestreUEComputationExpr(db.Model):
"""Formules utilisateurs pour calcul moyenne UE"""
__tablename__ = "notes_formsemestre_ue_computation_expr"
@ -192,7 +370,7 @@ class NotesFormsemestreUEComputationExpr(db.Model):
computation_expr = db.Column(db.Text())
class NotesFormsemestreCustomMenu(db.Model):
class FormSemestreCustomMenu(db.Model):
"""Menu custom associe au semestre"""
__tablename__ = "notes_formsemestre_custommenu"
@ -208,7 +386,7 @@ class NotesFormsemestreCustomMenu(db.Model):
idx = db.Column(db.Integer, default=0, server_default="0") # rang dans le menu
class NotesFormsemestreInscription(db.Model):
class FormSemestreInscription(db.Model):
"""Inscription à un semestre de formation"""
__tablename__ = "notes_formsemestre_inscription"
@ -217,99 +395,30 @@ class NotesFormsemestreInscription(db.Model):
id = db.Column(db.Integer, primary_key=True)
formsemestre_inscription_id = db.synonym("id")
etudid = db.Column(db.Integer, db.ForeignKey("identite.id"))
etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True)
formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
index=True,
)
etud = db.relationship(
Identite,
backref=db.backref("formsemestre_inscriptions", cascade="all, delete-orphan"),
)
formsemestre = db.relationship(
FormSemestre,
backref=db.backref(
"inscriptions",
cascade="all, delete-orphan",
order_by="FormSemestreInscription.etudid",
),
)
# I inscrit, D demission en cours de semestre, DEF si "defaillant"
etat = db.Column(db.String(CODE_STR_LEN))
etat = db.Column(db.String(CODE_STR_LEN), index=True)
# etape apogee d'inscription (experimental 2020)
etape = db.Column(db.String(APO_CODE_STR_LEN))
class NotesModuleImpl(db.Model):
"""Mise en oeuvre d'un module pour une annee/semestre"""
__tablename__ = "notes_moduleimpl"
__table_args__ = (db.UniqueConstraint("formsemestre_id", "module_id"),)
id = db.Column(db.Integer, primary_key=True)
moduleimpl_id = db.synonym("id")
module_id = db.Column(
db.Integer,
db.ForeignKey("notes_modules.id"),
)
formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
index=True,
)
responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id"))
# formule de calcul moyenne:
computation_expr = db.Column(db.Text())
# Enseignants (chargés de TD ou TP) d'un moduleimpl
notes_modules_enseignants = db.Table(
"notes_modules_enseignants",
db.Column(
"moduleimpl_id",
db.Integer,
db.ForeignKey("notes_moduleimpl.id"),
),
db.Column("ens_id", db.Integer, db.ForeignKey("user.id")),
# ? db.UniqueConstraint("moduleimpl_id", "ens_id"),
)
# XXX il manque probablement une relation pour gérer cela
class NotesModuleImplInscription(db.Model):
"""Inscription à un module (etudiants,moduleimpl)"""
__tablename__ = "notes_moduleimpl_inscription"
id = db.Column(db.Integer, primary_key=True)
moduleimpl_inscription_id = db.synonym("id")
moduleimpl_id = db.Column(
db.Integer,
db.ForeignKey("notes_moduleimpl.id"),
index=True,
)
etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True)
class NotesEvaluation(db.Model):
"""Evaluation (contrôle, examen, ...)"""
__tablename__ = "notes_evaluation"
id = db.Column(db.Integer, primary_key=True)
evaluation_id = db.synonym("id")
moduleimpl_id = db.Column(
db.Integer, db.ForeignKey("notes_moduleimpl.id"), index=True
)
jour = db.Column(db.Date)
heure_debut = db.Column(db.Time)
heure_fin = db.Column(db.Time)
description = db.Column(db.Text)
note_max = db.Column(db.Float)
coefficient = db.Column(db.Float)
visibulletin = db.Column(
db.Boolean, nullable=False, default=True, server_default="true"
)
publish_incomplete = db.Column(
db.Boolean, nullable=False, default=False, server_default="false"
)
# type d'evaluation: 0 normale, 1 rattrapage, 2 "2eme session"
evaluation_type = db.Column(
db.Integer, nullable=False, default=0, server_default="0"
)
# ordre de presentation (par défaut, le plus petit numero
# est la plus ancienne eval):
numero = db.Column(db.Integer)
class NotesSemSet(db.Model):
"""semsets: ensemble de formsemestres pour exports Apogée"""

View File

@ -5,9 +5,7 @@
from typing import Any
from app import db
from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN
from app.models import GROUPNAME_STR_LEN

129
app/models/moduleimpls.py Normal file
View File

@ -0,0 +1,129 @@
# -*- coding: UTF-8 -*
"""ScoDoc models: moduleimpls
"""
import pandas as pd
from app import db
from app.comp import df_cache
from app.models import UniteEns, Identite
import app.scodoc.notesdb as ndb
from app.scodoc import sco_utils as scu
class ModuleImpl(db.Model):
"""Mise en oeuvre d'un module pour une annee/semestre"""
__tablename__ = "notes_moduleimpl"
__table_args__ = (db.UniqueConstraint("formsemestre_id", "module_id"),)
id = db.Column(db.Integer, primary_key=True)
moduleimpl_id = db.synonym("id")
module_id = db.Column(
db.Integer,
db.ForeignKey("notes_modules.id"),
)
formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
index=True,
)
responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id"))
# formule de calcul moyenne:
computation_expr = db.Column(db.Text())
evaluations = db.relationship("Evaluation", lazy="dynamic", backref="moduleimpl")
enseignants = db.relationship(
"User",
secondary="notes_modules_enseignants",
lazy="dynamic",
backref="moduleimpl",
viewonly=True,
)
def __init__(self, **kwargs):
super(ModuleImpl, self).__init__(**kwargs)
def __repr__(self):
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
def get_evaluations_poids(self) -> pd.DataFrame:
"""Les poids des évaluations vers les UE (accès via cache)"""
evaluations_poids = df_cache.EvaluationsPoidsCache.get(self.id)
if evaluations_poids is None:
from app.comp import moy_mod
evaluations_poids, _ = moy_mod.df_load_evaluations_poids(self.id)
df_cache.EvaluationsPoidsCache.set(self.id, evaluations_poids)
return evaluations_poids
def invalidate_evaluations_poids(self):
"""Invalide poids cachés"""
df_cache.EvaluationsPoidsCache.delete(self.id)
def check_apc_conformity(self) -> bool:
"""true si les poids des évaluations du module permettent de satisfaire
les coefficients du PN.
"""
if not self.module.formation.get_parcours().APC_SAE or (
self.module.module_type != scu.ModuleType.RESSOURCE
and self.module.module_type != scu.ModuleType.SAE
):
return True
from app.comp import moy_mod
return moy_mod.check_moduleimpl_conformity(
self,
self.get_evaluations_poids(),
self.module.formation.get_module_coefs(self.module.semestre_id),
)
def to_dict(self):
"""as a dict, with the same conversions as in ScoDoc7"""
e = dict(self.__dict__)
e.pop("_sa_instance_state", None)
# ScoDoc7 output_formators: (backward compat)
e["moduleimpl_id"] = self.id
e["ens"] = [
{"moduleimpl_id": self.id, "ens_id": e.id} for e in self.enseignants
]
e["module"] = self.module.to_dict()
return e
# Enseignants (chargés de TD ou TP) d'un moduleimpl
notes_modules_enseignants = db.Table(
"notes_modules_enseignants",
db.Column(
"moduleimpl_id",
db.Integer,
db.ForeignKey("notes_moduleimpl.id"),
),
db.Column("ens_id", db.Integer, db.ForeignKey("user.id")),
# ? db.UniqueConstraint("moduleimpl_id", "ens_id"),
)
# XXX il manque probablement une relation pour gérer cela
class ModuleImplInscription(db.Model):
"""Inscription à un module (etudiants,moduleimpl)"""
__tablename__ = "notes_moduleimpl_inscription"
__table_args__ = (db.UniqueConstraint("moduleimpl_id", "etudid"),)
id = db.Column(db.Integer, primary_key=True)
moduleimpl_inscription_id = db.synonym("id")
moduleimpl_id = db.Column(
db.Integer,
db.ForeignKey("notes_moduleimpl.id"),
index=True,
)
etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True)
etud = db.relationship(
Identite,
backref=db.backref("moduleimpl_inscriptions", cascade="all, delete-orphan"),
)
modimpl = db.relationship(
ModuleImpl,
backref=db.backref("inscriptions", cascade="all, delete-orphan"),
)

202
app/models/modules.py Normal file
View File

@ -0,0 +1,202 @@
"""ScoDoc 9 models : Modules
"""
from app import db
from app.models import APO_CODE_STR_LEN
from app.scodoc import sco_utils as scu
from app.scodoc.sco_utils import ModuleType
class Module(db.Model):
"""Module"""
__tablename__ = "notes_modules"
id = db.Column(db.Integer, primary_key=True)
module_id = db.synonym("id")
titre = db.Column(db.Text())
abbrev = db.Column(db.Text()) # nom court
# certains départements ont des codes infiniment longs: donc Text !
code = db.Column(db.Text(), nullable=False)
heures_cours = db.Column(db.Float)
heures_td = db.Column(db.Float)
heures_tp = db.Column(db.Float)
coefficient = db.Column(db.Float) # coef PPN (sauf en APC)
ects = db.Column(db.Float) # Crédits ECTS
ue_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), index=True)
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
matiere_id = db.Column(db.Integer, db.ForeignKey("notes_matieres.id"))
# pas un id mais le numéro du semestre: 1, 2, ...
# note: en APC, le semestre qui fait autorité est celui de l'UE
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
numero = db.Column(db.Integer) # ordre de présentation
# id de l'element pedagogique Apogee correspondant:
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
# Type: ModuleType: DEFAULT, MALUS, RESSOURCE, MODULE_SAE (enum)
module_type = db.Column(db.Integer)
# Relations:
modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic")
ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True)
tags = db.relationship(
"NotesTag",
secondary="notes_modules_tags",
lazy=True,
backref=db.backref("modules", lazy=True),
)
def __init__(self, **kwargs):
self.ue_coefs = []
super(Module, self).__init__(**kwargs)
def __repr__(self):
return f"<Module{ModuleType(self.module_type or ModuleType.STANDARD).name} id={self.id} code={self.code}>"
def to_dict(self):
e = dict(self.__dict__)
e.pop("_sa_instance_state", None)
# ScoDoc7 output_formators: (backward compat)
e["module_id"] = self.id
e["heures_cours"] = 0.0 if self.heures_cours is None else self.heures_cours
e["heures_td"] = 0.0 if self.heures_td is None else self.heures_td
e["heures_tp"] = 0.0 if self.heures_tp is None else self.heures_tp
e["numero"] = 0 if self.numero is None else self.numero
e["coefficient"] = 0.0 if self.coefficient is None else self.coefficient
e["module_type"] = 0 if self.module_type is None else self.module_type
return e
def is_apc(self):
"True si module SAÉ ou Ressource"
return self.module_type and scu.ModuleType(self.module_type) in {
scu.ModuleType.RESSOURCE,
scu.ModuleType.SAE,
}
def type_name(self):
return scu.MODULE_TYPE_NAMES[self.module_type]
def set_ue_coef(self, ue, coef: float) -> None:
"""Set coef module vers cette UE"""
self.update_ue_coef_dict({ue.id: coef})
def set_ue_coef_dict(self, ue_coef_dict: dict) -> None:
"""set coefs vers les UE (remplace existants)
ue_coef_dict = { ue_id : coef }
Les coefs nuls (zéro) ne sont pas stockés: la relation est supprimée.
"""
changed = False
for ue_id, coef in ue_coef_dict.items():
# Existant ?
coefs = [c for c in self.ue_coefs if c.ue_id == ue_id]
if coefs:
ue_coef = coefs[0]
if coef == 0.0: # supprime ce coef
db.session.delete(ue_coef)
changed = True
elif coef != ue_coef.coef:
ue_coef.coef = coef
db.session.add(ue_coef)
changed = True
else:
# crée nouveau coef:
if coef != 0.0:
ue = UniteEns.query.get(ue_id)
ue_coef = ModuleUECoef(module=self, ue=ue, coef=coef)
self.ue_coefs.append(ue_coef)
changed = True
if changed:
self.formation.invalidate_module_coefs()
def update_ue_coef_dict(self, ue_coef_dict: dict):
"""update coefs vers UE (ajoute aux existants)"""
current = self.get_ue_coef_dict()
current.update(ue_coef_dict)
self.set_ue_coef_dict(current)
def get_ue_coef_dict(self):
"""returns { ue_id : coef }"""
return {p.ue.id: p.coef for p in self.ue_coefs}
def delete_ue_coef(self, ue):
"""delete coef"""
ue_coef = ModuleUECoef.query.get((self.id, ue.id))
if ue_coef:
db.session.delete(ue_coef)
self.formation.invalidate_module_coefs()
def get_ue_coefs_sorted(self):
"les coefs d'UE, trié par numéro d'UE"
# je n'ai pas su mettre un order_by sur le backref sans avoir
# à redéfinir les relationships...
return sorted(self.ue_coefs, key=lambda x: x.ue.numero)
def ue_coefs_descr(self):
"""List of tuples [ (ue_acronyme, coef) ]"""
return [(c.ue.acronyme, c.coef) for c in self.get_ue_coefs_sorted()]
class ModuleUECoef(db.Model):
"""Coefficients des modules vers les UE (APC, BUT)
En mode APC, ces coefs remplacent le coefficient "PPN" du module.
"""
__tablename__ = "module_ue_coef"
module_id = db.Column(
db.Integer,
db.ForeignKey("notes_modules.id", ondelete="CASCADE"),
primary_key=True,
)
ue_id = db.Column(
db.Integer,
db.ForeignKey("notes_ue.id", ondelete="CASCADE"),
primary_key=True,
)
coef = db.Column(
db.Float,
nullable=False,
)
module = db.relationship(
Module,
backref=db.backref(
"ue_coefs",
passive_deletes=True,
cascade="save-update, merge, delete, delete-orphan",
),
)
ue = db.relationship(
"UniteEns",
backref=db.backref(
"module_ue_coefs",
passive_deletes=True,
cascade="save-update, merge, delete, delete-orphan",
),
)
class NotesTag(db.Model):
"""Tag sur un module"""
__tablename__ = "notes_tags"
__table_args__ = (db.UniqueConstraint("title", "dept_id"),)
id = db.Column(db.Integer, primary_key=True)
tag_id = db.synonym("id")
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
title = db.Column(db.Text(), nullable=False)
# Association tag <-> module
notes_modules_tags = db.Table(
"notes_modules_tags",
db.Column(
"tag_id",
db.Integer,
db.ForeignKey("notes_tags.id", ondelete="CASCADE"),
),
db.Column(
"module_id", db.Integer, db.ForeignKey("notes_modules.id", ondelete="CASCADE")
),
)
from app.models.ues import UniteEns

View File

@ -4,7 +4,6 @@
"""
from app import db
from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN
@ -40,7 +39,7 @@ class ScolarEvent(db.Model):
)
class ScolarFormsemestreValidation(db.Model):
class ScolarFormSemestreValidation(db.Model):
"""Décisions de jury"""
__tablename__ = "scolar_formsemestre_validation"
@ -52,16 +51,19 @@ class ScolarFormsemestreValidation(db.Model):
etudid = db.Column(
db.Integer,
db.ForeignKey("identite.id"),
index=True,
)
formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
index=True,
)
ue_id = db.Column(
db.Integer,
db.ForeignKey("notes_ue.id"),
index=True,
)
code = db.Column(db.String(CODE_STR_LEN), nullable=False)
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
# NULL pour les UE, True|False pour les semestres:
assidu = db.Column(db.Boolean)
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
@ -100,9 +102,10 @@ class ScolarAutorisationInscription(db.Model):
)
class NotesAppreciations(db.Model):
class BulAppreciations(db.Model):
"""Appréciations sur bulletins"""
__tablename__ = "notes_appreciations"
id = db.Column(db.Integer, primary_key=True)
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
etudid = db.Column(

View File

@ -4,6 +4,7 @@
"""
from app import db, log
from app.scodoc import bonus_sport
from app.scodoc.sco_exceptions import ScoValueError
class ScoPreference(db.Model):
@ -94,7 +95,7 @@ class ScoDocSiteConfig(db.Model):
"""returns bonus func with specified name.
If name not specified, return the configured function.
None if no bonus function configured.
Raises NameError if func_name not found in module bonus_sport.
Raises ScoValueError if func_name not found in module bonus_sport.
"""
if func_name is None:
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
@ -103,7 +104,13 @@ class ScoDocSiteConfig(db.Model):
func_name = c.value
if func_name == "": # pas de bonus défini
return None
return getattr(bonus_sport, func_name)
try:
return getattr(bonus_sport, func_name)
except AttributeError:
raise ScoValueError(
f"""Fonction de calcul maison inexistante: {func_name}.
(contacter votre administrateur local)."""
)
@classmethod
def get_bonus_sport_func_names(cls):

102
app/models/ues.py Normal file
View File

@ -0,0 +1,102 @@
"""ScoDoc 9 models : Unités d'Enseignement (UE)
"""
from app import db
from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN
from app.scodoc import notesdb as ndb
from app.scodoc import sco_utils as scu
class UniteEns(db.Model):
"""Unité d'Enseignement (UE)"""
__tablename__ = "notes_ue"
id = db.Column(db.Integer, primary_key=True)
ue_id = db.synonym("id")
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
acronyme = db.Column(db.Text(), nullable=False)
numero = db.Column(db.Integer) # ordre de présentation
titre = db.Column(db.Text())
# Le semestre_idx n'est pas un id mais le numéro du semestre: 1, 2, ...
# En ScoDoc7 et pour les formations classiques, il est NULL
# (le numéro du semestre étant alors déterminé par celui des modules de l'UE)
# Pour les formations APC, il est obligatoire (de 1 à 6 pour le BUT):
semestre_idx = db.Column(db.Integer, nullable=True, index=True)
# Type d'UE: 0 normal ("fondamentale"), 1 "sport", 2 "projet et stage (LP)",
# 4 "élective"
type = db.Column(db.Integer, default=0, server_default="0")
# Les UE sont "compatibles" (pour la capitalisation) ssi elles ont ^m code
# note: la fonction SQL notes_newid_ucod doit être créée à part
ue_code = db.Column(
db.String(SHORT_STR_LEN),
server_default=db.text("notes_newid_ucod()"),
nullable=False,
)
ects = db.Column(db.Float) # nombre de credits ECTS
is_external = db.Column(db.Boolean(), default=False, server_default="false")
# id de l'element pedagogique Apogee correspondant:
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
# coef UE, utilise seulement si l'option use_ue_coefs est activée:
coefficient = db.Column(db.Float)
# relations
matieres = db.relationship("Matiere", lazy="dynamic", backref="ue")
modules = db.relationship("Module", lazy="dynamic", backref="ue")
def __repr__(self):
return f"""<{self.__class__.__name__}(id={self.id}, formation_id={
self.formation_id}, acronyme='{self.acronyme}', semestre_idx={
self.semestre_idx} {
'EXTERNE' if self.is_external else ''})>"""
def to_dict(self):
"""as a dict, with the same conversions as in ScoDoc7"""
e = dict(self.__dict__)
e.pop("_sa_instance_state", None)
# ScoDoc7 output_formators
e["ue_id"] = self.id
e["numero"] = e["numero"] if e["numero"] else 0
e["ects"] = e["ects"] if e["ects"] else 0.0
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
return e
def is_locked(self):
"""True if UE should not be modified
(contains modules used in a locked formsemestre)
"""
# XXX todo : à ré-écrire avec SQLAlchemy
from app.scodoc import sco_edit_ue
return sco_edit_ue.ue_is_locked(self.id)
def guess_semestre_idx(self) -> None:
"""Lorsqu'on prend une ancienne formation non APC,
les UE n'ont pas d'indication de semestre.
Cette méthode fixe le semestre en prenant celui du premier module,
ou à défaut le met à 1.
"""
if self.semestre_idx is None:
module = self.modules.first()
if module is None:
self.semestre_idx = 1
else:
self.semestre_idx = module.semestre_id
db.session.add(self)
db.session.commit()
def get_ressources(self):
"Liste des modules ressources rattachés à cette UE"
return self.modules.filter_by(module_type=scu.ModuleType.RESSOURCE).all()
def get_saes(self):
"Liste des modules SAE rattachés à cette UE"
return self.modules.filter_by(module_type=scu.ModuleType.SAE).all()
def get_modules_not_apc(self):
"Listes des modules non SAE et non ressource (standards, mais aussi bonus...)"
return self.modules.filter(
(Module.module_type != scu.ModuleType.SAE),
(Module.module_type != scu.ModuleType.RESSOURCE),
).all()

8
app/pe/README.md Normal file
View File

@ -0,0 +1,8 @@
# Module "Avis de poursuite d'étude"
Conçu et développé sur ScoDoc 7 par Cléo Baras (IUT de Grenoble) pour le DUT.
Actuellement non opérationnel dans ScoDoc 9.

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -33,9 +33,9 @@
import os
import codecs
import re
from app.scodoc import pe_jurype
from app.scodoc import pe_tagtable
from app.scodoc import pe_tools
from app.pe import pe_tagtable
from app.pe import pe_jurype
from app.pe import pe_tools
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
@ -48,7 +48,7 @@ from app.scodoc import sco_etud
DEBUG = False # Pour debug et repérage des prints à changer en Log
DONNEE_MANQUANTE = (
u"" # Caractère de remplacement des données manquantes dans un avis PE
"" # Caractère de remplacement des données manquantes dans un avis PE
)
# ----------------------------------------------------------------------------------------
@ -102,17 +102,17 @@ def comp_latex_parcourstimeline(etudiant, promo, taille=17):
result: chaine unicode (EV:)
"""
codelatexDebut = (
u"""
""""
\\begin{parcourstimeline}{**debut**}{**fin**}{**nbreSemestres**}{%d}
"""
% taille
)
modeleEvent = u"""
modeleEvent = """
\\parcoursevent{**nosem**}{**nomsem**}{**descr**}
"""
codelatexFin = u"""
codelatexFin = """
\\end{parcourstimeline}
"""
reslatex = codelatexDebut
@ -125,13 +125,13 @@ def comp_latex_parcourstimeline(etudiant, promo, taille=17):
for no_sem in range(etudiant["nbSemestres"]):
descr = modeleEvent
nom_semestre_dans_parcours = parcours[no_sem]["nom_semestre_dans_parcours"]
descr = descr.replace(u"**nosem**", str(no_sem + 1))
descr = descr.replace("**nosem**", str(no_sem + 1))
if no_sem % 2 == 0:
descr = descr.replace(u"**nomsem**", nom_semestre_dans_parcours)
descr = descr.replace(u"**descr**", u"")
descr = descr.replace("**nomsem**", nom_semestre_dans_parcours)
descr = descr.replace("**descr**", "")
else:
descr = descr.replace(u"**nomsem**", u"")
descr = descr.replace(u"**descr**", nom_semestre_dans_parcours)
descr = descr.replace("**nomsem**", "")
descr = descr.replace("**descr**", nom_semestre_dans_parcours)
reslatex += descr
reslatex += codelatexFin
return reslatex
@ -166,7 +166,7 @@ def get_code_latex_avis_etudiant(
result: chaine unicode
"""
if not donnees_etudiant or not un_avis_latex: # Cas d'un template vide
return annotationPE if annotationPE else u""
return annotationPE if annotationPE else ""
# Le template latex (corps + footer)
code = un_avis_latex + "\n\n" + footer_latex
@ -189,17 +189,17 @@ def get_code_latex_avis_etudiant(
)
# La macro parcourstimeline
elif tag_latex == u"parcourstimeline":
elif tag_latex == "parcourstimeline":
valeur = comp_latex_parcourstimeline(
donnees_etudiant, donnees_etudiant["promo"]
)
# Le tag annotationPE
elif tag_latex == u"annotation":
elif tag_latex == "annotation":
valeur = annotationPE
# Le tag bilanParTag
elif tag_latex == u"bilanParTag":
elif tag_latex == "bilanParTag":
valeur = get_bilanParTag(donnees_etudiant)
# Les tags "simples": par ex. nom, prenom, civilite, ...
@ -249,14 +249,14 @@ def get_annotation_PE(etudid, tag_annotation_pe):
]["comment_u"]
annotationPE = exp.sub(
u"", annotationPE
"", annotationPE
) # Suppression du tag d'annotation PE
annotationPE = annotationPE.replace(u"\r", u"") # Suppression des \r
annotationPE = annotationPE.replace("\r", "") # Suppression des \r
annotationPE = annotationPE.replace(
u"<br/>", u"\n\n"
"<br/>", "\n\n"
) # Interprète les retours chariots html
return annotationPE
return u"" # pas d'annotations
return "" # pas d'annotations
# ----------------------------------------------------------------------------------------
@ -282,7 +282,7 @@ def str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, champ)
):
donnees_numeriques = donnees_etudiant[aggregat][groupe][tag_scodoc]
if champ == "rang":
valeur = u"%s/%d" % (
valeur = "%s/%d" % (
donnees_numeriques[
pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS.index("rang")
],
@ -303,9 +303,9 @@ def str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, champ)
if isinstance(
donnees_numeriques[indice_champ], float
): # valeur numérique avec formattage unicode
valeur = u"%2.2f" % donnees_numeriques[indice_champ]
valeur = "%2.2f" % donnees_numeriques[indice_champ]
else:
valeur = u"%s" % donnees_numeriques[indice_champ]
valeur = "%s" % donnees_numeriques[indice_champ]
return valeur
@ -356,29 +356,27 @@ def get_bilanParTag(donnees_etudiant, groupe="groupe"):
("\\textit{" + rang + "}") if note else ""
) # rang masqué si pas de notes
code_latex = u"\\begin{tabular}{|c|" + "|c" * (len(entete)) + "|}\n"
code_latex += u"\\hline \n"
code_latex = "\\begin{tabular}{|c|" + "|c" * (len(entete)) + "|}\n"
code_latex += "\\hline \n"
code_latex += (
u" & "
" & "
+ " & ".join(["\\textbf{" + intitule + "}" for (agg, intitule, _) in entete])
+ " \\\\ \n"
)
code_latex += u"\\hline"
code_latex += u"\\hline \n"
code_latex += "\\hline"
code_latex += "\\hline \n"
for (i, ligne_val) in enumerate(valeurs["note"]):
titre = lignes[i] # règle le pb d'encodage
code_latex += "\\textbf{" + titre + "} & " + " & ".join(ligne_val) + "\\\\ \n"
code_latex += (
u"\\textbf{" + titre + u"} & " + " & ".join(ligne_val) + u"\\\\ \n"
)
code_latex += (
u" & "
+ u" & ".join(
[u"{\\scriptsize " + clsmt + u"}" for clsmt in valeurs["rang"][i]]
" & "
+ " & ".join(
["{\\scriptsize " + clsmt + "}" for clsmt in valeurs["rang"][i]]
)
+ u"\\\\ \n"
+ "\\\\ \n"
)
code_latex += u"\\hline \n"
code_latex += u"\\end{tabular}"
code_latex += "\\hline \n"
code_latex += "\\end{tabular}"
return code_latex
@ -397,21 +395,15 @@ def get_avis_poursuite_par_etudiant(
nom = jury.syntheseJury[etudid]["nom"].replace(" ", "-")
prenom = jury.syntheseJury[etudid]["prenom"].replace(" ", "-")
nom_fichier = (
u"avis_poursuite_"
+ pe_tools.remove_accents(nom)
+ "_"
+ pe_tools.remove_accents(prenom)
+ "_"
+ str(etudid)
nom_fichier = scu.sanitize_filename(
"avis_poursuite_%s_%s_%s" % (nom, prenom, etudid)
)
if pe_tools.PE_DEBUG:
pe_tools.pe_print("fichier latex =" + nom_fichier, type(nom_fichier))
# Entete (commentaire)
contenu_latex = (
u"%% ---- Etudiant: " + civilite_str + " " + nom + " " + prenom + u"\n"
"%% ---- Etudiant: " + civilite_str + " " + nom + " " + prenom + "\n"
)
# les annnotations

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -52,10 +52,10 @@ from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours # sco_codes_parcours.NEXT -> sem suivant
from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre
from app.scodoc import pe_tagtable
from app.scodoc import pe_tools
from app.scodoc import pe_semestretag
from app.scodoc import pe_settag
from app.pe import pe_tagtable
from app.pe import pe_tools
from app.pe import pe_semestretag
from app.pe import pe_settag
# ----------------------------------------------------------------------------------------
def comp_nom_semestre_dans_parcours(sem):
@ -946,7 +946,7 @@ class JuryPE(object):
return list(taglist)
def get_allTagInSyntheseJury(self):
"""Extrait tous les tags du dictionnaire syntheseJury trié par ordre alphabétique. [] si aucun tag """
"""Extrait tous les tags du dictionnaire syntheseJury trié par ordre alphabétique. [] si aucun tag"""
allTags = set()
for nom in JuryPE.PARCOURS.keys():
allTags = allTags.union(set(self.get_allTagForAggregat(nom)))

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -40,7 +40,7 @@ from app import log
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_cache
from app.scodoc import sco_tag_module
from app.scodoc import pe_tagtable
from app.pe import pe_tagtable
class SemestreTag(pe_tagtable.TableTag):

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -36,8 +36,8 @@ Created on Fri Sep 9 09:15:05 2016
@author: barasc
"""
from app.scodoc.pe_tools import pe_print, PE_DEBUG
from app.scodoc import pe_tagtable
from app.pe.pe_tools import pe_print, PE_DEBUG
from app.pe import pe_tagtable
class SetTag(pe_tagtable.TableTag):

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -44,7 +44,7 @@ import unicodedata
import app.scodoc.sco_utils as scu
from app import log
import six
from app.scodoc.sco_logos import find_logo
PE_DEBUG = 0
@ -145,7 +145,7 @@ def escape_for_latex(s):
}
exp = re.compile(
"|".join(
re.escape(six.text_type(key))
re.escape(key)
for key in sorted(list(conv.keys()), key=lambda item: -len(item))
)
)
@ -167,8 +167,19 @@ def list_directory_filenames(path):
def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip):
"""Read pathname server file and add content to zip under path_in_zip"""
rooted_path_in_zip = os.path.join(ziproot, path_in_zip)
data = open(pathname).read()
zipfile.writestr(rooted_path_in_zip, data)
zipfile.write(filename=pathname, arcname=rooted_path_in_zip)
# data = open(pathname).read()
# zipfile.writestr(rooted_path_in_zip, data)
def add_refs_to_register(register, directory):
"""Ajoute les fichiers trouvés dans directory au registre (dictionaire) sous la forme
filename => pathname
"""
length = len(directory)
for pathname in list_directory_filenames(directory):
filename = pathname[length + 1 :]
register[filename] = pathname
def add_pe_stuff_to_zip(zipfile, ziproot):
@ -179,37 +190,23 @@ def add_pe_stuff_to_zip(zipfile, ziproot):
Also copy logos
"""
register = {}
# first add standard (distrib references)
distrib_dir = os.path.join(REP_DEFAULT_AVIS, "distrib")
distrib_pathnames = list_directory_filenames(
distrib_dir
) # eg /opt/scodoc/tools/doc_poursuites_etudes/distrib/modeles/toto.tex
l = len(distrib_dir)
distrib_filenames = {x[l + 1 :] for x in distrib_pathnames} # eg modeles/toto.tex
add_refs_to_register(register=register, directory=distrib_dir)
# then add local references (some oh them may overwrite distrib refs)
local_dir = os.path.join(REP_LOCAL_AVIS, "local")
local_pathnames = list_directory_filenames(local_dir)
l = len(local_dir)
local_filenames = {x[l + 1 :] for x in local_pathnames}
for filename in distrib_filenames | local_filenames:
if filename in local_filenames:
add_local_file_to_zip(
zipfile, ziproot, os.path.join(local_dir, filename), "avis/" + filename
)
else:
add_local_file_to_zip(
zipfile,
ziproot,
os.path.join(distrib_dir, filename),
"avis/" + filename,
)
add_refs_to_register(register=register, directory=local_dir)
# at this point register contains all refs (filename, pathname) to be saved
for filename, pathname in register.items():
add_local_file_to_zip(zipfile, ziproot, pathname, "avis/" + filename)
# Logos: (add to logos/ directory in zip)
logos_names = ["logo_header.jpg", "logo_footer.jpg"]
for f in logos_names:
logo = os.path.join(scu.SCODOC_LOGOS_DIR, f)
if os.path.isfile(logo):
add_local_file_to_zip(zipfile, ziproot, logo, "avis/logos/" + f)
logos_names = ["header", "footer"]
for name in logos_names:
logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id)
if logo is not None:
add_local_file_to_zip(zipfile, ziproot, logo, "avis/logos/" + logo.filename)
# ----------------------------------------------------------------------------------------

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -42,10 +42,9 @@ from app.scodoc import sco_formsemestre
from app.scodoc import html_sco_header
from app.scodoc import sco_preferences
from app.scodoc import pe_tools
from app.scodoc.pe_tools import PE_LATEX_ENCODING
from app.scodoc import pe_jurype
from app.scodoc import pe_avislatex
from app.pe import pe_tools
from app.pe import pe_jurype
from app.pe import pe_avislatex
def _pe_view_sem_recap_form(formsemestre_id):
@ -90,7 +89,6 @@ def pe_view_sem_recap(
semBase = sco_formsemestre.get_formsemestre(formsemestre_id)
jury = pe_jurype.JuryPE(semBase)
# Ajout avis LaTeX au même zip:
etudids = list(jury.syntheseJury.keys())
@ -150,18 +148,14 @@ def pe_view_sem_recap(
footer_latex,
prefs,
)
jury.add_file_to_zip(
("avis/" + nom_fichier + ".tex").encode(PE_LATEX_ENCODING),
contenu_latex.encode(PE_LATEX_ENCODING),
)
jury.add_file_to_zip("avis/" + nom_fichier + ".tex", contenu_latex)
latex_pages[nom_fichier] = contenu_latex # Sauvegarde dans un dico
# Nouvelle version : 1 fichier par étudiant avec 1 fichier appelant créée ci-dessous
doc_latex = "\n% -----\n".join(
["\\include{" + nom + "}" for nom in sorted(latex_pages.keys())]
)
jury.add_file_to_zip("avis/avis_poursuite.tex", doc_latex.encode(PE_LATEX_ENCODING))
jury.add_file_to_zip("avis/avis_poursuite.tex", doc_latex)
# Ajoute image, LaTeX class file(s) and modeles
pe_tools.add_pe_stuff_to_zip(jury.zipfile, jury.NOM_EXPORT_ZIP)

View File

@ -8,6 +8,13 @@
v 1.3 (python3)
"""
import html
import re
# re validant dd/mm/yyyy
DMY_REGEXP = re.compile(
r"^(?:(?:31(\/|-|\.)(?:0?[13578]|1[02]))\1|(?:(?:29|30)(\/|-|\.)(?:0?[13-9]|1[0-2])\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})$|^(?:29(\/|-|\.)0?2\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:0?[1-9]|1\d|2[0-8])(\/|-|\.)(?:(?:0?[1-9])|(?:1[0-2]))\4(?:(?:1[6-9]|[2-9]\d)?\d{2})$"
)
def TrivialFormulator(
@ -50,7 +57,7 @@ def TrivialFormulator(
allow_null : if true, field can be left empty (default true)
type : 'string', 'int', 'float' (default to string), 'list' (only for hidden)
readonly : default False. if True, no form element, display current value.
convert_numbers: covert int and float values (from string)
convert_numbers: convert int and float values (from string)
allowed_values : list of possible values (default: any value)
validator : function validating the field (called with (value,field)).
min_value : minimum value (for floats and ints)
@ -65,8 +72,8 @@ def TrivialFormulator(
HTML elements:
input_type : 'text', 'textarea', 'password',
'radio', 'menu', 'checkbox',
'hidden', 'separator', 'file', 'date', 'boolcheckbox',
'text_suggest'
'hidden', 'separator', 'file', 'date', 'datedmy' (avec validation),
'boolcheckbox', 'text_suggest'
(default text)
size : text field width
rows, cols: textarea geometry
@ -134,7 +141,7 @@ class TF(object):
is_submitted=False,
):
self.form_url = form_url
self.values = values
self.values = values.copy()
self.formdescription = list(formdescription)
self.initvalues = initvalues
self.method = method
@ -242,6 +249,8 @@ class TF(object):
"Le champ '%s' doit être renseigné" % descr.get("title", field)
)
ok = 0
elif val == "" or val == None:
continue # allowed empty field, skip
# type
typ = descr.get("type", "string")
if val != "" and val != None:
@ -299,6 +308,10 @@ class TF(object):
if not descr["validator"](val, field):
msg.append("valeur invalide (%s) pour le champ '%s'" % (val, field))
ok = 0
elif descr.get("input_type") == "datedmy":
if not DMY_REGEXP.match(val):
msg.append("valeur invalide (%s) pour la date '%s'" % (val, field))
ok = 0
# boolean checkbox
if descr.get("input_type", None) == "boolcheckbox":
if int(val):
@ -333,7 +346,7 @@ class TF(object):
buttons_markup = ""
if self.submitbutton:
buttons_markup += (
'<input type="submit" name="%s_submit" id="%s_submit" value="%s" %s/>'
'<input type="submit" name="%s_submit" id="%s_submit" value="%s" %s>'
% (
self.formid,
self.formid,
@ -343,7 +356,7 @@ class TF(object):
)
if self.cancelbutton:
buttons_markup += (
' <input type="submit" name="%s_cancel" id="%s_cancel" value="%s"/>'
' <input type="submit" name="%s_cancel" id="%s_cancel" value="%s">'
% (self.formid, self.formid, self.cancelbutton)
)
@ -363,7 +376,7 @@ class TF(object):
'<form action="%s" method="%s" id="%s" enctype="%s" name="%s" %s>'
% (self.form_url, self.method, self.formid, enctype, name, klass)
)
R.append('<input type="hidden" name="%s_submitted" value="1"/>' % self.formid)
R.append('<input type="hidden" name="%s_submitted" value="1">' % self.formid)
if self.top_buttons:
R.append(buttons_markup + "<p></p>")
R.append('<table class="tf">')
@ -405,7 +418,7 @@ class TF(object):
else:
checked = ""
lab.append(
'<input type="checkbox" name="%s:list" value="%s" onclick="tf_enable_elem(this)" %s/>'
'<input type="checkbox" name="%s:list" value="%s" onclick="tf_enable_elem(this)" %s>'
% ("tf-checked", field, checked)
)
if title_bubble:
@ -438,13 +451,13 @@ class TF(object):
add_no_enter_js = True
# lem.append('onchange="document.%s.%s.focus()"'%(name,nextitemname))
# lem.append('onblur="document.%s.%s.focus()"'%(name,nextitemname))
lem.append(('value="%(' + field + ')s" />') % values)
lem.append(('value="%(' + field + ')s" >') % values)
elif input_type == "password":
lem.append(
'<input type="password" name="%s" id="%s" size="%d" %s'
% (field, wid, size, attribs)
)
lem.append(('value="%(' + field + ')s" />') % values)
lem.append(('value="%(' + field + ')s" >') % values)
elif input_type == "radio":
labels = descr.get("labels", descr["allowed_values"])
for i in range(len(labels)):
@ -548,24 +561,26 @@ class TF(object):
if descr.get("type", "") == "list":
for v in values[field]:
lem.append(
'<input type="hidden" name="%s:list" value="%s" %s />'
'<input type="hidden" name="%s:list" value="%s" %s >'
% (field, v, attribs)
)
else:
lem.append(
'<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)
)
elif input_type == "separator":
pass
elif input_type == "file":
lem.append(
'<input type="file" name="%s" size="%s" value="%s" %s/>'
'<input type="file" name="%s" size="%s" value="%s" %s>'
% (field, size, values[field], attribs)
)
elif input_type == "date": # JavaScript widget for date input
elif (
input_type == "date" or input_type == "datedmy"
): # JavaScript widget for date input
lem.append(
'<input type="text" name="%s" size="10" value="%s" class="datepicker"/>'
'<input type="text" name="%s" size="10" value="%s" class="datepicker">'
% (field, values[field])
)
elif input_type == "text_suggest":
@ -715,14 +730,16 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
bool_val = 0
R.append(labels[bool_val])
if bool_val:
R.append('<input type="hidden" name="%s" value="1"/>' % field)
R.append('<input type="hidden" name="%s" value="1">' % field)
else:
labels = descr.get("labels", descr["allowed_values"])
for i in range(len(labels)):
if str(descr["allowed_values"][i]) == str(self.values[field]):
R.append('<span class="tf-ro-value">%s</span>' % labels[i])
elif input_type == "textarea":
R.append('<div class="tf-ro-textarea">%s</div>' % self.values[field])
R.append(
'<div class="tf-ro-textarea">%s</div>' % html.escape(self.values[field])
)
elif input_type == "separator" or input_type == "hidden":
pass
elif input_type == "file":

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -167,6 +167,23 @@ def bonus_iutlh(notes_sport, coefs, infos=None):
return bonus
def bonus_nantes(notes_sport, coefs, infos=None):
"""IUT de Nantes (Septembre 2018)
Nous avons différents types de bonification
bonfication Sport / Culture / engagement citoyen
Nous ajoutons sur le bulletin une bonification de 0,2 pour chaque item
la bonification totale ne doit pas excéder les 0,5 point.
Sur le bulletin nous ne mettons pas une note sur 20 mais directement les bonifications.
Dans ScoDoc: on a déclaré une UE "sport&culture" dans laquelle on aura des modules
pour chaque activité (Sport, Associations, ...)
avec à chaque fois une note (ScoDoc l'affichera comme une note sur 20, mais en fait ce sera la
valeur de la bonification: entrer 0,1/20 signifiera un bonus de 0,1 point la moyenne générale)
"""
bonus = min(0.5, sum([x for x in notes_sport])) # plafonnement à 0.5 points
return bonus
# Bonus sport IUT Tours
def bonus_tours(notes_sport, coefs, infos=None):
"""Calcul bonus sport & culture IUT Tours sur moyenne generale
@ -177,7 +194,8 @@ def bonus_tours(notes_sport, coefs, infos=None):
def bonus_iutr(notes_sport, coefs, infos=None):
"""Calcul du bonus , regle de l'IUT de Roanne (contribuée par Raphael C., nov 2012)
"""Calcul du bonus , règle de l'IUT de Roanne
(contribuée par Raphael C., nov 2012)
Le bonus est compris entre 0 et 0.35 point.
cette procédure modifie la moyenne de chaque UE capitalisable.
@ -379,6 +397,39 @@ def bonus_iutbethune(notes_sport, coefs, infos=None):
return bonus
def bonus_iutbeziers(notes_sport, coefs, infos=None):
"""Calcul bonus modules optionels (sport, culture), regle IUT BEZIERS
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
sport , etc) non rattaches à une unité d'enseignement. Les points
au-dessus de 10 sur 20 obtenus dans chacune des matières
optionnelles sont cumulés et 3% de ces points cumulés s'ajoutent à
la moyenne générale du semestre déjà obtenue par l'étudiant.
"""
sumc = sum(coefs) # assumes sum. coefs > 0
# note_sport = sum(map(mul, notes_sport, coefs)) / sumc # moyenne pondérée
bonus = sum([(x - 10) * 0.03 for x in notes_sport if x > 10])
# le total du bonus ne doit pas dépasser 0.3 - Fred, 28/01/2020
if bonus > 0.3:
bonus = 0.3
return bonus
def bonus_iutlr(notes_sport, coefs, infos=None):
"""Calcul bonus modules optionels (sport, culture), règle IUT La Rochelle
Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point
Si la note de sport est comprise entre 10.1 et 20 : ajout de 1% de cette note sur la moyenne générale du semestre
"""
# les coefs sont ignorés
# une seule note
note_sport = notes_sport[0]
if note_sport <= 10:
return 0
bonus = note_sport * 0.01 # 1%
return bonus
def bonus_demo(notes_sport, coefs, infos=None):
"""Fausse fonction "bonus" pour afficher les informations disponibles
et aider les développeurs.
@ -386,8 +437,8 @@ def bonus_demo(notes_sport, coefs, infos=None):
qui est ECRASE à chaque appel.
*** Ne pas utiliser en production !!! ***
"""
f = open("/tmp/scodoc_bonus.log", "w") # mettre 'a' pour ajouter en fin
f.write("\n---------------\n" + pprint.pformat(infos) + "\n")
with open("/tmp/scodoc_bonus.log", "w") as f: # mettre 'a' pour ajouter en fin
f.write("\n---------------\n" + pprint.pformat(infos) + "\n")
# Statut de chaque UE
# for ue_id in infos['moy_ues']:
# ue_status = infos['moy_ues'][ue_id]

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -58,6 +58,7 @@ from app.scodoc import sco_utils as scu
from app.scodoc import sco_excel
from app.scodoc import sco_pdf
from app.scodoc import sco_xml
from app.scodoc.sco_exceptions import ScoPDFFormatError
from app.scodoc.sco_pdf import SU
from app import log
@ -185,6 +186,9 @@ class GenTable(object):
else:
self.preferences = DEFAULT_TABLE_PREFERENCES()
def __repr__(self):
return f"<gen_table( nrows={self.get_nb_rows()}, ncols={self.get_nb_cols()} )>"
def get_nb_cols(self):
return len(self.columns_ids)
@ -468,7 +472,10 @@ class GenTable(object):
def excel(self, wb=None):
"""Simple Excel representation of the table"""
ses = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb)
if wb is None:
ses = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb)
else:
ses = wb.create_sheet(sheet_name=self.xls_sheet_name)
ses.rows += self.xls_before_table
style_bold = sco_excel.excel_make_style(bold=True)
style_base = sco_excel.excel_make_style()
@ -482,9 +489,7 @@ class GenTable(object):
ses.append_blank_row() # empty line
ses.append_single_cell_row(self.origin, style_base)
if wb is None:
return ses.generate_standalone()
else:
ses.generate_embeded()
return ses.generate()
def text(self):
"raw text representation of the table"
@ -494,7 +499,7 @@ class GenTable(object):
headline = []
return "\n".join(
[
self.text_fields_separator.join([x for x in line])
self.text_fields_separator.join([str(x) for x in line])
for line in headline + self.get_data_list()
]
)
@ -535,17 +540,18 @@ class GenTable(object):
#
# titles = ["<para><b>%s</b></para>" % x for x in self.get_titles_list()]
pdf_style_list = []
Pt = [
[Paragraph(SU(str(x)), CellStyle) for x in line]
for line in (
self.get_data_list(
pdf_mode=True,
pdf_style_list=pdf_style_list,
with_titles=True,
omit_hidden_lines=True,
)
)
]
data_list = self.get_data_list(
pdf_mode=True,
pdf_style_list=pdf_style_list,
with_titles=True,
omit_hidden_lines=True,
)
try:
Pt = [
[Paragraph(SU(str(x)), CellStyle) for x in line] for line in data_list
]
except ValueError as exc:
raise ScoPDFFormatError(str(exc)) from exc
pdf_style_list += self.pdf_table_style
T = Table(Pt, repeatRows=1, colWidths=self.pdf_col_widths, style=pdf_style_list)
@ -573,7 +579,7 @@ class GenTable(object):
"""
doc = ElementTree.Element(
self.xml_outer_tag,
id=self.table_id,
id=str(self.table_id),
origin=self.origin or "",
caption=self.caption or "",
)
@ -587,7 +593,7 @@ class GenTable(object):
v = row.get(cid, "")
if v is None:
v = ""
x_cell = ElementTree.Element(cid, value=str(v))
x_cell = ElementTree.Element(str(cid), value=str(v))
x_row.append(x_cell)
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING)
@ -610,7 +616,6 @@ class GenTable(object):
format="html",
page_title="",
filename=None,
REQUEST=None,
javascripts=[],
with_html_headers=True,
publish=True,
@ -643,35 +648,53 @@ class GenTable(object):
H.append(html_sco_header.sco_footer())
return "\n".join(H)
elif format == "pdf":
objects = self.pdf()
doc = sco_pdf.pdf_basic_page(
objects, title=title, preferences=self.preferences
pdf_objs = self.pdf()
pdf_doc = sco_pdf.pdf_basic_page(
pdf_objs, title=title, preferences=self.preferences
)
if publish:
return scu.sendPDFFile(REQUEST, doc, filename + ".pdf")
return scu.send_file(
pdf_doc,
filename,
suffix=".pdf",
mime=scu.PDF_MIMETYPE,
)
else:
return doc
elif format == "xls" or format == "xlsx":
return pdf_doc
elif format == "xls" or format == "xlsx": # dans les 2 cas retourne du xlsx
xls = self.excel()
if publish:
return sco_excel.send_excel_file(
REQUEST, xls, filename + scu.XLSX_SUFFIX
return scu.send_file(
xls,
filename,
suffix=scu.XLSX_SUFFIX,
mime=scu.XLSX_MIMETYPE,
)
else:
return xls
elif format == "text":
return self.text()
elif format == "csv":
return scu.sendCSVFile(REQUEST, self.text(), filename + ".csv")
return scu.send_file(
self.text(),
filename,
suffix=".csv",
mime=scu.CSV_MIMETYPE,
attached=True,
)
elif format == "xml":
xml = self.xml()
if REQUEST and publish:
REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE)
if publish:
return scu.send_file(
xml, filename, suffix=".xml", mime=scu.XML_MIMETYPE
)
return xml
elif format == "json":
js = self.json()
if REQUEST and publish:
REQUEST.RESPONSE.setHeader("content-type", scu.JSON_MIMETYPE)
if publish:
return scu.send_file(
js, filename, suffix=".json", mime=scu.JSON_MIMETYPE
)
return js
else:
log("make_page: format=%s" % format)
@ -731,6 +754,8 @@ if __name__ == "__main__":
)
document.build(objects)
data = doc.getvalue()
open("/tmp/gen_table.pdf", "wb").write(data)
p = T.make_page(format="pdf", REQUEST=None)
open("toto.pdf", "wb").write(p)
with open("/tmp/gen_table.pdf", "wb") as f:
f.write(data)
p = T.make_page(format="pdf")
with open("toto.pdf", "wb") as f:
f.write(p)

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -87,50 +87,48 @@ Problème de connexion (identifiant, mot de passe): <em>contacter votre responsa
)
_TOP_LEVEL_CSS = """
<style type="text/css">
</style>"""
_HTML_BEGIN = """<!DOCTYPE html>
<html lang="fr">
_HTML_BEGIN = """<?xml version="1.0" encoding="%(encoding)s"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>%(page_title)s</title>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Type" content="text/html; charset=%(encoding)s" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta name="LANG" content="fr" />
<meta name="DESCRIPTION" content="ScoDoc" />
<title>%(page_title)s</title>
<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />
<link href="/ScoDoc/static/css/scodoc.css" rel="stylesheet" type="text/css" />
<link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" />
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/menu.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/sorttable.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/bubble.js"></script>
<script type="text/javascript">
<script src="/ScoDoc/static/libjs/menu.js"></script>
<script src="/ScoDoc/static/libjs/sorttable.js"></script>
<script src="/ScoDoc/static/libjs/bubble.js"></script>
<script>
window.onload=function(){enableTooltips("gtrcontent")};
</script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/jQuery/jquery.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/jQuery/jquery-migrate-1.2.0.min.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery.field.min.js"></script>
<script src="/ScoDoc/static/jQuery/jquery.js"></script>
<script src="/ScoDoc/static/jQuery/jquery-migrate-1.2.0.min.js"></script>
<script src="/ScoDoc/static/libjs/jquery.field.min.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
<script src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
<script src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.css" />
<script language="javascript" type="text/javascript" src="/ScoDoc/static/js/scodoc.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/js/etud_info.js"></script>
<script src="/ScoDoc/static/js/scodoc.js"></script>
<script src="/ScoDoc/static/js/etud_info.js"></script>
"""
def scodoc_top_html_header(page_title="ScoDoc: bienvenue"):
H = [
_HTML_BEGIN % {"page_title": page_title, "encoding": scu.SCO_ENCODING},
_TOP_LEVEL_CSS,
"""</head><body class="gtrcontent" id="gtrcontent">""",
"""</head><body id="gtrcontent">""",
scu.CUSTOM_HTML_HEADER_CNX,
]
return "\n".join(H)
@ -145,8 +143,6 @@ def sco_header(
javascripts=[], # additionals JS filenames to load
scripts=[], # script to put in page header
bodyOnLoad="", # JS
init_jquery=True, # load and init jQuery
init_jquery_ui=True, # include all stuff for jquery-ui and initialize scripts
init_qtip=False, # include qTip
init_google_maps=False, # Google maps
init_datatables=True,
@ -181,17 +177,11 @@ def sco_header(
else:
params["margin_left"] = "140px"
if init_jquery_ui or init_qtip or init_datatables:
init_jquery = True
H = [
"""<?xml version="1.0" encoding="%(encoding)s"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
"""<!DOCTYPE html><html lang="fr">
<head>
<meta charset="utf-8"/>
<title>%(page_title)s</title>
<meta http-equiv="Content-Type" content="text/html; charset=%(encoding)s" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta name="LANG" content="fr" />
<meta name="DESCRIPTION" content="ScoDoc" />
@ -199,16 +189,13 @@ def sco_header(
% params
]
# jQuery UI
if init_jquery_ui:
# can modify loaded theme here
H.append(
'<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />\n'
)
# can modify loaded theme here
H.append(
'<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />\n'
)
if init_google_maps:
# It may be necessary to add an API key:
H.append(
'<script type="text/javascript" src="https://maps.google.com/maps/api/js"></script>'
)
H.append('<script src="https://maps.google.com/maps/api/js"></script>')
# Feuilles de style additionnelles:
for cssstyle in cssstyles:
@ -223,9 +210,9 @@ def sco_header(
<link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" />
<link href="/ScoDoc/static/css/gt_table.css" rel="stylesheet" type="text/css" />
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/menu.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/bubble.js"></script>
<script type="text/javascript">
<script src="/ScoDoc/static/libjs/menu.js"></script>
<script src="/ScoDoc/static/libjs/bubble.js"></script>
<script>
window.onload=function(){enableTooltips("gtrcontent")};
var SCO_URL="%(ScoURL)s";
@ -234,52 +221,41 @@ def sco_header(
)
# jQuery
if init_jquery:
H.append(
"""<script language="javascript" type="text/javascript" src="/ScoDoc/static/jQuery/jquery.js"></script>
"""
)
H.append(
'<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery.field.min.js"></script>'
)
H.append(
"""<script src="/ScoDoc/static/jQuery/jquery.js"></script>
"""
)
H.append('<script src="/ScoDoc/static/libjs/jquery.field.min.js"></script>')
# qTip
if init_qtip:
H.append(
'<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>'
'<script src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>'
)
H.append(
'<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.css" />'
)
if init_jquery_ui:
H.append(
'<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>'
)
# H.append('<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery-ui/js/jquery-ui-i18n.js"></script>')
H.append(
'<script language="javascript" type="text/javascript" src="/ScoDoc/static/js/scodoc.js"></script>'
)
H.append(
'<script src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>'
)
H.append('<script src="/ScoDoc/static/js/scodoc.js"></script>')
if init_google_maps:
H.append(
'<script type="text/javascript" src="/ScoDoc/static/libjs/jquery.ui.map.full.min.js"></script>'
'<script src="/ScoDoc/static/libjs/jquery.ui.map.full.min.js"></script>'
)
if init_datatables:
H.append(
'<link rel="stylesheet" type="text/css" href="/ScoDoc/static/DataTables/datatables.min.css"/>'
)
H.append(
'<script type="text/javascript" src="/ScoDoc/static/DataTables/datatables.min.js"></script>'
)
H.append('<script src="/ScoDoc/static/DataTables/datatables.min.js"></script>')
# JS additionels
for js in javascripts:
H.append(
"""<script language="javascript" type="text/javascript" src="/ScoDoc/static/%s"></script>\n"""
% js
)
H.append("""<script src="/ScoDoc/static/%s"></script>\n""" % js)
H.append(
"""<style type="text/css">
.gtrcontent {
"""<style>
#gtrcontent {
margin-left: %(margin_left)s;
height: 100%%;
margin-bottom: 10px;
@ -290,7 +266,7 @@ def sco_header(
)
# Scripts de la page:
if scripts:
H.append("""<script language="javascript" type="text/javascript">""")
H.append("""<script>""")
for script in scripts:
H.append(script)
H.append("""</script>""")
@ -303,7 +279,7 @@ def sco_header(
#
if not no_side_bar:
H.append(html_sidebar.sidebar())
H.append("""<div class="gtrcontent" id="gtrcontent">""")
H.append("""<div id="gtrcontent">""")
#
# Barre menu semestre:
H.append(formsemestre_page_title())
@ -337,13 +313,7 @@ def sco_footer():
def html_sem_header(
REQUEST,
title,
sem=None,
with_page_header=True,
with_h2=True,
page_title=None,
**args
title, sem=None, with_page_header=True, with_h2=True, page_title=None, **args
):
"Titre d'une page semestre avec lien vers tableau de bord"
# sem now unused and thus optional...

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -28,29 +28,25 @@
"""
Génération de la "sidebar" (marge gauche des pages HTML)
"""
from flask import url_for
from flask import g
from flask import request
from flask import render_template, url_for
from flask import g, request
from flask_login import current_user
import app.scodoc.sco_utils as scu
from app.scodoc import sco_preferences
from app.scodoc.sco_permissions import Permission
from sco_version import SCOVERSION
def sidebar_common():
"partie commune à toutes les sidebar"
params = {
"ScoURL": scu.ScoURL(),
"UsersURL": scu.UsersURL(),
"NotesURL": scu.NotesURL(),
"AbsencesURL": scu.AbsencesURL(),
"authuser": current_user.user_name,
}
home_link = url_for("scodoc.index", scodoc_dept=g.scodoc_dept)
H = [
f"""<a class="scodoc_title" href="about">ScoDoc 9</a>
f"""<a class="scodoc_title" href="{home_link}">ScoDoc {SCOVERSION}</a><br>
<a href="{home_link}" class="sidebar">Accueil</a> <br>
<div id="authuser"><a id="authuserlink" href="{
url_for("users.user_info_page", scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
url_for("users.user_info_page",
scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
}">{current_user.user_name}</a>
<br/><a id="deconnectlink" href="{url_for("auth.logout")}">déconnexion</a>
</div>
@ -71,7 +67,8 @@ def sidebar_common():
if current_user.has_permission(Permission.ScoChangePreferences):
H.append(
f"""<a href="{url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)}" class="sidebar">Paramétrage</a> <br/>"""
f"""<a href="{url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)}"
class="sidebar">Paramétrage</a> <br/>"""
)
return "".join(H)
@ -97,11 +94,12 @@ def sidebar():
"""
]
# ---- Il y-a-t-il un etudiant selectionné ?
etudid = None
if request.method == "GET":
etudid = request.args.get("etudid", None)
elif request.method == "POST":
etudid = request.form.get("etudid", None)
etudid = g.get("etudid", None)
if not etudid:
if request.method == "GET":
etudid = request.args.get("etudid", None)
elif request.method == "POST":
etudid = request.form.get("etudid", None)
if etudid:
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
@ -155,8 +153,9 @@ def sidebar():
<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>
</div></div>
<div class="logo-logo"><a href= { url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }
">{ scu.icontag("scologo_img", no_size=True) }</a>
<div class="logo-logo">
<a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }">
{ scu.icontag("scologo_img", no_size=True) }</a>
</div>
</div>
<!-- end of sidebar -->
@ -167,19 +166,7 @@ def sidebar():
def sidebar_dept():
"""Partie supérieure de la marge de gauche"""
H = [
f"""<h2 class="insidebar">Dépt. {sco_preferences.get_preference("DeptName")}</h2>
<a href="{url_for("scodoc.index")}" class="sidebar">Accueil</a> <br/> """
]
dept_intranet_url = sco_preferences.get_preference("DeptIntranetURL")
if dept_intranet_url:
H.append(
f"""<a href="{dept_intranet_url}" class="sidebar">{
sco_preferences.get_preference("DeptIntranetTitle")}</a> <br/>"""
)
# Entreprises pas encore supporté en ScoDoc8
# H.append(
# """<br/><a href="%(ScoURL)s/Entreprises" class="sidebar">Entreprises</a> <br/>"""
# % infos
# )
return "\n".join(H)
return render_template(
"sidebar_dept.html",
prefs=sco_preferences.SemPreferences(),
)

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -27,9 +27,12 @@
"""Various HTML generation functions
"""
from html.parser import HTMLParser
from html.entities import name2codepoint
import re
from flask import g, url_for
import app.scodoc.sco_utils as scu
from . import listhistogram
@ -104,6 +107,8 @@ def make_menu(title, items, css_class="", alone=False):
item["urlq"] = url_for(
item["endpoint"], scodoc_dept=g.scodoc_dept, **args
)
elif "url" in item:
item["urlq"] = item["url"]
else:
item["urlq"] = "#"
item["attr"] = item.get("attr", "")
@ -128,3 +133,63 @@ def make_menu(title, items, css_class="", alone=False):
if alone:
H.append("</ul>")
return "".join(H)
"""
HTML <-> text conversions.
http://stackoverflow.com/questions/328356/extracting-text-from-html-file-using-python
"""
class _HTMLToText(HTMLParser):
def __init__(self):
HTMLParser.__init__(self)
self._buf = []
self.hide_output = False
def handle_starttag(self, tag, attrs):
if tag in ("p", "br") and not self.hide_output:
self._buf.append("\n")
elif tag in ("script", "style"):
self.hide_output = True
def handle_startendtag(self, tag, attrs):
if tag == "br":
self._buf.append("\n")
def handle_endtag(self, tag):
if tag == "p":
self._buf.append("\n")
elif tag in ("script", "style"):
self.hide_output = False
def handle_data(self, text):
if text and not self.hide_output:
self._buf.append(re.sub(r"\s+", " ", text))
def handle_entityref(self, name):
if name in name2codepoint and not self.hide_output:
c = chr(name2codepoint[name])
self._buf.append(c)
def handle_charref(self, name):
if not self.hide_output:
n = int(name[1:], 16) if name.startswith("x") else int(name)
self._buf.append(chr(n))
def get_text(self):
return re.sub(r" +", " ", "".join(self._buf))
def html_to_text(html):
"""
Given a piece of HTML, return the plain text it contains.
This handles entities and char refs, but not javascript and stylesheets.
"""
parser = _HTMLToText()
try:
parser.feed(html)
parser.close()
except: # HTMLParseError: No good replacement?
pass
return parser.get_text()

View File

@ -4,11 +4,8 @@
# Code from http://code.activestate.com/recipes/457411/
from __future__ import print_function
from bisect import bisect_left, bisect_right
from six.moves import zip
class intervalmap(object):
"""

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -27,25 +27,21 @@
"""Calculs sur les notes et cache des resultats
"""
import inspect
import os
import pdb
import time
from operator import itemgetter
from flask import g, url_for
from app.but import bulletin_but
from app.models import FormSemestre, Identite
from app.models import ScoDocSiteConfig
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb
from app import log
from app.scodoc.sco_formulas import NoteVector
from app.scodoc.sco_exceptions import (
AccessDenied,
NoteProcessError,
ScoException,
ScoValueError,
)
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_formsemestre import (
formsemestre_uecoef_list,
formsemestre_uecoef_create,
@ -109,15 +105,13 @@ def get_sem_ues_modimpls(formsemestre_id, modimpls=None):
(utilisé quand on ne peut pas construire nt et faire nt.get_ues())
"""
if modimpls is None:
modimpls = sco_moduleimpl.do_moduleimpl_list(formsemestre_id=formsemestre_id)
modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
uedict = {}
for modimpl in modimpls:
mod = sco_edit_module.do_module_list(args={"module_id": modimpl["module_id"]})[
0
]
mod = sco_edit_module.module_list(args={"module_id": modimpl["module_id"]})[0]
modimpl["module"] = mod
if not mod["ue_id"] in uedict:
ue = sco_edit_ue.do_ue_list(args={"ue_id": mod["ue_id"]})[0]
ue = sco_edit_ue.ue_list(args={"ue_id": mod["ue_id"]})[0]
uedict[ue["ue_id"]] = ue
ues = list(uedict.values())
ues.sort(key=lambda u: u["numero"])
@ -149,7 +143,7 @@ def comp_etud_sum_coef_modules_ue(formsemestre_id, etudid, ue_id):
return s
class NotesTable(object):
class NotesTable:
"""Une NotesTable représente un tableau de notes pour un semestre de formation.
Les colonnes sont des modules.
Les lignes des étudiants.
@ -186,6 +180,8 @@ class NotesTable(object):
self.use_ue_coefs = sco_preferences.get_preference(
"use_ue_coefs", formsemestre_id
)
# si vrai, bloque calcul des moy gen. et d'UE.:
self.block_moyennes = self.sem["block_moyennes"]
# Infos sur les etudiants
self.inscrlist = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
args={"formsemestre_id": formsemestre_id}
@ -204,9 +200,7 @@ class NotesTable(object):
self.inscrlist.sort(key=itemgetter("nomp"))
# { etudid : rang dans l'ordre alphabetique }
rangalpha = {}
for i in range(len(self.inscrlist)):
rangalpha[self.inscrlist[i]["etudid"]] = i
self.rang_alpha = {e["etudid"]: i for i, e in enumerate(self.inscrlist)}
self.bonus = scu.DictDefault(defaultvalue=0)
# Notes dans les modules { moduleimpl_id : { etudid: note_moyenne_dans_ce_module } }
@ -217,26 +211,29 @@ class NotesTable(object):
valid_evals,
mods_att,
self.expr_diagnostics,
) = sco_compute_moy.do_formsemestre_moyennes(self, formsemestre_id)
) = sco_compute_moy.formsemestre_compute_modimpls_moyennes(
self, formsemestre_id
)
self._mods_att = mods_att # liste des modules avec des notes en attente
self._matmoys = {} # moyennes par matieres
self._valid_evals = {} # { evaluation_id : eval }
for e in valid_evals:
self._valid_evals[e["evaluation_id"]] = e # Liste des modules et UE
uedict = {} # public member: { ue_id : ue }
self.uedict = uedict
self.uedict = uedict # les ues qui ont un modimpl dans ce semestre
for modimpl in self._modimpls:
mod = modimpl["module"] # has been added here by do_formsemestre_moyennes
# module has been added by formsemestre_compute_modimpls_moyennes
mod = modimpl["module"]
if not mod["ue_id"] in uedict:
ue = sco_edit_ue.do_ue_list(args={"ue_id": mod["ue_id"]})[0]
ue = sco_edit_ue.ue_list(args={"ue_id": mod["ue_id"]})[0]
uedict[ue["ue_id"]] = ue
else:
ue = uedict[mod["ue_id"]]
modimpl["ue"] = ue # add ue dict to moduleimpl
self._matmoys[mod["matiere_id"]] = {}
mat = sco_edit_matiere.do_matiere_list(
args={"matiere_id": mod["matiere_id"]}
)[0]
mat = sco_edit_matiere.matiere_list(args={"matiere_id": mod["matiere_id"]})[
0
]
modimpl["mat"] = mat # add matiere dict to moduleimpl
# calcul moyennes du module et stocke dans le module
# nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif=
@ -248,6 +245,14 @@ class NotesTable(object):
self.formation["type_parcours"]
)
# En APC, il faut avoir toutes les UE du semestre
# (elles n'ont pas nécessairement un module rattaché):
if self.parcours.APC_SAE:
formsemestre = FormSemestre.query.get(formsemestre_id)
for ue in formsemestre.query_ues():
if ue.id not in self.uedict:
self.uedict[ue.id] = ue.to_dict()
# Decisions jury et UE capitalisées
self.comp_decisions_jury()
self.comp_ue_capitalisees()
@ -257,7 +262,7 @@ class NotesTable(object):
self._ues.sort(key=lambda u: u["numero"])
T = []
# XXX self.comp_ue_coefs(cnx)
self.moy_gen = {} # etudid : moy gen (avec UE capitalisées)
self.moy_ue = {} # ue_id : { etudid : moy ue } (valeur numerique)
self.etud_moy_infos = {} # etudid : resultats de comp_etud_moy_gen()
@ -297,22 +302,12 @@ class NotesTable(object):
t.append(val)
#
t.append(etudid)
T.append(tuple(t))
T.append(t)
self.T = T
# tri par moyennes décroissantes,
# en laissant les demissionnaires a la fin, par ordre alphabetique
def row_key(x):
"""clé de tri par moyennes décroissantes,
en laissant les demissionnaires a la fin, par ordre alphabetique.
(moy_gen, rang_alpha)
"""
try:
moy = -float(x[0])
except (ValueError, TypeError):
moy = 1000.0
return (moy, rangalpha[x[-1]])
T.sort(key=row_key)
self.T = T
self.T.sort(key=self._row_key)
if len(valid_moy):
self.moy_min = min(valid_moy)
@ -342,7 +337,7 @@ class NotesTable(object):
ue_eff = len(
[x for x in val_ids if isinstance(x[0], float)]
) # nombre d'étudiants avec une note dans l'UE
val_ids.sort(key=row_key)
val_ids.sort(key=self._row_key)
ue_rangs[ue_id] = (
comp_ranks(val_ids),
ue_eff,
@ -353,13 +348,24 @@ class NotesTable(object):
for modimpl in self._modimpls:
vals = self._modmoys[modimpl["moduleimpl_id"]]
val_ids = [(vals[etudid], etudid) for etudid in vals.keys()]
val_ids.sort(key=row_key)
val_ids.sort(key=self._row_key)
self.mod_rangs[modimpl["moduleimpl_id"]] = (comp_ranks(val_ids), len(vals))
#
self.compute_moy_moy()
#
log(f"NotesTable( formsemestre_id={formsemestre_id} ) done.")
def _row_key(self, x):
"""clé de tri par moyennes décroissantes,
en laissant les demissionnaires a la fin, par ordre alphabetique.
(moy_gen, rang_alpha)
"""
try:
moy = -float(x[0])
except (ValueError, TypeError):
moy = 1000.0
return (moy, self.rang_alpha[x[-1]])
def get_etudids(self, sorted=False):
if sorted:
# Tri par moy. generale décroissante
@ -611,7 +617,7 @@ class NotesTable(object):
# si 'NI', etudiant non inscrit a ce module
if val != "NI":
est_inscrit = True
if modimpl["module"]["module_type"] == scu.MODULE_STANDARD:
if modimpl["module"]["module_type"] == ModuleType.STANDARD:
coef = modimpl["module"]["coefficient"]
if modimpl["ue"]["type"] != UE_SPORT:
notes.append(val, name=modimpl["module"]["code"])
@ -634,7 +640,8 @@ class NotesTable(object):
matiere_sum_notes += val * coef
matiere_sum_coefs += coef
matiere_id_last = matiere_id
except:
except TypeError: # val == "NI" "NA"
assert val == "NI" or val == "NA" or val == "ERR"
nb_missing = nb_missing + 1
coefs.append(0)
coefs_mask.append(0)
@ -647,11 +654,17 @@ class NotesTable(object):
except:
# log('comp_etud_moy_ue: exception: val=%s coef=%s' % (val,coef))
pass
elif modimpl["module"]["module_type"] == scu.MODULE_MALUS:
elif modimpl["module"]["module_type"] == ModuleType.MALUS:
try:
ue_malus += val
except:
pass # si non inscrit ou manquant, ignore
elif modimpl["module"]["module_type"] in (
ModuleType.RESSOURCE,
ModuleType.SAE,
):
# XXX temporaire pour ne pas bloquer durant le dev
pass
else:
raise ValueError(
"invalid module type (%s)" % modimpl["module"]["module_type"]
@ -675,7 +688,7 @@ class NotesTable(object):
# Recalcule la moyenne en utilisant une formule utilisateur
expr_diag = {}
formula = sco_compute_moy.get_ue_expression(self.formsemestre_id, ue_id, cnx)
formula = sco_compute_moy.get_ue_expression(self.formsemestre_id, ue_id)
if formula:
moy = sco_compute_moy.compute_user_formula(
self.sem,
@ -732,12 +745,11 @@ class NotesTable(object):
Prend toujours en compte les UE capitalisées.
"""
# log('comp_etud_moy_gen(etudid=%s)' % etudid)
# Si l'étudiant a Demissionné ou est DEFaillant, on n'enregistre pas ses moyennes
# Si l'étudiant a Démissionné ou est DEFaillant, on n'enregistre pas ses moyennes
block_computation = (
self.inscrdict[etudid]["etat"] == "D"
or self.inscrdict[etudid]["etat"] == DEF
or self.block_moyennes
)
moy_ues = {}
@ -968,8 +980,8 @@ class NotesTable(object):
def get_table_moyennes_triees(self):
return self.T
def get_etud_rang(self, etudid):
return self.rangs[etudid]
def get_etud_rang(self, etudid) -> str:
return self.rangs.get(etudid, "999")
def get_etud_rang_group(self, etudid, group_id):
"""Returns rank of etud in this group and number of etuds in group.
@ -1056,7 +1068,7 @@ class NotesTable(object):
"Warning: %s capitalized an UE %s which is not part of current sem %s"
% (etudid, ue_id, self.formsemestre_id)
)
ue = sco_edit_ue.do_ue_list(args={"ue_id": ue_id})[0]
ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
self.uedict[ue_id] = ue # record this UE
if ue_id not in self._uecoef:
cl = formsemestre_uecoef_list(
@ -1262,7 +1274,7 @@ class NotesTable(object):
),
self.get_nom_long(etudid),
url_for(
"scolar.formsemestre_edit_uecoefs",
"notes.formsemestre_edit_uecoefs",
scodoc_dept=g.scodoc_dept,
formsemestre_id=self.formsemestre_id,
err_ue_id=ue["ue_id"],
@ -1323,3 +1335,27 @@ class NotesTable(object):
for e in self.get_evaluations_etats()
if e["moduleimpl_id"] == moduleimpl_id
]
def apc_recompute_moyennes(self):
"""recalcule les moyennes en APC (BUT)
et modifie en place le tableau T.
XXX Raccord provisoire avant refonte de cette classe.
"""
assert self.parcours.APC_SAE
formsemestre = FormSemestre.query.get(self.formsemestre_id)
results = bulletin_but.ResultatsSemestreBUT(formsemestre)
# Rappel des épisodes précédents: T est une liste de liste
# Colonnes: 0 moy_gen, moy_ue1, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid
ues = self.get_ues() # incluant le(s) UE de sport
for t in self.T:
etudid = t[-1]
if etudid in results.etud_moy_gen: # evite les démissionnaires
t[0] = results.etud_moy_gen[etudid]
for i, ue in enumerate(ues, start=1):
if ue["type"] != UE_SPORT:
t[i] = results.etud_moy_ue[ue["id"]][etudid]
# re-trie selon la nouvelle moyenne générale:
self.T.sort(key=self._row_key)
# Remplace aussi le rang:
self.rangs = results.etud_moy_gen_ranks

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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

View File

@ -53,7 +53,7 @@ def close_db_connection():
del g.db_conn
def GetDBConnexion(autocommit=True): # on n'utilise plus autocommit
def GetDBConnexion():
return g.db_conn
@ -88,7 +88,15 @@ def SimpleDictFetch(query, args, cursor=None):
return cursor.dictfetchall()
def DBInsertDict(cnx, table, vals, commit=0, convert_empty_to_nulls=1, return_id=True):
def DBInsertDict(
cnx,
table,
vals,
commit=0,
convert_empty_to_nulls=1,
return_id=True,
ignore_conflicts=False,
) -> int:
"""insert into table values in dict 'vals'
Return: id de l'object créé
"""
@ -103,13 +111,18 @@ def DBInsertDict(cnx, table, vals, commit=0, convert_empty_to_nulls=1, return_id
fmt = ",".join(["%%(%s)s" % col for col in cols])
# print 'insert into %s (%s) values (%s)' % (table,colnames,fmt)
oid = None
if ignore_conflicts:
ignore = " ON CONFLICT DO NOTHING"
else:
ignore = ""
try:
if vals:
cursor.execute(
"insert into %s (%s) values (%s)" % (table, colnames, fmt), vals
"insert into %s (%s) values (%s)%s" % (table, colnames, fmt, ignore),
vals,
)
else:
cursor.execute("insert into %s default values" % table)
cursor.execute("insert into %s default values%s" % (table, ignore))
if return_id:
cursor.execute(f"SELECT CURRVAL('{table}_id_seq')") # id créé
oid = cursor.fetchone()[0]
@ -252,8 +265,11 @@ def DBUpdateArgs(cnx, table, vals, where=None, commit=False, convert_empty_to_nu
cursor.execute(req, vals)
# log('req=%s\n'%req)
# log('vals=%s\n'%vals)
except psycopg2.errors.StringDataRightTruncation:
cnx.rollback()
raise ScoValueError("champs de texte trop long !")
except:
cnx.commit() # get rid of this transaction
cnx.rollback() # get rid of this transaction
log('Exception in DBUpdateArgs:\n\treq="%s"\n\tvals="%s"\n' % (req, vals))
raise # and re-raise exception
if commit:
@ -291,6 +307,7 @@ class EditableTable(object):
fields_creators={}, # { field : [ sql_command_to_create_it ] }
filter_nulls=True, # dont allow to set fields to null
filter_dept=False, # ajoute selection sur g.scodoc_dept_id
insert_ignore_conflicts=False,
):
self.table_name = table_name
self.id_name = id_name
@ -311,8 +328,9 @@ class EditableTable(object):
self.filter_nulls = filter_nulls
self.filter_dept = filter_dept
self.sql_default_values = None
self.insert_ignore_conflicts = insert_ignore_conflicts
def create(self, cnx, args):
def create(self, cnx, args) -> int:
"create object in table"
vals = dictfilter(args, self.dbfields, self.filter_nulls)
if self.id_name in vals:
@ -336,6 +354,7 @@ class EditableTable(object):
vals,
commit=True,
return_id=(self.id_name is not None),
ignore_conflicts=self.insert_ignore_conflicts,
)
return new_id
@ -581,6 +600,22 @@ def float_null_is_null(x):
return float(x)
BOOL_STR = {
"": False,
"false": False,
"0": False,
"1": True,
"true": True,
}
def bool_or_str(x) -> bool:
"""a boolean, may also be encoded as a string "0", "False", "1", "True" """
if isinstance(x, str):
return BOOL_STR[x.lower()]
return bool(x)
# post filtering
#
def UniqListofDicts(L, key):

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -474,7 +474,7 @@ def _get_abs_description(a, cursor=None):
desc = a["description"]
if a["moduleimpl_id"] and a["moduleimpl_id"] != "NULL":
# Trouver le nom du module
Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list(
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(
moduleimpl_id=a["moduleimpl_id"]
)
if Mlist:
@ -546,21 +546,21 @@ def list_abs_non_just(etudid, datedebut):
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"""SELECT ETUDID, JOUR, MATIN FROM ABSENCES A
"""SELECT ETUDID, JOUR, MATIN FROM ABSENCES A
WHERE A.ETUDID = %(etudid)s
AND A.estabs
AND A.estabs
AND A.jour >= %(datedebut)s
EXCEPT SELECT ETUDID, JOUR, MATIN FROM ABSENCES B
WHERE B.estjust
EXCEPT SELECT ETUDID, JOUR, MATIN FROM ABSENCES B
WHERE B.estjust
AND B.ETUDID = %(etudid)s
ORDER BY JOUR
""",
vars(),
)
A = cursor.dictfetchall()
for a in A:
abs_list = cursor.dictfetchall()
for a in abs_list:
a["description"] = _get_abs_description(a, cursor=cursor)
return A
return abs_list
def list_abs_just(etudid, datedebut):
@ -570,7 +570,7 @@ def list_abs_just(etudid, datedebut):
cursor.execute(
"""SELECT DISTINCT A.ETUDID, A.JOUR, A.MATIN FROM ABSENCES A, ABSENCES B
WHERE A.ETUDID = %(etudid)s
AND A.ETUDID = B.ETUDID
AND A.ETUDID = B.ETUDID
AND A.JOUR = B.JOUR AND A.MATIN = B.MATIN AND A.JOUR >= %(datedebut)s
AND A.ESTABS AND (A.ESTJUST OR B.ESTJUST)
ORDER BY A.JOUR
@ -626,7 +626,6 @@ def add_absence(
jour,
matin,
estjust,
REQUEST,
description=None,
moduleimpl_id=None,
):
@ -639,8 +638,12 @@ def add_absence(
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"""
INSERT into absences (etudid,jour,estabs,estjust,matin,description, moduleimpl_id)
VALUES (%(etudid)s, %(jour)s, true, %(estjust)s, %(matin)s, %(description)s, %(moduleimpl_id)s )
INSERT into absences
(etudid, jour, estabs, estjust, matin, description, moduleimpl_id)
VALUES
(%(etudid)s, %(jour)s, true, %(estjust)s, %(matin)s,
%(description)s, %(moduleimpl_id)s
)
""",
vars(),
)
@ -656,7 +659,7 @@ def add_absence(
sco_abs_notification.abs_notify(etudid, jour)
def add_justif(etudid, jour, matin, REQUEST, description=None):
def add_justif(etudid, jour, matin, description=None):
"Ajoute un justificatif dans la base"
# unpublished
if _isFarFutur(jour):
@ -665,7 +668,9 @@ def add_justif(etudid, jour, matin, REQUEST, description=None):
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"insert into absences (etudid,jour,estabs,estjust,matin, description) values (%(etudid)s,%(jour)s, FALSE, TRUE, %(matin)s, %(description)s )",
"""INSERT INTO absences (etudid, jour, estabs, estjust, matin, description)
VALUES (%(etudid)s, %(jour)s, FALSE, TRUE, %(matin)s, %(description)s)
""",
vars(),
)
logdb(
@ -678,7 +683,7 @@ def add_justif(etudid, jour, matin, REQUEST, description=None):
invalidate_abs_etud_date(etudid, jour)
def _add_abslist(abslist, REQUEST, moduleimpl_id=None):
def add_abslist(abslist, moduleimpl_id=None):
for a in abslist:
etudid, jour, ampm = a.split(":")
if ampm == "am":
@ -689,7 +694,7 @@ def _add_abslist(abslist, REQUEST, moduleimpl_id=None):
raise ValueError("invalid ampm !")
# ajoute abs si pas deja absent
if count_abs(etudid, jour, jour, matin, moduleimpl_id) == 0:
add_absence(etudid, jour, matin, 0, REQUEST, "", moduleimpl_id)
add_absence(etudid, jour, matin, 0, "", moduleimpl_id)
def annule_absence(etudid, jour, matin, moduleimpl_id=None):
@ -721,7 +726,7 @@ def annule_absence(etudid, jour, matin, moduleimpl_id=None):
invalidate_abs_etud_date(etudid, jour)
def annule_justif(etudid, jour, matin, REQUEST=None):
def annule_justif(etudid, jour, matin):
"Annule un justificatif"
# unpublished
matin = _toboolean(matin)
@ -1027,20 +1032,26 @@ def get_abs_count(etudid, sem):
tuple (nb abs non justifiées, nb abs justifiées)
Utilise un cache.
"""
date_debut = sem["date_debut_iso"]
date_fin = sem["date_fin_iso"]
key = str(etudid) + "_" + date_debut + "_" + date_fin
return get_abs_count_in_interval(etudid, sem["date_debut_iso"], sem["date_fin_iso"])
def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso):
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
tuple (nb abs non justifiées, nb abs justifiées)
Utilise un cache.
"""
key = str(etudid) + "_" + date_debut_iso + "_" + date_fin_iso
r = sco_cache.AbsSemEtudCache.get(key)
if not r:
nb_abs = count_abs( # was CountAbs XXX
nb_abs = count_abs(
etudid=etudid,
debut=date_debut,
fin=date_fin,
debut=date_debut_iso,
fin=date_fin_iso,
)
nb_abs_just = count_abs_just( # XXX was CountAbsJust
nb_abs_just = count_abs_just(
etudid=etudid,
debut=date_debut,
fin=date_fin,
debut=date_debut_iso,
fin=date_fin_iso,
)
r = (nb_abs, nb_abs_just)
ans = sco_cache.AbsSemEtudCache.set(key, r)

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -30,7 +30,7 @@
"""
import datetime
from flask import url_for, g
from flask import url_for, g, request, abort
import app.scodoc.sco_utils as scu
from app.scodoc import notesdb as ndb
@ -58,7 +58,6 @@ def doSignaleAbsence(
estjust=False,
description=None,
etudid=False,
REQUEST=None,
): # etudid implied
"""Signalement d'une absence.
@ -69,7 +68,8 @@ def doSignaleAbsence(
demijournee: 2 si journée complète, 1 matin, 0 après-midi
estjust: absence justifiée
description: str
etudid: etudiant concerné. Si non spécifié, cherche dans REQUEST.form
etudid: etudiant concerné. Si non spécifié, cherche dans
les paramètres de la requête courante.
"""
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
etudid = etud["etudid"]
@ -86,7 +86,6 @@ def doSignaleAbsence(
jour,
False,
estjust,
REQUEST,
description_abs,
moduleimpl_id,
)
@ -95,7 +94,6 @@ def doSignaleAbsence(
jour,
True,
estjust,
REQUEST,
description_abs,
moduleimpl_id,
)
@ -106,7 +104,6 @@ def doSignaleAbsence(
jour,
demijournee,
estjust,
REQUEST,
description_abs,
moduleimpl_id,
)
@ -118,7 +115,7 @@ def doSignaleAbsence(
J = "NON "
M = ""
if moduleimpl_id and moduleimpl_id != "NULL":
mod = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
mod = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
formsemestre_id = mod["formsemestre_id"]
nt = sco_cache.NotesTableCache.get(formsemestre_id)
ues = nt.get_ues(etudid=etudid)
@ -156,7 +153,7 @@ def doSignaleAbsence(
return "\n".join(H)
def SignaleAbsenceEtud(REQUEST=None): # etudid implied
def SignaleAbsenceEtud(): # etudid implied
"""Formulaire individuel simple de signalement d'une absence"""
# brute-force portage from very old dtml code ...
etud = sco_etud.get_etud_info(filled=True)[0]
@ -228,7 +225,6 @@ def SignaleAbsenceEtud(REQUEST=None): # etudid implied
sco_photos.etud_photo_html(
etudid=etudid,
title="fiche de " + etud["nomprenom"],
REQUEST=REQUEST,
),
"""</a></td></tr></table>""",
"""
@ -281,7 +277,6 @@ def doJustifAbsence(
demijournee,
description=None,
etudid=False,
REQUEST=None,
): # etudid implied
"""Justification d'une absence
@ -291,7 +286,8 @@ def doJustifAbsence(
demijournee: 2 si journée complète, 1 matin, 0 après-midi
estjust: absence justifiée
description: str
etudid: etudiant concerné. Si non spécifié, cherche dans REQUEST.form
etudid: etudiant concerné. Si non spécifié, cherche dans les
paramètres de la requête.
"""
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
etudid = etud["etudid"]
@ -305,14 +301,12 @@ def doJustifAbsence(
etudid=etudid,
jour=jour,
matin=False,
REQUEST=REQUEST,
description=description_abs,
)
sco_abs.add_justif(
etudid=etudid,
jour=jour,
matin=True,
REQUEST=REQUEST,
description=description_abs,
)
nbadded += 2
@ -321,7 +315,6 @@ def doJustifAbsence(
etudid=etudid,
jour=jour,
matin=demijournee,
REQUEST=REQUEST,
description=description_abs,
)
nbadded += 1
@ -357,7 +350,7 @@ def doJustifAbsence(
return "\n".join(H)
def JustifAbsenceEtud(REQUEST=None): # etudid implied
def JustifAbsenceEtud(): # etudid implied
"""Formulaire individuel simple de justification d'une absence"""
# brute-force portage from very old dtml code ...
etud = sco_etud.get_etud_info(filled=True)[0]
@ -376,7 +369,6 @@ def JustifAbsenceEtud(REQUEST=None): # etudid implied
sco_photos.etud_photo_html(
etudid=etudid,
title="fiche de " + etud["nomprenom"],
REQUEST=REQUEST,
),
"""</a></td></tr></table>""",
"""
@ -412,9 +404,7 @@ Raison: <input type="text" name="description" size="42"/> (optionnel)
return "\n".join(H)
def doAnnuleAbsence(
datedebut, datefin, demijournee, etudid=False, REQUEST=None
): # etudid implied
def doAnnuleAbsence(datedebut, datefin, demijournee, etudid=False): # etudid implied
"""Annulation des absences pour une demi journée"""
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
etudid = etud["etudid"]
@ -462,7 +452,7 @@ autre absence pour <b>%(nomprenom)s</b></a></li>
return "\n".join(H)
def AnnuleAbsenceEtud(REQUEST=None): # etudid implied
def AnnuleAbsenceEtud(): # etudid implied
"""Formulaire individuel simple d'annulation d'une absence"""
# brute-force portage from very old dtml code ...
etud = sco_etud.get_etud_info(filled=True)[0]
@ -482,7 +472,6 @@ def AnnuleAbsenceEtud(REQUEST=None): # etudid implied
sco_photos.etud_photo_html(
etudid=etudid,
title="fiche de " + etud["nomprenom"],
REQUEST=REQUEST,
),
"""</a></td></tr></table>""",
"""<p>A n'utiliser que suite à une erreur de saisie ou lorsqu'il s'avère que l'étudiant était en fait présent. </p>
@ -548,7 +537,7 @@ def AnnuleAbsenceEtud(REQUEST=None): # etudid implied
return "\n".join(H)
def doAnnuleJustif(datedebut0, datefin0, demijournee, REQUEST=None): # etudid implied
def doAnnuleJustif(datedebut0, datefin0, demijournee): # etudid implied
"""Annulation d'une justification"""
etud = sco_etud.get_etud_info(filled=True)[0]
etudid = etud["etudid"]
@ -558,11 +547,11 @@ def doAnnuleJustif(datedebut0, datefin0, demijournee, REQUEST=None): # etudid i
for jour in dates:
# Attention: supprime matin et après-midi
if demijournee == 2:
sco_abs.annule_justif(etudid, jour, False, REQUEST=REQUEST)
sco_abs.annule_justif(etudid, jour, True, REQUEST=REQUEST)
sco_abs.annule_justif(etudid, jour, False)
sco_abs.annule_justif(etudid, jour, True)
nbadded += 2
else:
sco_abs.annule_justif(etudid, jour, demijournee, REQUEST=REQUEST)
sco_abs.annule_justif(etudid, jour, demijournee)
nbadded += 1
#
H = [
@ -716,7 +705,6 @@ def formChoixSemestreGroupe(all=False):
def CalAbs(etudid, sco_year=None):
"""Calendrier des absences d'un etudiant"""
# crude portage from 1999 DTML
REQUEST = None # XXX
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
etudid = etud["etudid"]
anneescolaire = int(scu.AnneeScolaire(sco_year))
@ -766,7 +754,6 @@ def CalAbs(etudid, sco_year=None):
sco_photos.etud_photo_html(
etudid=etudid,
title="fiche de " + etud["nomprenom"],
REQUEST=REQUEST,
),
),
CalHTML,
@ -786,12 +773,12 @@ def CalAbs(etudid, sco_year=None):
def ListeAbsEtud(
etudid,
etudid=None,
code_nip=None,
with_evals=True,
format="html",
absjust_only=0,
sco_year=None,
REQUEST=None,
):
"""Liste des absences d'un étudiant sur l'année en cours
En format 'html': page avec deux tableaux (non justifiées et justifiées).
@ -804,18 +791,23 @@ def ListeAbsEtud(
absjust_only: si vrai, renvoie table absences justifiées
sco_year: année scolaire à utiliser. Si non spécifier, utilie l'année en cours. e.g. "2005"
"""
absjust_only = int(absjust_only) # si vrai, table absjust seule (export xls ou pdf)
# si absjust_only, table absjust seule (export xls ou pdf)
absjust_only = ndb.bool_or_str(absjust_only)
datedebut = "%s-08-01" % scu.AnneeScolaire(sco_year=sco_year)
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
etudid = etudid or False
etuds = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=True)
if not etuds:
log(f"ListeAbsEtud: no etuds with etudid={etudid} or nip={code_nip}")
abort(404)
etud = etuds[0]
etudid = etud["etudid"]
# Liste des absences et titres colonnes tables:
titles, columns_ids, absnonjust, absjust = _TablesAbsEtud(
titles, columns_ids, absnonjust, absjust = _tables_abs_etud(
etudid, datedebut, with_evals=with_evals, format=format
)
if REQUEST:
base_url_nj = "%s?etudid=%s&absjust_only=0" % (REQUEST.URL0, etudid)
base_url_j = "%s?etudid=%s&absjust_only=1" % (REQUEST.URL0, etudid)
if request.base_url:
base_url_nj = "%s?etudid=%s&absjust_only=0" % (request.base_url, etudid)
base_url_j = "%s?etudid=%s&absjust_only=1" % (request.base_url, etudid)
else:
base_url_nj = base_url_j = ""
tab_absnonjust = GenTable(
@ -844,9 +836,9 @@ def ListeAbsEtud(
# Formats non HTML et demande d'une seule table:
if format != "html" and format != "text":
if absjust_only == 1:
return tab_absjust.make_page(format=format, REQUEST=REQUEST)
return tab_absjust.make_page(format=format)
else:
return tab_absnonjust.make_page(format=format, REQUEST=REQUEST)
return tab_absnonjust.make_page(format=format)
if format == "html":
# Mise en forme HTML:
@ -896,13 +888,12 @@ def ListeAbsEtud(
raise ValueError("Invalid format !")
def _TablesAbsEtud(
def _tables_abs_etud(
etudid,
datedebut,
with_evals=True,
format="html",
absjust_only=0,
REQUEST=None,
):
"""Tables des absences justifiees et non justifiees d'un étudiant
sur l'année en cours
@ -928,11 +919,11 @@ def _TablesAbsEtud(
cursor.execute(
"""SELECT mi.moduleimpl_id
FROM absences abs, notes_moduleimpl_inscription mi, notes_moduleimpl m
WHERE abs.matin = %(matin)s
and abs.jour = %(jour)s
and abs.etudid = %(etudid)s
and abs.moduleimpl_id = mi.moduleimpl_id
and mi.moduleimpl_id = m.id
WHERE abs.matin = %(matin)s
and abs.jour = %(jour)s
and abs.etudid = %(etudid)s
and abs.moduleimpl_id = mi.moduleimpl_id
and mi.moduleimpl_id = m.id
and mi.etudid = %(etudid)s
""",
{
@ -954,13 +945,14 @@ def _TablesAbsEtud(
return ""
ex = []
for ev in a["evals"]:
mod = sco_moduleimpl.do_moduleimpl_withmodule_list(
mod = sco_moduleimpl.moduleimpl_withmodule_list(
moduleimpl_id=ev["moduleimpl_id"]
)[0]
if format == "html":
ex.append(
'<a href="Notes/moduleimpl_status?moduleimpl_id=%s">%s</a>'
% (mod["moduleimpl_id"], mod["module"]["code"])
f"""<a href="{url_for('notes.moduleimpl_status',
scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"])}
">{mod["module"]["code"]}</a>"""
)
else:
ex.append(mod["module"]["code"])
@ -971,13 +963,14 @@ def _TablesAbsEtud(
def descr_abs(a):
ex = []
for ev in a.get("absent", []):
mod = sco_moduleimpl.do_moduleimpl_withmodule_list(
mod = sco_moduleimpl.moduleimpl_withmodule_list(
moduleimpl_id=ev["moduleimpl_id"]
)[0]
if format == "html":
ex.append(
'<a href="Notes/moduleimpl_status?moduleimpl_id=%s">%s</a>'
% (mod["moduleimpl_id"], mod["module"]["code"])
f"""<a href="{url_for('notes.moduleimpl_status',
scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"])}
">{mod["module"]["code"]}</a>"""
)
else:
ex.append(mod["module"]["code"])

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -108,13 +108,14 @@ def apo_compare_csv(A_file, B_file, autodetect=True):
def _load_apo_data(csvfile, autodetect=True):
"Read data from request variable and build ApoData"
data = csvfile.read()
data_b = csvfile.read()
if autodetect:
data, message = sco_apogee_csv.fix_data_encoding(data)
data_b, message = sco_apogee_csv.fix_data_encoding(data_b)
if message:
log("apo_compare_csv: %s" % message)
if not data:
if not data_b:
raise ScoValueError("apo_compare_csv: no data")
data = data_b.decode(sco_apogee_csv.APO_INPUT_ENCODING)
apo_data = sco_apogee_csv.ApoData(data, orig_filename=csvfile.filename)
return apo_data
@ -256,8 +257,8 @@ def apo_table_compare_etud_results(A, B):
"prenom": "Prénom",
"elt_code": "Element",
"type_res": "Type",
"val_A": "A: %s" % A.orig_filename or "",
"val_B": "B: %s" % B.orig_filename or "",
"val_A": "A: %s" % (A.orig_filename or ""),
"val_B": "B: %s" % (B.orig_filename or ""),
},
columns_ids=("nip", "nom", "prenom", "elt_code", "type_res", "val_A", "val_B"),
html_class="table_leftalign",

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -98,7 +98,7 @@ from chardet import detect as chardet_detect
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log
from app.scodoc.sco_exceptions import ScoValueError, FormatError
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_vdi import ApoEtapeVDI
from app.scodoc.sco_codes_parcours import code_semestre_validant
@ -173,8 +173,10 @@ def guess_data_encoding(text, threshold=0.6):
def fix_data_encoding(
text, default_source_encoding=APO_INPUT_ENCODING, dest_encoding=APO_INPUT_ENCODING
):
text: bytes,
default_source_encoding=APO_INPUT_ENCODING,
dest_encoding=APO_INPUT_ENCODING,
) -> bytes:
"""Try to ensure that text is using dest_encoding
returns converted text, and a message describing the conversion.
"""
@ -200,7 +202,7 @@ def fix_data_encoding(
class StringIOFileLineWrapper(object):
def __init__(self, data):
def __init__(self, data: str):
self.f = io.StringIO(data)
self.lineno = 0
@ -655,7 +657,7 @@ class ApoEtud(dict):
class ApoData(object):
def __init__(
self,
data,
data: str,
periode=None,
export_res_etape=True,
export_res_sem=True,
@ -681,7 +683,7 @@ class ApoData(object):
self.periode = periode #
try:
self.read_csv(data)
except FormatError as e:
except ScoFormatError as e:
# essaie de retrouver le nom du fichier pour enrichir le message d'erreur
filename = ""
if self.orig_filename is None:
@ -689,11 +691,11 @@ class ApoData(object):
filename = self.titles.get("apoC_Fichier_Exp", filename)
else:
filename = self.orig_filename
raise FormatError(
raise ScoFormatError(
"<h3>Erreur lecture du fichier Apogée <tt>%s</tt></h3><p>" % filename
+ e.args[0]
+ "</p>"
)
) from e
self.etape_apogee = self.get_etape_apogee() # 'V1RT'
self.vdi_apogee = self.get_vdi_apogee() # '111'
self.etape = ApoEtapeVDI(etape=self.etape_apogee, vdi=self.vdi_apogee)
@ -759,16 +761,18 @@ class ApoData(object):
def read_csv(self, data: str):
if not data:
raise FormatError("Fichier Apogée vide !")
raise ScoFormatError("Fichier Apogée vide !")
f = StringIOFileLineWrapper(data) # pour traiter comme un fichier
# check that we are at the begining of Apogee CSV
line = f.readline().strip()
if line != "XX-APO_TITRES-XX":
raise FormatError("format incorrect: pas de XX-APO_TITRES-XX")
raise ScoFormatError("format incorrect: pas de XX-APO_TITRES-XX")
# 1-- En-tête: du début jusqu'à la balise XX-APO_VALEURS-XX
idx = data.index("XX-APO_VALEURS-XX")
try:
idx = data.index("XX-APO_VALEURS-XX")
except ValueError as exc:
raise ScoFormatError("format incorrect: pas de XX-APO_VALEURS-XX") from exc
self.header = data[:idx]
# 2-- Titres:
@ -779,13 +783,13 @@ class ApoData(object):
# 3-- La section XX-APO_TYP_RES-XX est ignorée:
line = f.readline().strip()
if line != "XX-APO_TYP_RES-XX":
raise FormatError("format incorrect: pas de XX-APO_TYP_RES-XX")
raise ScoFormatError("format incorrect: pas de XX-APO_TYP_RES-XX")
_apo_skip_section(f)
# 4-- Définition de colonnes: (on y trouve aussi l'étape)
line = f.readline().strip()
if line != "XX-APO_COLONNES-XX":
raise FormatError("format incorrect: pas de XX-APO_COLONNES-XX")
raise ScoFormatError("format incorrect: pas de XX-APO_COLONNES-XX")
self.cols = _apo_read_cols(f)
self.apo_elts = self._group_elt_cols(self.cols)
@ -794,7 +798,7 @@ class ApoData(object):
while True: # skip
line = f.readline()
if not line:
raise FormatError("format incorrect: pas de XX-APO_VALEURS-XX")
raise ScoFormatError("format incorrect: pas de XX-APO_VALEURS-XX")
if line.strip() == "XX-APO_VALEURS-XX":
break
self.column_titles = f.readline()
@ -885,7 +889,7 @@ class ApoData(object):
"""
m = re.match("[12][0-9]{3}", self.titles["apoC_annee"])
if not m:
raise FormatError(
raise ScoFormatError(
'Annee scolaire (apoC_annee) invalide: "%s"' % self.titles["apoC_annee"]
)
return int(m.group(0))
@ -943,7 +947,7 @@ class ApoData(object):
log("Fichier Apogee invalide:")
log("Colonnes declarees: %s" % declared)
log("Colonnes presentes: %s" % present)
raise FormatError(
raise ScoFormatError(
"""Fichier Apogee invalide<br/>Colonnes declarees: <tt>%s</tt>
<br/>Colonnes presentes: <tt>%s</tt>"""
% (declared, present)
@ -1032,7 +1036,7 @@ def _apo_read_cols(f):
line = f.readline().strip(" " + APO_NEWLINE)
fs = line.split(APO_SEP)
if fs[0] != "apoL_a01_code":
raise FormatError("invalid line: %s (expecting apoL_a01_code)" % line)
raise ScoFormatError("invalid line: %s (expecting apoL_a01_code)" % line)
col_keys = fs
while True: # skip premiere partie (apoL_a02_nom, ...)
@ -1052,14 +1056,14 @@ def _apo_read_cols(f):
# sanity check
col_id = fs[0] # apoL_c0001, ...
if col_id in cols:
raise FormatError("duplicate column definition: %s" % col_id)
raise ScoFormatError("duplicate column definition: %s" % col_id)
m = re.match(r"^apoL_c([0-9]{4})$", col_id)
if not m:
raise FormatError(
raise ScoFormatError(
"invalid column id: %s (expecting apoL_c%04d)" % (line, col_id)
)
if int(m.group(1)) != i:
raise FormatError("invalid column id: %s for index %s" % (col_id, i))
raise ScoFormatError("invalid column id: %s for index %s" % (col_id, i))
cols[col_id] = DictCol(list(zip(col_keys, fs)))
cols[col_id].lineno = f.lineno # for debuging purpose
@ -1083,14 +1087,14 @@ def _apo_read_TITRES(f):
else:
log("Error read CSV: \nline=%s\nfields=%s" % (line, fields))
log(dir(f))
raise FormatError(
raise ScoFormatError(
"Fichier Apogee incorrect (section titres, %d champs au lieu de 2)"
% len(fields)
)
d[k] = v
#
if not d.get("apoC_Fichier_Exp", None):
raise FormatError("Fichier Apogee incorrect: pas de titre apoC_Fichier_Exp")
raise ScoFormatError("Fichier Apogee incorrect: pas de titre apoC_Fichier_Exp")
# keep only basename: may be a windows or unix pathname
s = d["apoC_Fichier_Exp"].split("/")[-1]
s = s.split("\\")[-1] # for DOS paths, eg C:\TEMP\VL4RT_V3ASR.TXT
@ -1178,7 +1182,7 @@ def nar_etuds_table(apo_data, NAR_Etuds):
def export_csv_to_apogee(
apo_csv_data,
apo_csv_data: str,
periode=None,
dest_zip=None,
export_res_etape=True,

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -29,37 +29,41 @@
Archives are plain files, stored in
<SCODOC_VAR_DIR>/archives/<deptid>
(where <SCODOC_VAR_DIR> is usually /opt/scodoc-data, and <deptid> a departement id)
<SCODOC_VAR_DIR>/archives/<dept_id>
(where <SCODOC_VAR_DIR> is usually /opt/scodoc-data, and <dept_id> a departement id (int))
Les PV de jurys et documents associés sont stockées dans un sous-repertoire de la forme
<archivedir>/<dept>/<formsemestre_id>/<YYYY-MM-DD-HH-MM-SS>
(formsemestre_id est ici FormSemestre.scodoc7_id ou à défaut FormSemestre.id)
(formsemestre_id est ici FormSemestre.id)
Les documents liés à l'étudiant sont dans
<archivedir>/docetuds/<dept>/<etudid>/<YYYY-MM-DD-HH-MM-SS>
(etudid est ici soit Identite.scodoc7id, soit à défaut Identite.id)
<archivedir>/docetuds/<dept_id>/<etudid>/<YYYY-MM-DD-HH-MM-SS>
(etudid est ici Identite.id)
Les maquettes Apogée pour l'export des notes sont dans
<archivedir>/apo_csv/<dept>/<annee_scolaire>-<sem_id>/<YYYY-MM-DD-HH-MM-SS>/<code_etape>.csv
<archivedir>/apo_csv/<dept_id>/<annee_scolaire>-<sem_id>/<YYYY-MM-DD-HH-MM-SS>/<code_etape>.csv
Un répertoire d'archive contient des fichiers quelconques, et un fichier texte nommé _description.txt
qui est une description (humaine, format libre) de l'archive.
"""
import os
import time
import chardet
import datetime
import glob
import mimetypes
import os
import re
import shutil
import glob
import time
import flask
from flask import g
from flask import g, request
from flask_login import current_user
import app.scodoc.sco_utils as scu
from config import Config
from app import log
from app.models import Departement
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_exceptions import (
AccessDenied,
@ -108,7 +112,8 @@ class BaseArchiver(object):
If directory does not yet exist, create it.
"""
self.initialize()
dept_dir = os.path.join(self.root, g.scodoc_dept)
dept = Departement.query.filter_by(acronym=g.scodoc_dept).first()
dept_dir = os.path.join(self.root, str(dept.id))
try:
scu.GSL.acquire()
if not os.path.isdir(dept_dir):
@ -127,7 +132,8 @@ class BaseArchiver(object):
:return: list of archive oids
"""
self.initialize()
base = os.path.join(self.root, g.scodoc_dept) + os.path.sep
dept = Departement.query.filter_by(acronym=g.scodoc_dept).first()
base = os.path.join(self.root, str(dept.id)) + os.path.sep
dirs = glob.glob(base + "*")
return [os.path.split(x)[1] for x in dirs]
@ -198,7 +204,17 @@ class BaseArchiver(object):
def get_archive_description(self, archive_id):
"""Return description of archive"""
self.initialize()
return open(os.path.join(archive_id, "_description.txt")).read()
filename = os.path.join(archive_id, "_description.txt")
try:
with open(filename) as f:
descr = f.read()
except UnicodeDecodeError:
# some (old) files may have saved under exotic encodings
with open(filename, "rb") as f:
data = f.read()
descr = data.decode(chardet.detect(data)["encoding"])
return descr
def create_obj_archive(self, oid: int, description: str):
"""Creates a new archive for this object and returns its id."""
@ -227,9 +243,8 @@ class BaseArchiver(object):
try:
scu.GSL.acquire()
fname = os.path.join(archive_id, filename)
f = open(fname, "wb")
f.write(data)
f.close()
with open(fname, "wb") as f:
f.write(data)
finally:
scu.GSL.release()
return filename
@ -242,33 +257,19 @@ class BaseArchiver(object):
raise ValueError("invalid filename")
fname = os.path.join(archive_id, filename)
log("reading archive file %s" % fname)
return open(fname, "rb").read()
with open(fname, "rb") as f:
data = f.read()
return data
def get_archived_file(self, REQUEST, oid, archive_name, filename):
def get_archived_file(self, oid, archive_name, filename):
"""Recupere donnees du fichier indiqué et envoie au client"""
# XXX très incomplet: devrait inférer et assigner un type MIME
archive_id = self.get_id_from_name(oid, archive_name)
data = self.get(archive_id, filename)
ext = os.path.splitext(filename.lower())[1]
if ext == ".html" or ext == ".htm":
return data
elif ext == ".xml":
REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE)
return data
elif ext == ".xls":
return sco_excel.send_excel_file(
REQUEST, data, filename, mime=scu.XLS_MIMETYPE
)
elif ext == ".xlsx":
return sco_excel.send_excel_file(
REQUEST, data, filename, mime=scu.XLSX_MIMETYPE
)
elif ext == ".csv":
return scu.sendCSVFile(REQUEST, data, filename)
elif ext == ".pdf":
return scu.sendPDFFile(REQUEST, data, filename)
REQUEST.RESPONSE.setHeader("content-type", "application/octet-stream")
return data # should set mimetype for known files like images
mime = mimetypes.guess_type(filename)[0]
if mime is None:
mime = "application/octet-stream"
return scu.send_file(data, filename, mime=mime)
class SemsArchiver(BaseArchiver):
@ -283,7 +284,6 @@ PVArchive = SemsArchiver()
def do_formsemestre_archive(
REQUEST,
formsemestre_id,
group_ids=[], # si indiqué, ne prend que ces groupes
description="",
@ -305,7 +305,7 @@ def do_formsemestre_archive(
from app.scodoc.sco_recapcomplet import make_formsemestre_recapcomplet
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
sem_archive_id = sem["scodoc7_id"] or formsemestre_id
sem_archive_id = formsemestre_id
archive_id = PVArchive.create_obj_archive(sem_archive_id, description)
date = PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M")
@ -351,14 +351,12 @@ def do_formsemestre_archive(
data = data.encode(scu.SCO_ENCODING)
PVArchive.store(archive_id, "Bulletins.xml", data)
# Decisions de jury, en XLS
data = sco_pvjury.formsemestre_pvjury(
formsemestre_id, format="xls", REQUEST=REQUEST, publish=False
)
data = sco_pvjury.formsemestre_pvjury(formsemestre_id, format="xls", publish=False)
if data:
PVArchive.store(archive_id, "Decisions_Jury" + scu.XLSX_SUFFIX, data)
# Classeur bulletins (PDF)
data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
formsemestre_id, REQUEST, version=bulVersion
formsemestre_id, version=bulVersion
)
if data:
PVArchive.store(archive_id, "Bulletins.pdf", data)
@ -389,14 +387,12 @@ def do_formsemestre_archive(
PVArchive.store(archive_id, "PV_Jury%s.pdf" % groups_filename, data)
def formsemestre_archive(REQUEST, formsemestre_id, group_ids=[]):
def formsemestre_archive(formsemestre_id, group_ids=[]):
"""Make and store new archive for this formsemestre.
(all students or only selected groups)
"""
if not sco_permissions_check.can_edit_pv(formsemestre_id):
raise AccessDenied(
"opération non autorisée pour %s" % str(REQUEST.AUTHENTICATED_USER)
)
raise AccessDenied("opération non autorisée pour %s" % str(current_user))
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if not group_ids:
@ -408,7 +404,6 @@ def formsemestre_archive(REQUEST, formsemestre_id, group_ids=[]):
H = [
html_sco_header.html_sem_header(
REQUEST,
"Archiver les PV et résultats du semestre",
sem=sem,
javascripts=sco_groups_view.JAVASCRIPTS,
@ -469,8 +464,8 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
)
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
descr,
cancelbutton="Annuler",
method="POST",
@ -492,7 +487,6 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
else:
tf[2]["anonymous"] = False
do_formsemestre_archive(
REQUEST,
formsemestre_id,
group_ids=group_ids,
description=tf[2]["description"],
@ -516,10 +510,10 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
)
def formsemestre_list_archives(REQUEST, formsemestre_id):
def formsemestre_list_archives(formsemestre_id):
"""Page listing archives"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
sem_archive_id = sem["scodoc7_id"] or formsemestre_id
sem_archive_id = formsemestre_id
L = []
for archive_id in PVArchive.list_obj_archives(sem_archive_id):
a = {
@ -530,7 +524,7 @@ def formsemestre_list_archives(REQUEST, formsemestre_id):
}
L.append(a)
H = [html_sco_header.html_sem_header(REQUEST, "Archive des PV et résultats ", sem)]
H = [html_sco_header.html_sem_header("Archive des PV et résultats ", sem)]
if not L:
H.append("<p>aucune archive enregistrée</p>")
else:
@ -559,23 +553,19 @@ def formsemestre_list_archives(REQUEST, formsemestre_id):
return "\n".join(H) + html_sco_header.sco_footer()
def formsemestre_get_archived_file(REQUEST, formsemestre_id, archive_name, filename):
def formsemestre_get_archived_file(formsemestre_id, archive_name, filename):
"""Send file to client."""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
sem_archive_id = sem["scodoc7_id"] or formsemestre_id
return PVArchive.get_archived_file(REQUEST, sem_archive_id, archive_name, filename)
sem_archive_id = formsemestre_id
return PVArchive.get_archived_file(sem_archive_id, archive_name, filename)
def formsemestre_delete_archive(
REQUEST, formsemestre_id, archive_name, dialog_confirmed=False
):
def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed=False):
"""Delete an archive"""
if not sco_permissions_check.can_edit_pv(formsemestre_id):
raise AccessDenied(
"opération non autorisée pour %s" % str(REQUEST.AUTHENTICATED_USER)
)
raise AccessDenied("opération non autorisée pour %s" % str(current_user))
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
sem_archive_id = sem["scodoc7_id"] or formsemestre_id
sem_archive_id = formsemestre_id
archive_id = PVArchive.get_id_from_name(sem_archive_id, archive_name)
dest_url = "formsemestre_list_archives?formsemestre_id=%s" % (formsemestre_id)

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -30,7 +30,9 @@
les dossiers d'admission et autres pièces utiles.
"""
import flask
from flask import url_for, g
from flask import url_for, render_template
from flask import g, request
from flask_login import current_user
import app.scodoc.sco_utils as scu
from app.scodoc import sco_import_etuds
@ -58,14 +60,14 @@ def can_edit_etud_archive(authuser):
return authuser.has_permission(Permission.ScoEtudAddAnnotations)
def etud_list_archives_html(REQUEST, etudid):
def etud_list_archives_html(etudid):
"""HTML snippet listing archives"""
can_edit = can_edit_etud_archive(REQUEST.AUTHENTICATED_USER)
can_edit = can_edit_etud_archive(current_user)
etuds = sco_etud.get_etud_info(etudid=etudid)
if not etuds:
raise ScoValueError("étudiant inexistant")
etud = etuds[0]
etud_archive_id = etud["scodoc7_id"] or etudid
etud_archive_id = etudid
L = []
for archive_id in EtudsArchive.list_obj_archives(etud_archive_id):
a = {
@ -118,7 +120,7 @@ def add_archives_info_to_etud_list(etuds):
"""
for etud in etuds:
l = []
etud_archive_id = etud["scodoc7_id"] or etud["etudid"]
etud_archive_id = etud["etudid"]
for archive_id in EtudsArchive.list_obj_archives(etud_archive_id):
l.append(
"%s (%s)"
@ -130,13 +132,11 @@ def add_archives_info_to_etud_list(etuds):
etud["etudarchive"] = ", ".join(l)
def etud_upload_file_form(REQUEST, etudid):
def etud_upload_file_form(etudid):
"""Page with a form to choose and upload a file, with a description."""
# check permission
if not can_edit_etud_archive(REQUEST.AUTHENTICATED_USER):
raise AccessDenied(
"opération non autorisée pour %s" % REQUEST.AUTHENTICATED_USER
)
if not can_edit_etud_archive(current_user):
raise AccessDenied("opération non autorisée pour %s" % current_user)
etuds = sco_etud.get_etud_info(filled=True)
if not etuds:
raise ScoValueError("étudiant inexistant")
@ -153,8 +153,8 @@ def etud_upload_file_form(REQUEST, etudid):
% (scu.CONFIG.ETUD_MAX_FILE_SIZE // (1024 * 1024)),
]
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
(
("etudid", {"default": etudid, "input_type": "hidden"}),
("datafile", {"input_type": "file", "title": "Fichier", "size": 30}),
@ -181,7 +181,7 @@ def etud_upload_file_form(REQUEST, etudid):
data = tf[2]["datafile"].read()
descr = tf[2]["description"]
filename = tf[2]["datafile"].filename
etud_archive_id = etud["scodoc7_id"] or etud["etudid"]
etud_archive_id = etud["etudid"]
_store_etud_file_to_new_archive(
etud_archive_id, data, filename, description=descr
)
@ -199,18 +199,16 @@ def _store_etud_file_to_new_archive(etud_archive_id, data, filename, description
EtudsArchive.store(archive_id, filename, data)
def etud_delete_archive(REQUEST, etudid, archive_name, dialog_confirmed=False):
def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
"""Delete an archive"""
# check permission
if not can_edit_etud_archive(REQUEST.AUTHENTICATED_USER):
raise AccessDenied(
"opération non autorisée pour %s" % str(REQUEST.AUTHENTICATED_USER)
)
if not can_edit_etud_archive(current_user):
raise AccessDenied("opération non autorisée pour %s" % str(current_user))
etuds = sco_etud.get_etud_info(filled=True)
if not etuds:
raise ScoValueError("étudiant inexistant")
etud = etuds[0]
etud_archive_id = etud["scodoc7_id"] or etud["etudid"]
etud_archive_id = etud["etudid"]
archive_id = EtudsArchive.get_id_from_name(etud_archive_id, archive_name)
if not dialog_confirmed:
return scu.confirm_dialog(
@ -242,20 +240,18 @@ def etud_delete_archive(REQUEST, etudid, archive_name, dialog_confirmed=False):
)
def etud_get_archived_file(REQUEST, etudid, archive_name, filename):
def etud_get_archived_file(etudid, archive_name, filename):
"""Send file to client."""
etuds = sco_etud.get_etud_info(filled=True)
etuds = sco_etud.get_etud_info(etudid=etudid, filled=True)
if not etuds:
raise ScoValueError("étudiant inexistant")
etud = etuds[0]
etud_archive_id = etud["scodoc7_id"] or etud["etudid"]
return EtudsArchive.get_archived_file(
REQUEST, etud_archive_id, archive_name, filename
)
etud_archive_id = etud["etudid"]
return EtudsArchive.get_archived_file(etud_archive_id, archive_name, filename)
# --- Upload d'un ensemble de fichiers (pour un groupe d'étudiants)
def etudarchive_generate_excel_sample(group_id=None, REQUEST=None):
def etudarchive_generate_excel_sample(group_id=None):
"""Feuille excel pour import fichiers etudiants (utilisé pour admissions)"""
fmt = sco_import_etuds.sco_import_format()
data = sco_import_etuds.sco_import_generate_excel_sample(
@ -271,12 +267,15 @@ def etudarchive_generate_excel_sample(group_id=None, REQUEST=None):
],
extra_cols=["fichier_a_charger"],
)
return sco_excel.send_excel_file(
REQUEST, data, "ImportFichiersEtudiants" + scu.XLSX_SUFFIX
return scu.send_file(
data,
"ImportFichiersEtudiants",
suffix=scu.XLSX_SUFFIX,
mime=scu.XLSX_MIMETYPE,
)
def etudarchive_import_files_form(group_id, REQUEST=None):
def etudarchive_import_files_form(group_id):
"""Formulaire pour importation fichiers d'un groupe"""
H = [
html_sco_header.sco_header(
@ -310,8 +309,8 @@ def etudarchive_import_files_form(group_id, REQUEST=None):
]
F = html_sco_header.sco_footer()
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
(
("xlsfile", {"title": "Fichier Excel:", "input_type": "file", "size": 40}),
("zipfile", {"title": "Fichier zip:", "input_type": "file", "size": 40}),
@ -330,9 +329,9 @@ def etudarchive_import_files_form(group_id, REQUEST=None):
if tf[0] == 0:
return "\n".join(H) + tf[1] + "</li></ol>" + F
elif tf[0] == -1:
# retrouve le semestre à partir du groupe:
group = sco_groups.get_group(group_id)
# retrouve le semestre à partir du groupe:
group = sco_groups.get_group(group_id)
if tf[0] == -1:
return flask.redirect(
url_for(
"notes.formsemestre_status",
@ -342,21 +341,41 @@ def etudarchive_import_files_form(group_id, REQUEST=None):
)
else:
return etudarchive_import_files(
group_id=tf[2]["group_id"],
formsemestre_id=group["formsemestre_id"],
xlsfile=tf[2]["xlsfile"],
zipfile=tf[2]["zipfile"],
description=tf[2]["description"],
)
def etudarchive_import_files(group_id=None, xlsfile=None, zipfile=None, description=""):
def etudarchive_import_files(
formsemestre_id=None, xlsfile=None, zipfile=None, description=""
):
"Importe des fichiers"
def callback(etud, data, filename):
_store_etud_file_to_new_archive(etud["etudid"], data, filename, description)
filename_title = "fichier_a_charger"
page_title = "Téléchargement de fichiers associés aux étudiants"
# Utilise la fontion au depart developpee pour les photos
r = sco_trombino.zip_excel_import_files(
xlsfile, zipfile, callback, filename_title, page_title
# Utilise la fontion developpée au depart pour les photos
(
ignored_zipfiles,
unmatched_files,
stored_etud_filename,
) = sco_trombino.zip_excel_import_files(
xlsfile=xlsfile,
zipfile=zipfile,
callback=callback,
filename_title="fichier_a_charger",
)
return render_template(
"scolar/photos_import_files.html",
page_title="Téléchargement de fichiers associés aux étudiants",
ignored_zipfiles=ignored_zipfiles,
unmatched_files=unmatched_files,
stored_etud_filename=stored_etud_filename,
next_page=url_for(
"scolar.groups_view",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
),
)
return r + html_sco_header.sco_footer()

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -28,6 +28,7 @@
"""Génération des bulletins de notes
"""
from app.models import formsemestre
import time
import pprint
import email
@ -35,20 +36,20 @@ from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email.header import Header
from reportlab.lib.colors import Color
import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error
import urllib
from flask import g
from flask import g, request
from flask import url_for
from flask_login import current_user
from flask_mail import Message
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb
from app import log
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import AccessDenied
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_abs
@ -59,7 +60,7 @@ from app.scodoc import sco_bulletins_xml
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_cache
from app.scodoc import sco_etud
from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
@ -121,9 +122,7 @@ def make_context_dict(sem, etud):
return C
def formsemestre_bulletinetud_dict(
formsemestre_id, etudid, version="long", REQUEST=None
):
def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
"""Collecte informations pour bulletin de notes
Retourne un dictionnaire (avec valeur par défaut chaine vide).
Le contenu du dictionnaire dépend des options (rangs, ...)
@ -138,15 +137,13 @@ def formsemestre_bulletinetud_dict(
prefs = sco_preferences.SemPreferences(formsemestre_id)
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes
if not nt.get_etud_etat(etudid):
raise ScoValueError("Etudiant non inscrit à ce semestre")
I = scu.DictDefault(defaultvalue="")
I["etudid"] = etudid
I["formsemestre_id"] = formsemestre_id
I["sem"] = nt.sem
if REQUEST:
I["server_name"] = REQUEST.BASE0
else:
I["server_name"] = ""
I["server_name"] = request.url_root
# Formation et parcours
I["formation"] = sco_formations.formation_list(
@ -432,7 +429,7 @@ def _ue_mod_bulletin(etudid, formsemestre_id, ue_id, modimpls, nt, version):
mod_moy = nt.get_etud_mod_moy(
modimpl["moduleimpl_id"], etudid
) # peut etre 'NI'
is_malus = mod["module"]["module_type"] == scu.MODULE_MALUS
is_malus = mod["module"]["module_type"] == ModuleType.MALUS
if bul_show_abs_modules:
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem)
mod_abs = [nbabs, nbabsjust]
@ -562,7 +559,7 @@ def _ue_mod_bulletin(etudid, formsemestre_id, ue_id, modimpls, nt, version):
mod["evaluations_incompletes"] = []
if sco_preferences.get_preference("bul_show_all_evals", formsemestre_id):
complete_eval_ids = set([e["evaluation_id"] for e in evals])
all_evals = sco_evaluations.do_evaluation_list(
all_evals = sco_evaluation_db.do_evaluation_list(
args={"moduleimpl_id": modimpl["moduleimpl_id"]}
)
all_evals.reverse() # plus ancienne d'abord
@ -771,14 +768,16 @@ def formsemestre_bulletinetud(
xml_with_decisions=False,
force_publishing=False, # force publication meme si semestre non publie sur "portail"
prefer_mail_perso=False,
REQUEST=None,
):
"page bulletin de notes"
try:
etud = sco_etud.get_etud_info(filled=True)[0]
etudid = etud["etudid"]
except:
return scu.log_unknown_etud(REQUEST, format=format)
sco_etud.log_unknown_etud()
raise ScoValueError("étudiant inconnu")
# API, donc erreurs admises en ScoValueError
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
bulletin = do_formsemestre_bulletinetud(
formsemestre_id,
@ -788,15 +787,15 @@ def formsemestre_bulletinetud(
xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing,
prefer_mail_perso=prefer_mail_perso,
REQUEST=REQUEST,
)[0]
if format not in {"html", "pdfmail"}:
return bulletin
filename = scu.bul_filename(sem, etud, format)
return scu.send_file(bulletin, filename, mime=scu.get_mime_suffix(format)[0])
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [
_formsemestre_bulletinetud_header_html(
etud, etudid, sem, formsemestre_id, format, version, REQUEST
etud, etudid, sem, formsemestre_id, format, version
),
bulletin,
]
@ -854,7 +853,6 @@ def do_formsemestre_bulletinetud(
etudid,
version="long", # short, long, selectedevals
format="html",
REQUEST=None,
nohtml=False,
xml_with_decisions=False, # force decisions dans XML
force_publishing=False, # force publication meme si semestre non publie sur "portail"
@ -862,14 +860,13 @@ def do_formsemestre_bulletinetud(
):
"""Génère le bulletin au format demandé.
Retourne: (bul, filigranne)
bul est au format demandé (html, pdf, pdfmail, pdfpart, xml)
bul est str ou bytes au format demandé (html, pdf, pdfmail, pdfpart, xml, json)
et filigranne est un message à placer en "filigranne" (eg "Provisoire").
"""
if format == "xml":
bul = sco_bulletins_xml.make_xml_formsemestre_bulletinetud(
formsemestre_id,
etudid,
REQUEST=REQUEST,
xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing,
version=version,
@ -881,19 +878,18 @@ def do_formsemestre_bulletinetud(
bul = sco_bulletins_json.make_json_formsemestre_bulletinetud(
formsemestre_id,
etudid,
REQUEST=REQUEST,
xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing,
version=version,
)
return bul, ""
I = formsemestre_bulletinetud_dict(formsemestre_id, etudid, REQUEST=REQUEST)
I = formsemestre_bulletinetud_dict(formsemestre_id, etudid)
etud = I["etud"]
if format == "html":
htm, _ = sco_bulletins_generator.make_formsemestre_bulletinetud(
I, version=version, format="html", REQUEST=REQUEST
I, version=version, format="html"
)
return htm, I["filigranne"]
@ -903,11 +899,10 @@ def do_formsemestre_bulletinetud(
version=version,
format="pdf",
stand_alone=(format != "pdfpart"),
REQUEST=REQUEST,
)
if format == "pdf":
return (
scu.sendPDFFile(REQUEST, bul, filename),
scu.sendPDFFile(bul, filename),
I["filigranne"],
) # unused ret. value
else:
@ -923,11 +918,11 @@ def do_formsemestre_bulletinetud(
htm = "" # speed up if html version not needed
else:
htm, _ = sco_bulletins_generator.make_formsemestre_bulletinetud(
I, version=version, format="html", REQUEST=REQUEST
I, version=version, format="html"
)
pdfdata, filename = sco_bulletins_generator.make_formsemestre_bulletinetud(
I, version=version, format="pdf", REQUEST=REQUEST
I, version=version, format="pdf"
)
if prefer_mail_perso:
@ -993,12 +988,11 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr):
bcc = copy_addr.strip()
else:
bcc = ""
msg = Message(subject, sender=sender, recipients=recipients, bcc=bcc)
msg = Message(subject, sender=sender, recipients=recipients, bcc=[bcc])
msg.body = hea
# Attach pdf
msg.attach(filename, scu.PDF_MIMETYPE, pdfdata)
log("mail bulletin a %s" % recipient_addr)
email.send_message(msg)
@ -1010,7 +1004,6 @@ def _formsemestre_bulletinetud_header_html(
formsemestre_id=None,
format=None,
version=None,
REQUEST=None,
):
H = [
html_sco_header.sco_header(
@ -1033,7 +1026,7 @@ def _formsemestre_bulletinetud_header_html(
),
"""
<form name="f" method="GET" action="%s">"""
% REQUEST.URL0,
% request.base_url,
f"""Bulletin <span class="bull_liensemestre"><a href="{
url_for("notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
@ -1063,14 +1056,20 @@ def _formsemestre_bulletinetud_header_html(
H.append("""</select></td>""")
# Menu
endpoint = "notes.formsemestre_bulletinetud"
url = REQUEST.URL0
qurl = six.moves.urllib.parse.quote_plus(url + "?" + REQUEST.QUERY_STRING)
menuBul = [
{
"title": "Réglages bulletins",
"endpoint": "notes.formsemestre_edit_options",
"args": {"formsemestre_id": formsemestre_id, "target_url": qurl},
"args": {
"formsemestre_id": formsemestre_id,
# "target_url": url_for(
# "notes.formsemestre_bulletinetud",
# scodoc_dept=g.scodoc_dept,
# formsemestre_id=formsemestre_id,
# etudid=etudid,
# ),
},
"enabled": (current_user.id in sem["responsables"])
or current_user.has_permission(Permission.ScoImplement),
},
@ -1113,6 +1112,16 @@ def _formsemestre_bulletinetud_header_html(
"enabled": etud["emailperso"]
and can_send_bulletin_by_mail(formsemestre_id),
},
{
"title": "Version json",
"endpoint": endpoint,
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
"version": version,
"format": "json",
},
},
{
"title": "Version XML",
"endpoint": endpoint,
@ -1188,9 +1197,14 @@ def _formsemestre_bulletinetud_header_html(
H.append(
'<td> <a href="%s">%s</a></td>'
% (
url
+ "?formsemestre_id=%s&etudid=%s&format=pdf&version=%s"
% (formsemestre_id, etudid, version),
url_for(
"notes.formsemestre_bulletinetud",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etudid=etudid,
format="pdf",
version=version,
),
scu.ICON_PDF,
)
)
@ -1201,9 +1215,7 @@ def _formsemestre_bulletinetud_header_html(
"""
% (
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
sco_photos.etud_photo_html(
etud, title="fiche de " + etud["nom"], REQUEST=REQUEST
),
sco_photos.etud_photo_html(etud, title="fiche de " + etud["nom"]),
)
)
H.append(

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -52,6 +52,9 @@ import reportlab
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
from flask import request
from flask_login import current_user
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import NoteProcessError
from app import log
@ -148,14 +151,7 @@ class BulletinGenerator(object):
def get_filename(self):
"""Build a filename to be proposed to the web client"""
sem = sco_formsemestre.get_formsemestre(self.infos["formsemestre_id"])
dt = time.strftime("%Y-%m-%d")
filename = "bul-%s-%s-%s.pdf" % (
sem["titre_num"],
dt,
self.infos["etud"]["nom"],
)
filename = scu.unescape_html(filename).replace(" ", "_").replace("&", "")
return filename
return scu.bul_filename(sem, self.infos["etud"], "pdf")
def generate(self, format="", stand_alone=True):
"""Return bulletin in specified format"""
@ -260,7 +256,6 @@ def make_formsemestre_bulletinetud(
version="long", # short, long, selectedevals
format="pdf", # html, pdf
stand_alone=True,
REQUEST=None,
):
"""Bulletin de notes
@ -286,10 +281,10 @@ def make_formsemestre_bulletinetud(
PDFLOCK.acquire()
bul_generator = gen_class(
infos,
authuser=REQUEST.AUTHENTICATED_USER,
authuser=current_user,
version=version,
filigranne=infos["filigranne"],
server_name=REQUEST.BASE0,
server_name=request.url_root,
)
if format not in bul_generator.supported_formats:
# use standard generator
@ -301,10 +296,10 @@ def make_formsemestre_bulletinetud(
gen_class = bulletin_get_class(bul_class_name)
bul_generator = gen_class(
infos,
authuser=REQUEST.AUTHENTICATED_USER,
authuser=current_user,
version=version,
filigranne=infos["filigranne"],
server_name=REQUEST.BASE0,
server_name=request.url_root,
)
data = bul_generator.generate(format=format, stand_alone=stand_alone)

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -31,12 +31,16 @@
import datetime
import json
from app.but import bulletin_but
from app.models.formsemestre import FormSemestre
from app.models.etudiants import Identite
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc import sco_abs
from app.scodoc import sco_cache
from app.scodoc import sco_edit_ue
from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_photos
@ -47,27 +51,22 @@ from app.scodoc import sco_etud
def make_json_formsemestre_bulletinetud(
formsemestre_id,
etudid,
REQUEST=None,
formsemestre_id: int,
etudid: int,
xml_with_decisions=False,
version="long",
force_publishing=False, # force publication meme si semestre non publie sur "portail"
):
) -> str:
"""Renvoie bulletin en chaine JSON"""
d = formsemestre_bulletinetud_published_dict(
formsemestre_id,
etudid,
force_publishing=force_publishing,
REQUEST=REQUEST,
xml_with_decisions=xml_with_decisions,
version=version,
)
if REQUEST:
REQUEST.RESPONSE.setHeader("content-type", scu.JSON_MIMETYPE)
return json.dumps(d, cls=scu.ScoDocJSONEncoder)
@ -79,7 +78,6 @@ def formsemestre_bulletinetud_published_dict(
etudid,
force_publishing=False,
xml_nodate=False,
REQUEST=None,
xml_with_decisions=False, # inclue les decisions même si non publiées
version="long",
):
@ -88,9 +86,17 @@ def formsemestre_bulletinetud_published_dict(
"""
from app.scodoc import sco_bulletins
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
etud = Identite.query.get(etudid)
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if formsemestre.formation.is_apc():
nt = bulletin_but.APCNotesTableCompat(formsemestre)
else:
nt = sco_cache.NotesTableCache.get(formsemestre_id)
d = {}
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if (not sem["bul_hide_xml"]) or force_publishing:
published = 1
else:
@ -135,6 +141,11 @@ def formsemestre_bulletinetud_published_dict(
if not published:
return d # stop !
etat_inscription = etud.etat_inscription(formsemestre.id)
if etat_inscription != scu.INSCRIT:
d.update(dict_decision_jury(etudid, formsemestre_id, with_decisions=True))
return d
# Groupes:
partitions = sco_groups.get_partitions_list(formsemestre_id, with_default=False)
partitions_etud_groups = {} # { partition_id : { etudid : group } }
@ -142,7 +153,6 @@ def formsemestre_bulletinetud_published_dict(
pid = partition["partition_id"]
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes
ues = nt.get_ues()
modimpls = nt.get_modimpls()
nbetuds = len(nt.rangs)
@ -283,7 +293,7 @@ def formsemestre_bulletinetud_published_dict(
if sco_preferences.get_preference(
"bul_show_all_evals", formsemestre_id
):
all_evals = sco_evaluations.do_evaluation_list(
all_evals = sco_evaluation_db.do_evaluation_list(
args={"moduleimpl_id": modimpl["moduleimpl_id"]}
)
all_evals.reverse() # plus ancienne d'abord
@ -331,60 +341,9 @@ def formsemestre_bulletinetud_published_dict(
d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust)
# --- Decision Jury
if (
sco_preferences.get_preference("bul_show_decision", formsemestre_id)
or xml_with_decisions
):
infos, dpv = sco_bulletins.etud_descr_situation_semestre(
etudid,
formsemestre_id,
format="xml",
show_uevalid=sco_preferences.get_preference(
"bul_show_uevalid", formsemestre_id
),
)
d["situation"] = scu.quote_xml_attr(infos["situation"])
if dpv:
decision = dpv["decisions"][0]
etat = decision["etat"]
if decision["decision_sem"]:
code = decision["decision_sem"]["code"]
else:
code = ""
d["decision"] = dict(code=code, etat=etat)
if (
decision["decision_sem"]
and "compense_formsemestre_id" in decision["decision_sem"]
):
d["decision"]["compense_formsemestre_id"] = decision["decision_sem"][
"compense_formsemestre_id"
]
d["decision_ue"] = []
if decision[
"decisions_ue"
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
for ue_id in decision["decisions_ue"].keys():
ue = sco_edit_ue.do_ue_list({"ue_id": ue_id})[0]
d["decision_ue"].append(
dict(
ue_id=ue["ue_id"],
numero=scu.quote_xml_attr(ue["numero"]),
acronyme=scu.quote_xml_attr(ue["acronyme"]),
titre=scu.quote_xml_attr(ue["titre"]),
code=decision["decisions_ue"][ue_id]["code"],
ects=scu.quote_xml_attr(ue["ects"] or ""),
)
)
d["autorisation_inscription"] = []
for aut in decision["autorisations"]:
d["autorisation_inscription"].append(
dict(semestre_id=aut["semestre_id"])
)
else:
d["decision"] = dict(code="", etat="DEM")
d.update(
dict_decision_jury(etudid, formsemestre_id, with_decisions=xml_with_decisions)
)
# --- Appreciations
cnx = ndb.GetDBConnexion()
apprecs = sco_etud.appreciations_list(
@ -401,3 +360,71 @@ def formsemestre_bulletinetud_published_dict(
#
return d
def dict_decision_jury(etudid, formsemestre_id, with_decisions=False):
"dict avec decision pour bulletins json"
from app.scodoc import sco_bulletins
d = {}
if (
sco_preferences.get_preference("bul_show_decision", formsemestre_id)
or with_decisions
):
infos, dpv = sco_bulletins.etud_descr_situation_semestre(
etudid,
formsemestre_id,
show_uevalid=sco_preferences.get_preference(
"bul_show_uevalid", formsemestre_id
),
)
d["situation"] = infos["situation"]
if dpv:
decision = dpv["decisions"][0]
etat = decision["etat"]
if decision["decision_sem"]:
code = decision["decision_sem"]["code"]
date = ndb.DateDMYtoISO(
dpv["decisions"][0]["decision_sem"]["event_date"]
)
else:
code = ""
date = ""
d["decision"] = dict(
code=code,
etat=etat,
date=date,
)
if (
decision["decision_sem"]
and "compense_formsemestre_id" in decision["decision_sem"]
):
d["decision"]["compense_formsemestre_id"] = decision["decision_sem"][
"compense_formsemestre_id"
]
d["decision_ue"] = []
if decision[
"decisions_ue"
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
for ue_id in decision["decisions_ue"].keys():
ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
d["decision_ue"].append(
dict(
ue_id=ue["ue_id"],
numero=ue["numero"],
acronyme=ue["acronyme"],
titre=ue["titre"],
code=decision["decisions_ue"][ue_id]["code"],
ects=ue["ects"] or "",
)
)
d["autorisation_inscription"] = []
for aut in decision["autorisations"]:
d["autorisation_inscription"].append(
dict(semestre_id=aut["semestre_id"])
)
else:
d["decision"] = dict(code="", etat="DEM")
return d

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -51,23 +51,24 @@ Chaque semestre peut si nécessaire utiliser un type de bulletin différent.
"""
import io
import os
import re
import time
import traceback
from pydoc import html
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
from flask import g, url_for
from flask import g, request
import app.scodoc.sco_utils as scu
from app import log
from app import log, ScoValueError
from app.scodoc import sco_cache
from app.scodoc import sco_formsemestre
from app.scodoc import sco_pdf
from app.scodoc import sco_preferences
from app.scodoc import sco_etud
import sco_version
from app.scodoc.sco_logos import find_logo
def pdfassemblebulletins(
@ -110,6 +111,17 @@ def pdfassemblebulletins(
return data
def replacement_function(match):
balise = match.group(1)
name = match.group(3)
logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id)
if logo is not None:
return r'<img %s src="%s"%s/>' % (match.group(2), logo.filepath, match.group(4))
raise ScoValueError(
'balise "%s": logo "%s" introuvable' % (html.escape(balise), html.escape(name))
)
def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
"""Process a field given in preferences, returns
- if format = 'pdf': a list of Platypus objects
@ -141,30 +153,24 @@ def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
return text
# --- PDF format:
# handle logos:
image_dir = scu.SCODOC_LOGOS_DIR + "/logos_" + g.scodoc_dept + "/"
if not os.path.exists(image_dir):
image_dir = scu.SCODOC_LOGOS_DIR + "/" # use global logos
if not os.path.exists(image_dir):
log(f"Warning: missing global logo directory ({image_dir})")
image_dir = None
text = re.sub(
r"<(\s*)logo(.*?)src\s*=\s*(.*?)>", r"<\1logo\2\3>", text
) # remove forbidden src attribute
if image_dir is not None:
text = re.sub(
r'<\s*logo(.*?)name\s*=\s*"(\w*?)"(.*?)/?>',
r'<img\1src="%s/logo_\2.jpg"\3/>' % image_dir,
text,
)
# nota: le match sur \w*? donne le nom du logo et interdit les .. et autres
# tentatives d'acceder à d'autres fichiers !
text = re.sub(
r'(<\s*logo(.*?)name\s*=\s*"(\w*?)"(.*?)/?>)',
replacement_function,
text,
)
# nota: le match sur \w*? donne le nom du logo et interdit les .. et autres
# tentatives d'acceder à d'autres fichiers !
# la protection contre des noms malveillants est aussi assurée par l'utilisation de
# secure_filename dans la classe Logo
# log('field: %s' % (text))
return sco_pdf.makeParas(text, style, suppress_empty=suppress_empty_pars)
def get_formsemestre_bulletins_pdf(formsemestre_id, REQUEST, version="selectedevals"):
def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
"document pdf et filename"
from app.scodoc import sco_bulletins
@ -184,7 +190,6 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, REQUEST, version="selectedev
etudid,
format="pdfpart",
version=version,
REQUEST=REQUEST,
)
fragments += frag
filigrannes[i] = filigranne
@ -192,8 +197,8 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, REQUEST, version="selectedev
i = i + 1
#
infos = {"DeptName": sco_preferences.get_preference("DeptName", formsemestre_id)}
if REQUEST:
server_name = REQUEST.BASE0
if request:
server_name = request.url_root
else:
server_name = ""
try:
@ -220,7 +225,7 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, REQUEST, version="selectedev
return pdfdoc, filename
def get_etud_bulletins_pdf(etudid, REQUEST, version="selectedevals"):
def get_etud_bulletins_pdf(etudid, version="selectedevals"):
"Bulletins pdf de tous les semestres de l'étudiant, et filename"
from app.scodoc import sco_bulletins
@ -235,15 +240,14 @@ def get_etud_bulletins_pdf(etudid, REQUEST, version="selectedevals"):
etudid,
format="pdfpart",
version=version,
REQUEST=REQUEST,
)
fragments += frag
filigrannes[i] = filigranne
bookmarks[i] = sem["session_id"] # eg RT-DUT-FI-S1-2015
i = i + 1
infos = {"DeptName": sco_preferences.get_preference("DeptName")}
if REQUEST:
server_name = REQUEST.BASE0
if request:
server_name = request.url_root
else:
server_name = ""
try:

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -56,7 +56,7 @@ et sur page "réglages bulletin" (avec formsemestre_id)
# import os
# def form_change_bul_sig(side, formsemestre_id=None, REQUEST=None):
# def form_change_bul_sig(side, formsemestre_id=None):
# """Change pdf signature"""
# filename = _get_sig_existing_filename(
# side, formsemestre_id=formsemestre_id
@ -69,7 +69,7 @@ et sur page "réglages bulletin" (avec formsemestre_id)
# raise ValueError("invalid value for 'side' parameter")
# signatureloc = get_bul_sig_img()
# H = [
# self.sco_header(REQUEST, page_title="Changement de signature"),
# self.sco_header(page_title="Changement de signature"),
# """<h2>Changement de la signature bulletin de %(sidetxt)s</h2>
# """
# % (sidetxt,),

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -47,11 +47,13 @@ from xml.etree.ElementTree import Element
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log
from app.but.bulletin_but_xml_compat import bulletin_but_xml_compat
from app.models.formsemestre import FormSemestre
from app.scodoc import sco_abs
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_cache
from app.scodoc import sco_edit_ue
from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_photos
@ -69,22 +71,31 @@ def make_xml_formsemestre_bulletinetud(
doc=None, # XML document
force_publishing=False,
xml_nodate=False,
REQUEST=None,
xml_with_decisions=False, # inclue les decisions même si non publiées
version="long",
):
) -> str:
"bulletin au format XML"
from app.scodoc import sco_bulletins
log("xml_bulletin( formsemestre_id=%s, etudid=%s )" % (formsemestre_id, etudid))
if REQUEST:
REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE)
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if formsemestre.formation.is_apc():
return bulletin_but_xml_compat(
formsemestre_id,
etudid,
doc=doc,
force_publishing=force_publishing,
xml_nodate=xml_nodate,
xml_with_decisions=xml_with_decisions, # inclue les decisions même si non publiées
version=version,
)
if (not sem["bul_hide_xml"]) or force_publishing:
published = "1"
published = 1
else:
published = "0"
published = 0
if xml_nodate:
docdate = ""
else:
@ -94,7 +105,7 @@ def make_xml_formsemestre_bulletinetud(
"etudid": str(etudid),
"formsemestre_id": str(formsemestre_id),
"date": docdate,
"publie": published,
"publie": str(published),
}
if sem["etapes"]:
el["etape_apo"] = str(sem["etapes"][0]) or ""
@ -130,7 +141,9 @@ def make_xml_formsemestre_bulletinetud(
# Disponible pour publication ?
if not published:
return doc # stop !
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(
scu.SCO_ENCODING
) # stop !
# Groupes:
partitions = sco_groups.get_partitions_list(formsemestre_id, with_default=False)
@ -292,7 +305,7 @@ def make_xml_formsemestre_bulletinetud(
if sco_preferences.get_preference(
"bul_show_all_evals", formsemestre_id
):
all_evals = sco_evaluations.do_evaluation_list(
all_evals = sco_evaluation_db.do_evaluation_list(
args={"moduleimpl_id": modimpl["moduleimpl_id"]}
)
all_evals.reverse() # plus ancienne d'abord
@ -388,7 +401,7 @@ def make_xml_formsemestre_bulletinetud(
"decisions_ue"
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
for ue_id in decision["decisions_ue"].keys():
ue = sco_edit_ue.do_ue_list({"ue_id": ue_id})[0]
ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
doc.append(
Element(
"decision_ue",

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -29,7 +29,7 @@
-écrite pour ScoDoc8, utilise flask_caching et REDIS
ScoDoc est maintenant multiprocessus / mono-thread, avec un cache en mémoire partagé.
ScoDoc est maintenant multiprocessus / mono-thread, avec un cache partagé.
"""
@ -46,9 +46,9 @@
# sco_cache.NotesTableCache.delete_many(formsemestre_id_list)
#
# Bulletins PDF:
# sco_cache.PDFBulCache.get(formsemestre_id, version)
# sco_cache.PDFBulCache.set(formsemestre_id, version, filename, pdfdoc)
# sco_cache.PDFBulCache.delete(formsemestre_id) suppr. toutes les versions
# sco_cache.SemBulletinsPDFCache.get(formsemestre_id, version)
# sco_cache.SemBulletinsPDFCache.set(formsemestre_id, version, filename, pdfdoc)
# sco_cache.SemBulletinsPDFCache.delete(formsemestre_id) suppr. toutes les versions
# Evaluations:
# sco_cache.EvaluationCache.get(evaluation_id), set(evaluation_id, value), delete(evaluation_id),
@ -155,9 +155,19 @@ class EvaluationCache(ScoDocCache):
cls.delete_many(evaluation_ids)
class ResultatsSemestreBUTCache(ScoDocCache):
"""Cache pour les résultats ResultatsSemestreBUT.
Clé: formsemestre_id
Valeur: { un paquet de dataframes }
"""
prefix = "RBUT"
timeout = 1 * 60 # ttl 1 minutes (en phase de mise au point)
class AbsSemEtudCache(ScoDocCache):
"""Cache pour les comptes d'absences d'un étudiant dans un semestre.
Ce cache étant indépendant des semestre, le compte peut être faux lorsqu'on
Ce cache étant indépendant des semestres, le compte peut être faux lorsqu'on
change les dates début/fin d'un semestre.
C'est pourquoi il expire après timeout secondes.
Le timeout evite aussi d'éliminer explicitement ces éléments cachés lors
@ -289,10 +299,11 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
SemInscriptionsCache.delete_many(formsemestre_ids)
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
ResultatsSemestreBUTCache.delete_many(formsemestre_ids)
class DefferedSemCacheManager:
"""Experimental: pour effectuer des opérations indépendantes dans la
"""Contexte pour effectuer des opérations indépendantes dans la
même requete qui invalident le cache. Par exemple, quand on inscrit
des étudiants un par un à un semestre, chaque inscription va invalider
le cache, et la suivante va le reconstruire... pour l'invalider juste après.

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -28,7 +28,41 @@
"""Semestres: Codes gestion parcours (constantes)
"""
import collections
from six.moves import range
import enum
from app import log
@enum.unique
class CodesParcours(enum.IntEnum):
"""Codes numériques de sparcours, enregistrés en base
dans notes_formations.type_parcours
Ne pas modifier.
"""
Legacy = 0
DUT = 100
DUT4 = 110
DUTMono = 120
DUT2 = 130
LP = 200
LP2sem = 210
LP2semEvry = 220
LP2014 = 230
LP2sem2014 = 240
M2 = 250
M2noncomp = 251
Mono = 300
MasterLMD = 402
MasterIG = 403
LicenceUCAC3 = 501
MasterUCAC2 = 502
MonoUCAC = 503
GEN_6_SEM = 600
BUT = 700
ISCID6 = 1001
ISCID4 = 1002
NOTES_TOLERANCE = 0.00499999999999 # si note >= (BARRE-TOLERANCE), considere ok
# (permet d'eviter d'afficher 10.00 sous barre alors que la moyenne vaut 9.999)
@ -44,6 +78,7 @@ UE_STAGE_LP = 2 # ue "projet tuteuré et stage" dans les Lic. Pro.
UE_STAGE_10 = 3 # ue "stage" avec moyenne requise > 10
UE_ELECTIVE = 4 # UE "élective" dans certains parcours (UCAC?, ISCID)
UE_PROFESSIONNELLE = 5 # UE "professionnelle" (ISCID, ...)
UE_OPTIONNELLE = 6 # UE non fondamentales (ILEPS, ...)
def UE_is_fondamentale(ue_type):
@ -62,7 +97,8 @@ UE_TYPE_NAME = {
UE_STAGE_LP: "Projet tuteuré et stage (Lic. Pro.)",
UE_STAGE_10: "Stage (moyenne min. 10/20)",
UE_ELECTIVE: "Elective (ISCID)",
UE_PROFESSIONNELLE: "Professionnelle (ISCID)"
UE_PROFESSIONNELLE: "Professionnelle (ISCID)",
UE_OPTIONNELLE: "Optionnelle",
# UE_FONDAMENTALE : '"Fondamentale" (eg UCAC)',
# UE_OPTIONNELLE : '"Optionnelle" (UCAC)'
}
@ -215,6 +251,8 @@ class TypeParcours(object):
ALLOWED_UE_TYPES = list(
UE_TYPE_NAME.keys()
) # par defaut, autorise tous les types d'UE
APC_SAE = False # Approche par compétences avec ressources et SAÉs
USE_REFERENTIEL_COMPETENCES = False # Lien avec ref. comp.
def check(self, formation=None):
return True, "" # status, diagnostic_message
@ -254,13 +292,27 @@ class TypeParcours(object):
return False, """<b>%d UE sous la barre</b>""" % n
TYPES_PARCOURS = (
collections.OrderedDict()
) # liste des parcours définis (instances de sous-classes de TypeParcours)
# Parcours définis (instances de sous-classes de TypeParcours):
TYPES_PARCOURS = collections.OrderedDict() # type : Parcours
def register_parcours(Parcours):
TYPES_PARCOURS[Parcours.TYPE_PARCOURS] = Parcours
TYPES_PARCOURS[int(Parcours.TYPE_PARCOURS)] = Parcours
class ParcoursBUT(TypeParcours):
"""BUT Bachelor Universitaire de Technologie"""
TYPE_PARCOURS = 700
NAME = "BUT"
NB_SEM = 6
COMPENSATION_UE = False
APC_SAE = True
USE_REFERENTIEL_COMPETENCES = True
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT]
register_parcours(ParcoursBUT())
class ParcoursDUT(TypeParcours):
@ -303,7 +355,7 @@ register_parcours(ParcoursDUTMono())
class ParcoursDUT2(ParcoursDUT):
"""DUT en deux semestres (par ex.: années spéciales semestrialisées)"""
TYPE_PARCOURS = 130
TYPE_PARCOURS = CodesParcours.DUT2
NAME = "DUT2"
NB_SEM = 2
@ -316,7 +368,7 @@ class ParcoursLP(TypeParcours):
(pour anciennes LP. Après 2014, préférer ParcoursLP2014)
"""
TYPE_PARCOURS = 200
TYPE_PARCOURS = CodesParcours.LP
NAME = "LP"
NB_SEM = 1
COMPENSATION_UE = False
@ -333,7 +385,7 @@ register_parcours(ParcoursLP())
class ParcoursLP2sem(ParcoursLP):
"""Licence Pro (en deux "semestres")"""
TYPE_PARCOURS = 210
TYPE_PARCOURS = CodesParcours.LP2sem
NAME = "LP2sem"
NB_SEM = 2
COMPENSATION_UE = True
@ -346,7 +398,7 @@ register_parcours(ParcoursLP2sem())
class ParcoursLP2semEvry(ParcoursLP):
"""Licence Pro (en deux "semestres", U. Evry)"""
TYPE_PARCOURS = 220
TYPE_PARCOURS = CodesParcours.LP2semEvry
NAME = "LP2semEvry"
NB_SEM = 2
COMPENSATION_UE = True
@ -372,7 +424,7 @@ class ParcoursLP2014(TypeParcours):
# l'établissement d'un coefficient qui peut varier dans un rapport de 1 à 3. ", etc ne sont _pas_
# vérifiés par ScoDoc)
TYPE_PARCOURS = 230
TYPE_PARCOURS = CodesParcours.LP2014
NAME = "LP2014"
NB_SEM = 1
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT, UE_STAGE_LP]
@ -416,7 +468,7 @@ register_parcours(ParcoursLP2014())
class ParcoursLP2sem2014(ParcoursLP):
"""Licence Pro (en deux "semestres", selon arrêté du 22/01/2014)"""
TYPE_PARCOURS = 240
TYPE_PARCOURS = CodesParcours.LP2sem2014
NAME = "LP2014_2sem"
NB_SEM = 2
@ -428,7 +480,7 @@ register_parcours(ParcoursLP2sem2014())
class ParcoursM2(TypeParcours):
"""Master 2 (en deux "semestres")"""
TYPE_PARCOURS = 250
TYPE_PARCOURS = CodesParcours.M2
NAME = "M2sem"
NB_SEM = 2
COMPENSATION_UE = True
@ -441,7 +493,7 @@ register_parcours(ParcoursM2())
class ParcoursM2noncomp(ParcoursM2):
"""Master 2 (en deux "semestres") sans compensation"""
TYPE_PARCOURS = 251
TYPE_PARCOURS = CodesParcours.M2noncomp
NAME = "M2noncomp"
COMPENSATION_UE = False
UNUSED_CODES = set((ADC, ATT, ATB))
@ -453,7 +505,7 @@ register_parcours(ParcoursM2noncomp())
class ParcoursMono(TypeParcours):
"""Formation générique en une session"""
TYPE_PARCOURS = 300
TYPE_PARCOURS = CodesParcours.Mono
NAME = "Mono"
NB_SEM = 1
COMPENSATION_UE = False
@ -466,7 +518,7 @@ register_parcours(ParcoursMono())
class ParcoursLegacy(TypeParcours):
"""DUT (ancien ScoDoc, ne plus utiliser)"""
TYPE_PARCOURS = 0
TYPE_PARCOURS = CodesParcours.Legacy
NAME = "DUT"
NB_SEM = 4
COMPENSATION_UE = None # backward compat: defini dans formsemestre
@ -500,7 +552,7 @@ class ParcoursBachelorISCID6(ParcoursISCID):
"""ISCID: Bachelor en 3 ans (6 sem.)"""
NAME = "ParcoursBachelorISCID6"
TYPE_PARCOURS = 1001
TYPE_PARCOURS = CodesParcours.ISCID6
NAME = ""
NB_SEM = 6
ECTS_PROF_DIPL = 8 # crédits professionnels requis pour obtenir le diplôme
@ -511,7 +563,7 @@ register_parcours(ParcoursBachelorISCID6())
class ParcoursMasterISCID4(ParcoursISCID):
"ISCID: Master en 2 ans (4 sem.)"
TYPE_PARCOURS = 1002
TYPE_PARCOURS = CodesParcours.ISCID4
NAME = "ParcoursMasterISCID4"
NB_SEM = 4
ECTS_PROF_DIPL = 15 # crédits professionnels requis pour obtenir le diplôme
@ -520,6 +572,34 @@ class ParcoursMasterISCID4(ParcoursISCID):
register_parcours(ParcoursMasterISCID4())
class ParcoursILEPS(TypeParcours):
"""Superclasse pour les parcours de l'ILEPS"""
# SESSION_NAME = "année"
# SESSION_NAME_A = "de l'"
# SESSION_ABBRV = 'A' # A1, A2, ...
COMPENSATION_UE = False
UNUSED_CODES = set((ADC, ATT, ATB, ATJ))
ALLOWED_UE_TYPES = [UE_STANDARD, UE_OPTIONNELLE]
# Barre moy gen. pour validation semestre:
BARRE_MOY = 10.0
# Barre pour UE ILEPS: 8/20 pour UE standards ("fondamentales")
# et pas de barre (-1.) pour UE élective.
BARRE_UE = {UE_STANDARD: 8.0, UE_OPTIONNELLE: 0.0}
BARRE_UE_DEFAULT = 0.0 # pas de barre sur les autres UE
class ParcoursLicenceILEPS6(ParcoursILEPS):
"""ILEPS: Licence 6 semestres"""
TYPE_PARCOURS = 1010
NAME = "LicenceILEPS6"
NB_SEM = 6
register_parcours(ParcoursLicenceILEPS6())
class ParcoursUCAC(TypeParcours):
"""Règles de validation UCAC"""
@ -537,7 +617,7 @@ class ParcoursUCAC(TypeParcours):
class ParcoursLicenceUCAC3(ParcoursUCAC):
"""UCAC: Licence en 3 sessions d'un an"""
TYPE_PARCOURS = 501
TYPE_PARCOURS = CodesParcours.LicenceUCAC3
NAME = "Licence UCAC en 3 sessions d'un an"
NB_SEM = 3
@ -548,7 +628,7 @@ register_parcours(ParcoursLicenceUCAC3())
class ParcoursMasterUCAC2(ParcoursUCAC):
"""UCAC: Master en 2 sessions d'un an"""
TYPE_PARCOURS = 502
TYPE_PARCOURS = CodesParcours.MasterUCAC2
NAME = "Master UCAC en 2 sessions d'un an"
NB_SEM = 2
@ -559,7 +639,7 @@ register_parcours(ParcoursMasterUCAC2())
class ParcoursMonoUCAC(ParcoursUCAC):
"""UCAC: Formation en 1 session de durée variable"""
TYPE_PARCOURS = 503
TYPE_PARCOURS = CodesParcours.MonoUCAC
NAME = "Formation UCAC en 1 session de durée variable"
NB_SEM = 1
UNUSED_CODES = set((ADC, ATT, ATB))
@ -571,7 +651,7 @@ register_parcours(ParcoursMonoUCAC())
class Parcours6Sem(TypeParcours):
"""Parcours générique en 6 semestres"""
TYPE_PARCOURS = 600
TYPE_PARCOURS = CodesParcours.GEN_6_SEM
NAME = "Formation en 6 semestres"
NB_SEM = 6
COMPENSATION_UE = True
@ -593,7 +673,7 @@ register_parcours(Parcours6Sem())
class ParcoursMasterLMD(TypeParcours):
"""Master générique en 4 semestres dans le LMD"""
TYPE_PARCOURS = 402
TYPE_PARCOURS = CodesParcours.MasterLMD
NAME = "Master LMD"
NB_SEM = 4
COMPENSATION_UE = True # variabale inutilisée
@ -606,7 +686,7 @@ register_parcours(ParcoursMasterLMD())
class ParcoursMasterIG(ParcoursMasterLMD):
"""Master de l'Institut Galilée (U. Paris 13) en 4 semestres (LMD)"""
TYPE_PARCOURS = 403
TYPE_PARCOURS = CodesParcours.MasterIG
NAME = "Master IG P13"
BARRE_MOY = 10.0
NOTES_BARRE_VALID_UE_TH = 10.0 # seuil pour valider UE
@ -673,4 +753,9 @@ FORMATION_PARCOURS_TYPES = [p[0] for p in _tp] # codes numeriques (TYPE_PARCOUR
def get_parcours_from_code(code_parcours):
return TYPES_PARCOURS[code_parcours]
parcours = TYPES_PARCOURS.get(code_parcours)
if parcours is None:
log(f"Warning: invalid code_parcours: {code_parcours}")
# default to legacy
parcours = TYPES_PARCOURS.get(0)
return parcours

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -27,24 +27,25 @@
"""Calcul des moyennes de module
"""
import traceback
import pprint
import traceback
from flask import url_for, g
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc.sco_utils import (
ModuleType,
NOTES_ATTENTE,
NOTES_NEUTRALISE,
EVALUATION_NORMALE,
EVALUATION_RATTRAPAGE,
EVALUATION_SESSION2,
)
from app.scodoc.sco_exceptions import ScoException
from app.scodoc.sco_exceptions import ScoValueError
from app import log
from app.scodoc import sco_abs
from app.scodoc import sco_edit_module
from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_formulas
@ -65,7 +66,8 @@ def moduleimpl_has_expression(mod):
def formsemestre_expressions_use_abscounts(formsemestre_id):
"""True si les notes de ce semestre dépendent des compteurs d'absences.
Cela n'est normalement pas le cas, sauf si des formules utilisateur utilisent ces compteurs.
Cela n'est normalement pas le cas, sauf si des formules utilisateur
utilisent ces compteurs.
"""
# check presence of 'nbabs' in expressions
ab = "nb_abs" # chaine recherchée
@ -79,7 +81,7 @@ def formsemestre_expressions_use_abscounts(formsemestre_id):
if expr and expr[0] != "#" and ab in expr:
return True
# 2- moyennes de modules
for mod in sco_moduleimpl.do_moduleimpl_list(formsemestre_id=formsemestre_id):
for mod in sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id):
if moduleimpl_has_expression(mod) and ab in mod["computation_expr"]:
return True
return False
@ -102,8 +104,9 @@ formsemestre_ue_computation_expr_list = _formsemestre_ue_computation_exprEditor.
formsemestre_ue_computation_expr_edit = _formsemestre_ue_computation_exprEditor.edit
def get_ue_expression(formsemestre_id, ue_id, cnx, html_quote=False):
def get_ue_expression(formsemestre_id, ue_id, html_quote=False):
"""Returns UE expression (formula), or None if no expression has been defined"""
cnx = ndb.GetDBConnexion()
el = formsemestre_ue_computation_expr_list(
cnx, {"formsemestre_id": formsemestre_id, "ue_id": ue_id}
)
@ -128,7 +131,7 @@ def compute_user_formula(
coefs,
coefs_mask,
formula,
diag_info={}, # infos supplementaires a placer ds messages d'erreur
diag_info=None, # infos supplementaires a placer ds messages d'erreur
use_abs=True,
):
"""Calcul moyenne a partir des notes et coefs, en utilisant la formule utilisateur (une chaine).
@ -159,14 +162,19 @@ def compute_user_formula(
# log('expression : %s\nvariables=%s\n' % (formula, variables)) # debug
user_moy = sco_formulas.eval_user_expression(formula, variables)
# log('user_moy=%s' % user_moy)
if user_moy != "NA0" and user_moy != "NA":
if user_moy != "NA":
user_moy = float(user_moy)
if (user_moy > 20) or (user_moy < 0):
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
raise ScoException(
"""valeur moyenne %s hors limite pour <a href="formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s">%s</a>"""
% (user_moy, sem["formsemestre_id"], etudid, etud["nomprenom"])
raise ScoValueError(
f"""
Valeur moyenne {user_moy} hors limite pour
<a href="{url_for('notes.formsemestre_bulletinetud',
scodoc_dept=g.scodoc_dept,
formsemestre_id=sem["formsemestre_id"],
etudid=etudid
)}">{etud["nomprenom"]}</a>"""
)
except:
log(
@ -183,7 +191,8 @@ def compute_user_formula(
return user_moy
def do_moduleimpl_moyennes(nt, mod):
# XXX OBSOLETE
def compute_moduleimpl_moyennes(nt, modimpl):
"""Retourne dict { etudid : note_moyenne } pour tous les etuds inscrits
au moduleimpl mod, la liste des evaluations "valides" (toutes notes entrées
ou en attente), et att (vrai s'il y a des notes en attente dans ce module).
@ -193,13 +202,13 @@ def do_moduleimpl_moyennes(nt, mod):
S'il manque des notes et que le coef n'est pas nul,
la moyenne n'est pas calculée: NA
Ne prend en compte que les evaluations toutes les notes sont entrées.
Le résultat est une note sur 20.
Le résultat note_moyenne est une note sur 20.
"""
diag_info = {} # message d'erreur formule
moduleimpl_id = mod["moduleimpl_id"]
is_malus = mod["module"]["module_type"] == scu.MODULE_MALUS
sem = sco_formsemestre.get_formsemestre(mod["formsemestre_id"])
etudids = sco_moduleimpl.do_moduleimpl_listeetuds(
moduleimpl_id = modimpl["moduleimpl_id"]
is_malus = modimpl["module"]["module_type"] == ModuleType.MALUS
sem = sco_formsemestre.get_formsemestre(modimpl["formsemestre_id"])
etudids = sco_moduleimpl.moduleimpl_listeetuds(
moduleimpl_id
) # tous, y compris demissions
# Inscrits au semestre (pour traiter les demissions):
@ -207,7 +216,7 @@ def do_moduleimpl_moyennes(nt, mod):
[
x["etudid"]
for x in sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits(
mod["formsemestre_id"]
modimpl["formsemestre_id"]
)
]
)
@ -218,24 +227,25 @@ def do_moduleimpl_moyennes(nt, mod):
key=lambda x: (x["numero"], x["jour"], x["heure_debut"])
) # la plus ancienne en tête
user_expr = moduleimpl_has_expression(mod)
user_expr = moduleimpl_has_expression(modimpl)
attente = False
# recupere les notes de toutes les evaluations
# récupere les notes de toutes les evaluations
eval_rattr = None
for e in evals:
e["nb_inscrits"] = e["etat"]["nb_inscrits"]
NotesDB = sco_evaluations.do_evaluation_get_all_notes(
# XXX OBSOLETE
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
e["evaluation_id"]
) # toutes, y compris demissions
# restreint aux étudiants encore inscrits à ce module
notes = [
NotesDB[etudid]["value"] for etudid in NotesDB if (etudid in insmod_set)
notes_db[etudid]["value"] for etudid in notes_db if (etudid in insmod_set)
]
e["nb_notes"] = len(notes)
e["nb_abs"] = len([x for x in notes if x is None])
e["nb_neutre"] = len([x for x in notes if x == NOTES_NEUTRALISE])
e["nb_att"] = len([x for x in notes if x == NOTES_ATTENTE])
e["notes"] = NotesDB
e["notes"] = notes_db
if e["etat"]["evalattente"]:
attente = True
@ -268,7 +278,7 @@ def do_moduleimpl_moyennes(nt, mod):
]
#
R = {}
formula = scu.unescape_html(mod["computation_expr"])
formula = scu.unescape_html(modimpl["computation_expr"])
formula_use_abs = "abs" in formula
for etudid in insmod_set: # inscrits au semestre et au module
@ -289,15 +299,17 @@ def do_moduleimpl_moyennes(nt, mod):
# il manque une note ! (si publish_incomplete, cela peut arriver, on ignore)
if e["coefficient"] > 0 and not e["publish_incomplete"]:
nb_missing += 1
# ne devrait pas arriver ?
log("\nXXX SCM298\n")
if nb_missing == 0 and sum_coefs > 0:
if sum_coefs > 0:
R[etudid] = sum_notes / sum_coefs
moy_valid = True
else:
R[etudid] = "na"
R[etudid] = "NA"
moy_valid = False
else:
R[etudid] = "NA%d" % nb_missing
R[etudid] = "NA"
moy_valid = False
if user_expr:
@ -348,14 +360,14 @@ def do_moduleimpl_moyennes(nt, mod):
if etudid in eval_rattr["notes"]:
note = eval_rattr["notes"][etudid]["value"]
if note != None and note != NOTES_NEUTRALISE and note != NOTES_ATTENTE:
if isinstance(R[etudid], float):
if not isinstance(R[etudid], float):
R[etudid] = note
else:
note_sur_20 = note * 20.0 / eval_rattr["note_max"]
if eval_rattr["evaluation_type"] == EVALUATION_RATTRAPAGE:
# rattrapage classique: prend la meilleure note entre moyenne
# module et note eval rattrapage
if (R[etudid] == "NA0") or (note_sur_20 > R[etudid]):
if (R[etudid] == "NA") or (note_sur_20 > R[etudid]):
# log('note_sur_20=%s' % note_sur_20)
R[etudid] = note_sur_20
elif eval_rattr["evaluation_type"] == EVALUATION_SESSION2:
@ -365,7 +377,7 @@ def do_moduleimpl_moyennes(nt, mod):
return R, valid_evals, attente, diag_info
def do_formsemestre_moyennes(nt, formsemestre_id):
def formsemestre_compute_modimpls_moyennes(nt, formsemestre_id):
"""retourne dict { moduleimpl_id : { etudid, note_moyenne_dans_ce_module } },
la liste des moduleimpls, la liste des evaluations valides,
liste des moduleimpls avec notes en attente.
@ -375,7 +387,7 @@ def do_formsemestre_moyennes(nt, formsemestre_id):
# args={"formsemestre_id": formsemestre_id}
# )
# etudids = [x["etudid"] for x in inscr]
modimpls = sco_moduleimpl.do_moduleimpl_list(formsemestre_id=formsemestre_id)
modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
# recupere les moyennes des etudiants de tous les modules
D = {}
valid_evals = []
@ -383,15 +395,16 @@ def do_formsemestre_moyennes(nt, formsemestre_id):
mods_att = []
expr_diags = []
for modimpl in modimpls:
mod = sco_edit_module.do_module_list(args={"module_id": modimpl["module_id"]})[
0
]
mod = sco_edit_module.module_list(args={"module_id": modimpl["module_id"]})[0]
modimpl["module"] = mod # add module dict to moduleimpl (used by nt)
moduleimpl_id = modimpl["moduleimpl_id"]
assert moduleimpl_id not in D
D[moduleimpl_id], valid_evals_mod, attente, expr_diag = do_moduleimpl_moyennes(
nt, modimpl
)
(
D[moduleimpl_id],
valid_evals_mod,
attente,
expr_diag,
) = compute_moduleimpl_moyennes(nt, modimpl)
valid_evals_per_mod[moduleimpl_id] = valid_evals_mod
valid_evals += valid_evals_mod
if attente:

View File

@ -0,0 +1,179 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# ScoDoc
#
# Copyright (c) 1999 - 2022 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
#
##############################################################################
"""
"""
from app.models import ScoDocSiteConfig
from app.scodoc.sco_logos import write_logo, find_logo, delete_logo
import app
from flask import current_app
class Action:
"""Base class for all classes describing an action from from config form."""
def __init__(self, message, parameters):
self.message = message
self.parameters = parameters
@staticmethod
def build_action(parameters, stream=None):
"""Check (from parameters) if some action has to be done and
then return list of action (or else return empty list)."""
raise NotImplementedError
def display(self):
"""return a str describing the action to be done"""
return self.message.format_map(self.parameters)
def execute(self):
"""Executes the action"""
raise NotImplementedError
GLOBAL = "_"
class LogoUpdate(Action):
"""Action: change a logo
dept_id: dept_id or '_',
logo_id: logo_id,
upload: image file replacement
"""
def __init__(self, parameters):
super().__init__(
f"Modification du logo {parameters['logo_id']} pour le département {parameters['dept_id']}",
parameters,
)
@staticmethod
def build_action(parameters):
dept_id = parameters["dept_key"]
if dept_id == GLOBAL:
dept_id = None
parameters["dept_id"] = dept_id
if parameters["upload"] is not None:
return LogoUpdate(parameters)
return None
def execute(self):
current_app.logger.info(self.message)
write_logo(
stream=self.parameters["upload"],
dept_id=self.parameters["dept_id"],
name=self.parameters["logo_id"],
)
class LogoDelete(Action):
"""Action: Delete an existing logo
dept_id: dept_id or '_',
logo_id: logo_id
"""
def __init__(self, parameters):
super().__init__(
f"Suppression du logo {parameters['logo_id']} pour le département {parameters['dept_id'] or 'tous'}.",
parameters,
)
@staticmethod
def build_action(parameters):
parameters["dept_id"] = parameters["dept_key"]
if parameters["dept_key"] == GLOBAL:
parameters["dept_id"] = None
if parameters["do_delete"]:
return LogoDelete(parameters)
return None
def execute(self):
current_app.logger.info(self.message)
delete_logo(name=self.parameters["logo_id"], dept_id=self.parameters["dept_id"])
class LogoInsert(Action):
"""Action: add a new logo
dept_key: dept_id or '_',
logo_id: logo_id,
upload: image file replacement
"""
def __init__(self, parameters):
super().__init__(
f"Ajout du logo {parameters['name']} pour le département {parameters['dept_key']} ({parameters['upload'].filename}).",
parameters,
)
@staticmethod
def build_action(parameters):
if parameters["dept_key"] == GLOBAL:
parameters["dept_id"] = None
if parameters["upload"] and parameters["name"]:
logo = find_logo(
logoname=parameters["name"], dept_id=parameters["dept_key"]
)
if logo is None:
return LogoInsert(parameters)
return None
def execute(self):
dept_id = self.parameters["dept_key"]
if dept_id == GLOBAL:
dept_id = None
current_app.logger.info(self.message)
write_logo(
stream=self.parameters["upload"],
name=self.parameters["name"],
dept_id=dept_id,
)
class BonusSportUpdate(Action):
"""Action: Change bonus_sport_function_name.
bonus_sport_function_name: the new value"""
def __init__(self, parameters):
super().__init__(
f"Changement du calcul de bonus sport pour ({parameters['bonus_sport_func_name']}).",
parameters,
)
@staticmethod
def build_action(parameters):
if (
parameters["bonus_sport_func_name"]
!= ScoDocSiteConfig.get_bonus_sport_func_name()
):
return [BonusSportUpdate(parameters)]
return []
def execute(self):
current_app.logger.info(self.message)
ScoDocSiteConfig.set_bonus_sport_func(self.parameters["bonus_sport_func_name"])
app.clear_scodoc_cache()

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -30,6 +30,8 @@
(coût théorique en heures équivalent TD)
"""
from flask import request
import app.scodoc.sco_utils as scu
from app.scodoc.gen_tables import GenTable
from app.scodoc import sco_formsemestre
@ -45,7 +47,6 @@ def formsemestre_table_estim_cost(
n_group_tp=1,
coef_tp=1,
coef_cours=1.5,
REQUEST=None,
):
"""
Rapports estimation coût de formation basé sur le programme pédagogique
@ -58,9 +59,7 @@ def formsemestre_table_estim_cost(
"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
sco_formsemestre_status.fill_formsemestre(sem)
Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id
)
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
T = []
for M in Mlist:
Mod = M["module"]
@ -156,7 +155,6 @@ def formsemestre_estim_cost(
coef_tp=1,
coef_cours=1.5,
format="html",
REQUEST=None,
):
"""Page (formulaire) estimation coûts"""
@ -171,7 +169,6 @@ def formsemestre_estim_cost(
n_group_tp=n_group_tp,
coef_tp=coef_tp,
coef_cours=coef_cours,
REQUEST=REQUEST,
)
h = """
<form name="f" method="get" action="%s">
@ -182,7 +179,7 @@ def formsemestre_estim_cost(
<br/>
</form>
""" % (
REQUEST.URL0,
request.base_url,
formsemestre_id,
n_group_td,
n_group_tp,
@ -190,11 +187,11 @@ def formsemestre_estim_cost(
)
tab.html_before_table = h
tab.base_url = "%s?formsemestre_id=%s&n_group_td=%s&n_group_tp=%s&coef_tp=%s" % (
REQUEST.URL0,
request.base_url,
formsemestre_id,
n_group_td,
n_group_tp,
coef_tp,
)
return tab.make_page(format=format, REQUEST=REQUEST)
return tab.make_page(format=format)

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -29,7 +29,7 @@
Rapport (table) avec dernier semestre fréquenté et débouché de chaque étudiant
"""
import http
from flask import url_for, g
from flask import url_for, g, request
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
@ -47,10 +47,20 @@ from app.scodoc import sco_etud
import sco_version
def report_debouche_date(start_year=None, format="html", REQUEST=None):
"""Rapport (table) pour les débouchés des étudiants sortis à partir de l'année indiquée."""
def report_debouche_date(start_year=None, format="html"):
"""Rapport (table) pour les débouchés des étudiants sortis
à partir de l'année indiquée.
"""
if not start_year:
return report_debouche_ask_date(REQUEST=REQUEST)
return report_debouche_ask_date("Année de début de la recherche")
else:
try:
start_year = int(start_year)
except ValueError:
return report_debouche_ask_date(
"Année invalide. Année de début de la recherche"
)
if format == "xls":
keep_numeric = True # pas de conversion des notes en strings
else:
@ -64,13 +74,12 @@ def report_debouche_date(start_year=None, format="html", REQUEST=None):
"Généré par %s le " % sco_version.SCONAME + scu.timedate_human_repr() + ""
)
tab.caption = "Récapitulatif débouchés à partir du 1/1/%s." % start_year
tab.base_url = "%s?start_year=%s" % (REQUEST.URL0, start_year)
tab.base_url = "%s?start_year=%s" % (request.base_url, start_year)
return tab.make_page(
title="""<h2 class="formsemestre">Débouchés étudiants </h2>""",
init_qtip=True,
javascripts=["js/etud_info.js"],
format=format,
REQUEST=REQUEST,
with_html_headers=True,
)
@ -97,8 +106,9 @@ def get_etudids_with_debouche(start_year):
FROM notes_formsemestre_inscription i, notes_formsemestre s, itemsuivi it
WHERE i.etudid = it.etudid
AND i.formsemestre_id = s.id AND s.date_fin >= %(start_date)s
AND s.dept_id = %(dept_id)s
""",
{"start_date": start_date},
{"start_date": start_date, "dept_id": g.scodoc_dept_id},
)
return [x["etudid"] for x in r]
@ -194,15 +204,16 @@ def table_debouche_etudids(etudids, keep_numeric=True):
return tab
def report_debouche_ask_date(REQUEST=None):
def report_debouche_ask_date(msg: str) -> str:
"""Formulaire demande date départ"""
return (
html_sco_header.sco_header()
+ """<form method="GET">
Date de départ de la recherche: <input type="text" name="start_year" value="" size=10/>
</form>"""
+ html_sco_header.sco_footer()
)
return f"""{html_sco_header.sco_header()}
<h2>Table des débouchés des étudiants</h2>
<form method="GET">
{msg}
<input type="text" name="start_year" value="" size=10/>
</form>
{html_sco_header.sco_footer()}
"""
# ----------------------------------------------------------------------------
@ -249,7 +260,7 @@ def itemsuivi_get(cnx, itemsuivi_id, ignore_errors=False):
return None
def itemsuivi_suppress(itemsuivi_id, REQUEST=None):
def itemsuivi_suppress(itemsuivi_id):
"""Suppression d'un item"""
if not sco_permissions_check.can_edit_suivi():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
@ -259,9 +270,10 @@ def itemsuivi_suppress(itemsuivi_id, REQUEST=None):
_itemsuivi_delete(cnx, itemsuivi_id)
logdb(cnx, method="itemsuivi_suppress", etudid=item["etudid"])
log("suppressed itemsuivi %s" % (itemsuivi_id,))
return ("", 204)
def itemsuivi_create(etudid, item_date=None, situation="", REQUEST=None, format=None):
def itemsuivi_create(etudid, item_date=None, situation="", format=None):
"""Creation d'un item"""
if not sco_permissions_check.can_edit_suivi():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
@ -273,11 +285,11 @@ def itemsuivi_create(etudid, item_date=None, situation="", REQUEST=None, format=
log("created itemsuivi %s for %s" % (itemsuivi_id, etudid))
item = itemsuivi_get(cnx, itemsuivi_id)
if format == "json":
return scu.sendJSON(REQUEST, item)
return scu.sendJSON(item)
return item
def itemsuivi_set_date(itemsuivi_id, item_date, REQUEST=None):
def itemsuivi_set_date(itemsuivi_id, item_date):
"""set item date
item_date is a string dd/mm/yyyy
"""
@ -288,9 +300,10 @@ def itemsuivi_set_date(itemsuivi_id, item_date, REQUEST=None):
item = itemsuivi_get(cnx, itemsuivi_id)
item["item_date"] = item_date
_itemsuivi_edit(cnx, item)
return ("", 204)
def itemsuivi_set_situation(object, value, REQUEST=None):
def itemsuivi_set_situation(object, value):
"""set situation"""
if not sco_permissions_check.can_edit_suivi():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
@ -304,14 +317,14 @@ def itemsuivi_set_situation(object, value, REQUEST=None):
return situation or scu.IT_SITUATION_MISSING_STR
def itemsuivi_list_etud(etudid, format=None, REQUEST=None):
def itemsuivi_list_etud(etudid, format=None):
"""Liste des items pour cet étudiant, avec tags"""
cnx = ndb.GetDBConnexion()
items = _itemsuivi_list(cnx, {"etudid": etudid})
for it in items:
it["tags"] = ", ".join(itemsuivi_tag_list(it["itemsuivi_id"]))
if format == "json":
return scu.sendJSON(REQUEST, items)
return scu.sendJSON(items)
return items
@ -328,7 +341,7 @@ def itemsuivi_tag_list(itemsuivi_id):
return [x["title"] for x in r]
def itemsuivi_tag_search(term, REQUEST=None):
def itemsuivi_tag_search(term):
"""List all used tag names (for auto-completion)"""
# restrict charset to avoid injections
if not scu.ALPHANUM_EXP.match(term):
@ -343,10 +356,10 @@ def itemsuivi_tag_search(term, REQUEST=None):
)
data = [x["title"] for x in r]
return scu.sendJSON(REQUEST, data)
return scu.sendJSON(data)
def itemsuivi_tag_set(itemsuivi_id="", taglist=[], REQUEST=None):
def itemsuivi_tag_set(itemsuivi_id="", taglist=None):
"""taglist may either be:
a string with tag names separated by commas ("un;deux")
or a list of strings (["un", "deux"])

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -28,7 +28,7 @@
"""Page accueil département (liste des semestres, etc)
"""
from flask import g
from flask import g, request
from flask_login import current_user
import app
@ -46,8 +46,9 @@ from app.scodoc import sco_up_to_date
from app.scodoc import sco_users
def index_html(REQUEST=None, showcodes=0, showsemtable=0):
def index_html(showcodes=0, showsemtable=0):
"Page accueil département (liste des semestres)"
showcodes = int(showcodes)
showsemtable = int(showsemtable)
H = []
@ -78,7 +79,7 @@ def index_html(REQUEST=None, showcodes=0, showsemtable=0):
# Responsable de formation:
sco_formsemestre.sem_set_responsable_name(sem)
if showcodes == "1":
if showcodes:
sem["tmpcode"] = "<td><tt>%s</tt></td>" % sem["formsemestre_id"]
else:
sem["tmpcode"] = ""
@ -126,12 +127,12 @@ def index_html(REQUEST=None, showcodes=0, showsemtable=0):
"""
% sco_preferences.get_preference("DeptName")
)
H.append(_sem_table_gt(sems).html())
H.append(_sem_table_gt(sems, showcodes=showcodes).html())
H.append("</table>")
if not showsemtable:
H.append(
'<hr/><p><a href="%s?showsemtable=1">Voir tous les semestres</a></p>'
% REQUEST.URL0
% request.base_url
)
H.append(
@ -242,7 +243,7 @@ def _sem_table_gt(sems, showcodes=False):
rows=sems,
html_class="table_leftalign semlist",
html_sortable=True,
# base_url = '%s?formsemestre_id=%s' % (REQUEST.URL0, formsemestre_id),
# base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id),
# caption='Maquettes enregistrées',
preferences=sco_preferences.SemPreferences(),
)

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -51,6 +51,7 @@ import fcntl
import subprocess
import requests
from flask_login import current_user
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
@ -64,7 +65,7 @@ from app.scodoc.sco_exceptions import ScoValueError
SCO_DUMP_LOCK = "/tmp/scodump.lock"
def sco_dump_and_send_db(REQUEST=None):
def sco_dump_and_send_db():
"""Dump base de données et l'envoie anonymisée pour debug"""
H = [html_sco_header.sco_header(page_title="Assistance technique")]
# get currect (dept) DB name:
@ -93,7 +94,7 @@ def sco_dump_and_send_db(REQUEST=None):
_anonymize_db(ano_db_name)
# Send
r = _send_db(REQUEST, ano_db_name)
r = _send_db(ano_db_name)
if (
r.status_code
== requests.codes.INSUFFICIENT_STORAGE # pylint: disable=no-member
@ -166,34 +167,33 @@ def _anonymize_db(ano_db_name):
def _get_scodoc_serial():
try:
return int(open(os.path.join(scu.SCODOC_VERSION_DIR, "scodoc.sn")).read())
with open(os.path.join(scu.SCODOC_VERSION_DIR, "scodoc.sn")) as f:
return int(f.read())
except:
return 0
def _send_db(REQUEST, ano_db_name):
def _send_db(ano_db_name):
"""Dump this (anonymized) database and send it to tech support"""
log("dumping anonymized database {}".format(ano_db_name))
log(f"dumping anonymized database {ano_db_name}")
try:
data = subprocess.check_output("pg_dump {} | gzip".format(ano_db_name), shell=1)
except subprocess.CalledProcessError as e:
log("sco_dump_and_send_db: exception in anonymisation: {}".format(e))
raise ScoValueError(
"erreur lors de l'anonymisation de la base {}".format(ano_db_name)
dump = subprocess.check_output(
f"pg_dump --format=custom {ano_db_name}", shell=1
)
except subprocess.CalledProcessError as e:
log(f"sco_dump_and_send_db: exception in anonymisation: {e}")
raise ScoValueError(f"erreur lors de l'anonymisation de la base {ano_db_name}")
log("uploading anonymized dump...")
files = {"file": (ano_db_name + ".gz", data)}
files = {"file": (ano_db_name + ".dump", dump)}
r = requests.post(
scu.SCO_DUMP_UP_URL,
files=files,
data={
"dept_name": sco_preferences.get_preference("DeptName"),
"serial": _get_scodoc_serial(),
"sco_user": str(REQUEST.AUTHENTICATED_USER),
"sent_by": sco_users.user_info(str(REQUEST.AUTHENTICATED_USER))[
"nomcomplet"
],
"sco_user": str(current_user),
"sent_by": sco_users.user_info(str(current_user))["nomcomplet"],
"sco_version": sco_version.SCOVERSION,
"sco_fullversion": scu.get_scodoc_version(),
},

175
app/scodoc/sco_edit_apc.py Normal file
View File

@ -0,0 +1,175 @@
##############################################################################
#
# ScoDoc
#
# Copyright (c) 1999 - 2022 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
#
##############################################################################
"""Édition formation APC (BUT)
"""
import flask
from flask import url_for
from flask.templating import render_template
from flask import g, request
from flask_login import current_user
from app import db
from app.models import Formation, UniteEns, Matiere, Module, FormSemestre, ModuleImpl
from app.models.notes import ScolarFormSemestreValidation
import app.scodoc.sco_utils as scu
from app.scodoc import sco_groups
from app.scodoc.sco_utils import ModuleType
def html_edit_formation_apc(
formation,
semestre_idx=None,
editable=True,
tag_editable=True,
):
"""Formulaire html pour visualisation ou édition d'une formation APC.
- Les UEs
- Les ressources
- Les SAÉs
"""
parcours = formation.get_parcours()
assert parcours.APC_SAE
ressources = formation.modules.filter_by(module_type=ModuleType.RESSOURCE).order_by(
Module.semestre_id, Module.numero, Module.code
)
saes = formation.modules.filter_by(module_type=ModuleType.SAE).order_by(
Module.semestre_id, Module.numero, Module.code
)
if semestre_idx is None:
semestre_ids = range(1, parcours.NB_SEM + 1)
else:
semestre_ids = [semestre_idx]
other_modules = formation.modules.filter(
Module.module_type.is_distinct_from(ModuleType.SAE),
Module.module_type.is_distinct_from(ModuleType.RESSOURCE),
).order_by(
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
)
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
icons = {
"arrow_up": arrow_up,
"arrow_down": arrow_down,
"arrow_none": arrow_none,
"delete": scu.icontag(
"delete_small_img",
title="Supprimer (module inutilisé)",
alt="supprimer",
),
"delete_disabled": scu.icontag(
"delete_small_dis_img",
title="Suppression impossible (utilisé dans des semestres)",
),
}
H = [
render_template(
"pn/form_ues.html",
formation=formation,
semestre_ids=semestre_ids,
editable=editable,
tag_editable=tag_editable,
icons=icons,
UniteEns=UniteEns,
),
]
for semestre_idx in semestre_ids:
ressources_in_sem = ressources.filter_by(semestre_id=semestre_idx)
saes_in_sem = saes.filter_by(semestre_id=semestre_idx)
other_modules_in_sem = other_modules.filter_by(semestre_id=semestre_idx)
H += [
render_template(
"pn/form_mods.html",
formation=formation,
titre=f"Ressources du S{semestre_idx}",
create_element_msg="créer une nouvelle ressource",
modules=ressources_in_sem,
module_type=ModuleType.RESSOURCE,
editable=editable,
tag_editable=tag_editable,
icons=icons,
scu=scu,
),
render_template(
"pn/form_mods.html",
formation=formation,
titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}",
create_element_msg="créer une nouvelle SAÉ",
modules=saes_in_sem,
module_type=ModuleType.SAE,
editable=editable,
tag_editable=tag_editable,
icons=icons,
scu=scu,
),
render_template(
"pn/form_mods.html",
formation=formation,
titre=f"Autres modules (non BUT) du S{semestre_idx}",
create_element_msg="créer un nouveau module",
modules=other_modules_in_sem,
module_type=ModuleType.STANDARD,
editable=editable,
tag_editable=tag_editable,
icons=icons,
scu=scu,
),
]
return "\n".join(H)
def html_ue_infos(ue):
"""page d'information sur une UE"""
from app.views import ScoData
formsemestres = (
db.session.query(FormSemestre)
.filter(
ue.id == Module.ue_id,
Module.id == ModuleImpl.module_id,
FormSemestre.id == ModuleImpl.formsemestre_id,
)
.all()
)
nb_etuds_valid_ue = ScolarFormSemestreValidation.query.filter_by(
ue_id=ue.id
).count()
can_safely_be_suppressed = (
(nb_etuds_valid_ue == 0)
and (len(formsemestres) == 0)
and ue.modules.count() == 0
and ue.matieres.count() == 0
)
return render_template(
"pn/ue_infos.html",
# "pn/tmp.html",
titre=f"UE {ue.acronyme} {ue.titre}",
ue=ue,
formsemestres=formsemestres,
nb_etuds_valid_ue=nb_etuds_valid_ue,
can_safely_be_suppressed=can_safely_be_suppressed,
sco=ScoData(),
)

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -29,11 +29,16 @@
(portage from DTML)
"""
import flask
from flask import g, url_for
from flask import g, url_for, request
from app import db
from app import log
from app.models import SHORT_STR_LEN
from app.models.formations import Formation
from app.models.modules import Module
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app import log
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError
@ -47,7 +52,7 @@ from app.scodoc import sco_formsemestre
from app.scodoc import sco_news
def formation_delete(formation_id=None, dialog_confirmed=False, REQUEST=None):
def formation_delete(formation_id=None, dialog_confirmed=False):
"""Delete a formation"""
F = sco_formations.formation_list(args={"formation_id": formation_id})
if not F:
@ -104,7 +109,7 @@ def do_formation_delete(oid):
raise ScoLockedFormError()
cnx = ndb.GetDBConnexion()
# delete all UE in this formation
ues = sco_edit_ue.do_ue_list({"formation_id": oid})
ues = sco_edit_ue.ue_list({"formation_id": oid})
for ue in ues:
sco_edit_ue.do_ue_delete(ue["ue_id"], force=True)
@ -119,12 +124,12 @@ def do_formation_delete(oid):
)
def formation_create(REQUEST=None):
def formation_create():
"""Creation d'une formation"""
return formation_edit(create=True, REQUEST=REQUEST)
return formation_edit(create=True)
def formation_edit(formation_id=None, create=False, REQUEST=None):
def formation_edit(formation_id=None, create=False):
"""Edit or create a formation"""
if create:
H = [
@ -159,8 +164,8 @@ def formation_edit(formation_id=None, create=False, REQUEST=None):
)
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
(
("formation_id", {"default": formation_id, "input_type": "hidden"}),
(
@ -205,6 +210,7 @@ def formation_edit(formation_id=None, create=False, REQUEST=None):
"size": 12,
"title": "Code formation",
"explanation": "code interne. Toutes les formations partageant le même code sont compatibles (compensation de semestres, capitalisation d'UE). Laisser vide si vous ne savez pas, ou entrer le code d'une formation existante.",
"validator": lambda val, _: len(val) < SHORT_STR_LEN,
},
),
(
@ -252,7 +258,7 @@ def formation_edit(formation_id=None, create=False, REQUEST=None):
do_formation_edit(tf[2])
return flask.redirect(
url_for(
"notes.ue_list", scodoc_dept=g.scodoc_dept, formation_id=formation_id
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id
)
)
@ -298,66 +304,64 @@ def do_formation_edit(args):
cnx = ndb.GetDBConnexion()
sco_formations._formationEditor.edit(cnx, args)
invalidate_sems_in_formation(args["formation_id"])
formation: Formation = Formation.query.get(args["formation_id"])
formation.invalidate_cached_sems()
def invalidate_sems_in_formation(formation_id):
"Invalide les semestres utilisant cette formation"
for sem in sco_formsemestre.do_formsemestre_list(
args={"formation_id": formation_id}
):
sco_cache.invalidate_formsemestre(
formsemestre_id=sem["formsemestre_id"]
) # > formation modif.
def module_move(module_id, after=0, REQUEST=None, redirect=1):
def module_move(module_id, after=0, redirect=True):
"""Move before/after previous one (decrement/increment numero)"""
module = sco_edit_module.do_module_list({"module_id": module_id})[0]
redirect = int(redirect)
redirect = bool(redirect)
module = Module.query.get_or_404(module_id)
after = int(after) # 0: deplace avant, 1 deplace apres
if after not in (0, 1):
raise ValueError('invalid value for "after"')
formation_id = module["formation_id"]
others = sco_edit_module.do_module_list({"matiere_id": module["matiere_id"]})
# log('others=%s' % others)
if len(others) > 1:
idx = [p["module_id"] for p in others].index(module_id)
# log('module_move: after=%s idx=%s' % (after, idx))
raise ValueError(f'invalid value for "after" ({after})')
if module.formation.is_apc():
# pas de matières, mais on prend tous les modules de même type de la formation
query = Module.query.filter_by(
semestre_id=module.semestre_id,
formation=module.formation,
module_type=module.module_type,
)
else:
query = Module.query.filter_by(matiere=module.matiere)
modules = query.order_by(Module.numero, Module.code).all()
if len({o.numero for o in modules}) != len(modules):
# il y a des numeros identiques !
scu.objects_renumber(db, modules)
if len(modules) > 1:
idx = [m.id for m in modules].index(module.id)
neigh = None # object to swap with
if after == 0 and idx > 0:
neigh = others[idx - 1]
elif after == 1 and idx < len(others) - 1:
neigh = others[idx + 1]
if neigh: #
# swap numero between partition and its neighbor
# log('moving module %s' % module_id)
cnx = ndb.GetDBConnexion()
module["numero"], neigh["numero"] = neigh["numero"], module["numero"]
if module["numero"] == neigh["numero"]:
neigh["numero"] -= 2 * after - 1
sco_edit_module._moduleEditor.edit(cnx, module)
sco_edit_module._moduleEditor.edit(cnx, neigh)
neigh = modules[idx - 1]
elif after == 1 and idx < len(modules) - 1:
neigh = modules[idx + 1]
if neigh: # échange les numéros
module.numero, neigh.numero = neigh.numero, module.numero
db.session.add(module)
db.session.add(neigh)
db.session.commit()
# redirect to ue_list page:
if redirect:
return flask.redirect(
url_for(
"notes.ue_list", scodoc_dept=g.scodoc_dept, formation_id=formation_id
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=module.formation.id,
semestre_idx=module.ue.semestre_idx,
)
)
def ue_move(ue_id, after=0, redirect=1):
"""Move UE before/after previous one (decrement/increment numero)"""
o = sco_edit_ue.do_ue_list({"ue_id": ue_id})[0]
o = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
# log('ue_move %s (#%s) after=%s' % (ue_id, o['numero'], after))
redirect = int(redirect)
after = int(after) # 0: deplace avant, 1 deplace apres
if after not in (0, 1):
raise ValueError('invalid value for "after"')
formation_id = o["formation_id"]
others = sco_edit_ue.do_ue_list({"formation_id": formation_id})
others = sco_edit_ue.ue_list({"formation_id": formation_id})
if len(others) > 1:
idx = [p["ue_id"] for p in others].index(ue_id)
neigh = None # object to swap with
@ -378,8 +382,9 @@ def ue_move(ue_id, after=0, redirect=1):
if redirect:
return flask.redirect(
url_for(
"notes.ue_list",
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=o["formation_id"],
semestre_idx=o["semestre_idx"],
)
)

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -29,13 +29,19 @@
(portage from DTML)
"""
import flask
from flask import g, url_for
from flask import g, url_for, request
from app.models.formations import Matiere
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app import log
from app.models import Formation
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError
from app.scodoc.sco_exceptions import (
ScoValueError,
ScoLockedFormError,
ScoNonEmptyFormationObject,
)
from app.scodoc import html_sco_header
_matiereEditor = ndb.EditableTable(
@ -47,7 +53,7 @@ _matiereEditor = ndb.EditableTable(
)
def do_matiere_list(*args, **kw):
def matiere_list(*args, **kw):
"list matieres"
cnx = ndb.GetDBConnexion()
return _matiereEditor.list(cnx, *args, **kw)
@ -60,13 +66,13 @@ def do_matiere_edit(*args, **kw):
cnx = ndb.GetDBConnexion()
# check
mat = do_matiere_list({"matiere_id": args[0]["matiere_id"]})[0]
mat = matiere_list({"matiere_id": args[0]["matiere_id"]})[0]
if matiere_is_locked(mat["matiere_id"]):
raise ScoLockedFormError()
# edit
_matiereEditor.edit(cnx, *args, **kw)
formation_id = sco_edit_ue.do_ue_list({"ue_id": mat["ue_id"]})[0]["formation_id"]
sco_edit_formation.invalidate_sems_in_formation(formation_id)
formation_id = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]["formation_id"]
Formation.query.get(formation_id).invalidate_cached_sems()
def do_matiere_create(args):
@ -77,7 +83,7 @@ def do_matiere_create(args):
cnx = ndb.GetDBConnexion()
# check
ue = sco_edit_ue.do_ue_list({"ue_id": args["ue_id"]})[0]
ue = sco_edit_ue.ue_list({"ue_id": args["ue_id"]})[0]
# create matiere
r = _matiereEditor.create(cnx, args)
@ -92,11 +98,11 @@ def do_matiere_create(args):
return r
def matiere_create(ue_id=None, REQUEST=None):
def matiere_create(ue_id=None):
"""Creation d'une matiere"""
from app.scodoc import sco_edit_ue
UE = sco_edit_ue.do_ue_list(args={"ue_id": ue_id})[0]
UE = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
H = [
html_sco_header.sco_header(page_title="Création d'une matière"),
"""<h2>Création d'une matière dans l'UE %(titre)s (%(acronyme)s)</h2>""" % UE,
@ -116,8 +122,8 @@ associé.
</p>""",
]
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
(
("ue_id", {"input_type": "hidden", "default": ue_id}),
("titre", {"size": 30, "explanation": "nom de la matière."}),
@ -134,7 +140,7 @@ associé.
)
dest_url = url_for(
"notes.ue_list", scodoc_dept=g.scodoc_dept, formation_id=UE["formation_id"]
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=UE["formation_id"]
)
if tf[0] == 0:
@ -143,7 +149,7 @@ associé.
return flask.redirect(dest_url)
else:
# check unicity
mats = do_matiere_list(args={"ue_id": ue_id, "titre": tf[2]["titre"]})
mats = matiere_list(args={"ue_id": ue_id, "titre": tf[2]["titre"]})
if mats:
return (
"\n".join(H)
@ -155,6 +161,16 @@ associé.
return flask.redirect(dest_url)
def can_delete_matiere(matiere: Matiere) -> tuple[bool, str]:
"True si la matiere n'est pas utilisée dans des formsemestre"
locked = matiere_is_locked(matiere.id)
if locked:
return False
if any(m.modimpls.all() for m in matiere.modules):
return False
return True
def do_matiere_delete(oid):
"delete matiere and attached modules"
from app.scodoc import sco_formations
@ -164,17 +180,16 @@ def do_matiere_delete(oid):
cnx = ndb.GetDBConnexion()
# check
mat = do_matiere_list({"matiere_id": oid})[0]
ue = sco_edit_ue.do_ue_list({"ue_id": mat["ue_id"]})[0]
locked = matiere_is_locked(mat["matiere_id"])
if locked:
log("do_matiere_delete: mat=%s" % mat)
log("do_matiere_delete: ue=%s" % ue)
log("do_matiere_delete: locked sems: %s" % locked)
raise ScoLockedFormError()
log("do_matiere_delete: matiere_id=%s" % oid)
matiere = Matiere.query.get_or_404(oid)
mat = matiere_list({"matiere_id": oid})[0] # compat sco7
ue = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]
if not can_delete_matiere(matiere):
# il y a au moins un modimpl dans un module de cette matière
raise ScoNonEmptyFormationObject("Matière", matiere.titre)
log("do_matiere_delete: matiere_id=%s" % matiere.id)
# delete all modules in this matiere
mods = sco_edit_module.do_module_list({"matiere_id": oid})
mods = sco_edit_module.module_list({"matiere_id": matiere.id})
for mod in mods:
sco_edit_module.do_module_delete(mod["module_id"])
_matiereEditor.delete(cnx, oid)
@ -189,23 +204,41 @@ def do_matiere_delete(oid):
)
def matiere_delete(matiere_id=None, REQUEST=None):
"""Delete an UE"""
def matiere_delete(matiere_id=None):
"""Delete matière"""
from app.scodoc import sco_edit_ue
M = do_matiere_list(args={"matiere_id": matiere_id})[0]
UE = sco_edit_ue.do_ue_list(args={"ue_id": M["ue_id"]})[0]
matiere = Matiere.query.get_or_404(matiere_id)
if not can_delete_matiere(matiere):
# il y a au moins un modimpl dans un module de cette matière
raise ScoNonEmptyFormationObject(
"Matière",
matiere.titre,
dest_url=url_for(
"notes.ue_table",
formation_id=matiere.ue.formation_id,
semestre_idx=matiere.ue.semestre_idx,
scodoc_dept=g.scodoc_dept,
),
)
mat = matiere_list(args={"matiere_id": matiere_id})[0]
UE = sco_edit_ue.ue_list(args={"ue_id": mat["ue_id"]})[0]
H = [
html_sco_header.sco_header(page_title="Suppression d'une matière"),
"<h2>Suppression de la matière %(titre)s" % M,
"<h2>Suppression de la matière %(titre)s" % mat,
" dans l'UE (%(acronyme)s))</h2>" % UE,
]
dest_url = scu.NotesURL() + "/ue_list?formation_id=" + str(UE["formation_id"])
dest_url = url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(UE["formation_id"]),
)
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
(("matiere_id", {"input_type": "hidden"}),),
initvalues=M,
initvalues=mat,
submitlabel="Confirmer la suppression",
cancelbutton="Annuler",
)
@ -218,22 +251,22 @@ def matiere_delete(matiere_id=None, REQUEST=None):
return flask.redirect(dest_url)
def matiere_edit(matiere_id=None, REQUEST=None):
def matiere_edit(matiere_id=None):
"""Edit matiere"""
from app.scodoc import sco_formations
from app.scodoc import sco_edit_ue
F = do_matiere_list(args={"matiere_id": matiere_id})
F = matiere_list(args={"matiere_id": matiere_id})
if not F:
raise ScoValueError("Matière inexistante !")
F = F[0]
U = sco_edit_ue.do_ue_list(args={"ue_id": F["ue_id"]})
if not F:
ues = sco_edit_ue.ue_list(args={"ue_id": F["ue_id"]})
if not ues:
raise ScoValueError("UE inexistante !")
U = U[0]
Fo = sco_formations.formation_list(args={"formation_id": U["formation_id"]})[0]
ue = ues[0]
Fo = sco_formations.formation_list(args={"formation_id": ue["formation_id"]})[0]
ues = sco_edit_ue.do_ue_list(args={"formation_id": U["formation_id"]})
ues = sco_edit_ue.ue_list(args={"formation_id": ue["formation_id"]})
ue_names = ["%(acronyme)s (%(titre)s)" % u for u in ues]
ue_ids = [u["ue_id"] for u in ues]
H = [
@ -256,8 +289,8 @@ des notes.</em>
associé.
</p>"""
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
(
("matiere_id", {"input_type": "hidden"}),
(
@ -278,15 +311,18 @@ associé.
submitlabel="Modifier les valeurs",
)
dest_url = scu.NotesURL() + "/ue_list?formation_id=" + str(U["formation_id"])
dest_url = url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(ue["formation_id"]),
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + help + html_sco_header.sco_footer()
elif tf[0] == -1:
return flask.redirect(dest_url)
else:
# check unicity
mats = do_matiere_list(args={"ue_id": tf[2]["ue_id"], "titre": tf[2]["titre"]})
mats = matiere_list(args={"ue_id": tf[2]["ue_id"], "titre": tf[2]["titre"]})
if len(mats) > 1 or (len(mats) == 1 and mats[0]["matiere_id"] != matiere_id):
return (
"\n".join(H)
@ -323,4 +359,4 @@ def matiere_is_locked(matiere_id):
""",
{"matiere_id": matiere_id},
)
return len(r) > 0
return len(r) > 0

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -29,36 +29,32 @@
(portage from DTML)
"""
import flask
from flask import url_for, g
from flask import url_for, render_template
from flask import g, request
from flask_login import current_user
from app.models import APO_CODE_STR_LEN
from app.models import Matiere, Module, UniteEns
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
from app import log
from app import models
from app.models import Formation
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError, ScoGenError
from app.scodoc.sco_exceptions import (
ScoValueError,
ScoLockedFormError,
ScoGenError,
ScoNonEmptyFormationObject,
)
from app.scodoc import html_sco_header
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_news
_MODULE_HELP = """<p class="help">
Les modules sont décrits dans le programme pédagogique. Un module est pour ce
logiciel l'unité pédagogique élémentaire. On va lui associer une note
à travers des <em>évaluations</em>. <br/>
Cette note (moyenne de module) sera utilisée pour calculer la moyenne
générale (et la moyenne de l'UE à laquelle appartient le module). Pour
cela, on utilisera le <em>coefficient</em> associé au module.
</p>
<p class="help">Un module possède un enseignant responsable
(typiquement celui qui dispense le cours magistral). On peut associer
au module une liste d'enseignants (typiquement les chargés de TD).
Tous ces enseignants, plus le responsable du semestre, pourront
saisir et modifier les notes de ce module.
</p> """
_moduleEditor = ndb.EditableTable(
"notes_modules",
"module_id",
@ -93,14 +89,14 @@ _moduleEditor = ndb.EditableTable(
)
def do_module_list(*args, **kw):
def module_list(*args, **kw):
"list modules"
cnx = ndb.GetDBConnexion()
return _moduleEditor.list(cnx, *args, **kw)
def do_module_create(args) -> int:
"create a module"
"Create a module. Returns id of new object."
# create
from app.scodoc import sco_formations
@ -118,77 +114,164 @@ def do_module_create(args) -> int:
return r
def module_create(matiere_id=None, REQUEST=None):
"""Creation d'un module"""
def module_create(matiere_id=None, module_type=None, semestre_id=None):
"""Création d'un module"""
from app.scodoc import sco_formations
from app.scodoc import sco_edit_ue
if matiere_id is None:
matiere = Matiere.query.get_or_404(matiere_id)
if matiere is None:
raise ScoValueError("invalid matiere !")
M = sco_edit_matiere.do_matiere_list(args={"matiere_id": matiere_id})[0]
UE = sco_edit_ue.do_ue_list(args={"ue_id": M["ue_id"]})[0]
Fo = sco_formations.formation_list(args={"formation_id": UE["formation_id"]})[0]
parcours = sco_codes_parcours.get_parcours_from_code(Fo["type_parcours"])
semestres_indices = list(range(1, parcours.NB_SEM + 1))
H = [
html_sco_header.sco_header(page_title="Création d'un module"),
"""<h2>Création d'un module dans la matière %(titre)s""" % M,
""" (UE %(acronyme)s)</h2>""" % UE,
_MODULE_HELP,
]
# cherche le numero adequat (pour placer le module en fin de liste)
Mods = do_module_list(args={"matiere_id": matiere_id})
if Mods:
default_num = max([m["numero"] for m in Mods]) + 10
ue = matiere.ue
parcours = ue.formation.get_parcours()
is_apc = parcours.APC_SAE
ues = ue.formation.ues.order_by(
UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme
).all()
# cherche le numero adéquat (pour placer le module en fin de liste)
modules = matiere.ue.formation.modules.all()
if modules:
default_num = max([m.numero or 0 for m in modules]) + 10
else:
default_num = 10
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
if is_apc and module_type is not None:
object_name = scu.MODULE_TYPE_NAMES[module_type]
else:
object_name = "Module"
H = [
html_sco_header.sco_header(page_title=f"Création {object_name}"),
]
if is_apc:
H += [
f"""<h2>Création {object_name} dans la formation {ue.formation.acronyme}</h2>"""
]
else:
H += [
f"""<h2>Création {object_name} dans la matière {matiere.titre},
(UE {ue.acronyme})</h2>
"""
]
H += [
render_template(
"scodoc/help/modules.html",
is_apc=is_apc,
ue=ue,
semestre_id=semestre_id,
)
]
descr = [
(
"code",
{
"size": 10,
"explanation": "code du module, ressource ou SAÉ. Exemple M1203, R2.01, ou SAÉ 3.4. Ce code doit être unique dans la formation.",
"allow_null": False,
"validator": lambda val, field, formation_id=ue.formation_id: check_module_code_unicity(
val, field, formation_id
),
},
),
(
"titre",
{
"size": 30,
"explanation": "nom du module. Exemple: <em>Introduction à la démarche ergonomique</em>",
},
),
(
"abbrev",
{
"size": 20,
"explanation": "nom abrégé (pour les bulletins). Exemple: <em>Intro. à l'ergonomie</em>",
},
),
]
semestres_indices = list(range(1, parcours.NB_SEM + 1))
if is_apc: # BUT: choix de l'UE de rattachement (qui donnera le semestre)
descr += [
(
"code",
{
"size": 10,
"explanation": "code du module (doit être unique dans la formation)",
"allow_null": False,
"validator": lambda val, field, formation_id=Fo[
"formation_id"
]: check_module_code_unicity(val, field, formation_id),
},
),
("titre", {"size": 30, "explanation": "nom du module"}),
("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}),
(
"module_type",
"ue_id",
{
"input_type": "menu",
"title": "Type",
"explanation": "",
"labels": ("Standard", "Malus"),
"allowed_values": (str(scu.MODULE_STANDARD), str(scu.MODULE_MALUS)),
"type": "int",
"title": "UE de rattachement",
"explanation": "utilisée pour la présentation dans certains documents",
"labels": [f"{u.acronyme} {u.titre}" for u in ues],
"allowed_values": [u.id for u in ues],
},
),
]
else:
# Formations classiques: choix du semestre
descr += [
(
"heures_cours",
{"size": 4, "type": "float", "explanation": "nombre d'heures de cours"},
),
(
"heures_td",
"semestre_id",
{
"size": 4,
"type": "float",
"explanation": "nombre d'heures de Travaux Dirigés",
"input_type": "menu",
"type": "int",
"title": parcours.SESSION_NAME.capitalize(),
"explanation": "%s du module" % parcours.SESSION_NAME,
"labels": [str(x) for x in semestres_indices],
"allowed_values": semestres_indices,
},
),
]
descr += [
(
"module_type",
{
"input_type": "menu",
"title": "Type",
"explanation": "",
"labels": [x.name.capitalize() for x in scu.ModuleType],
"allowed_values": [str(int(x)) for x in scu.ModuleType],
},
),
(
"heures_cours",
{
"title": "Heures de cours",
"size": 4,
"type": "float",
"explanation": "nombre d'heures de cours (optionnel)",
},
),
(
"heures_td",
{
"title": "Heures de TD",
"size": 4,
"type": "float",
"explanation": "nombre d'heures de Travaux Dirigés (optionnel)",
},
),
(
"heures_tp",
{
"title": "Heures de TP",
"size": 4,
"type": "float",
"explanation": "nombre d'heures de Travaux Pratiques (optionnel)",
},
),
]
if is_apc:
descr += [
(
"heures_tp",
"sep_ue_coefs",
{
"size": 4,
"type": "float",
"explanation": "nombre d'heures de Travaux Pratiques",
"input_type": "separator",
"title": """
<div>(<em>les coefficients vers les UE se fixent sur la page dédiée</em>)
</div>""",
},
),
]
else:
descr += [
(
"coefficient",
{
@ -198,72 +281,95 @@ def module_create(matiere_id=None, REQUEST=None):
"allow_null": False,
},
),
# ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS (inutilisés: les crédits sont associés aux UE)' }),
("formation_id", {"default": UE["formation_id"], "input_type": "hidden"}),
("ue_id", {"default": M["ue_id"], "input_type": "hidden"}),
("matiere_id", {"default": M["matiere_id"], "input_type": "hidden"}),
(
"semestre_id",
{
"input_type": "menu",
"type": "int",
"title": parcours.SESSION_NAME.capitalize(),
"explanation": "%s de début du module dans la formation standard"
% parcours.SESSION_NAME,
"labels": [str(x) for x in semestres_indices],
"allowed_values": semestres_indices,
},
),
(
"code_apogee",
{
"title": "Code Apogée",
"size": 25,
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
},
),
(
"numero",
{
"size": 2,
"explanation": "numéro (1,2,3,4...) pour ordre d'affichage",
"type": "int",
"default": default_num,
},
),
]
descr += [
# ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS (inutilisés: les crédits sont associés aux UE)' }),
("formation_id", {"default": ue.formation_id, "input_type": "hidden"}),
("ue_id", {"default": ue.id, "input_type": "hidden"}),
("matiere_id", {"default": matiere.id, "input_type": "hidden"}),
(
"code_apogee",
{
"title": "Code Apogée",
"size": 25,
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
},
),
(
"numero",
{
"size": 2,
"explanation": "numéro (1,2,3,4...) pour ordre d'affichage",
"type": "int",
"default": default_num,
},
),
]
args = scu.get_request_args()
tf = TrivialFormulator(
request.base_url,
args,
descr,
submitlabel="Créer ce module",
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
else:
do_module_create(tf[2])
if is_apc:
# BUT: l'UE indique le semestre
selected_ue = UniteEns.query.get(tf[2]["ue_id"])
if selected_ue is None:
raise ValueError("UE invalide")
tf[2]["semestre_id"] = selected_ue.semestre_idx
_ = do_module_create(tf[2])
return flask.redirect(
url_for(
"notes.ue_list",
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=UE["formation_id"],
formation_id=ue.formation_id,
semestre_idx=tf[2]["semestre_id"],
)
)
def can_delete_module(module):
"True si le module n'est pas utilisée dans des formsemestre"
return len(module.modimpls.all()) == 0
def do_module_delete(oid):
"delete module"
from app.scodoc import sco_formations
mod = do_module_list({"module_id": oid})[0]
if module_is_locked(mod["module_id"]):
module = Module.query.get_or_404(oid)
mod = module_list({"module_id": oid})[0] # sco7
if module_is_locked(module.id):
raise ScoLockedFormError()
if not can_delete_module(module):
raise ScoNonEmptyFormationObject(
"Module",
msg=module.titre,
dest_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=module.formation_id,
semestre_idx=module.ue.semestre_idx,
),
)
# S'il y a des moduleimpls, on ne peut pas detruire le module !
mods = sco_moduleimpl.do_moduleimpl_list(module_id=oid)
mods = sco_moduleimpl.moduleimpl_list(module_id=oid)
if mods:
err_page = scu.confirm_dialog(
message="""<h3>Destruction du module impossible car il est utilisé dans des semestres existants !</h3>""",
helpmsg="""Il faut d'abord supprimer le semestre. Mais il est peut être préférable de laisser ce programme intact et d'en créer une nouvelle version pour la modifier.""",
dest_url="ue_list",
parameters={"formation_id": mod["formation_id"]},
)
err_page = f"""<h3>Destruction du module impossible car il est utilisé dans des semestres existants !</h3>
<p class="help">Il faut d'abord supprimer le semestre (ou en retirer ce module). Mais il est peut être préférable de
laisser ce programme intact et d'en créer une nouvelle version pour la modifier sans affecter les semestres déjà en place.
</p>
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
formation_id=mod["formation_id"])}">reprendre</a>
"""
raise ScoGenError(err_page)
# delete
cnx = ndb.GetDBConnexion()
@ -279,25 +385,38 @@ def do_module_delete(oid):
)
def module_delete(module_id=None, REQUEST=None):
def module_delete(module_id=None):
"""Delete a module"""
if not module_id:
raise ScoValueError("invalid module !")
Mods = do_module_list(args={"module_id": module_id})
if not Mods:
raise ScoValueError("Module inexistant !")
Mod = Mods[0]
module = Module.query.get_or_404(module_id)
mod = module_list(args={"module_id": module_id})[0] # sco7
if not can_delete_module(module):
raise ScoNonEmptyFormationObject(
"Module",
msg=module.titre,
dest_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=module.formation_id,
semestre_idx=module.ue.semestre_idx,
),
)
H = [
html_sco_header.sco_header(page_title="Suppression d'un module"),
"""<h2>Suppression du module %(titre)s (%(code)s)</h2>""" % Mod,
"""<h2>Suppression du module %(titre)s (%(code)s)</h2>""" % mod,
]
dest_url = scu.NotesURL() + "/ue_list?formation_id=" + str(Mod["formation_id"])
dest_url = url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(mod["formation_id"]),
)
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
(("module_id", {"input_type": "hidden"}),),
initvalues=Mod,
initvalues=mod,
submitlabel="Confirmer la suppression",
cancelbutton="Annuler",
)
@ -310,67 +429,68 @@ def module_delete(module_id=None, REQUEST=None):
return flask.redirect(dest_url)
def do_module_edit(val):
def do_module_edit(vals: dict) -> None:
"edit a module"
from app.scodoc import sco_edit_formation
# check
mod = do_module_list({"module_id": val["module_id"]})[0]
mod = module_list({"module_id": vals["module_id"]})[0]
if module_is_locked(mod["module_id"]):
# formation verrouillée: empeche de modifier certains champs:
protected_fields = ("coefficient", "ue_id", "matiere_id", "semestre_id")
for f in protected_fields:
if f in val:
del val[f]
if f in vals:
del vals[f]
# edit
cnx = ndb.GetDBConnexion()
_moduleEditor.edit(cnx, val)
sco_edit_formation.invalidate_sems_in_formation(mod["formation_id"])
_moduleEditor.edit(cnx, vals)
Formation.query.get(mod["formation_id"]).invalidate_cached_sems()
def check_module_code_unicity(code, field, formation_id, module_id=None):
"true si code module unique dans la formation"
Mods = do_module_list(args={"code": code, "formation_id": formation_id})
Mods = module_list(args={"code": code, "formation_id": formation_id})
if module_id: # edition: supprime le module en cours
Mods = [m for m in Mods if m["module_id"] != module_id]
return len(Mods) == 0
def module_edit(module_id=None, REQUEST=None):
def module_edit(module_id=None):
"""Edit a module"""
from app.scodoc import sco_formations
from app.scodoc import sco_tag_module
if not module_id:
raise ScoValueError("invalid module !")
Mod = do_module_list(args={"module_id": module_id})
if not Mod:
modules = module_list(args={"module_id": module_id})
if not modules:
raise ScoValueError("invalid module !")
Mod = Mod[0]
module = modules[0]
a_module = models.Module.query.get(module_id)
unlocked = not module_is_locked(module_id)
Fo = sco_formations.formation_list(args={"formation_id": Mod["formation_id"]})[0]
parcours = sco_codes_parcours.get_parcours_from_code(Fo["type_parcours"])
M = ndb.SimpleDictFetch(
formation_id = module["formation_id"]
formation = sco_formations.formation_list(args={"formation_id": formation_id})[0]
parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"])
is_apc = parcours.APC_SAE
ues_matieres = ndb.SimpleDictFetch(
"""SELECT ue.acronyme, mat.*, mat.id AS matiere_id
FROM notes_matieres mat, notes_ue ue
WHERE mat.ue_id = ue.id
AND ue.formation_id = %(formation_id)s
ORDER BY ue.numero, mat.numero
""",
{"formation_id": Mod["formation_id"]},
{"formation_id": formation_id},
)
Mnames = ["%s / %s" % (x["acronyme"], x["titre"]) for x in M]
Mids = ["%s!%s" % (x["ue_id"], x["matiere_id"]) for x in M]
Mod["ue_matiere_id"] = "%s!%s" % (Mod["ue_id"], Mod["matiere_id"])
mat_names = ["%s / %s" % (x["acronyme"], x["titre"]) for x in ues_matieres]
ue_mat_ids = ["%s!%s" % (x["ue_id"], x["matiere_id"]) for x in ues_matieres]
module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"])
semestres_indices = list(range(1, parcours.NB_SEM + 1))
dest_url = scu.NotesURL() + "/ue_list?formation_id=" + str(Mod["formation_id"])
H = [
html_sco_header.sco_header(
page_title="Modification du module %(titre)s" % Mod,
page_title="Modification du module %(titre)s" % module,
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
javascripts=[
"libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
@ -378,65 +498,80 @@ def module_edit(module_id=None, REQUEST=None):
"js/module_tag_editor.js",
],
),
"""<h2>Modification du module %(titre)s""" % Mod,
""" (formation %(acronyme)s, version %(version)s)</h2>""" % Fo,
_MODULE_HELP,
"""<h2>Modification du module %(titre)s""" % module,
""" (formation %(acronyme)s, version %(version)s)</h2>""" % formation,
render_template("scodoc/help/modules.html", is_apc=is_apc),
]
if not unlocked:
H.append(
"""<div class="ue_warning"><span>Formation verrouillée, seuls certains éléments peuvent être modifiés</span></div>"""
)
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
descr = [
(
"code",
{
"size": 10,
"explanation": "code du module (doit être unique dans la formation)",
"allow_null": False,
"validator": lambda val, field, formation_id=formation_id: check_module_code_unicity(
val, field, formation_id, module_id=module_id
),
},
),
("titre", {"size": 30, "explanation": "nom du module"}),
("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}),
(
"module_type",
{
"input_type": "menu",
"title": "Type",
"explanation": "",
"labels": [x.name.capitalize() for x in scu.ModuleType],
"allowed_values": [str(int(x)) for x in scu.ModuleType],
"enabled": unlocked,
},
),
(
"heures_cours",
{"size": 4, "type": "float", "explanation": "nombre d'heures de cours"},
),
(
"heures_td",
{
"size": 4,
"type": "float",
"explanation": "nombre d'heures de Travaux Dirigés",
},
),
(
"heures_tp",
{
"size": 4,
"type": "float",
"explanation": "nombre d'heures de Travaux Pratiques",
},
),
]
if is_apc:
coefs_descr = a_module.ue_coefs_descr()
if coefs_descr:
coefs_descr_txt = ", ".join(["%s: %s" % x for x in coefs_descr])
else:
coefs_descr_txt = """<span class="missing_value">non définis</span>"""
descr += [
(
"code",
"ue_coefs",
{
"size": 10,
"explanation": "code du module (doit être unique dans la formation)",
"allow_null": False,
"validator": lambda val, field, formation_id=Mod[
"formation_id"
]: check_module_code_unicity(
val, field, formation_id, module_id=module_id
),
"readonly": True,
"title": "Coefficients vers les UE",
"default": coefs_descr_txt,
"explanation": "passer par la page d'édition de la formation pour modifier les coefficients",
},
),
("titre", {"size": 30, "explanation": "nom du module"}),
("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}),
(
"module_type",
{
"input_type": "menu",
"title": "Type",
"explanation": "",
"labels": ("Standard", "Malus"),
"allowed_values": (str(scu.MODULE_STANDARD), str(scu.MODULE_MALUS)),
"enabled": unlocked,
},
),
(
"heures_cours",
{"size": 4, "type": "float", "explanation": "nombre d'heures de cours"},
),
(
"heures_td",
{
"size": 4,
"type": "float",
"explanation": "nombre d'heures de Travaux Dirigés",
},
),
(
"heures_tp",
{
"size": 4,
"type": "float",
"explanation": "nombre d'heures de Travaux Pratiques",
},
),
)
]
else: # Module classique avec coef scalaire:
descr += [
(
"coefficient",
{
@ -447,21 +582,39 @@ def module_edit(module_id=None, REQUEST=None):
"enabled": unlocked,
},
),
# ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS', 'enabled' : unlocked }),
("formation_id", {"input_type": "hidden"}),
("ue_id", {"input_type": "hidden"}),
("module_id", {"input_type": "hidden"}),
]
descr += [
("formation_id", {"input_type": "hidden"}),
("ue_id", {"input_type": "hidden"}),
("module_id", {"input_type": "hidden"}),
(
"ue_matiere_id",
{
"input_type": "menu",
"title": "Rattachement :" if is_apc else "Matière :",
"explanation": "UE de rattachement, utilisée pour la présentation"
if is_apc
else "un module appartient à une seule matière.",
"labels": mat_names,
"allowed_values": ue_mat_ids,
"enabled": unlocked,
},
),
]
if is_apc:
# le semestre du module est toujours celui de son UE
descr += [
(
"ue_matiere_id",
"semestre_id",
{
"input_type": "menu",
"title": "Matière",
"explanation": "un module appartient à une seule matière.",
"labels": Mnames,
"allowed_values": Mids,
"enabled": unlocked,
"input_type": "hidden",
"type": "int",
"readonly": True,
},
),
)
]
else:
descr += [
(
"semestre_id",
{
@ -474,52 +627,83 @@ def module_edit(module_id=None, REQUEST=None):
"allowed_values": semestres_indices,
"enabled": unlocked,
},
),
(
"code_apogee",
{
"title": "Code Apogée",
"size": 25,
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
},
),
(
"numero",
{
"size": 2,
"explanation": "numéro (1,2,3,4...) pour ordre d'affichage",
"type": "int",
},
),
)
]
descr += [
(
"code_apogee",
{
"title": "Code Apogée",
"size": 25,
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
"validator": lambda val, _: len(val) < APO_CODE_STR_LEN,
},
),
(
"numero",
{
"size": 2,
"explanation": "numéro (1,2,3,4...) pour ordre d'affichage",
"type": "int",
},
),
]
# force module semestre_idx to its UE
if a_module.ue.semestre_idx:
module["semestre_id"] = a_module.ue.semestre_idx
# Filet de sécurité si jamais l'UE n'a pas non plus de semestre:
if not module["semestre_id"]:
module["semestre_id"] = 1
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
descr,
html_foot_markup="""<div style="width: 90%;"><span class="sco_tag_edit"><textarea data-module_id="{}" class="module_tag_editor">{}</textarea></span></div>""".format(
module_id, ",".join(sco_tag_module.module_tag_list(module_id))
),
initvalues=Mod,
initvalues=module,
submitlabel="Modifier ce module",
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1:
return flask.redirect(dest_url)
return flask.redirect(
url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=formation_id,
semestre_idx=module["semestre_id"],
)
)
else:
# l'UE peut changer
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
# En APC, force le semestre égal à celui de l'UE
if is_apc:
selected_ue = UniteEns.query.get(tf[2]["ue_id"])
if selected_ue is None:
raise ValueError("UE invalide")
tf[2]["semestre_id"] = selected_ue.semestre_idx
# Check unicité code module dans la formation
do_module_edit(tf[2])
return flask.redirect(dest_url)
return flask.redirect(
url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=formation_id,
semestre_idx=tf[2]["semestre_id"],
)
)
# Edition en ligne du code Apogee
def edit_module_set_code_apogee(id=None, value=None, REQUEST=None):
def edit_module_set_code_apogee(id=None, value=None):
"Set UE code apogee"
module_id = id
value = value.strip("-_ \t")
value = str(value).strip("-_ \t")
log("edit_module_set_code_apogee: module_id=%s code_apogee=%s" % (module_id, value))
modules = do_module_list(args={"module_id": module_id})
modules = module_list(args={"module_id": module_id})
if not modules:
return "module invalide" # should not occur
@ -529,7 +713,7 @@ def edit_module_set_code_apogee(id=None, value=None, REQUEST=None):
return value
def module_list(formation_id, REQUEST=None):
def module_table(formation_id):
"""Liste des modules de la formation
(XXX inutile ou a revoir)
"""
@ -544,9 +728,9 @@ def module_list(formation_id, REQUEST=None):
% F,
'<ul class="notes_module_list">',
]
editable = REQUEST.AUTHENTICATED_USER.has_permission(Permission.ScoChangeFormation)
editable = current_user.has_permission(Permission.ScoChangeFormation)
for Mod in do_module_list(args={"formation_id": formation_id}):
for Mod in module_list(args={"formation_id": formation_id}):
H.append('<li class="notes_module_list">%s' % Mod)
if editable:
H.append('<a href="module_edit?module_id=%(module_id)s">modifier</a>' % Mod)
@ -578,37 +762,41 @@ def module_is_locked(module_id):
def module_count_moduleimpls(module_id):
"Number of moduleimpls using this module"
mods = sco_moduleimpl.do_moduleimpl_list(module_id=module_id)
mods = sco_moduleimpl.moduleimpl_list(module_id=module_id)
return len(mods)
def formation_add_malus_modules(formation_id, titre=None, REQUEST=None):
def formation_add_malus_modules(formation_id, titre=None, redirect=True):
"""Création d'un module de "malus" dans chaque UE d'une formation"""
from app.scodoc import sco_edit_ue
ue_list = sco_edit_ue.do_ue_list(args={"formation_id": formation_id})
ues = sco_edit_ue.ue_list(args={"formation_id": formation_id})
for ue in ue_list:
for ue in ues:
# Un seul module de malus par UE:
nb_mod_malus = len(
[
mod
for mod in do_module_list(args={"ue_id": ue["ue_id"]})
if mod["module_type"] == scu.MODULE_MALUS
for mod in module_list(args={"ue_id": ue["ue_id"]})
if mod["module_type"] == ModuleType.MALUS
]
)
if nb_mod_malus == 0:
ue_add_malus_module(ue["ue_id"], titre=titre, REQUEST=REQUEST)
ue_add_malus_module(ue["ue_id"], titre=titre)
if REQUEST:
return flask.redirect("ue_list?formation_id=" + str(formation_id))
if redirect:
return flask.redirect(
url_for(
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id
)
)
def ue_add_malus_module(ue_id, titre=None, code=None, REQUEST=None):
def ue_add_malus_module(ue_id, titre=None, code=None):
"""Add a malus module in this ue"""
from app.scodoc import sco_edit_ue
ue = sco_edit_ue.do_ue_list(args={"ue_id": ue_id})[0]
ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
if titre is None:
titre = ""
@ -627,7 +815,7 @@ def ue_add_malus_module(ue_id, titre=None, code=None, REQUEST=None):
)
# Matiere pour placer le module malus
Matlist = sco_edit_matiere.do_matiere_list(args={"ue_id": ue_id})
Matlist = sco_edit_matiere.matiere_list(args={"ue_id": ue_id})
numero = max([mat["numero"] for mat in Matlist]) + 10
matiere_id = sco_edit_matiere.do_matiere_create(
{"ue_id": ue_id, "titre": "Malus", "numero": numero}
@ -642,7 +830,7 @@ def ue_add_malus_module(ue_id, titre=None, code=None, REQUEST=None):
"matiere_id": matiere_id,
"formation_id": ue["formation_id"],
"semestre_id": semestre_id,
"module_type": scu.MODULE_MALUS,
"module_type": ModuleType.MALUS,
},
)

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -33,10 +33,10 @@ XXX incompatible avec les ics HyperPlanning Paris 13 (était pour GPU).
"""
import six.moves.urllib.request, six.moves.urllib.error, six.moves.urllib.parse
import traceback
import icalendar
import pprint
import traceback
import urllib
import app.scodoc.sco_utils as scu
from app import log
@ -80,7 +80,7 @@ def formsemestre_load_ics(sem):
ics_data = ""
else:
log("Loading edt from %s" % ics_url)
f = six.moves.urllib.request.urlopen(
f = urllib.request.urlopen(
ics_url, timeout=5
) # 5s TODO: add config parameter, eg for slow networks
ics_data = f.read()
@ -123,7 +123,7 @@ def get_edt_transcodage_groups(formsemestre_id):
return edt2sco, sco2edt, msg
def group_edt_json(group_id, start="", end="", REQUEST=None): # actuellement inutilisé
def group_edt_json(group_id, start="", end=""): # actuellement inutilisé
"""EDT complet du semestre, au format JSON
TODO: indiquer un groupe
TODO: utiliser start et end (2 dates au format ISO YYYY-MM-DD)
@ -149,7 +149,7 @@ def group_edt_json(group_id, start="", end="", REQUEST=None): # actuellement in
}
J.append(d)
return scu.sendJSON(REQUEST, J)
return scu.sendJSON(J)
"""XXX
@ -159,9 +159,7 @@ for e in events:
"""
def experimental_calendar(
group_id=None, formsemestre_id=None, REQUEST=None
): # inutilisé
def experimental_calendar(group_id=None, formsemestre_id=None): # inutilisé
"""experimental page"""
return "\n".join(
[

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -32,18 +32,18 @@
Voir sco_apogee_csv.py pour la structure du fichier Apogée.
Stockage: utilise sco_archive.py
=> /opt/scodoc/var/scodoc/archives/apo_csv/RT/2016-1/2016-07-03-16-12-19/V3ASR.csv
=> /opt/scodoc/var/scodoc/archives/apo_csv/<dept_id>/2016-1/2016-07-03-16-12-19/V3ASR.csv
pour une maquette de l'année scolaire 2016, semestre 1, etape V3ASR
ou bien (à partir de ScoDoc 1678) :
/opt/scodoc/var/scodoc/archives/apo_csv/RT/2016-1/2016-07-03-16-12-19/V3ASR!111.csv
/opt/scodoc/var/scodoc/archives/apo_csv/<dept_id>/2016-1/2016-07-03-16-12-19/V3ASR!111.csv
pour une maquette de l'étape V3ASR version VDI 111.
La version VDI sera ignorée sauf si elle est indiquée dans l'étape du semestre.
apo_csv_get()
API:
apo_csv_store( annee_scolaire, sem_id)
# apo_csv_store(csv_data, annee_scolaire, sem_id)
store maq file (archive)
apo_csv_get(etape_apo, annee_scolaire, sem_id, vdi_apo=None)
@ -101,7 +101,7 @@ ApoCSVArchive = ApoCSVArchiver()
def apo_csv_store(csv_data: str, annee_scolaire, sem_id):
"""
csv_data: maquette content (string)
csv_data: maquette content (str))
annee_scolaire: int (2016)
sem_id: 0 (année ?), 1 (premier semestre de l'année) ou 2 (deuxième semestre)
:return: etape_apo du fichier CSV stocké
@ -378,7 +378,7 @@ e.associate_sco( apo_data)
print apo_csv_list_stored_archives()
apo_csv_store(csv_data, annee_scolaire, sem_id)
# apo_csv_store(csv_data, annee_scolaire, sem_id)

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -32,7 +32,7 @@ import io
from zipfile import ZipFile
import flask
from flask import url_for, g, send_file
from flask import url_for, g, send_file, request
# from werkzeug.utils import send_file
@ -48,7 +48,7 @@ from app.scodoc import sco_preferences
from app.scodoc import sco_semset
from app.scodoc import sco_etud
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_apogee_csv import APO_PORTAL_ENCODING, APO_INPUT_ENCODING
from app.scodoc.sco_apogee_csv import APO_INPUT_ENCODING, APO_OUTPUT_ENCODING
from app.scodoc.sco_exceptions import ScoValueError
@ -62,7 +62,6 @@ def apo_semset_maq_status(
block_export_res_ues=False,
block_export_res_modules=False,
block_export_res_sdj=True,
REQUEST=None,
):
"""Page statut / tableau de bord"""
if not semset_id:
@ -83,7 +82,7 @@ def apo_semset_maq_status(
prefs = sco_preferences.SemPreferences()
tab_archives = table_apo_csv_list(semset, REQUEST=REQUEST)
tab_archives = table_apo_csv_list(semset)
(
ok_for_export,
@ -250,7 +249,7 @@ def apo_semset_maq_status(
"""<form name="f" method="get" action="%s">
<input type="hidden" name="semset_id" value="%s"></input>
<div><input type="checkbox" name="allow_missing_apo" value="1" onchange="document.f.submit()" """
% (REQUEST.URL0, semset_id)
% (request.base_url, semset_id)
)
if allow_missing_apo:
H.append("checked")
@ -357,7 +356,7 @@ def apo_semset_maq_status(
H.append(
", ".join(
[
'<a class="stdlink" href="ue_list?formation_id=%(formation_id)s">%(acronyme)s v%(version)s</a>'
'<a class="stdlink" href="ue_table?formation_id=%(formation_id)s">%(acronyme)s v%(version)s</a>'
% f
for f in formations
]
@ -430,7 +429,7 @@ def apo_semset_maq_status(
return "\n".join(H)
def table_apo_csv_list(semset, REQUEST=None):
def table_apo_csv_list(semset):
"""Table des archives (triée par date d'archivage)"""
annee_scolaire = semset["annee_scolaire"]
sem_id = semset["sem_id"]
@ -476,7 +475,7 @@ def table_apo_csv_list(semset, REQUEST=None):
rows=T,
html_class="table_leftalign apo_maq_list",
html_sortable=True,
# base_url = '%s?formsemestre_id=%s' % (REQUEST.URL0, formsemestre_id),
# base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id),
# caption='Maquettes enregistrées',
preferences=sco_preferences.SemPreferences(),
)
@ -484,7 +483,7 @@ def table_apo_csv_list(semset, REQUEST=None):
return tab
def view_apo_etuds(semset_id, title="", nip_list="", format="html", REQUEST=None):
def view_apo_etuds(semset_id, title="", nip_list="", format="html"):
"""Table des étudiants Apogée par nips
nip_list est une chaine, codes nip séparés par des ,
"""
@ -517,11 +516,10 @@ def view_apo_etuds(semset_id, title="", nip_list="", format="html", REQUEST=None
etuds=list(etuds.values()),
keys=("nip", "etape_apo", "nom", "prenom", "inscriptions_scodoc"),
format=format,
REQUEST=REQUEST,
)
def view_scodoc_etuds(semset_id, title="", nip_list="", format="html", REQUEST=None):
def view_scodoc_etuds(semset_id, title="", nip_list="", format="html"):
"""Table des étudiants ScoDoc par nips ou etudids"""
if not isinstance(nip_list, str):
nip_list = str(nip_list)
@ -541,13 +539,10 @@ def view_scodoc_etuds(semset_id, title="", nip_list="", format="html", REQUEST=N
etuds=etuds,
keys=("code_nip", "nom", "prenom"),
format=format,
REQUEST=REQUEST,
)
def _view_etuds_page(
semset_id, title="", etuds=[], keys=(), format="html", REQUEST=None
):
def _view_etuds_page(semset_id, title="", etuds=[], keys=(), format="html"):
# Tri les étudiants par nom:
if etuds:
etuds.sort(key=lambda x: (x["nom"], x["prenom"]))
@ -578,7 +573,7 @@ def _view_etuds_page(
preferences=sco_preferences.SemPreferences(),
)
if format != "html":
return tab.make_page(format=format, REQUEST=REQUEST)
return tab.make_page(format=format)
H.append(tab.html())
@ -590,9 +585,7 @@ def _view_etuds_page(
return "\n".join(H) + html_sco_header.sco_footer()
def view_apo_csv_store(
semset_id="", csvfile=None, data="", autodetect=False, REQUEST=None
):
def view_apo_csv_store(semset_id="", csvfile=None, data: bytes = "", autodetect=False):
"""Store CSV data
Le semset identifie l'annee scolaire et le semestre
Si csvfile, lit depuis FILE, sinon utilise data
@ -600,9 +593,8 @@ def view_apo_csv_store(
if not semset_id:
raise ValueError("invalid null semset_id")
semset = sco_semset.SemSet(semset_id=semset_id)
if csvfile:
data = csvfile.read()
data = csvfile.read() # bytes
if autodetect:
# check encoding (although documentation states that users SHOULD upload LATIN1)
data, message = sco_apogee_csv.fix_data_encoding(data)
@ -612,22 +604,29 @@ def view_apo_csv_store(
log("view_apo_csv_store: autodetection of encoding disabled by user")
if not data:
raise ScoValueError("view_apo_csv_store: no data")
# data est du bytes, encodé en APO_INPUT_ENCODING
data_str = data.decode(APO_INPUT_ENCODING)
# check si etape maquette appartient bien au semset
apo_data = sco_apogee_csv.ApoData(
data, periode=semset["sem_id"]
data_str, periode=semset["sem_id"]
) # parse le fichier -> exceptions
if apo_data.etape not in semset["etapes"]:
raise ScoValueError(
"Le code étape de ce fichier ne correspond pas à ceux de cet ensemble"
)
sco_etape_apogee.apo_csv_store(data, semset["annee_scolaire"], semset["sem_id"])
sco_etape_apogee.apo_csv_store(data_str, semset["annee_scolaire"], semset["sem_id"])
return flask.redirect("apo_semset_maq_status?semset_id=" + semset_id)
return flask.redirect(
url_for(
"notes.apo_semset_maq_status",
scodoc_dept=g.scodoc_dept,
semset_id=semset_id,
)
)
def view_apo_csv_download_and_store(etape_apo="", semset_id="", REQUEST=None):
def view_apo_csv_download_and_store(etape_apo="", semset_id=""):
"""Download maquette and store it"""
if not semset_id:
raise ValueError("invalid null semset_id")
@ -636,20 +635,18 @@ def view_apo_csv_download_and_store(etape_apo="", semset_id="", REQUEST=None):
data = sco_portal_apogee.get_maquette_apogee(
etape=etape_apo, annee_scolaire=semset["annee_scolaire"]
)
# here, data is utf8
# here, data is str
# but we store and generate latin1 files, to ease further import in Apogée
data = data.decode(APO_PORTAL_ENCODING).encode(APO_INPUT_ENCODING) # XXX #py3
return view_apo_csv_store(semset_id, data=data, autodetect=False, REQUEST=REQUEST)
data = data.encode(APO_OUTPUT_ENCODING)
return view_apo_csv_store(semset_id, data=data, autodetect=False)
def view_apo_csv_delete(
etape_apo="", semset_id="", dialog_confirmed=False, REQUEST=None
):
def view_apo_csv_delete(etape_apo="", semset_id="", dialog_confirmed=False):
"""Delete CSV file"""
if not semset_id:
raise ValueError("invalid null semset_id")
semset = sco_semset.SemSet(semset_id=semset_id)
dest_url = "apo_semset_maq_status?semset_id=" + semset_id
dest_url = f"apo_semset_maq_status?semset_id={semset_id}"
if not dialog_confirmed:
return scu.confirm_dialog(
"""<h2>Confirmer la suppression du fichier étape <tt>%s</tt>?</h2>
@ -667,7 +664,7 @@ def view_apo_csv_delete(
return flask.redirect(dest_url + "&head_message=Archive%20supprimée")
def view_apo_csv(etape_apo="", semset_id="", format="html", REQUEST=None):
def view_apo_csv(etape_apo="", semset_id="", format="html"):
"""Visualise une maquette stockée
Si format="raw", renvoie le fichier maquette tel quel
"""
@ -678,7 +675,8 @@ def view_apo_csv(etape_apo="", semset_id="", format="html", REQUEST=None):
sem_id = semset["sem_id"]
csv_data = sco_etape_apogee.apo_csv_get(etape_apo, annee_scolaire, sem_id)
if format == "raw":
return scu.sendCSVFile(REQUEST, csv_data, etape_apo + ".txt")
return scu.send_file(csv_data, etape_apo, suffix=".txt", mime=scu.CSV_MIMETYPE)
apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
(
@ -746,14 +744,15 @@ def view_apo_csv(etape_apo="", semset_id="", format="html", REQUEST=None):
rows=etuds,
html_sortable=True,
html_class="table_leftalign apo_maq_table",
base_url="%s?etape_apo=%s&semset_id=%s" % (REQUEST.URL0, etape_apo, semset_id),
base_url="%s?etape_apo=%s&semset_id=%s"
% (request.base_url, etape_apo, semset_id),
filename="students_" + etape_apo,
caption="Etudiants Apogée en " + etape_apo,
preferences=sco_preferences.SemPreferences(),
)
if format != "html":
return tab.make_page(format=format, REQUEST=REQUEST)
return tab.make_page(format=format)
H += [
tab.html(),
@ -768,7 +767,7 @@ def view_apo_csv(etape_apo="", semset_id="", format="html", REQUEST=None):
return "\n".join(H)
# called from Web
# called from Web (GET)
def apo_csv_export_results(
semset_id,
block_export_res_etape=False,

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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

View File

@ -5,7 +5,7 @@
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# Copyright (c) 1999 - 2022 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
@ -31,57 +31,21 @@
# Ancien module "scolars"
import os
import time
from flask import url_for, g, request
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.header import Header
from email.mime.base import MIMEBase
from operator import itemgetter
from flask import url_for, g, request
from flask_mail import Message
from app import email
from app import log
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import SCO_ENCODING
import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
from app import log
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc import safehtml
from app.scodoc import sco_preferences
from app.scodoc.scolog import logdb
from flask_mail import Message
from app import mail
MONTH_NAMES_ABBREV = [
"Jan ",
"Fév ",
"Mars",
"Avr ",
"Mai ",
"Juin",
"Jul ",
"Août",
"Sept",
"Oct ",
"Nov ",
"Déc ",
]
MONTH_NAMES = [
"janvier",
"février",
"mars",
"avril",
"mai",
"juin",
"juillet",
"août",
"septembre",
"octobre",
"novembre",
"décembre",
]
from app.scodoc.TrivialFormulator import TrivialFormulator
def format_etud_ident(etud):
@ -158,7 +122,7 @@ def format_nom(s, uppercase=True):
def input_civilite(s):
"""Converts external representation of civilite to internal:
'M', 'F', or 'X' (and nothing else).
Raises valueError if conversion fails.
Raises ScoValueError if conversion fails.
"""
s = s.upper().strip()
if s in ("M", "M.", "MR", "H"):
@ -167,12 +131,13 @@ def input_civilite(s):
return "F"
elif s == "X" or not s:
return "X"
raise ValueError("valeur invalide pour la civilité: %s" % s)
raise ScoValueError("valeur invalide pour la civilité: %s" % s)
def format_civilite(civilite):
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
personne ne souhaitant pas d'affichage)
personne ne souhaitant pas d'affichage).
Raises ScoValueError if conversion fails.
"""
try:
return {
@ -181,7 +146,7 @@ def format_civilite(civilite):
"X": "",
}[civilite]
except KeyError:
raise ValueError("valeur invalide pour la civilité: %s" % civilite)
raise ScoValueError("valeur invalide pour la civilité: %s" % civilite)
def format_lycee(nomlycee):
@ -256,7 +221,6 @@ _identiteEditor = ndb.EditableTable(
"photo_filename",
"code_ine",
"code_nip",
"scodoc7_id",
),
filter_dept=True,
sortkey="nom",
@ -321,9 +285,7 @@ def check_nom_prenom(cnx, nom="", prenom="", etudid=None):
return True, len(res)
def _check_duplicate_code(
cnx, args, code_name, disable_notify=False, edit=True, REQUEST=None
):
def _check_duplicate_code(cnx, args, code_name, disable_notify=False, edit=True):
etudid = args.get("etudid", None)
if args.get(code_name, None):
etuds = identite_list(cnx, {code_name: str(args[code_name])})
@ -345,31 +307,33 @@ def _check_duplicate_code(
)
if etudid:
OK = "retour à la fiche étudiant"
dest_url = "ficheEtud"
dest_endpoint = "scolar.ficheEtud"
parameters = {"etudid": etudid}
else:
if "tf_submitted" in args:
del args["tf_submitted"]
OK = "Continuer"
dest_url = "etudident_create_form"
dest_endpoint = "scolar.etudident_create_form"
parameters = args
else:
OK = "Annuler"
dest_url = ""
dest_endpoint = "notes.index_html"
parameters = {}
if not disable_notify:
err_page = scu.confirm_dialog(
message="""<h3>Code étudiant (%s) dupliqué !</h3>""" % code_name,
helpmsg="""Le %s %s est déjà utilisé: un seul étudiant peut avoir ce code. Vérifier votre valeur ou supprimer l'autre étudiant avec cette valeur.<p><ul><li>"""
% (code_name, args[code_name])
+ "</li><li>".join(listh)
+ "</li></ul><p>",
OK=OK,
dest_url=dest_url,
parameters=parameters,
)
err_page = f"""<h3><h3>Code étudiant ({code_name}) dupliqué !</h3>
<p class="help">Le {code_name} {args[code_name]} est déjà utilisé: un seul étudiant peut avoir
ce code. Vérifier votre valeur ou supprimer l'autre étudiant avec cette valeur.
</p>
<ul><li>
{ '</li><li>'.join(listh) }
</li></ul>
<p>
<a href="{ url_for(dest_endpoint, scodoc_dept=g.scodoc_dept, **parameters) }
">{OK}</a>
</p>
"""
else:
err_page = """<h3>Code étudiant (%s) dupliqué !</h3>""" % code_name
err_page = f"""<h3>Code étudiant ({code_name}) dupliqué !</h3>"""
log("*** error: code %s duplique: %s" % (code_name, args[code_name]))
raise ScoGenError(err_page)
@ -379,15 +343,15 @@ def _check_civilite(args):
args["civilite"] = input_civilite(civilite) # TODO: A faire valider
def identite_edit(cnx, args, disable_notify=False, REQUEST=None):
def identite_edit(cnx, args, disable_notify=False):
"""Modifie l'identite d'un étudiant.
Si pref notification et difference, envoie message notification, sauf si disable_notify
"""
_check_duplicate_code(
cnx, args, "code_nip", disable_notify=disable_notify, edit=True, REQUEST=REQUEST
cnx, args, "code_nip", disable_notify=disable_notify, edit=True
)
_check_duplicate_code(
cnx, args, "code_ine", disable_notify=disable_notify, edit=True, REQUEST=REQUEST
cnx, args, "code_ine", disable_notify=disable_notify, edit=True
)
notify_to = None
if not disable_notify:
@ -415,10 +379,10 @@ def identite_edit(cnx, args, disable_notify=False, REQUEST=None):
)
def identite_create(cnx, args, REQUEST=None):
def identite_create(cnx, args):
"check unique etudid, then create"
_check_duplicate_code(cnx, args, "code_nip", edit=False, REQUEST=REQUEST)
_check_duplicate_code(cnx, args, "code_ine", edit=False, REQUEST=REQUEST)
_check_duplicate_code(cnx, args, "code_nip", edit=False)
_check_duplicate_code(cnx, args, "code_ine", edit=False)
_check_civilite(args)
if "etudid" in args:
@ -456,7 +420,7 @@ def notify_etud_change(email_addr, etud, before, after, subject):
log("notify_etud_change: sending notification to %s" % email_addr)
log("notify_etud_change: subject: %s" % subject)
log(txt)
mail.send_email(
email.send_email(
subject, sco_preferences.get_preference("email_from_addr"), [email_addr], txt
)
return txt
@ -559,7 +523,6 @@ _admissionEditor = ndb.EditableTable(
"villelycee",
"codepostallycee",
"codelycee",
"debouche",
"type_admission",
"boursier_prec",
),
@ -583,8 +546,8 @@ admission_edit = _admissionEditor.edit
# Edition simultanee de identite et admission
class EtudIdentEditor(object):
def create(self, cnx, args, REQUEST=None):
etudid = identite_create(cnx, args, REQUEST)
def create(self, cnx, args):
etudid = identite_create(cnx, args)
args["etudid"] = etudid
admission_create(cnx, args)
return etudid
@ -615,8 +578,8 @@ class EtudIdentEditor(object):
res.sort(key=itemgetter("nom", "prenom"))
return res
def edit(self, cnx, args, disable_notify=False, REQUEST=None):
identite_edit(cnx, args, disable_notify=disable_notify, REQUEST=REQUEST)
def edit(self, cnx, args, disable_notify=False):
identite_edit(cnx, args, disable_notify=disable_notify)
if "adm_id" in args: # safety net
admission_edit(cnx, args)
@ -656,11 +619,17 @@ def make_etud_args(etudid=None, code_nip=None, use_request=True, raise_exc=True)
return args
def log_unknown_etud():
"""Log request: cas ou getEtudInfo n'a pas ramene de resultat"""
etud_args = make_etud_args(raise_exc=False)
log(f"unknown student: args={etud_args}")
def get_etud_info(etudid=False, code_nip=False, filled=False) -> list:
"""infos sur un etudiant (API). If not foud, returns empty list.
On peut specifier etudid ou code_nip
ou bien cherche dans REQUEST.form: etudid, code_nip, code_ine
(dans cet ordre).
"""infos sur un etudiant (API). If not found, returns empty list.
On peut spécifier etudid ou code_nip
ou bien cherche dans les arguments de la requête courante:
etudid, code_nip, code_ine (dans cet ordre).
"""
if etudid is None:
return []
@ -673,7 +642,20 @@ def get_etud_info(etudid=False, code_nip=False, filled=False) -> list:
return etud
def create_etud(cnx, args={}, REQUEST=None):
# Optim par cache local, utilité non prouvée mais
# on s'oriente vers un cahce persistent dans Redis ou bien l'utilisation de NT
# def get_etud_info_filled_by_etudid(etudid, cnx=None) -> dict:
# """Infos sur un étudiant, avec cache local à la requête"""
# if etudid in g.stored_etud_info:
# return g.stored_etud_info[etudid]
# cnx = cnx or ndb.GetDBConnexion()
# etud = etudident_list(cnx, args={"etudid": etudid})
# fill_etuds_info(etud)
# g.stored_etud_info[etudid] = etud[0]
# return etud[0]
def create_etud(cnx, args={}):
"""Creation d'un étudiant. génère aussi évenement et "news".
Args:
@ -685,7 +667,7 @@ def create_etud(cnx, args={}, REQUEST=None):
from app.scodoc import sco_news
# creation d'un etudiant
etudid = etudident_create(cnx, args, REQUEST=REQUEST)
etudid = etudident_create(cnx, args)
# crée une adresse vide (chaque etudiant doit etre dans la table "adresse" !)
_ = adresse_create(
cnx,
@ -819,8 +801,8 @@ appreciations_edit = _appreciationsEditor.edit
def read_etablissements():
filename = os.path.join(scu.SCO_TOOLS_DIR, scu.CONFIG.ETABL_FILENAME)
log("reading %s" % filename)
f = open(filename)
L = [x[:-1].split(";") for x in f]
with open(filename) as f:
L = [x[:-1].split(";") for x in f]
E = {}
for l in L[1:]:
E[l[0]] = {

View File

@ -0,0 +1,254 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 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
#
##############################################################################
"""Vérification des abasneces à une évaluation
"""
from flask import url_for, g
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc import html_sco_header
from app.scodoc import sco_abs
from app.scodoc import sco_etud
from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
# matin et/ou après-midi ?
def _eval_demijournee(E):
"1 si matin, 0 si apres midi, 2 si toute la journee"
am, pm = False, False
if E["heure_debut"] < "13:00":
am = True
if E["heure_fin"] > "13:00":
pm = True
if am and pm:
demijournee = 2
elif am:
demijournee = 1
else:
demijournee = 0
pm = True
return am, pm, demijournee
def evaluation_check_absences(evaluation_id):
"""Vérifie les absences au moment de cette évaluation.
Cas incohérents que l'on peut rencontrer pour chaque étudiant:
note et absent
ABS et pas noté absent
ABS et absent justifié
EXC et pas noté absent
EXC et pas justifie
Ramene 3 listes d'etudid
"""
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
if not E["jour"]:
return [], [], [], [], [] # evaluation sans date
am, pm, demijournee = _eval_demijournee(E)
# Liste les absences à ce moment:
A = sco_abs.list_abs_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm)
As = set([x["etudid"] for x in A]) # ensemble des etudiants absents
NJ = sco_abs.list_abs_non_just_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm)
NJs = set([x["etudid"] for x in NJ]) # ensemble des etudiants absents non justifies
Just = sco_abs.list_abs_jour(
ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm, is_abs=None, is_just=True
)
Justs = set([x["etudid"] for x in Just]) # ensemble des etudiants avec justif
# Les notes:
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
ValButAbs = [] # une note mais noté absent
AbsNonSignalee = [] # note ABS mais pas noté absent
ExcNonSignalee = [] # note EXC mais pas noté absent
ExcNonJust = [] # note EXC mais absent non justifie
AbsButExc = [] # note ABS mais justifié
for etudid, _ in sco_groups.do_evaluation_listeetuds_groups(
evaluation_id, getallstudents=True
):
if etudid in notes_db:
val = notes_db[etudid]["value"]
if (
val != None and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_ATTENTE
) and etudid in As:
# note valide et absent
ValButAbs.append(etudid)
if val is None and not etudid in As:
# absent mais pas signale comme tel
AbsNonSignalee.append(etudid)
if val == scu.NOTES_NEUTRALISE and not etudid in As:
# Neutralisé mais pas signale absent
ExcNonSignalee.append(etudid)
if val == scu.NOTES_NEUTRALISE and etudid in NJs:
# EXC mais pas justifié
ExcNonJust.append(etudid)
if val is None and etudid in Justs:
# ABS mais justificatif
AbsButExc.append(etudid)
return ValButAbs, AbsNonSignalee, ExcNonSignalee, ExcNonJust, AbsButExc
def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True):
"""Affiche état vérification absences d'une évaluation"""
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
am, pm, demijournee = _eval_demijournee(E)
(
ValButAbs,
AbsNonSignalee,
ExcNonSignalee,
ExcNonJust,
AbsButExc,
) = evaluation_check_absences(evaluation_id)
if with_header:
H = [
html_sco_header.html_sem_header("Vérification absences à l'évaluation"),
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
"""<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.</p>""",
]
else:
# pas de header, mais un titre
H = [
"""<h2 class="eval_check_absences">%s du %s """
% (E["description"], E["jour"])
]
if (
not ValButAbs
and not AbsNonSignalee
and not ExcNonSignalee
and not ExcNonJust
):
H.append(': <span class="eval_check_absences_ok">ok</span>')
H.append("</h2>")
def etudlist(etudids, linkabs=False):
H.append("<ul>")
if not etudids and show_ok:
H.append("<li>aucun</li>")
for etudid in etudids:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
H.append(
'<li><a class="discretelink" href="%s">'
% url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
)
+ "%(nomprenom)s</a>" % etud
)
if linkabs:
H.append(
f"""<a class="stdlink" href="{url_for(
'absences.doSignaleAbsence',
scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"],
datedebut=E["jour"],
datefin=E["jour"],
demijournee=demijournee,
moduleimpl_id=E["moduleimpl_id"],
)
}">signaler cette absence</a>"""
)
H.append("</li>")
H.append("</ul>")
if ValButAbs or show_ok:
H.append(
"<h3>Etudiants ayant une note alors qu'ils sont signalés absents:</h3>"
)
etudlist(ValButAbs)
if AbsNonSignalee or show_ok:
H.append(
"""<h3>Etudiants avec note "ABS" alors qu'ils ne sont <em>pas</em> signalés absents:</h3>"""
)
etudlist(AbsNonSignalee, linkabs=True)
if ExcNonSignalee or show_ok:
H.append(
"""<h3>Etudiants avec note "EXC" alors qu'ils ne sont <em>pas</em> signalés absents:</h3>"""
)
etudlist(ExcNonSignalee)
if ExcNonJust or show_ok:
H.append(
"""<h3>Etudiants avec note "EXC" alors qu'ils sont absents <em>non justifiés</em>:</h3>"""
)
etudlist(ExcNonJust)
if AbsButExc or show_ok:
H.append(
"""<h3>Etudiants avec note "ABS" alors qu'ils ont une <em>justification</em>:</h3>"""
)
etudlist(AbsButExc)
if with_header:
H.append(html_sco_header.sco_footer())
return "\n".join(H)
def formsemestre_check_absences_html(formsemestre_id):
"""Affiche etat verification absences pour toutes les evaluations du semestre !"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [
html_sco_header.html_sem_header(
"Vérification absences aux évaluations de ce semestre",
sem,
),
"""<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.
Sont listés tous les modules avec des évaluations.<br/>Aucune action n'est effectuée:
il vous appartient de corriger les erreurs détectées si vous le jugez nécessaire.
</p>""",
]
# Modules, dans l'ordre
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
for M in Mlist:
evals = sco_evaluation_db.do_evaluation_list(
{"moduleimpl_id": M["moduleimpl_id"]}
)
if evals:
H.append(
'<div class="module_check_absences"><h2><a href="moduleimpl_status?moduleimpl_id=%s">%s: %s</a></h2>'
% (M["moduleimpl_id"], M["module"]["code"], M["module"]["abbrev"])
)
for E in evals:
H.append(
evaluation_check_absences_html(
E["evaluation_id"],
with_header=False,
show_ok=False,
)
)
if evals:
H.append("</div>")
H.append(html_sco_header.sco_footer())
return "\n".join(H)

View File

@ -0,0 +1,482 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 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@gmail.com
#
##############################################################################
"""Gestion evaluations (ScoDoc7, sans SQlAlchemy)
"""
import datetime
import pprint
import flask
from flask import url_for, g
from flask_login import current_user
from app import log
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc import sco_cache
from app.scodoc import sco_edit_module
from app.scodoc import sco_formsemestre
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_news
from app.scodoc import sco_permissions_check
_evaluationEditor = ndb.EditableTable(
"notes_evaluation",
"evaluation_id",
(
"evaluation_id",
"moduleimpl_id",
"jour",
"heure_debut",
"heure_fin",
"description",
"note_max",
"coefficient",
"visibulletin",
"publish_incomplete",
"evaluation_type",
"numero",
),
sortkey="numero desc, jour desc, heure_debut desc", # plus recente d'abord
output_formators={
"jour": ndb.DateISOtoDMY,
"numero": ndb.int_null_is_zero,
},
input_formators={
"jour": ndb.DateDMYtoISO,
"heure_debut": ndb.TimetoISO8601, # converti par evaluation_enrich_dict
"heure_fin": ndb.TimetoISO8601, # converti par evaluation_enrich_dict
"visibulletin": bool,
"publish_incomplete": bool,
"evaluation_type": int,
},
)
def evaluation_enrich_dict(e):
"""add or convert some fileds in an evaluation dict"""
# For ScoDoc7 compat
heure_debut_dt = e["heure_debut"] or datetime.time(
8, 00
) # au cas ou pas d'heure (note externe?)
heure_fin_dt = e["heure_fin"] or datetime.time(8, 00)
e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"])
e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"])
e["jouriso"] = ndb.DateDMYtoISO(e["jour"])
heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
d = ndb.TimeDuration(heure_debut, heure_fin)
if d is not None:
m = d % 60
e["duree"] = "%dh" % (d / 60)
if m != 0:
e["duree"] += "%02d" % m
else:
e["duree"] = ""
if heure_debut and (not heure_fin or heure_fin == heure_debut):
e["descrheure"] = " à " + heure_debut
elif heure_debut and heure_fin:
e["descrheure"] = " de %s à %s" % (heure_debut, heure_fin)
else:
e["descrheure"] = ""
# matin, apresmidi: utile pour se referer aux absences:
if heure_debut_dt < datetime.time(12, 00):
e["matin"] = 1
else:
e["matin"] = 0
if heure_fin_dt > datetime.time(12, 00):
e["apresmidi"] = 1
else:
e["apresmidi"] = 0
return e
def do_evaluation_list(args, sortkey=None):
"""List evaluations, sorted by numero (or most recent date first).
Ajoute les champs:
'duree' : '2h30'
'matin' : 1 (commence avant 12:00) ou 0
'apresmidi' : 1 (termine après 12:00) ou 0
'descrheure' : ' de 15h00 à 16h30'
"""
# Attention: transformation fonction ScoDc7 en SQLAlchemy
cnx = ndb.GetDBConnexion()
evals = _evaluationEditor.list(cnx, args, sortkey=sortkey)
# calcule duree (chaine de car.) de chaque evaluation et ajoute jouriso, matin, apresmidi
for e in evals:
evaluation_enrich_dict(e)
return evals
def do_evaluation_list_in_formsemestre(formsemestre_id):
"list evaluations in this formsemestre"
mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
evals = []
for modimpl in mods:
evals += do_evaluation_list(args={"moduleimpl_id": modimpl["moduleimpl_id"]})
return evals
def _check_evaluation_args(args):
"Check coefficient, dates and duration, raises exception if invalid"
moduleimpl_id = args["moduleimpl_id"]
# check bareme
note_max = args.get("note_max", None)
if note_max is None:
raise ScoValueError("missing note_max")
try:
note_max = float(note_max)
except ValueError:
raise ScoValueError("Invalid note_max value")
if note_max < 0:
raise ScoValueError("Invalid note_max value (must be positive or null)")
# check coefficient
coef = args.get("coefficient", None)
if coef is None:
raise ScoValueError("missing coefficient")
try:
coef = float(coef)
except ValueError:
raise ScoValueError("Invalid coefficient value")
if coef < 0:
raise ScoValueError("Invalid coefficient value (must be positive or null)")
# check date
jour = args.get("jour", None)
args["jour"] = jour
if jour:
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
d, m, y = [int(x) for x in sem["date_debut"].split("/")]
date_debut = datetime.date(y, m, d)
d, m, y = [int(x) for x in sem["date_fin"].split("/")]
date_fin = datetime.date(y, m, d)
# passe par ndb.DateDMYtoISO pour avoir date pivot
y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")]
jour = datetime.date(y, m, d)
if (jour > date_fin) or (jour < date_debut):
raise ScoValueError(
"La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !"
% (d, m, y)
)
heure_debut = args.get("heure_debut", None)
args["heure_debut"] = heure_debut
heure_fin = args.get("heure_fin", None)
args["heure_fin"] = heure_fin
if jour and ((not heure_debut) or (not heure_fin)):
raise ScoValueError("Les heures doivent être précisées")
d = ndb.TimeDuration(heure_debut, heure_fin)
if d and ((d < 0) or (d > 60 * 12)):
raise ScoValueError("Heures de l'évaluation incohérentes !")
def do_evaluation_create(
moduleimpl_id=None,
jour=None,
heure_debut=None,
heure_fin=None,
description=None,
note_max=None,
coefficient=None,
visibulletin=None,
publish_incomplete=None,
evaluation_type=None,
numero=None,
**kw, # ceci pour absorber les arguments excedentaires de tf #sco8
):
"""Create an evaluation"""
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
raise AccessDenied(
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
)
args = locals()
log("do_evaluation_create: args=" + str(args))
_check_evaluation_args(args)
# Check numeros
module_evaluation_renumber(moduleimpl_id, only_if_unumbered=True)
if not "numero" in args or args["numero"] is None:
n = None
# determine le numero avec la date
# Liste des eval existantes triees par date, la plus ancienne en tete
mod_evals = do_evaluation_list(
args={"moduleimpl_id": moduleimpl_id},
sortkey="jour asc, heure_debut asc",
)
if args["jour"]:
next_eval = None
t = (
ndb.DateDMYtoISO(args["jour"], null_is_empty=True),
ndb.TimetoISO8601(args["heure_debut"], null_is_empty=True),
)
for e in mod_evals:
if (
ndb.DateDMYtoISO(e["jour"], null_is_empty=True),
ndb.TimetoISO8601(e["heure_debut"], null_is_empty=True),
) > t:
next_eval = e
break
if next_eval:
n = module_evaluation_insert_before(mod_evals, next_eval)
else:
n = None # a placer en fin
if n is None: # pas de date ou en fin:
if mod_evals:
log(pprint.pformat(mod_evals[-1]))
n = mod_evals[-1]["numero"] + 1
else:
n = 0 # the only one
# log("creating with numero n=%d" % n)
args["numero"] = n
#
cnx = ndb.GetDBConnexion()
r = _evaluationEditor.create(cnx, args)
# news
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
mod["moduleimpl_id"] = M["moduleimpl_id"]
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
sco_news.add(
typ=sco_news.NEWS_NOTE,
object=moduleimpl_id,
text='Création d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
url=mod["url"],
)
return r
def do_evaluation_edit(args):
"edit an evaluation"
evaluation_id = args["evaluation_id"]
the_evals = do_evaluation_list({"evaluation_id": evaluation_id})
if not the_evals:
raise ValueError("evaluation inexistante !")
moduleimpl_id = the_evals[0]["moduleimpl_id"]
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
raise AccessDenied(
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
)
args["moduleimpl_id"] = moduleimpl_id
_check_evaluation_args(args)
cnx = ndb.GetDBConnexion()
_evaluationEditor.edit(cnx, args)
# inval cache pour ce semestre
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"])
def do_evaluation_delete(evaluation_id):
"delete evaluation"
the_evals = do_evaluation_list({"evaluation_id": evaluation_id})
if not the_evals:
raise ValueError("evaluation inexistante !")
moduleimpl_id = the_evals[0]["moduleimpl_id"]
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
raise AccessDenied(
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
)
notes_db = do_evaluation_get_all_notes(evaluation_id) # { etudid : value }
notes = [x["value"] for x in notes_db.values()]
if notes:
raise ScoValueError(
"Impossible de supprimer cette évaluation: il reste des notes"
)
cnx = ndb.GetDBConnexion()
_evaluationEditor.delete(cnx, evaluation_id)
# inval cache pour ce semestre
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"])
# news
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
mod["moduleimpl_id"] = M["moduleimpl_id"]
mod["url"] = (
scu.NotesURL() + "/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
)
sco_news.add(
typ=sco_news.NEWS_NOTE,
object=moduleimpl_id,
text='Suppression d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
url=mod["url"],
)
# ancien _notes_getall
def do_evaluation_get_all_notes(
evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None
):
"""Toutes les notes pour une evaluation: { etudid : { 'value' : value, 'date' : date ... }}
Attention: inclut aussi les notes des étudiants qui ne sont plus inscrits au module.
"""
do_cache = (
filter_suppressed and table == "notes_notes" and (by_uid is None)
) # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant
if do_cache:
r = sco_cache.EvaluationCache.get(evaluation_id)
if r != None:
return r
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cond = " where evaluation_id=%(evaluation_id)s"
if by_uid:
cond += " and uid=%(by_uid)s"
cursor.execute(
"select * from " + table + cond,
{"evaluation_id": evaluation_id, "by_uid": by_uid},
)
res = cursor.dictfetchall()
d = {}
if filter_suppressed:
for x in res:
if x["value"] != scu.NOTES_SUPPRESS:
d[x["etudid"]] = x
else:
for x in res:
d[x["etudid"]] = x
if do_cache:
status = sco_cache.EvaluationCache.set(evaluation_id, d)
if not status:
log(f"Warning: EvaluationCache.set: {evaluation_id}\t{status}")
return d
def module_evaluation_renumber(moduleimpl_id, only_if_unumbered=False, redirect=0):
"""Renumber evaluations in this module, according to their date. (numero=0: oldest one)
Needed because previous versions of ScoDoc did not have eval numeros
Note: existing numeros are ignored
"""
redirect = int(redirect)
# log('module_evaluation_renumber( moduleimpl_id=%s )' % moduleimpl_id )
# List sorted according to date/heure, ignoring numeros:
# (note that we place evaluations with NULL date at the end)
mod_evals = do_evaluation_list(
args={"moduleimpl_id": moduleimpl_id},
sortkey="jour asc, heure_debut asc",
)
all_numbered = False not in [x["numero"] > 0 for x in mod_evals]
if all_numbered and only_if_unumbered:
return # all ok
# Reset all numeros:
i = 1
for e in mod_evals:
e["numero"] = i
do_evaluation_edit(e)
i += 1
# If requested, redirect to moduleimpl page:
if redirect:
return flask.redirect(
url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=moduleimpl_id,
)
)
def module_evaluation_insert_before(mod_evals, next_eval):
"""Renumber evals such that an evaluation with can be inserted before next_eval
Returns numero suitable for the inserted evaluation
"""
if next_eval:
n = next_eval["numero"]
if not n:
log("renumbering old evals")
module_evaluation_renumber(next_eval["moduleimpl_id"])
next_eval = do_evaluation_list(
args={"evaluation_id": next_eval["evaluation_id"]}
)[0]
n = next_eval["numero"]
else:
n = 1
# log('inserting at position numero %s' % n )
# all numeros >= n are incremented
for e in mod_evals:
if e["numero"] >= n:
e["numero"] += 1
# log('incrementing %s to %s' % (e['evaluation_id'], e['numero']))
do_evaluation_edit(e)
return n
def module_evaluation_move(evaluation_id, after=0, redirect=1):
"""Move before/after previous one (decrement/increment numero)
(published)
"""
e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
redirect = int(redirect)
# access: can change eval ?
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=e["moduleimpl_id"]):
raise AccessDenied(
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
)
module_evaluation_renumber(e["moduleimpl_id"], only_if_unumbered=True)
e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
after = int(after) # 0: deplace avant, 1 deplace apres
if after not in (0, 1):
raise ValueError('invalid value for "after"')
mod_evals = do_evaluation_list({"moduleimpl_id": e["moduleimpl_id"]})
if len(mod_evals) > 1:
idx = [p["evaluation_id"] for p in mod_evals].index(evaluation_id)
neigh = None # object to swap with
if after == 0 and idx > 0:
neigh = mod_evals[idx - 1]
elif after == 1 and idx < len(mod_evals) - 1:
neigh = mod_evals[idx + 1]
if neigh: #
if neigh["numero"] == e["numero"]:
log("Warning: module_evaluation_move: forcing renumber")
module_evaluation_renumber(e["moduleimpl_id"], only_if_unumbered=False)
else:
# swap numero with neighbor
e["numero"], neigh["numero"] = neigh["numero"], e["numero"]
do_evaluation_edit(e)
do_evaluation_edit(neigh)
# redirect to moduleimpl page:
if redirect:
return flask.redirect(
url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=e["moduleimpl_id"],
)
)

View File

@ -0,0 +1,339 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 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@gmail.com
#
##############################################################################
"""Formulaire ajout/édition d'une évaluation
"""
import time
import flask
from flask import url_for, render_template
from flask import g
from flask_login import current_user
from flask import request
from app import db
from app import log
from app import models
from app.models.formsemestre import FormSemestre
from app.models.moduleimpls import ModuleImpl
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc import html_sco_header
from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_permissions_check
def evaluation_create_form(
moduleimpl_id=None,
evaluation_id=None,
edit=False,
page_title="Évaluation",
):
"Formulaire création/édition d'une évaluation (pas de ses notes)"
if evaluation_id is not None:
evaluation = models.Evaluation.query.get(evaluation_id)
moduleimpl_id = evaluation.moduleimpl_id
#
modimpl_o = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[
0
]
mod = modimpl_o["module"]
formsemestre_id = modimpl_o["formsemestre_id"]
sem = FormSemestre.query.get(formsemestre_id)
sem_ues = sem.query_ues(with_sport=False).all()
is_malus = mod["module_type"] == ModuleType.MALUS
is_apc = mod["module_type"] in (ModuleType.RESSOURCE, ModuleType.SAE)
min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible
#
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
return (
html_sco_header.sco_header()
+ "<h2>Opération non autorisée</h2><p>"
+ "Modification évaluation impossible pour %s"
% current_user.get_nomplogin()
+ "</p>"
+ '<p><a href="moduleimpl_status?moduleimpl_id=%s">Revenir</a></p>'
% (moduleimpl_id,)
+ html_sco_header.sco_footer()
)
if not edit:
# création nouvel
if moduleimpl_id is None:
raise ValueError("missing moduleimpl_id parameter")
initvalues = {
"note_max": 20,
"jour": time.strftime("%d/%m/%Y", time.localtime()),
"publish_incomplete": is_malus,
}
submitlabel = "Créer cette évaluation"
action = "Création d'une évaluation"
link = ""
else:
# édition données existantes
# setup form init values
if evaluation_id is None:
raise ValueError("missing evaluation_id parameter")
initvalues = evaluation.to_dict()
moduleimpl_id = initvalues["moduleimpl_id"]
submitlabel = "Modifier les données"
action = "Modification d'une évaluation"
link = ""
# Note maximale actuelle dans cette éval ?
etat = sco_evaluations.do_evaluation_etat(evaluation_id)
if etat["maxi_num"] is not None:
min_note_max = max(scu.NOTES_PRECISION, etat["maxi_num"])
else:
min_note_max = scu.NOTES_PRECISION
#
if min_note_max > scu.NOTES_PRECISION:
min_note_max_str = scu.fmt_note(min_note_max)
else:
min_note_max_str = "0"
#
mod_descr = '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> %s' % (
moduleimpl_id,
mod["code"],
mod["titre"],
link,
)
H = [
f"""<h3>{action} en
{scu.MODULE_TYPE_NAMES[mod["module_type"]]} {mod_descr}</h3>
"""
]
heures = ["%02dh%02d" % (h, m) for h in range(8, 19) for m in (0, 30)]
#
initvalues["visibulletin"] = initvalues.get("visibulletin", True)
if initvalues["visibulletin"]:
initvalues["visibulletinlist"] = ["X"]
else:
initvalues["visibulletinlist"] = []
vals = scu.get_request_args()
if vals.get("tf_submitted", False) and "visibulletinlist" not in vals:
vals["visibulletinlist"] = []
#
if is_apc: # BUT: poids vers les UE
ue_coef_dict = ModuleImpl.query.get(moduleimpl_id).module.get_ue_coef_dict()
for ue in sem_ues:
if edit:
existing_poids = models.EvaluationUEPoids.query.filter_by(
ue=ue, evaluation=evaluation
).first()
else:
existing_poids = None
if existing_poids:
poids = existing_poids.poids
else:
coef_ue = ue_coef_dict.get(ue.id, 0.0) or 0.0
if coef_ue > 0:
poids = 1.0 # par defaut au départ
else:
poids = 0.0
initvalues[f"poids_{ue.id}"] = poids
#
form = [
("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}),
("moduleimpl_id", {"default": moduleimpl_id, "input_type": "hidden"}),
# ('jour', { 'title' : 'Date (j/m/a)', 'size' : 12, 'explanation' : 'date de l\'examen, devoir ou contrôle' }),
(
"jour",
{
"input_type": "datedmy",
"title": "Date",
"size": 12,
"explanation": "date de l'examen, devoir ou contrôle",
},
),
(
"heure_debut",
{
"title": "Heure de début",
"explanation": "heure du début de l'épreuve",
"input_type": "menu",
"allowed_values": heures,
"labels": heures,
},
),
(
"heure_fin",
{
"title": "Heure de fin",
"explanation": "heure de fin de l'épreuve",
"input_type": "menu",
"allowed_values": heures,
"labels": heures,
},
),
]
if is_malus: # pas de coefficient
form.append(("coefficient", {"input_type": "hidden", "default": "1."}))
elif not is_apc: # modules standard hors BUT
form.append(
(
"coefficient",
{
"size": 6,
"type": "float",
"explanation": "coef. dans le module (choisi librement par l'enseignant)",
"allow_null": False,
},
)
)
form += [
(
"note_max",
{
"size": 4,
"type": "float",
"title": "Notes de 0 à",
"explanation": "barème (note max actuelle: %s)" % min_note_max_str,
"allow_null": False,
"max_value": scu.NOTES_MAX,
"min_value": min_note_max,
},
),
(
"description",
{
"size": 36,
"type": "text",
"explanation": 'type d\'évaluation, apparait sur le bulletins longs. Exemples: "contrôle court", "examen de TP", "examen final".',
},
),
(
"visibulletinlist",
{
"input_type": "checkbox",
"allowed_values": ["X"],
"labels": [""],
"title": "Visible sur bulletins",
"explanation": "(pour les bulletins en version intermédiaire)",
},
),
(
"publish_incomplete",
{
"input_type": "boolcheckbox",
"title": "Prise en compte immédiate",
"explanation": "notes utilisées même si incomplètes",
},
),
(
"evaluation_type",
{
"input_type": "menu",
"title": "Modalité",
"allowed_values": (
scu.EVALUATION_NORMALE,
scu.EVALUATION_RATTRAPAGE,
scu.EVALUATION_SESSION2,
),
"type": "int",
"labels": (
"Normale",
"Rattrapage (remplace si meilleure note)",
"Deuxième session (remplace toujours)",
),
},
),
]
if is_apc: # ressources et SAÉs
form += [
(
"coefficient",
{
"size": 6,
"type": "float",
"explanation": "importance de l'évaluation (multiplie les poids ci-dessous)",
"allow_null": False,
},
),
]
# Liste des UE utilisées dans des modules de ce semestre:
for ue in sem_ues:
form.append(
(
f"poids_{ue.id}",
{
"title": f"Poids {ue.acronyme}",
"size": 2,
"type": "float",
"explanation": f"{ue.titre}",
"allow_null": False,
},
),
)
tf = TrivialFormulator(
request.base_url,
vals,
form,
cancelbutton="Annuler",
submitlabel=submitlabel,
initvalues=initvalues,
readonly=False,
)
dest_url = "moduleimpl_status?moduleimpl_id=%s" % modimpl_o["moduleimpl_id"]
if tf[0] == 0:
head = html_sco_header.sco_header(page_title=page_title)
return (
head
+ "\n".join(H)
+ "\n"
+ tf[1]
+ render_template("scodoc/help/evaluations.html", is_apc=is_apc)
+ html_sco_header.sco_footer()
)
elif tf[0] == -1:
return flask.redirect(dest_url)
else:
# form submission
if tf[2]["visibulletinlist"]:
tf[2]["visibulletin"] = True
else:
tf[2]["visibulletin"] = False
if edit:
sco_evaluation_db.do_evaluation_edit(tf[2])
else:
# creation d'une evaluation
evaluation_id = sco_evaluation_db.do_evaluation_create(**tf[2])
if is_apc:
# Set poids
evaluation = models.Evaluation.query.get(evaluation_id)
for ue in sem_ues:
evaluation.set_ue_poids(ue, tf[2][f"poids_{ue.id}"])
db.session.add(evaluation)
db.session.commit()
return flask.redirect(dest_url)

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