Compare commits

...

353 Commits

Author SHA1 Message Date
d0fc8f1320 change logos ressources 2021-12-07 16:44:42 +01:00
828cd35f29 add log 2021-12-07 16:44:42 +01:00
74f68ee353 suppress height from usage 2021-12-07 16:44:42 +01:00
726814e6e9 actions delete/update/insert... ok 2021-12-07 16:44:42 +01:00
0e047261a8 visualisation ok 2021-12-07 16:44:42 +01:00
25bfcc7ed0 wip 2021-12-07 16:44:42 +01:00
e97eca3fb4 fixes last obvious bugs 2021-12-07 16:44:42 +01:00
958e96dce9 delete logo now may delete several files & tests 2021-12-07 16:44:42 +01:00
ee5b610da4 add test for delete_logo 2021-12-07 16:44:42 +01:00
2f4be9b949 adapt tests for new spacs 2021-12-07 16:44:42 +01:00
7fdb4c3cb6 Revert "strip config_logo from scolar"
This reverts commit ac908c4f33019e177c1ff83046cb4ab38a174055.
2021-12-07 16:44:42 +01:00
cba292c1e8 temp 2021-12-07 16:44:42 +01:00
8e01bad6cb logos final ? 2021-12-07 16:44:42 +01:00
893b523b17 tmp 2021-12-07 16:44:42 +01:00
e1f36d9484 temp 2021-12-07 16:44:42 +01:00
f40c0d8a6e temp 2021-12-07 16:44:42 +01:00
a9986e9522 temp. le 19/11 12h 2021-12-07 16:44:42 +01:00
d6b05b58f3 build logo form (header & footer) 2021-12-07 16:44:42 +01:00
41fc0aea40 strip config_logo from scolar 2021-12-07 16:44:42 +01:00
8a5419e774 ajout localize-logo flask command 2021-12-07 16:44:42 +01:00
074a2dcfbe migration des logos_dept scodoc7 -> scodoc9 2021-12-07 16:44:42 +01:00
139f3aefe5 ajout tests logos 2021-12-07 16:44:42 +01:00
272f20d447 ajout URL de récupéraions des logos (deprecates /ScoDoc/logo_header et /ScoDoc/logo_footer) 2021-12-07 16:44:42 +01:00
5cd6956846 adaptation du template d'affichage des images (maintenant miniatures) 2021-12-07 16:44:42 +01:00
fa1a77caa6 adaptation du code de traitement des balises <logo...> des éditions paramétrées 2021-12-07 16:44:42 +01:00
adce970921 adaptation de code de traitement des templates pdf 2021-12-07 16:44:42 +01:00
ff9c05c769 adaptation du code d'édition des pv 2021-12-07 16:44:42 +01:00
f519b2ee57 adaptation de la sauvegarde des fichiers logos poursuites d'études 2021-12-07 16:44:42 +01:00
e61054cf23 Ecriture des fonctions d'accés aux logos (et aux images) 2021-12-07 16:44:42 +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
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
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
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
540a956fae Suppression de (%(jour)s) lorsque l'évaluation n'a pas de date 2021-12-03 21:17:30 +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
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
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
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
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
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
5b534abf5f Parcours Licence ILEPS 2021-11-13 18:15:15 +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
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
ddf3d73c92 enhance check xls upload notes 2021-11-10 09:37:45 +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
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
206 changed files with 9827 additions and 5862 deletions

1
.gitignore vendored
View File

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

9
API.yaml Normal file
View File

@ -0,0 +1,9 @@
openapi: 3.0.3
info:
title: API Scodoc
description: API Scodoc
version: 1.0.0
servers:
- url: 'https://192.168.1.24:5000'
paths:

View File

@ -3,16 +3,12 @@
(c) Emmanuel Viennet 1999 - 2021 (voir LICENCE.txt) (c) Emmanuel Viennet 1999 - 2021 (voir LICENCE.txt)
VERSION EXPERIMENTALE - NE PAS DEPLOYER - TESTS EN COURS
Installation: voir instructions à jour sur <https://scodoc.org/GuideInstallDebian11> Installation: voir instructions à jour sur <https://scodoc.org/GuideInstallDebian11>
Documentation utilisateur: <https://scodoc.org> Documentation utilisateur: <https://scodoc.org>
## Version ScoDoc 9 ## 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 La version ScoDoc 9 est basée sur Flask (au lieu de Zope) et sur
**python 3.9+**. **python 3.9+**.
@ -22,13 +18,13 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
### État actuel (27 août 21) ### État actuel (26 sept 21)
- Tests en cours, notamment système d'installation et de migration. - 9.0 reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf:
**Fonctionnalités non intégrées:** **Fonctionnalités non intégrées:**
- feuille "placement" (en cours) - génération LaTeX des avis de poursuite d'études
- ancien module "Entreprises" (obsolete) - ancien module "Entreprises" (obsolete)
@ -97,7 +93,7 @@ Certains tests ont besoin d'un département déjà créé, qui n'est pas créé
scripts de tests: scripts de tests:
Lancer au préalable: 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: Puis dérouler les tests unitaires:
@ -110,13 +106,15 @@ Ou avec couverture (`pip install pytest-cov`)
#### Utilisation des tests unitaires pour initialiser la base de dev #### Utilisation des tests unitaires pour initialiser la base de dev
On peut aussi utiliser les tests unitaires pour mettre la base 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 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 pytest tests/unit/test_sco_basic.py
@ -132,12 +130,13 @@ base de données (tous les départements, et les utilisateurs) avant de commence
On utilise SQLAlchemy avec Alembic et Flask-Migrate. 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 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 dropdb SCODOC_DEV
tools/create_database.sh SCODOC_DEV # créé base SQL tools/create_database.sh SCODOC_DEV # créé base SQL
@ -152,7 +151,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 migration, utiliser les commandes `flask db history`et `flask db stamp`pour se
positionner à la bonne étape. 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 Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
important est `postinst`qui se charge de configurer le système (install ou important est `postinst`qui se charge de configurer le système (install ou

View File

@ -1,6 +1,7 @@
# -*- coding: UTF-8 -* # -*- coding: UTF-8 -*
# pylint: disable=invalid-name # pylint: disable=invalid-name
import datetime
import os import os
import socket import socket
import sys import sys
@ -17,16 +18,22 @@ from flask import render_template
from flask.logging import default_handler from flask.logging import default_handler
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate from flask_migrate import Migrate
from flask_login import LoginManager from flask_login import LoginManager, current_user
from flask_mail import Mail from flask_mail import Mail
from flask_bootstrap import Bootstrap from flask_bootstrap import Bootstrap
from flask_moment import Moment from flask_moment import Moment
from flask_caching import Cache from flask_caching import Cache
import sqlalchemy import sqlalchemy
from app.scodoc.sco_exceptions import ScoValueError, APIInvalidParams from app.scodoc.sco_exceptions import (
AccessDenied,
ScoGenError,
ScoValueError,
APIInvalidParams,
)
from config import DevConfig from config import DevConfig
import sco_version import sco_version
from flask_debugtoolbar import DebugToolbarExtension
db = SQLAlchemy() db = SQLAlchemy()
migrate = Migrate(compare_type=True) migrate = Migrate(compare_type=True)
@ -50,10 +57,21 @@ def handle_sco_value_error(exc):
return render_template("sco_value_error.html", exc=exc), 404 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): def internal_server_error(e):
"""Bugs scodoc, erreurs 500""" """Bugs scodoc, erreurs 500"""
# note that we set the 500 status explicitly # 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): def handle_invalid_usage(error):
@ -82,7 +100,7 @@ def postgresql_server_error(e):
return render_raw_html("error_503.html", SCOVERSION=sco_version.SCOVERSION), 503 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""" """Ajoute URL et remote_addr for logging"""
def format(self, record): def format(self, record):
@ -92,10 +110,46 @@ class RequestFormatter(logging.Formatter):
else: else:
record.url = None record.url = None
record.remote_addr = 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) 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): class ScoSMTPHandler(SMTPHandler):
def getSubject(self, record: logging.LogRecord) -> str: def getSubject(self, record: logging.LogRecord) -> str:
stack_summary = traceback.extract_tb(record.exc_info[2]) stack_summary = traceback.extract_tb(record.exc_info[2])
@ -105,8 +159,24 @@ class ScoSMTPHandler(SMTPHandler):
return subject 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): def create_app(config_class=DevConfig):
app = Flask(__name__, static_url_path="/ScoDoc/static", static_folder="static") app = Flask(__name__, static_url_path="/ScoDoc/static", static_folder="static")
app.wsgi_app = ReverseProxied(app.wsgi_app)
app.logger.setLevel(logging.DEBUG) app.logger.setLevel(logging.DEBUG)
app.config.from_object(config_class) app.config.from_object(config_class)
@ -118,8 +188,11 @@ def create_app(config_class=DevConfig):
moment.init_app(app) moment.init_app(app)
cache.init_app(app) cache.init_app(app)
sco_cache.CACHE = cache sco_cache.CACHE = cache
toolbar = DebugToolbarExtension(app)
app.register_error_handler(ScoGenError, handle_sco_value_error)
app.register_error_handler(ScoValueError, handle_sco_value_error) app.register_error_handler(ScoValueError, handle_sco_value_error)
app.register_error_handler(AccessDenied, handle_access_denied)
app.register_error_handler(500, internal_server_error) app.register_error_handler(500, internal_server_error)
app.register_error_handler(503, postgresql_server_error) app.register_error_handler(503, postgresql_server_error)
app.register_error_handler(APIInvalidParams, handle_invalid_usage) app.register_error_handler(APIInvalidParams, handle_invalid_usage)
@ -148,9 +221,18 @@ def create_app(config_class=DevConfig):
absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences" absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences"
) )
app.register_blueprint(api_bp, url_prefix="/ScoDoc/api") app.register_blueprint(api_bp, url_prefix="/ScoDoc/api")
scodoc_exc_formatter = RequestFormatter( scodoc_log_formatter = LogRequestFormatter(
"[%(asctime)s] %(remote_addr)s requested %(url)s\n" "[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n"
"%(levelname)s in %(module)s: %(message)s" "%(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.testing:
if not app.debug: if not app.debug:
@ -179,7 +261,7 @@ def create_app(config_class=DevConfig):
app.logger.addHandler(mail_handler) app.logger.addHandler(mail_handler)
else: else:
# Pour logs en DEV uniquement: # Pour logs en DEV uniquement:
default_handler.setFormatter(scodoc_exc_formatter) default_handler.setFormatter(scodoc_log_formatter)
# Config logs pour DEV et PRODUCTION # Config logs pour DEV et PRODUCTION
# Configuration des logs (actifs aussi en mode development) # Configuration des logs (actifs aussi en mode development)
@ -188,9 +270,17 @@ def create_app(config_class=DevConfig):
file_handler = WatchedFileHandler( file_handler = WatchedFileHandler(
app.config["SCODOC_LOG_FILE"], encoding="utf-8" 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) file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler) 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.setLevel(logging.INFO)
app.logger.info(f"{sco_version.SCONAME} {sco_version.SCOVERSION} startup") app.logger.info(f"{sco_version.SCONAME} {sco_version.SCOVERSION} startup")
@ -199,15 +289,19 @@ def create_app(config_class=DevConfig):
) )
# ---- INITIALISATION SPECIFIQUES A SCODOC # ---- INITIALISATION SPECIFIQUES A SCODOC
from app.scodoc import sco_bulletins_generator 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_legacy import BulletinGeneratorLegacy
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
from app.scodoc.sco_bulletins_ucac import BulletinGeneratorUCAC from app.scodoc.sco_bulletins_ucac import BulletinGeneratorUCAC
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorExample) # l'ordre est important, le premeir sera le "défaut" pour les nouveaux départements.
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorLegacy)
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandard) sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandard)
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorLegacy)
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorUCAC) 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 return app
@ -225,6 +319,8 @@ def set_sco_dept(scodoc_dept: str):
g.scodoc_dept_id = dept.id # l'id g.scodoc_dept_id = dept.id # l'id
if not hasattr(g, "db_conn"): if not hasattr(g, "db_conn"):
ndb.open_db_connection() ndb.open_db_connection()
if not hasattr(g, "stored_get_formsemestre"):
g.stored_get_formsemestre = {}
def user_db_init(): def user_db_init():

View File

@ -6,3 +6,4 @@ from flask import Blueprint
bp = Blueprint("api", __name__) bp = Blueprint("api", __name__)
from app.api import sco_api from app.api import sco_api
from app.api import tokens

View File

@ -33,7 +33,8 @@ token_auth = HTTPTokenAuth()
@basic_auth.verify_password @basic_auth.verify_password
def verify_password(username, password): def verify_password(username, password):
user = User.query.filter_by(username=username).first() # breakpoint()
user = User.query.filter_by(user_name=username).first()
if user and user.check_password(password): if user and user.check_password(password):
return user return user
@ -51,3 +52,17 @@ def verify_token(token):
@token_auth.error_handler @token_auth.error_handler
def token_auth_error(status): def token_auth_error(status):
return error_response(status) return error_response(status)
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 # 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 # 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 # 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 from werkzeug.http import HTTP_STATUS_CODES

View File

@ -29,7 +29,7 @@
""" """
# PAS ENCORE IMPLEMENTEE, juste un essai # PAS ENCORE IMPLEMENTEE, juste un essai
# Pour P. Bouron, il faudrait en priorité l'équivalent de # 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_create
# Scolarite/Notes/evaluation_delete # Scolarite/Notes/evaluation_delete
# Scolarite/Notes/formation_list # Scolarite/Notes/formation_list
@ -48,9 +48,9 @@ from app.api.errors import bad_request
from app import models from app import models
@bp.route("/ScoDoc/api/list_depts", methods=["GET"]) @bp.route("list_depts", methods=["GET"])
@token_auth.login_required @token_auth.login_required
def list_depts(): def list_depts():
depts = models.Departement.query.filter_by(visible=True).all() 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) return jsonify(data)

View File

@ -8,7 +8,7 @@ TODO: à revoir complètement pour reprendre ZScoUsers et les pages d'authentifi
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo 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 _ = lambda x: x # sans babel
@ -43,8 +43,11 @@ class UserCreationForm(FlaskForm):
class ResetPasswordRequestForm(FlaskForm): class ResetPasswordRequestForm(FlaskForm):
email = StringField(_l("Email"), validators=[DataRequired(), Email()]) email = StringField(
submit = SubmitField(_l("Request Password Reset")) _l("Adresse email associée à votre compte ScoDoc:"),
validators=[DataRequired(), Email()],
)
submit = SubmitField(_l("Envoyer"))
class ResetPasswordForm(FlaskForm): class ResetPasswordForm(FlaskForm):
@ -52,7 +55,11 @@ class ResetPasswordForm(FlaskForm):
password2 = PasswordField( password2 = PasswordField(
_l("Répéter"), validators=[DataRequired(), EqualTo("password")] _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): class DeactivateUserForm(FlaskForm):

View File

@ -10,6 +10,7 @@ import re
from time import time from time import time
from typing import Optional from typing import Optional
import cracklib # pylint: disable=import-error
from flask import current_app, url_for, g from flask import current_app, url_for, g
from flask_login import UserMixin, AnonymousUserMixin from flask_login import UserMixin, AnonymousUserMixin
@ -25,7 +26,24 @@ from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import sco_etud # a deplacer dans 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): class User(UserMixin, db.Model):
@ -177,8 +195,9 @@ class User(UserMixin, db.Model):
if "roles_string" in data: if "roles_string" in data:
self.user_roles = [] self.user_roles = []
for r_d in data["roles_string"].split(","): for r_d in data["roles_string"].split(","):
role, dept = UserRole.role_dept_from_string(r_d) if r_d:
self.add_role(role, dept) role, dept = UserRole.role_dept_from_string(r_d)
self.add_role(role, dept)
def get_token(self, expires_in=3600): def get_token(self, expires_in=3600):
now = datetime.utcnow() now = datetime.utcnow()
@ -194,6 +213,9 @@ class User(UserMixin, db.Model):
@staticmethod @staticmethod
def check_token(token): 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() user = User.query.filter_by(token=token).first()
if user is None or user.token_expiration < datetime.utcnow(): if user is None or user.token_expiration < datetime.utcnow():
return None return None
@ -329,7 +351,7 @@ class Role(db.Model):
"""Roles for ScoDoc""" """Roles for ScoDoc"""
id = db.Column(db.Integer, primary_key=True) 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) default = db.Column(db.Boolean, default=False, index=True)
permissions = db.Column(db.BigInteger) # 64 bits permissions = db.Column(db.BigInteger) # 64 bits
users = db.relationship("User", secondary="user_role", viewonly=True) users = db.relationship("User", secondary="user_role", viewonly=True)
@ -388,7 +410,7 @@ class UserRole(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("user.id")) user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
role_id = db.Column(db.Integer, db.ForeignKey("role.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 = db.relationship(
User, backref=db.backref("user_roles", cascade="all, delete-orphan") User, backref=db.backref("user_roles", cascade="all, delete-orphan")
) )
@ -407,6 +429,9 @@ class UserRole(db.Model):
""" """
fields = role_dept.split("_", 1) # maxsplit=1, le dept peut contenir un "_" fields = role_dept.split("_", 1) # maxsplit=1, le dept peut contenir un "_"
if len(fields) != 2: if len(fields) != 2:
current_app.logger.warning(
f"role_dept_from_string: Invalid role_dept '{role_dept}'"
)
raise ScoValueError("Invalid role_dept") raise ScoValueError("Invalid role_dept")
role_name, dept = fields role_name, dept = fields
if dept == "": if dept == "":
@ -418,7 +443,7 @@ class UserRole(db.Model):
def get_super_admin(): 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. Utilisé par les tests unitaires et le script de migration.
""" """
admin_role = Role.query.filter_by(name="SuperAdmin").first() admin_role = Role.query.filter_by(name="SuperAdmin").first()

View File

@ -46,7 +46,10 @@ def login():
if not next_page or url_parse(next_page).netloc != "": if not next_page or url_parse(next_page).netloc != "":
next_page = url_for("scodoc.index") next_page = url_for("scodoc.index")
return redirect(next_page) 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") @bp.route("/logout")
@ -95,7 +98,9 @@ def reset_password_request():
current_app.logger.info( current_app.logger.info(
"reset_password_request: for unkown user '{}'".format(form.email.data) "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 redirect(url_for("auth.login"))
return render_template( return render_template(
"auth/reset_password_request.html", title=_("Reset Password"), form=form "auth/reset_password_request.html", title=_("Reset Password"), form=form
@ -113,6 +118,6 @@ def reset_password(token):
if form.validate_on_submit(): if form.validate_on_submit():
user.set_password(form.password.data) user.set_password(form.password.data)
db.session.commit() db.session.commit()
flash(_("Your password has been reset.")) flash(_("Votre mot de passe a été changé."))
return redirect(url_for("auth.login")) return redirect(url_for("auth.login"))
return render_template("auth/reset_password.html", form=form) return render_template("auth/reset_password.html", form=form, user=user)

View File

@ -10,16 +10,15 @@ import logging
import werkzeug import werkzeug
from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequest
import flask import flask
from flask import g from flask import g, current_app, request
from flask import abort, current_app from flask import abort, url_for, redirect
from flask import request
from flask_login import current_user from flask_login import current_user
from flask_login import login_required from flask_login import login_required
from flask import current_app
import flask_login import flask_login
import app import app
from app.auth.models import User from app.auth.models import User
import app.scodoc.sco_utils as scu
class ZUser(object): class ZUser(object):
@ -39,69 +38,6 @@ class ZUser(object):
raise NotImplementedError() 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): def scodoc(func):
"""Décorateur pour toutes les fonctions ScoDoc """Décorateur pour toutes les fonctions ScoDoc
Affecte le département à g Affecte le département à g
@ -114,6 +50,25 @@ def scodoc(func):
@wraps(func) @wraps(func)
def scodoc_function(*args, **kwargs): 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: if "scodoc_dept" in kwargs:
dept_acronym = kwargs["scodoc_dept"] dept_acronym = kwargs["scodoc_dept"]
# current_app.logger.info("setting dept to " + dept_acronym) # current_app.logger.info("setting dept to " + dept_acronym)
@ -123,6 +78,7 @@ def scodoc(func):
# current_app.logger.info("setting dept to None") # current_app.logger.info("setting dept to None")
g.scodoc_dept = None g.scodoc_dept = None
g.scodoc_dept_id = -1 # invalide g.scodoc_dept_id = -1 # invalide
return func(*args, **kwargs) return func(*args, **kwargs)
return scodoc_function return scodoc_function
@ -132,7 +88,6 @@ def permission_required(permission):
def decorator(f): def decorator(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
# current_app.logger.info("PERMISSION; kwargs=%s" % str(kwargs))
scodoc_dept = getattr(g, "scodoc_dept", None) scodoc_dept = getattr(g, "scodoc_dept", None)
if not current_user.has_permission(permission, scodoc_dept): if not current_user.has_permission(permission, scodoc_dept):
abort(403) abort(403)
@ -144,7 +99,7 @@ def permission_required(permission):
def permission_required_compat_scodoc7(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 Comme @permission_required mais autorise de passer directement
les informations d'auth en paramètres: les informations d'auth en paramètres:
__ac_name, __ac_password __ac_name, __ac_password
@ -153,8 +108,8 @@ def permission_required_compat_scodoc7(permission):
def decorator(f): def decorator(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
# current_app.logger.warning("PERMISSION; kwargs=%s" % str(kwargs))
# cherche les paramètre d'auth: # cherche les paramètre d'auth:
# print("@permission_required_compat_scodoc7")
auth_ok = False auth_ok = False
if request.method == "GET": if request.method == "GET":
user_name = request.args.get("__ac_name") 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): if u and u.check_password(user_password):
auth_ok = True auth_ok = True
flask_login.login_user(u) flask_login.login_user(u)
# reprend le chemin classique: # reprend le chemin classique:
scodoc_dept = getattr(g, "scodoc_dept", None) scodoc_dept = getattr(g, "scodoc_dept", None)
@ -193,7 +147,6 @@ def admin_required(f):
def scodoc7func(func): def scodoc7func(func):
"""Décorateur pour intégrer les fonctions Zope 2 de ScoDoc 7. """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. 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") 1. via a Flask route ("top level call")
2. or be called directly from Python. 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") # Détermine si on est appelé via une route ("toplevel")
# ou par un appel de fonction python normal. # 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: if not top_level:
# ne "redécore" pas # ne "redécore" pas
return func(*args, **kwargs) return func(*args, **kwargs)
g.scodoc7_decorated = True
# --- Emulate Zope's REQUEST # --- Emulate Zope's REQUEST
REQUEST = ZRequest() # REQUEST = ZRequest()
g.zrequest = REQUEST # g.zrequest = REQUEST
req_args = REQUEST.form # args from query string (get) or form (post) # args from query string (get) or form (post)
# --- Add positional arguments req_args = scu.get_request_args()
## --- Add positional arguments
pos_arg_values = [] pos_arg_values = []
argspec = inspect.getfullargspec(func) argspec = inspect.getfullargspec(func)
# current_app.logger.info("argspec=%s" % str(argspec)) # current_app.logger.info("argspec=%s" % str(argspec))
@ -227,10 +182,12 @@ def scodoc7func(func):
arg_names = argspec.args[:-nb_default_args] arg_names = argspec.args[:-nb_default_args]
else: else:
arg_names = argspec.args arg_names = argspec.args
for arg_name in arg_names: for arg_name in arg_names: # pour chaque arg de la fonction vue
if arg_name == "REQUEST": # special case if arg_name == "REQUEST": # ne devrait plus arriver !
pos_arg_values.append(REQUEST) # debug check, TODO remove after tests
raise ValueError("invalid REQUEST parameter !")
else: else:
# peut produire une KeyError s'il manque un argument attendu:
v = req_args[arg_name] v = req_args[arg_name]
# try to convert all arguments to INTEGERS # try to convert all arguments to INTEGERS
# necessary for db ids and boolean values # necessary for db ids and boolean values
@ -244,9 +201,9 @@ def scodoc7func(func):
# Add keyword arguments # Add keyword arguments
if nb_default_args: if nb_default_args:
for arg_name in argspec.args[-nb_default_args:]: for arg_name in argspec.args[-nb_default_args:]:
if arg_name == "REQUEST": # special case # if arg_name == "REQUEST": # special case
kwargs[arg_name] = REQUEST # kwargs[arg_name] = REQUEST
elif arg_name in req_args: if arg_name in req_args:
# set argument kw optionnel # set argument kw optionnel
v = req_args[arg_name] v = req_args[arg_name]
# try to convert all arguments to INTEGERS # try to convert all arguments to INTEGERS
@ -270,13 +227,13 @@ def scodoc7func(func):
# Build response, adding collected http headers: # Build response, adding collected http headers:
headers = [] headers = []
kw = {"response": value, "status": 200} kw = {"response": value, "status": 200}
if g.zrequest: # if g.zrequest:
headers = g.zrequest.RESPONSE.headers # headers = g.zrequest.RESPONSE.headers
if not headers: # if not headers:
# no customized header, speedup: # # no customized header, speedup:
return value # return value
if "content-type" in headers: # if "content-type" in headers:
kw["mimetype"] = headers["content-type"] # kw["mimetype"] = headers["content-type"]
r = flask.Response(**kw) r = flask.Response(**kw)
for h in headers: for h in headers:
r.headers[h] = headers[h] r.headers[h] = headers[h]

View File

@ -49,7 +49,7 @@ class AbsenceNotification(db.Model):
nbabsjust = db.Column(db.Integer) nbabsjust = db.Column(db.Integer)
formsemestre_id = db.Column( formsemestre_id = db.Column(
db.Integer, db.Integer,
db.ForeignKey("notes_formsemestre.id"), db.ForeignKey("notes_formsemestre.id", ondelete="CASCADE"),
) )
@ -73,3 +73,17 @@ class BilletAbsence(db.Model):
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
# true si l'absence _pourrait_ etre justifiée # true si l'absence _pourrait_ etre justifiée
justified = db.Column(db.Boolean(), default=False, server_default="false") 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

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

@ -0,0 +1,4 @@
"""ScoDoc 9 models : Formation BUT 2021
"""
# insérer ici idk

View File

@ -33,7 +33,7 @@ class Departement(db.Model):
semsets = db.relationship("NotesSemSet", lazy="dynamic", backref="departement") semsets = db.relationship("NotesSemSet", lazy="dynamic", backref="departement")
def __repr__(self): def __repr__(self):
return f"<Departement {self.acronym}>" return f"<{self.__class__.__name__}(id={self.id}, acronym='{self.acronym}')>"
def to_dict(self): def to_dict(self):
data = { data = {

View File

@ -41,7 +41,10 @@ class Identite(db.Model):
code_nip = db.Column(db.Text()) code_nip = db.Column(db.Text())
code_ine = db.Column(db.Text()) code_ine = db.Column(db.Text())
# Ancien id ScoDoc7 pour les migrations de bases anciennes # 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) scodoc7_id = db.Column(db.Text(), nullable=True)
#
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
class Adresse(db.Model): class Adresse(db.Model):
@ -146,10 +149,13 @@ class ItemSuiviTag(db.Model):
# Association tag <-> module # Association tag <-> module
itemsuivi_tags_assoc = db.Table( itemsuivi_tags_assoc = db.Table(
"itemsuivi_tags_assoc", "itemsuivi_tags_assoc",
db.Column("tag_id", db.Integer, db.ForeignKey("itemsuivi_tags.id")), db.Column(
db.Column("itemsuivi_id", db.Integer, db.ForeignKey("itemsuivi.id")), "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): class EtudAnnotation(db.Model):

View File

@ -11,7 +11,7 @@ class NotesFormation(db.Model):
"""Programme pédagogique d'une formation""" """Programme pédagogique d'une formation"""
__tablename__ = "notes_formations" __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) id = db.Column(db.Integer, primary_key=True)
formation_id = db.synonym("id") formation_id = db.synonym("id")
@ -30,7 +30,12 @@ class NotesFormation(db.Model):
type_parcours = db.Column(db.Integer, default=0, server_default="0") type_parcours = db.Column(db.Integer, default=0, server_default="0")
code_specialite = db.Column(db.String(SHORT_STR_LEN)) code_specialite = db.Column(db.String(SHORT_STR_LEN))
ues = db.relationship("NotesUE", backref="formation", lazy="dynamic")
formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation") formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation")
ues = db.relationship("NotesUE", lazy="dynamic", backref="formation")
def __repr__(self):
return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme}')>"
class NotesUE(db.Model): class NotesUE(db.Model):
@ -61,6 +66,13 @@ class NotesUE(db.Model):
# coef UE, utilise seulement si l'option use_ue_coefs est activée: # coef UE, utilise seulement si l'option use_ue_coefs est activée:
coefficient = db.Column(db.Float) coefficient = db.Column(db.Float)
# relations
matieres = db.relationship("NotesMatiere", lazy="dynamic", backref="ue")
modules = db.relationship("NotesModule", lazy="dynamic", backref="ue")
def __repr__(self):
return f"<{self.__class__.__name__}(id={self.id}, formation_id={self.formation_id}, acronyme='{self.acronyme}')>"
class NotesMatiere(db.Model): class NotesMatiere(db.Model):
"""Matières: regroupe les modules d'une UE """Matières: regroupe les modules d'une UE
@ -77,6 +89,8 @@ class NotesMatiere(db.Model):
titre = db.Column(db.Text()) titre = db.Column(db.Text())
numero = db.Column(db.Integer) # ordre de présentation numero = db.Column(db.Integer) # ordre de présentation
modules = db.relationship("NotesModule", lazy="dynamic", backref="matiere")
class NotesModule(db.Model): class NotesModule(db.Model):
"""Module""" """Module"""
@ -103,6 +117,8 @@ class NotesModule(db.Model):
# id de l'element pedagogique Apogee correspondant: # id de l'element pedagogique Apogee correspondant:
code_apogee = db.Column(db.String(APO_CODE_STR_LEN)) code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
module_type = db.Column(db.Integer) # NULL ou 0:defaut, 1: malus (NOTES_MALUS) module_type = db.Column(db.Integer) # NULL ou 0:defaut, 1: malus (NOTES_MALUS)
# Relations:
modimpls = db.relationship("NotesModuleImpl", backref="module", lazy="dynamic")
class NotesTag(db.Model): class NotesTag(db.Model):
@ -121,6 +137,12 @@ class NotesTag(db.Model):
# Association tag <-> module # Association tag <-> module
notes_modules_tags = db.Table( notes_modules_tags = db.Table(
"notes_modules_tags", "notes_modules_tags",
db.Column("tag_id", db.Integer, db.ForeignKey("notes_tags.id")), db.Column(
db.Column("module_id", db.Integer, db.ForeignKey("notes_modules.id")), "tag_id",
db.Integer,
db.ForeignKey("notes_tags.id", ondelete="CASCADE"),
),
db.Column(
"module_id", db.Integer, db.ForeignKey("notes_modules.id", ondelete="CASCADE")
),
) )

View File

@ -41,6 +41,10 @@ class FormSemestre(db.Model):
bul_hide_xml = db.Column( bul_hide_xml = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" db.Boolean(), nullable=False, default=False, server_default="false"
) )
# 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): # semestres decales (pour gestion jurys):
gestion_semestrielle = db.Column( gestion_semestrielle = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" db.Boolean(), nullable=False, default=False, server_default="false"
@ -66,10 +70,16 @@ class FormSemestre(db.Model):
# code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...' # code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'
elt_annee_apo = db.Column(db.Text()) elt_annee_apo = db.Column(db.Text())
# Relations:
etapes = db.relationship( etapes = db.relationship(
"NotesFormsemestreEtape", cascade="all,delete", backref="notes_formsemestre" "NotesFormsemestreEtape", cascade="all,delete", backref="formsemestre"
) )
formsemestres = db.relationship(
"NotesModuleImpl", backref="formsemestre", lazy="dynamic"
)
# Ancien id ScoDoc7 pour les migrations de bases anciennes # 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) scodoc7_id = db.Column(db.Text(), nullable=True)
def __init__(self, **kwargs): def __init__(self, **kwargs):
@ -268,6 +278,7 @@ class NotesModuleImplInscription(db.Model):
"""Inscription à un module (etudiants,moduleimpl)""" """Inscription à un module (etudiants,moduleimpl)"""
__tablename__ = "notes_moduleimpl_inscription" __tablename__ = "notes_moduleimpl_inscription"
__table_args__ = (db.UniqueConstraint("moduleimpl_id", "etudid"),)
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
moduleimpl_inscription_id = db.synonym("id") moduleimpl_inscription_id = db.synonym("id")

View File

@ -4,6 +4,7 @@
""" """
from app import db, log from app import db, log
from app.scodoc import bonus_sport from app.scodoc import bonus_sport
from app.scodoc.sco_exceptions import ScoValueError
class ScoPreference(db.Model): class ScoPreference(db.Model):
@ -94,7 +95,7 @@ class ScoDocSiteConfig(db.Model):
"""returns bonus func with specified name. """returns bonus func with specified name.
If name not specified, return the configured function. If name not specified, return the configured function.
None if no bonus function configured. 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: if func_name is None:
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
@ -103,7 +104,13 @@ class ScoDocSiteConfig(db.Model):
func_name = c.value func_name = c.value
if func_name == "": # pas de bonus défini if func_name == "": # pas de bonus défini
return None 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 @classmethod
def get_bonus_sport_func_names(cls): def get_bonus_sport_func_names(cls):

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

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

View File

@ -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_codes_parcours # sco_codes_parcours.NEXT -> sem suivant
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import pe_tagtable from app.pe import pe_tagtable
from app.scodoc import pe_tools from app.pe import pe_tools
from app.scodoc import pe_semestretag from app.pe import pe_semestretag
from app.scodoc import pe_settag from app.pe import pe_settag
# ---------------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------------
def comp_nom_semestre_dans_parcours(sem): def comp_nom_semestre_dans_parcours(sem):
@ -946,7 +946,7 @@ class JuryPE(object):
return list(taglist) return list(taglist)
def get_allTagInSyntheseJury(self): 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() allTags = set()
for nom in JuryPE.PARCOURS.keys(): for nom in JuryPE.PARCOURS.keys():
allTags = allTags.union(set(self.get_allTagForAggregat(nom))) allTags = allTags.union(set(self.get_allTagForAggregat(nom)))

View File

@ -40,7 +40,7 @@ from app import log
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_tag_module from app.scodoc import sco_tag_module
from app.scodoc import pe_tagtable from app.pe import pe_tagtable
class SemestreTag(pe_tagtable.TableTag): class SemestreTag(pe_tagtable.TableTag):

View File

@ -36,8 +36,8 @@ Created on Fri Sep 9 09:15:05 2016
@author: barasc @author: barasc
""" """
from app.scodoc.pe_tools import pe_print, PE_DEBUG from app.pe.pe_tools import pe_print, PE_DEBUG
from app.scodoc import pe_tagtable from app.pe import pe_tagtable
class SetTag(pe_tagtable.TableTag): class SetTag(pe_tagtable.TableTag):

View File

@ -44,7 +44,7 @@ import unicodedata
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import log
import six from app.scodoc.sco_logos import find_logo
PE_DEBUG = 0 PE_DEBUG = 0
@ -145,7 +145,7 @@ def escape_for_latex(s):
} }
exp = re.compile( exp = re.compile(
"|".join( "|".join(
re.escape(six.text_type(key)) re.escape(key)
for key in sorted(list(conv.keys()), key=lambda item: -len(item)) 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): 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""" """Read pathname server file and add content to zip under path_in_zip"""
rooted_path_in_zip = os.path.join(ziproot, path_in_zip) rooted_path_in_zip = os.path.join(ziproot, path_in_zip)
data = open(pathname).read() zipfile.write(filename=pathname, arcname=rooted_path_in_zip)
zipfile.writestr(rooted_path_in_zip, data) # 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): def add_pe_stuff_to_zip(zipfile, ziproot):
@ -179,37 +190,23 @@ def add_pe_stuff_to_zip(zipfile, ziproot):
Also copy logos Also copy logos
""" """
register = {}
# first add standard (distrib references)
distrib_dir = os.path.join(REP_DEFAULT_AVIS, "distrib") distrib_dir = os.path.join(REP_DEFAULT_AVIS, "distrib")
distrib_pathnames = list_directory_filenames( add_refs_to_register(register=register, directory=distrib_dir)
distrib_dir # then add local references (some oh them may overwrite distrib refs)
) # 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
local_dir = os.path.join(REP_LOCAL_AVIS, "local") local_dir = os.path.join(REP_LOCAL_AVIS, "local")
local_pathnames = list_directory_filenames(local_dir) add_refs_to_register(register=register, directory=local_dir)
l = len(local_dir) # at this point register contains all refs (filename, pathname) to be saved
local_filenames = {x[l + 1 :] for x in local_pathnames} for filename, pathname in register.items():
add_local_file_to_zip(zipfile, ziproot, pathname, "avis/" + filename)
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,
)
# Logos: (add to logos/ directory in zip) # Logos: (add to logos/ directory in zip)
logos_names = ["logo_header.jpg", "logo_footer.jpg"] logos_names = ["header", "footer"]
for f in logos_names: for name in logos_names:
logo = os.path.join(scu.SCODOC_LOGOS_DIR, f) logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id)
if os.path.isfile(logo): if logo is not None:
add_local_file_to_zip(zipfile, ziproot, logo, "avis/logos/" + f) add_local_file_to_zip(zipfile, ziproot, logo, "avis/logos/" + logo.filename)
# ---------------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------------

View File

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

View File

@ -8,6 +8,7 @@
v 1.3 (python3) v 1.3 (python3)
""" """
import html
def TrivialFormulator( def TrivialFormulator(
@ -134,7 +135,7 @@ class TF(object):
is_submitted=False, is_submitted=False,
): ):
self.form_url = form_url self.form_url = form_url
self.values = values self.values = values.copy()
self.formdescription = list(formdescription) self.formdescription = list(formdescription)
self.initvalues = initvalues self.initvalues = initvalues
self.method = method self.method = method
@ -722,7 +723,9 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
if str(descr["allowed_values"][i]) == str(self.values[field]): if str(descr["allowed_values"][i]) == str(self.values[field]):
R.append('<span class="tf-ro-value">%s</span>' % labels[i]) R.append('<span class="tf-ro-value">%s</span>' % labels[i])
elif input_type == "textarea": 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": elif input_type == "separator" or input_type == "hidden":
pass pass
elif input_type == "file": elif input_type == "file":

View File

@ -167,6 +167,23 @@ def bonus_iutlh(notes_sport, coefs, infos=None):
return bonus 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 # Bonus sport IUT Tours
def bonus_tours(notes_sport, coefs, infos=None): def bonus_tours(notes_sport, coefs, infos=None):
"""Calcul bonus sport & culture IUT Tours sur moyenne generale """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): 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. Le bonus est compris entre 0 et 0.35 point.
cette procédure modifie la moyenne de chaque UE capitalisable. cette procédure modifie la moyenne de chaque UE capitalisable.
@ -379,6 +397,25 @@ def bonus_iutbethune(notes_sport, coefs, infos=None):
return bonus 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_demo(notes_sport, coefs, infos=None): def bonus_demo(notes_sport, coefs, infos=None):
"""Fausse fonction "bonus" pour afficher les informations disponibles """Fausse fonction "bonus" pour afficher les informations disponibles
et aider les développeurs. et aider les développeurs.
@ -386,8 +423,8 @@ def bonus_demo(notes_sport, coefs, infos=None):
qui est ECRASE à chaque appel. qui est ECRASE à chaque appel.
*** Ne pas utiliser en production !!! *** *** Ne pas utiliser en production !!! ***
""" """
f = open("/tmp/scodoc_bonus.log", "w") # mettre 'a' pour ajouter en fin with open("/tmp/scodoc_bonus.log", "w") as f: # mettre 'a' pour ajouter en fin
f.write("\n---------------\n" + pprint.pformat(infos) + "\n") f.write("\n---------------\n" + pprint.pformat(infos) + "\n")
# Statut de chaque UE # Statut de chaque UE
# for ue_id in infos['moy_ues']: # for ue_id in infos['moy_ues']:
# ue_status = infos['moy_ues'][ue_id] # ue_status = infos['moy_ues'][ue_id]

View File

@ -185,6 +185,9 @@ class GenTable(object):
else: else:
self.preferences = DEFAULT_TABLE_PREFERENCES() 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): def get_nb_cols(self):
return len(self.columns_ids) return len(self.columns_ids)
@ -468,7 +471,10 @@ class GenTable(object):
def excel(self, wb=None): def excel(self, wb=None):
"""Simple Excel representation of the table""" """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 ses.rows += self.xls_before_table
style_bold = sco_excel.excel_make_style(bold=True) style_bold = sco_excel.excel_make_style(bold=True)
style_base = sco_excel.excel_make_style() style_base = sco_excel.excel_make_style()
@ -482,9 +488,7 @@ class GenTable(object):
ses.append_blank_row() # empty line ses.append_blank_row() # empty line
ses.append_single_cell_row(self.origin, style_base) ses.append_single_cell_row(self.origin, style_base)
if wb is None: if wb is None:
return ses.generate_standalone() return ses.generate()
else:
ses.generate_embeded()
def text(self): def text(self):
"raw text representation of the table" "raw text representation of the table"
@ -494,7 +498,7 @@ class GenTable(object):
headline = [] headline = []
return "\n".join( 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() for line in headline + self.get_data_list()
] ]
) )
@ -573,7 +577,7 @@ class GenTable(object):
""" """
doc = ElementTree.Element( doc = ElementTree.Element(
self.xml_outer_tag, self.xml_outer_tag,
id=self.table_id, id=str(self.table_id),
origin=self.origin or "", origin=self.origin or "",
caption=self.caption or "", caption=self.caption or "",
) )
@ -587,7 +591,7 @@ class GenTable(object):
v = row.get(cid, "") v = row.get(cid, "")
if v is None: if v is None:
v = "" v = ""
x_cell = ElementTree.Element(cid, value=str(v)) x_cell = ElementTree.Element(str(cid), value=str(v))
x_row.append(x_cell) x_row.append(x_cell)
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING) return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING)
@ -610,7 +614,6 @@ class GenTable(object):
format="html", format="html",
page_title="", page_title="",
filename=None, filename=None,
REQUEST=None,
javascripts=[], javascripts=[],
with_html_headers=True, with_html_headers=True,
publish=True, publish=True,
@ -643,35 +646,53 @@ class GenTable(object):
H.append(html_sco_header.sco_footer()) H.append(html_sco_header.sco_footer())
return "\n".join(H) return "\n".join(H)
elif format == "pdf": elif format == "pdf":
objects = self.pdf() pdf_objs = self.pdf()
doc = sco_pdf.pdf_basic_page( pdf_doc = sco_pdf.pdf_basic_page(
objects, title=title, preferences=self.preferences pdf_objs, title=title, preferences=self.preferences
) )
if publish: if publish:
return scu.sendPDFFile(REQUEST, doc, filename + ".pdf") return scu.send_file(
pdf_doc,
filename,
suffix=".pdf",
mime=scu.PDF_MIMETYPE,
)
else: else:
return doc return pdf_doc
elif format == "xls" or format == "xlsx": elif format == "xls" or format == "xlsx": # dans les 2 cas retourne du xlsx
xls = self.excel() xls = self.excel()
if publish: if publish:
return sco_excel.send_excel_file( return scu.send_file(
REQUEST, xls, filename + scu.XLSX_SUFFIX xls,
filename,
suffix=scu.XLSX_SUFFIX,
mime=scu.XLSX_MIMETYPE,
) )
else: else:
return xls return xls
elif format == "text": elif format == "text":
return self.text() return self.text()
elif format == "csv": 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": elif format == "xml":
xml = self.xml() xml = self.xml()
if REQUEST and publish: if publish:
REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE) return scu.send_file(
xml, filename, suffix=".xml", mime=scu.XML_MIMETYPE
)
return xml return xml
elif format == "json": elif format == "json":
js = self.json() js = self.json()
if REQUEST and publish: if publish:
REQUEST.RESPONSE.setHeader("content-type", scu.JSON_MIMETYPE) return scu.send_file(
js, filename, suffix=".json", mime=scu.JSON_MIMETYPE
)
return js return js
else: else:
log("make_page: format=%s" % format) log("make_page: format=%s" % format)
@ -731,6 +752,8 @@ if __name__ == "__main__":
) )
document.build(objects) document.build(objects)
data = doc.getvalue() data = doc.getvalue()
open("/tmp/gen_table.pdf", "wb").write(data) with open("/tmp/gen_table.pdf", "wb") as f:
p = T.make_page(format="pdf", REQUEST=None) f.write(data)
open("toto.pdf", "wb").write(p) p = T.make_page(format="pdf")
with open("toto.pdf", "wb") as f:
f.write(p)

View File

@ -87,10 +87,6 @@ Problème de connexion (identifiant, mot de passe): <em>contacter votre responsa
) )
_TOP_LEVEL_CSS = """
<style type="text/css">
</style>"""
_HTML_BEGIN = """<?xml version="1.0" encoding="%(encoding)s"?> _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"> <!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"> <html xmlns="http://www.w3.org/1999/xhtml">
@ -105,31 +101,30 @@ _HTML_BEGIN = """<?xml version="1.0" encoding="%(encoding)s"?>
<link href="/ScoDoc/static/css/scodoc.css" rel="stylesheet" type="text/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" /> <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 src="/ScoDoc/static/libjs/menu.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/sorttable.js"></script> <script src="/ScoDoc/static/libjs/sorttable.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/bubble.js"></script> <script src="/ScoDoc/static/libjs/bubble.js"></script>
<script type="text/javascript"> <script>
window.onload=function(){enableTooltips("gtrcontent")}; window.onload=function(){enableTooltips("gtrcontent")};
</script> </script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/jQuery/jquery.js"></script> <script 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 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/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" /> <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 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/etud_info.js"></script>
""" """
def scodoc_top_html_header(page_title="ScoDoc: bienvenue"): def scodoc_top_html_header(page_title="ScoDoc: bienvenue"):
H = [ H = [
_HTML_BEGIN % {"page_title": page_title, "encoding": scu.SCO_ENCODING}, _HTML_BEGIN % {"page_title": page_title, "encoding": scu.SCO_ENCODING},
_TOP_LEVEL_CSS,
"""</head><body class="gtrcontent" id="gtrcontent">""", """</head><body class="gtrcontent" id="gtrcontent">""",
scu.CUSTOM_HTML_HEADER_CNX, scu.CUSTOM_HTML_HEADER_CNX,
] ]
@ -185,13 +180,10 @@ def sco_header(
init_jquery = True init_jquery = True
H = [ H = [
"""<?xml version="1.0" encoding="%(encoding)s"?> """<!DOCTYPE html><html lang="fr">
<!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> <head>
<meta charset="utf-8"/>
<title>%(page_title)s</title> <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="LANG" content="fr" />
<meta name="DESCRIPTION" content="ScoDoc" /> <meta name="DESCRIPTION" content="ScoDoc" />
@ -206,9 +198,7 @@ def sco_header(
) )
if init_google_maps: if init_google_maps:
# It may be necessary to add an API key: # It may be necessary to add an API key:
H.append( H.append('<script src="https://maps.google.com/maps/api/js"></script>')
'<script type="text/javascript" src="https://maps.google.com/maps/api/js"></script>'
)
# Feuilles de style additionnelles: # Feuilles de style additionnelles:
for cssstyle in cssstyles: for cssstyle in cssstyles:
@ -223,9 +213,9 @@ def sco_header(
<link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" /> <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" /> <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 src="/ScoDoc/static/libjs/menu.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/bubble.js"></script> <script src="/ScoDoc/static/libjs/bubble.js"></script>
<script type="text/javascript"> <script>
window.onload=function(){enableTooltips("gtrcontent")}; window.onload=function(){enableTooltips("gtrcontent")};
var SCO_URL="%(ScoURL)s"; var SCO_URL="%(ScoURL)s";
@ -236,16 +226,14 @@ def sco_header(
# jQuery # jQuery
if init_jquery: if init_jquery:
H.append( H.append(
"""<script language="javascript" type="text/javascript" src="/ScoDoc/static/jQuery/jquery.js"></script> """<script src="/ScoDoc/static/jQuery/jquery.js"></script>
""" """
) )
H.append( H.append('<script src="/ScoDoc/static/libjs/jquery.field.min.js"></script>')
'<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery.field.min.js"></script>'
)
# qTip # qTip
if init_qtip: if init_qtip:
H.append( 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( H.append(
'<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.css" />' '<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.css" />'
@ -253,32 +241,25 @@ def sco_header(
if init_jquery_ui: if init_jquery_ui:
H.append( 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>' '<script 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/js/jquery-ui-i18n.js"></script>')
H.append('<script src="/ScoDoc/static/js/scodoc.js"></script>')
if init_google_maps: if init_google_maps:
H.append( 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: if init_datatables:
H.append( H.append(
'<link rel="stylesheet" type="text/css" href="/ScoDoc/static/DataTables/datatables.min.css"/>' '<link rel="stylesheet" type="text/css" href="/ScoDoc/static/DataTables/datatables.min.css"/>'
) )
H.append( H.append('<script src="/ScoDoc/static/DataTables/datatables.min.js"></script>')
'<script type="text/javascript" src="/ScoDoc/static/DataTables/datatables.min.js"></script>'
)
# JS additionels # JS additionels
for js in javascripts: for js in javascripts:
H.append( H.append("""<script src="/ScoDoc/static/%s"></script>\n""" % js)
"""<script language="javascript" type="text/javascript" src="/ScoDoc/static/%s"></script>\n"""
% js
)
H.append( H.append(
"""<style type="text/css"> """<style>
.gtrcontent { .gtrcontent {
margin-left: %(margin_left)s; margin-left: %(margin_left)s;
height: 100%%; height: 100%%;
@ -290,7 +271,7 @@ def sco_header(
) )
# Scripts de la page: # Scripts de la page:
if scripts: if scripts:
H.append("""<script language="javascript" type="text/javascript">""") H.append("""<script>""")
for script in scripts: for script in scripts:
H.append(script) H.append(script)
H.append("""</script>""") H.append("""</script>""")
@ -337,13 +318,7 @@ def sco_footer():
def html_sem_header( 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" "Titre d'une page semestre avec lien vers tableau de bord"
# sem now unused and thus optional... # sem now unused and thus optional...

View File

@ -28,9 +28,8 @@
""" """
Génération de la "sidebar" (marge gauche des pages HTML) Génération de la "sidebar" (marge gauche des pages HTML)
""" """
from flask import url_for from flask import render_template, url_for
from flask import g from flask import g, request
from flask import request
from flask_login import current_user from flask_login import current_user
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -40,17 +39,11 @@ from app.scodoc.sco_permissions import Permission
def sidebar_common(): def sidebar_common():
"partie commune à toutes les sidebar" "partie commune à toutes les sidebar"
params = {
"ScoURL": scu.ScoURL(),
"UsersURL": scu.UsersURL(),
"NotesURL": scu.NotesURL(),
"AbsencesURL": scu.AbsencesURL(),
"authuser": current_user.user_name,
}
H = [ H = [
f"""<a class="scodoc_title" href="about">ScoDoc 9</a> f"""<a class="scodoc_title" href="{url_for("scodoc.index", scodoc_dept=g.scodoc_dept)}">ScoDoc 9</a>
<div id="authuser"><a id="authuserlink" href="{ <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> }">{current_user.user_name}</a>
<br/><a id="deconnectlink" href="{url_for("auth.logout")}">déconnexion</a> <br/><a id="deconnectlink" href="{url_for("auth.logout")}">déconnexion</a>
</div> </div>
@ -71,7 +64,8 @@ def sidebar_common():
if current_user.has_permission(Permission.ScoChangePreferences): if current_user.has_permission(Permission.ScoChangePreferences):
H.append( H.append(
f"""<a href="{url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)}" 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) return "".join(H)
@ -97,11 +91,12 @@ def sidebar():
""" """
] ]
# ---- Il y-a-t-il un etudiant selectionné ? # ---- Il y-a-t-il un etudiant selectionné ?
etudid = None etudid = g.get("etudid", None)
if request.method == "GET": if not etudid:
etudid = request.args.get("etudid", None) if request.method == "GET":
elif request.method == "POST": etudid = request.args.get("etudid", None)
etudid = request.form.get("etudid", None) elif request.method == "POST":
etudid = request.form.get("etudid", None)
if etudid: if etudid:
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0] etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
@ -155,8 +150,9 @@ def sidebar():
<div class="sidebar-bottom"><a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }" class="sidebar">À propos</a><br/> <div class="sidebar-bottom"><a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }" class="sidebar">À propos</a><br/>
<a href="{ scu.SCO_USER_MANUAL }" target="_blank" class="sidebar">Aide</a> <a href="{ scu.SCO_USER_MANUAL }" target="_blank" class="sidebar">Aide</a>
</div></div> </div></div>
<div class="logo-logo"><a href= { url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) } <div class="logo-logo">
">{ scu.icontag("scologo_img", no_size=True) }</a> <a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }">
{ scu.icontag("scologo_img", no_size=True) }</a>
</div> </div>
</div> </div>
<!-- end of sidebar --> <!-- end of sidebar -->
@ -167,19 +163,7 @@ def sidebar():
def sidebar_dept(): def sidebar_dept():
"""Partie supérieure de la marge de gauche""" """Partie supérieure de la marge de gauche"""
H = [ return render_template(
f"""<h2 class="insidebar">Dépt. {sco_preferences.get_preference("DeptName")}</h2> "sidebar_dept.html",
<a href="{url_for("scodoc.index")}" class="sidebar">Accueil</a> <br/> """ prefs=sco_preferences.SemPreferences(),
] )
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)

View File

@ -27,9 +27,12 @@
"""Various HTML generation functions """Various HTML generation functions
""" """
from html.parser import HTMLParser
from html.entities import name2codepoint
import re
from flask import g, url_for from flask import g, url_for
import app.scodoc.sco_utils as scu
from . import listhistogram from . import listhistogram
@ -104,6 +107,8 @@ def make_menu(title, items, css_class="", alone=False):
item["urlq"] = url_for( item["urlq"] = url_for(
item["endpoint"], scodoc_dept=g.scodoc_dept, **args item["endpoint"], scodoc_dept=g.scodoc_dept, **args
) )
elif "url" in item:
item["urlq"] = item["url"]
else: else:
item["urlq"] = "#" item["urlq"] = "#"
item["attr"] = item.get("attr", "") item["attr"] = item.get("attr", "")
@ -128,3 +133,63 @@ def make_menu(title, items, css_class="", alone=False):
if alone: if alone:
H.append("</ul>") H.append("</ul>")
return "".join(H) 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/ # Code from http://code.activestate.com/recipes/457411/
from __future__ import print_function
from bisect import bisect_left, bisect_right from bisect import bisect_left, bisect_right
from six.moves import zip
class intervalmap(object): class intervalmap(object):
""" """

View File

@ -27,10 +27,7 @@
"""Calculs sur les notes et cache des resultats """Calculs sur les notes et cache des resultats
""" """
import inspect
import os
import pdb
import time
from operator import itemgetter from operator import itemgetter
from flask import g, url_for from flask import g, url_for
@ -40,12 +37,8 @@ import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app import log from app import log
from app.scodoc.sco_formulas import NoteVector from app.scodoc.sco_formulas import NoteVector
from app.scodoc.sco_exceptions import ( from app.scodoc.sco_exceptions import ScoValueError
AccessDenied,
NoteProcessError,
ScoException,
ScoValueError,
)
from app.scodoc.sco_formsemestre import ( from app.scodoc.sco_formsemestre import (
formsemestre_uecoef_list, formsemestre_uecoef_list,
formsemestre_uecoef_create, formsemestre_uecoef_create,
@ -109,15 +102,13 @@ def get_sem_ues_modimpls(formsemestre_id, modimpls=None):
(utilisé quand on ne peut pas construire nt et faire nt.get_ues()) (utilisé quand on ne peut pas construire nt et faire nt.get_ues())
""" """
if modimpls is None: if modimpls is None:
modimpls = sco_moduleimpl.do_moduleimpl_list(formsemestre_id=formsemestre_id) modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
uedict = {} uedict = {}
for modimpl in modimpls: for modimpl in modimpls:
mod = sco_edit_module.do_module_list(args={"module_id": modimpl["module_id"]})[ mod = sco_edit_module.module_list(args={"module_id": modimpl["module_id"]})[0]
0
]
modimpl["module"] = mod modimpl["module"] = mod
if not mod["ue_id"] in uedict: 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 uedict[ue["ue_id"]] = ue
ues = list(uedict.values()) ues = list(uedict.values())
ues.sort(key=lambda u: u["numero"]) ues.sort(key=lambda u: u["numero"])
@ -186,6 +177,8 @@ class NotesTable(object):
self.use_ue_coefs = sco_preferences.get_preference( self.use_ue_coefs = sco_preferences.get_preference(
"use_ue_coefs", formsemestre_id "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 # Infos sur les etudiants
self.inscrlist = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( self.inscrlist = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
args={"formsemestre_id": formsemestre_id} args={"formsemestre_id": formsemestre_id}
@ -217,26 +210,29 @@ class NotesTable(object):
valid_evals, valid_evals,
mods_att, mods_att,
self.expr_diagnostics, 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._mods_att = mods_att # liste des modules avec des notes en attente
self._matmoys = {} # moyennes par matieres self._matmoys = {} # moyennes par matieres
self._valid_evals = {} # { evaluation_id : eval } self._valid_evals = {} # { evaluation_id : eval }
for e in valid_evals: for e in valid_evals:
self._valid_evals[e["evaluation_id"]] = e # Liste des modules et UE self._valid_evals[e["evaluation_id"]] = e # Liste des modules et UE
uedict = {} # public member: { ue_id : 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: 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: 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 uedict[ue["ue_id"]] = ue
else: else:
ue = uedict[mod["ue_id"]] ue = uedict[mod["ue_id"]]
modimpl["ue"] = ue # add ue dict to moduleimpl modimpl["ue"] = ue # add ue dict to moduleimpl
self._matmoys[mod["matiere_id"]] = {} self._matmoys[mod["matiere_id"]] = {}
mat = sco_edit_matiere.do_matiere_list( mat = sco_edit_matiere.matiere_list(args={"matiere_id": mod["matiere_id"]})[
args={"matiere_id": mod["matiere_id"]} 0
)[0] ]
modimpl["mat"] = mat # add matiere dict to moduleimpl modimpl["mat"] = mat # add matiere dict to moduleimpl
# calcul moyennes du module et stocke dans le module # calcul moyennes du module et stocke dans le module
# nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif= # nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif=
@ -634,7 +630,8 @@ class NotesTable(object):
matiere_sum_notes += val * coef matiere_sum_notes += val * coef
matiere_sum_coefs += coef matiere_sum_coefs += coef
matiere_id_last = matiere_id 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 nb_missing = nb_missing + 1
coefs.append(0) coefs.append(0)
coefs_mask.append(0) coefs_mask.append(0)
@ -732,12 +729,11 @@ class NotesTable(object):
Prend toujours en compte les UE capitalisées. Prend toujours en compte les UE capitalisées.
""" """
# log('comp_etud_moy_gen(etudid=%s)' % etudid) # Si l'étudiant a Démissionné ou est DEFaillant, on n'enregistre pas ses moyennes
# Si l'étudiant a Demissionné ou est DEFaillant, on n'enregistre pas ses moyennes
block_computation = ( block_computation = (
self.inscrdict[etudid]["etat"] == "D" self.inscrdict[etudid]["etat"] == "D"
or self.inscrdict[etudid]["etat"] == DEF or self.inscrdict[etudid]["etat"] == DEF
or self.block_moyennes
) )
moy_ues = {} moy_ues = {}
@ -1056,7 +1052,7 @@ class NotesTable(object):
"Warning: %s capitalized an UE %s which is not part of current sem %s" "Warning: %s capitalized an UE %s which is not part of current sem %s"
% (etudid, ue_id, self.formsemestre_id) % (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 self.uedict[ue_id] = ue # record this UE
if ue_id not in self._uecoef: if ue_id not in self._uecoef:
cl = formsemestre_uecoef_list( cl = formsemestre_uecoef_list(
@ -1262,7 +1258,7 @@ class NotesTable(object):
), ),
self.get_nom_long(etudid), self.get_nom_long(etudid),
url_for( url_for(
"scolar.formsemestre_edit_uecoefs", "notes.formsemestre_edit_uecoefs",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=self.formsemestre_id, formsemestre_id=self.formsemestre_id,
err_ue_id=ue["ue_id"], err_ue_id=ue["ue_id"],

View File

@ -88,7 +88,15 @@ def SimpleDictFetch(query, args, cursor=None):
return cursor.dictfetchall() 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' """insert into table values in dict 'vals'
Return: id de l'object créé 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]) fmt = ",".join(["%%(%s)s" % col for col in cols])
# print 'insert into %s (%s) values (%s)' % (table,colnames,fmt) # print 'insert into %s (%s) values (%s)' % (table,colnames,fmt)
oid = None oid = None
if ignore_conflicts:
ignore = " ON CONFLICT DO NOTHING"
else:
ignore = ""
try: try:
if vals: if vals:
cursor.execute( 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: else:
cursor.execute("insert into %s default values" % table) cursor.execute("insert into %s default values%s" % (table, ignore))
if return_id: if return_id:
cursor.execute(f"SELECT CURRVAL('{table}_id_seq')") # id créé cursor.execute(f"SELECT CURRVAL('{table}_id_seq')") # id créé
oid = cursor.fetchone()[0] oid = cursor.fetchone()[0]
@ -291,6 +304,7 @@ class EditableTable(object):
fields_creators={}, # { field : [ sql_command_to_create_it ] } fields_creators={}, # { field : [ sql_command_to_create_it ] }
filter_nulls=True, # dont allow to set fields to null filter_nulls=True, # dont allow to set fields to null
filter_dept=False, # ajoute selection sur g.scodoc_dept_id filter_dept=False, # ajoute selection sur g.scodoc_dept_id
insert_ignore_conflicts=False,
): ):
self.table_name = table_name self.table_name = table_name
self.id_name = id_name self.id_name = id_name
@ -311,8 +325,9 @@ class EditableTable(object):
self.filter_nulls = filter_nulls self.filter_nulls = filter_nulls
self.filter_dept = filter_dept self.filter_dept = filter_dept
self.sql_default_values = None 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" "create object in table"
vals = dictfilter(args, self.dbfields, self.filter_nulls) vals = dictfilter(args, self.dbfields, self.filter_nulls)
if self.id_name in vals: if self.id_name in vals:
@ -336,6 +351,7 @@ class EditableTable(object):
vals, vals,
commit=True, commit=True,
return_id=(self.id_name is not None), return_id=(self.id_name is not None),
ignore_conflicts=self.insert_ignore_conflicts,
) )
return new_id return new_id
@ -581,6 +597,22 @@ def float_null_is_null(x):
return float(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 # post filtering
# #
def UniqListofDicts(L, key): def UniqListofDicts(L, key):

View File

@ -474,7 +474,7 @@ def _get_abs_description(a, cursor=None):
desc = a["description"] desc = a["description"]
if a["moduleimpl_id"] and a["moduleimpl_id"] != "NULL": if a["moduleimpl_id"] and a["moduleimpl_id"] != "NULL":
# Trouver le nom du module # Trouver le nom du module
Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list( Mlist = sco_moduleimpl.moduleimpl_withmodule_list(
moduleimpl_id=a["moduleimpl_id"] moduleimpl_id=a["moduleimpl_id"]
) )
if Mlist: if Mlist:
@ -626,7 +626,6 @@ def add_absence(
jour, jour,
matin, matin,
estjust, estjust,
REQUEST,
description=None, description=None,
moduleimpl_id=None, moduleimpl_id=None,
): ):
@ -656,7 +655,7 @@ def add_absence(
sco_abs_notification.abs_notify(etudid, jour) 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" "Ajoute un justificatif dans la base"
# unpublished # unpublished
if _isFarFutur(jour): if _isFarFutur(jour):
@ -665,7 +664,9 @@ def add_justif(etudid, jour, matin, REQUEST, description=None):
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute( 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(), vars(),
) )
logdb( logdb(
@ -678,7 +679,7 @@ def add_justif(etudid, jour, matin, REQUEST, description=None):
invalidate_abs_etud_date(etudid, jour) 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: for a in abslist:
etudid, jour, ampm = a.split(":") etudid, jour, ampm = a.split(":")
if ampm == "am": if ampm == "am":
@ -689,7 +690,7 @@ def _add_abslist(abslist, REQUEST, moduleimpl_id=None):
raise ValueError("invalid ampm !") raise ValueError("invalid ampm !")
# ajoute abs si pas deja absent # ajoute abs si pas deja absent
if count_abs(etudid, jour, jour, matin, moduleimpl_id) == 0: 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): def annule_absence(etudid, jour, matin, moduleimpl_id=None):
@ -721,7 +722,7 @@ def annule_absence(etudid, jour, matin, moduleimpl_id=None):
invalidate_abs_etud_date(etudid, jour) invalidate_abs_etud_date(etudid, jour)
def annule_justif(etudid, jour, matin, REQUEST=None): def annule_justif(etudid, jour, matin):
"Annule un justificatif" "Annule un justificatif"
# unpublished # unpublished
matin = _toboolean(matin) matin = _toboolean(matin)

View File

@ -30,7 +30,7 @@
""" """
import datetime import datetime
from flask import url_for, g from flask import url_for, g, request
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
@ -58,7 +58,6 @@ def doSignaleAbsence(
estjust=False, estjust=False,
description=None, description=None,
etudid=False, etudid=False,
REQUEST=None,
): # etudid implied ): # etudid implied
"""Signalement d'une absence. """Signalement d'une absence.
@ -69,7 +68,8 @@ def doSignaleAbsence(
demijournee: 2 si journée complète, 1 matin, 0 après-midi demijournee: 2 si journée complète, 1 matin, 0 après-midi
estjust: absence justifiée estjust: absence justifiée
description: str 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] etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
etudid = etud["etudid"] etudid = etud["etudid"]
@ -86,7 +86,6 @@ def doSignaleAbsence(
jour, jour,
False, False,
estjust, estjust,
REQUEST,
description_abs, description_abs,
moduleimpl_id, moduleimpl_id,
) )
@ -95,7 +94,6 @@ def doSignaleAbsence(
jour, jour,
True, True,
estjust, estjust,
REQUEST,
description_abs, description_abs,
moduleimpl_id, moduleimpl_id,
) )
@ -106,7 +104,6 @@ def doSignaleAbsence(
jour, jour,
demijournee, demijournee,
estjust, estjust,
REQUEST,
description_abs, description_abs,
moduleimpl_id, moduleimpl_id,
) )
@ -118,7 +115,7 @@ def doSignaleAbsence(
J = "NON " J = "NON "
M = "" M = ""
if moduleimpl_id and moduleimpl_id != "NULL": 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"] formsemestre_id = mod["formsemestre_id"]
nt = sco_cache.NotesTableCache.get(formsemestre_id) nt = sco_cache.NotesTableCache.get(formsemestre_id)
ues = nt.get_ues(etudid=etudid) ues = nt.get_ues(etudid=etudid)
@ -156,7 +153,7 @@ def doSignaleAbsence(
return "\n".join(H) return "\n".join(H)
def SignaleAbsenceEtud(REQUEST=None): # etudid implied def SignaleAbsenceEtud(): # etudid implied
"""Formulaire individuel simple de signalement d'une absence""" """Formulaire individuel simple de signalement d'une absence"""
# brute-force portage from very old dtml code ... # brute-force portage from very old dtml code ...
etud = sco_etud.get_etud_info(filled=True)[0] etud = sco_etud.get_etud_info(filled=True)[0]
@ -228,7 +225,6 @@ def SignaleAbsenceEtud(REQUEST=None): # etudid implied
sco_photos.etud_photo_html( sco_photos.etud_photo_html(
etudid=etudid, etudid=etudid,
title="fiche de " + etud["nomprenom"], title="fiche de " + etud["nomprenom"],
REQUEST=REQUEST,
), ),
"""</a></td></tr></table>""", """</a></td></tr></table>""",
""" """
@ -281,7 +277,6 @@ def doJustifAbsence(
demijournee, demijournee,
description=None, description=None,
etudid=False, etudid=False,
REQUEST=None,
): # etudid implied ): # etudid implied
"""Justification d'une absence """Justification d'une absence
@ -291,7 +286,8 @@ def doJustifAbsence(
demijournee: 2 si journée complète, 1 matin, 0 après-midi demijournee: 2 si journée complète, 1 matin, 0 après-midi
estjust: absence justifiée estjust: absence justifiée
description: str 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] etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
etudid = etud["etudid"] etudid = etud["etudid"]
@ -305,14 +301,12 @@ def doJustifAbsence(
etudid=etudid, etudid=etudid,
jour=jour, jour=jour,
matin=False, matin=False,
REQUEST=REQUEST,
description=description_abs, description=description_abs,
) )
sco_abs.add_justif( sco_abs.add_justif(
etudid=etudid, etudid=etudid,
jour=jour, jour=jour,
matin=True, matin=True,
REQUEST=REQUEST,
description=description_abs, description=description_abs,
) )
nbadded += 2 nbadded += 2
@ -321,7 +315,6 @@ def doJustifAbsence(
etudid=etudid, etudid=etudid,
jour=jour, jour=jour,
matin=demijournee, matin=demijournee,
REQUEST=REQUEST,
description=description_abs, description=description_abs,
) )
nbadded += 1 nbadded += 1
@ -357,7 +350,7 @@ def doJustifAbsence(
return "\n".join(H) return "\n".join(H)
def JustifAbsenceEtud(REQUEST=None): # etudid implied def JustifAbsenceEtud(): # etudid implied
"""Formulaire individuel simple de justification d'une absence""" """Formulaire individuel simple de justification d'une absence"""
# brute-force portage from very old dtml code ... # brute-force portage from very old dtml code ...
etud = sco_etud.get_etud_info(filled=True)[0] etud = sco_etud.get_etud_info(filled=True)[0]
@ -376,7 +369,6 @@ def JustifAbsenceEtud(REQUEST=None): # etudid implied
sco_photos.etud_photo_html( sco_photos.etud_photo_html(
etudid=etudid, etudid=etudid,
title="fiche de " + etud["nomprenom"], title="fiche de " + etud["nomprenom"],
REQUEST=REQUEST,
), ),
"""</a></td></tr></table>""", """</a></td></tr></table>""",
""" """
@ -412,9 +404,7 @@ Raison: <input type="text" name="description" size="42"/> (optionnel)
return "\n".join(H) return "\n".join(H)
def doAnnuleAbsence( def doAnnuleAbsence(datedebut, datefin, demijournee, etudid=False): # etudid implied
datedebut, datefin, demijournee, etudid=False, REQUEST=None
): # etudid implied
"""Annulation des absences pour une demi journée""" """Annulation des absences pour une demi journée"""
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0] etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
etudid = etud["etudid"] etudid = etud["etudid"]
@ -462,7 +452,7 @@ autre absence pour <b>%(nomprenom)s</b></a></li>
return "\n".join(H) return "\n".join(H)
def AnnuleAbsenceEtud(REQUEST=None): # etudid implied def AnnuleAbsenceEtud(): # etudid implied
"""Formulaire individuel simple d'annulation d'une absence""" """Formulaire individuel simple d'annulation d'une absence"""
# brute-force portage from very old dtml code ... # brute-force portage from very old dtml code ...
etud = sco_etud.get_etud_info(filled=True)[0] etud = sco_etud.get_etud_info(filled=True)[0]
@ -482,7 +472,6 @@ def AnnuleAbsenceEtud(REQUEST=None): # etudid implied
sco_photos.etud_photo_html( sco_photos.etud_photo_html(
etudid=etudid, etudid=etudid,
title="fiche de " + etud["nomprenom"], title="fiche de " + etud["nomprenom"],
REQUEST=REQUEST,
), ),
"""</a></td></tr></table>""", """</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> """<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) return "\n".join(H)
def doAnnuleJustif(datedebut0, datefin0, demijournee, REQUEST=None): # etudid implied def doAnnuleJustif(datedebut0, datefin0, demijournee): # etudid implied
"""Annulation d'une justification""" """Annulation d'une justification"""
etud = sco_etud.get_etud_info(filled=True)[0] etud = sco_etud.get_etud_info(filled=True)[0]
etudid = etud["etudid"] etudid = etud["etudid"]
@ -558,11 +547,11 @@ def doAnnuleJustif(datedebut0, datefin0, demijournee, REQUEST=None): # etudid i
for jour in dates: for jour in dates:
# Attention: supprime matin et après-midi # Attention: supprime matin et après-midi
if demijournee == 2: if demijournee == 2:
sco_abs.annule_justif(etudid, jour, False, REQUEST=REQUEST) sco_abs.annule_justif(etudid, jour, False)
sco_abs.annule_justif(etudid, jour, True, REQUEST=REQUEST) sco_abs.annule_justif(etudid, jour, True)
nbadded += 2 nbadded += 2
else: else:
sco_abs.annule_justif(etudid, jour, demijournee, REQUEST=REQUEST) sco_abs.annule_justif(etudid, jour, demijournee)
nbadded += 1 nbadded += 1
# #
H = [ H = [
@ -716,7 +705,6 @@ def formChoixSemestreGroupe(all=False):
def CalAbs(etudid, sco_year=None): def CalAbs(etudid, sco_year=None):
"""Calendrier des absences d'un etudiant""" """Calendrier des absences d'un etudiant"""
# crude portage from 1999 DTML # crude portage from 1999 DTML
REQUEST = None # XXX
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0] etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
etudid = etud["etudid"] etudid = etud["etudid"]
anneescolaire = int(scu.AnneeScolaire(sco_year)) anneescolaire = int(scu.AnneeScolaire(sco_year))
@ -766,7 +754,6 @@ def CalAbs(etudid, sco_year=None):
sco_photos.etud_photo_html( sco_photos.etud_photo_html(
etudid=etudid, etudid=etudid,
title="fiche de " + etud["nomprenom"], title="fiche de " + etud["nomprenom"],
REQUEST=REQUEST,
), ),
), ),
CalHTML, CalHTML,
@ -791,7 +778,6 @@ def ListeAbsEtud(
format="html", format="html",
absjust_only=0, absjust_only=0,
sco_year=None, sco_year=None,
REQUEST=None,
): ):
"""Liste des absences d'un étudiant sur l'année en cours """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). En format 'html': page avec deux tableaux (non justifiées et justifiées).
@ -804,18 +790,19 @@ def ListeAbsEtud(
absjust_only: si vrai, renvoie table absences justifiées 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" 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) datedebut = "%s-08-01" % scu.AnneeScolaire(sco_year=sco_year)
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
# Liste des absences et titres colonnes tables: # 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 etudid, datedebut, with_evals=with_evals, format=format
) )
if REQUEST: if request.base_url:
base_url_nj = "%s?etudid=%s&absjust_only=0" % (REQUEST.URL0, etudid) base_url_nj = "%s?etudid=%s&absjust_only=0" % (request.base_url, etudid)
base_url_j = "%s?etudid=%s&absjust_only=1" % (REQUEST.URL0, etudid) base_url_j = "%s?etudid=%s&absjust_only=1" % (request.base_url, etudid)
else: else:
base_url_nj = base_url_j = "" base_url_nj = base_url_j = ""
tab_absnonjust = GenTable( tab_absnonjust = GenTable(
@ -844,9 +831,9 @@ def ListeAbsEtud(
# Formats non HTML et demande d'une seule table: # Formats non HTML et demande d'une seule table:
if format != "html" and format != "text": if format != "html" and format != "text":
if absjust_only == 1: if absjust_only == 1:
return tab_absjust.make_page(format=format, REQUEST=REQUEST) return tab_absjust.make_page(format=format)
else: else:
return tab_absnonjust.make_page(format=format, REQUEST=REQUEST) return tab_absnonjust.make_page(format=format)
if format == "html": if format == "html":
# Mise en forme HTML: # Mise en forme HTML:
@ -896,13 +883,12 @@ def ListeAbsEtud(
raise ValueError("Invalid format !") raise ValueError("Invalid format !")
def _TablesAbsEtud( def _tables_abs_etud(
etudid, etudid,
datedebut, datedebut,
with_evals=True, with_evals=True,
format="html", format="html",
absjust_only=0, absjust_only=0,
REQUEST=None,
): ):
"""Tables des absences justifiees et non justifiees d'un étudiant """Tables des absences justifiees et non justifiees d'un étudiant
sur l'année en cours sur l'année en cours
@ -928,11 +914,11 @@ def _TablesAbsEtud(
cursor.execute( cursor.execute(
"""SELECT mi.moduleimpl_id """SELECT mi.moduleimpl_id
FROM absences abs, notes_moduleimpl_inscription mi, notes_moduleimpl m FROM absences abs, notes_moduleimpl_inscription mi, notes_moduleimpl m
WHERE abs.matin = %(matin)s WHERE abs.matin = %(matin)s
and abs.jour = %(jour)s and abs.jour = %(jour)s
and abs.etudid = %(etudid)s and abs.etudid = %(etudid)s
and abs.moduleimpl_id = mi.moduleimpl_id and abs.moduleimpl_id = mi.moduleimpl_id
and mi.moduleimpl_id = m.id and mi.moduleimpl_id = m.id
and mi.etudid = %(etudid)s and mi.etudid = %(etudid)s
""", """,
{ {
@ -954,13 +940,14 @@ def _TablesAbsEtud(
return "" return ""
ex = [] ex = []
for ev in a["evals"]: for ev in a["evals"]:
mod = sco_moduleimpl.do_moduleimpl_withmodule_list( mod = sco_moduleimpl.moduleimpl_withmodule_list(
moduleimpl_id=ev["moduleimpl_id"] moduleimpl_id=ev["moduleimpl_id"]
)[0] )[0]
if format == "html": if format == "html":
ex.append( ex.append(
'<a href="Notes/moduleimpl_status?moduleimpl_id=%s">%s</a>' f"""<a href="{url_for('notes.moduleimpl_status',
% (mod["moduleimpl_id"], mod["module"]["code"]) scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"])}
">{mod["module"]["code"]}</a>"""
) )
else: else:
ex.append(mod["module"]["code"]) ex.append(mod["module"]["code"])
@ -971,13 +958,14 @@ def _TablesAbsEtud(
def descr_abs(a): def descr_abs(a):
ex = [] ex = []
for ev in a.get("absent", []): for ev in a.get("absent", []):
mod = sco_moduleimpl.do_moduleimpl_withmodule_list( mod = sco_moduleimpl.moduleimpl_withmodule_list(
moduleimpl_id=ev["moduleimpl_id"] moduleimpl_id=ev["moduleimpl_id"]
)[0] )[0]
if format == "html": if format == "html":
ex.append( ex.append(
'<a href="Notes/moduleimpl_status?moduleimpl_id=%s">%s</a>' f"""<a href="{url_for('notes.moduleimpl_status',
% (mod["moduleimpl_id"], mod["module"]["code"]) scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"])}
">{mod["module"]["code"]}</a>"""
) )
else: else:
ex.append(mod["module"]["code"]) ex.append(mod["module"]["code"])

View File

@ -29,37 +29,40 @@
Archives are plain files, stored in Archives are plain files, stored in
<SCODOC_VAR_DIR>/archives/<deptid> <SCODOC_VAR_DIR>/archives/<dept_id>
(where <SCODOC_VAR_DIR> is usually /opt/scodoc-data, and <deptid> a departement 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 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> <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 Les documents liés à l'étudiant sont dans
<archivedir>/docetuds/<dept>/<etudid>/<YYYY-MM-DD-HH-MM-SS> <archivedir>/docetuds/<dept_id>/<etudid>/<YYYY-MM-DD-HH-MM-SS>
(etudid est ici soit Identite.scodoc7id, soit à défaut Identite.id) (etudid est ici Identite.id)
Les maquettes Apogée pour l'export des notes sont dans Les maquettes Apogée pour l'export des notes sont dans
<archivedir>/apo_csv/<dept>/<annee_scolaire>-<sem_id>/<YYYY-MM-DD-HH-MM-SS>/<code_etape>.csv <archivedir>/apo_csv/<dept_id>/<annee_scolaire>-<sem_id>/<YYYY-MM-DD-HH-MM-SS>/<code_etape>.csv
Un répertoire d'archive contient des fichiers quelconques, et un fichier texte nommé _description.txt Un répertoire d'archive contient des fichiers quelconques, et un fichier texte nommé _description.txt
qui est une description (humaine, format libre) de l'archive. qui est une description (humaine, format libre) de l'archive.
""" """
import os
import time
import datetime import datetime
import glob
import mimetypes
import os
import re import re
import shutil import shutil
import glob import time
import flask import flask
from flask import g from flask import g, request
from flask_login import current_user
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from config import Config from config import Config
from app import log from app import log
from app.models import Departement
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_exceptions import ( from app.scodoc.sco_exceptions import (
AccessDenied, AccessDenied,
@ -108,7 +111,8 @@ class BaseArchiver(object):
If directory does not yet exist, create it. If directory does not yet exist, create it.
""" """
self.initialize() 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: try:
scu.GSL.acquire() scu.GSL.acquire()
if not os.path.isdir(dept_dir): if not os.path.isdir(dept_dir):
@ -127,7 +131,8 @@ class BaseArchiver(object):
:return: list of archive oids :return: list of archive oids
""" """
self.initialize() 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 + "*") dirs = glob.glob(base + "*")
return [os.path.split(x)[1] for x in dirs] return [os.path.split(x)[1] for x in dirs]
@ -198,7 +203,9 @@ class BaseArchiver(object):
def get_archive_description(self, archive_id): def get_archive_description(self, archive_id):
"""Return description of archive""" """Return description of archive"""
self.initialize() self.initialize()
return open(os.path.join(archive_id, "_description.txt")).read() with open(os.path.join(archive_id, "_description.txt")) as f:
descr = f.read()
return descr
def create_obj_archive(self, oid: int, description: str): def create_obj_archive(self, oid: int, description: str):
"""Creates a new archive for this object and returns its id.""" """Creates a new archive for this object and returns its id."""
@ -227,9 +234,8 @@ class BaseArchiver(object):
try: try:
scu.GSL.acquire() scu.GSL.acquire()
fname = os.path.join(archive_id, filename) fname = os.path.join(archive_id, filename)
f = open(fname, "wb") with open(fname, "wb") as f:
f.write(data) f.write(data)
f.close()
finally: finally:
scu.GSL.release() scu.GSL.release()
return filename return filename
@ -242,33 +248,19 @@ class BaseArchiver(object):
raise ValueError("invalid filename") raise ValueError("invalid filename")
fname = os.path.join(archive_id, filename) fname = os.path.join(archive_id, filename)
log("reading archive file %s" % fname) 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""" """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) archive_id = self.get_id_from_name(oid, archive_name)
data = self.get(archive_id, filename) data = self.get(archive_id, filename)
ext = os.path.splitext(filename.lower())[1] mime = mimetypes.guess_type(filename)[0]
if ext == ".html" or ext == ".htm": if mime is None:
return data mime = "application/octet-stream"
elif ext == ".xml":
REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE) return scu.send_file(data, filename, mime=mime)
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
class SemsArchiver(BaseArchiver): class SemsArchiver(BaseArchiver):
@ -283,7 +275,6 @@ PVArchive = SemsArchiver()
def do_formsemestre_archive( def do_formsemestre_archive(
REQUEST,
formsemestre_id, formsemestre_id,
group_ids=[], # si indiqué, ne prend que ces groupes group_ids=[], # si indiqué, ne prend que ces groupes
description="", description="",
@ -305,7 +296,7 @@ def do_formsemestre_archive(
from app.scodoc.sco_recapcomplet import make_formsemestre_recapcomplet from app.scodoc.sco_recapcomplet import make_formsemestre_recapcomplet
sem = sco_formsemestre.get_formsemestre(formsemestre_id) 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) archive_id = PVArchive.create_obj_archive(sem_archive_id, description)
date = PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M") date = PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M")
@ -351,14 +342,12 @@ def do_formsemestre_archive(
data = data.encode(scu.SCO_ENCODING) data = data.encode(scu.SCO_ENCODING)
PVArchive.store(archive_id, "Bulletins.xml", data) PVArchive.store(archive_id, "Bulletins.xml", data)
# Decisions de jury, en XLS # Decisions de jury, en XLS
data = sco_pvjury.formsemestre_pvjury( data = sco_pvjury.formsemestre_pvjury(formsemestre_id, format="xls", publish=False)
formsemestre_id, format="xls", REQUEST=REQUEST, publish=False
)
if data: if data:
PVArchive.store(archive_id, "Decisions_Jury" + scu.XLSX_SUFFIX, data) PVArchive.store(archive_id, "Decisions_Jury" + scu.XLSX_SUFFIX, data)
# Classeur bulletins (PDF) # Classeur bulletins (PDF)
data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf( data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
formsemestre_id, REQUEST, version=bulVersion formsemestre_id, version=bulVersion
) )
if data: if data:
PVArchive.store(archive_id, "Bulletins.pdf", data) PVArchive.store(archive_id, "Bulletins.pdf", data)
@ -389,14 +378,12 @@ def do_formsemestre_archive(
PVArchive.store(archive_id, "PV_Jury%s.pdf" % groups_filename, data) 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. """Make and store new archive for this formsemestre.
(all students or only selected groups) (all students or only selected groups)
""" """
if not sco_permissions_check.can_edit_pv(formsemestre_id): if not sco_permissions_check.can_edit_pv(formsemestre_id):
raise AccessDenied( raise AccessDenied("opération non autorisée pour %s" % str(current_user))
"opération non autorisée pour %s" % str(REQUEST.AUTHENTICATED_USER)
)
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if not group_ids: if not group_ids:
@ -408,7 +395,6 @@ def formsemestre_archive(REQUEST, formsemestre_id, group_ids=[]):
H = [ H = [
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
REQUEST,
"Archiver les PV et résultats du semestre", "Archiver les PV et résultats du semestre",
sem=sem, sem=sem,
javascripts=sco_groups_view.JAVASCRIPTS, javascripts=sco_groups_view.JAVASCRIPTS,
@ -469,8 +455,8 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
) )
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
descr, descr,
cancelbutton="Annuler", cancelbutton="Annuler",
method="POST", method="POST",
@ -492,7 +478,6 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
else: else:
tf[2]["anonymous"] = False tf[2]["anonymous"] = False
do_formsemestre_archive( do_formsemestre_archive(
REQUEST,
formsemestre_id, formsemestre_id,
group_ids=group_ids, group_ids=group_ids,
description=tf[2]["description"], description=tf[2]["description"],
@ -516,10 +501,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""" """Page listing archives"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
sem_archive_id = sem["scodoc7_id"] or formsemestre_id sem_archive_id = formsemestre_id
L = [] L = []
for archive_id in PVArchive.list_obj_archives(sem_archive_id): for archive_id in PVArchive.list_obj_archives(sem_archive_id):
a = { a = {
@ -530,7 +515,7 @@ def formsemestre_list_archives(REQUEST, formsemestre_id):
} }
L.append(a) 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: if not L:
H.append("<p>aucune archive enregistrée</p>") H.append("<p>aucune archive enregistrée</p>")
else: else:
@ -559,23 +544,19 @@ def formsemestre_list_archives(REQUEST, formsemestre_id):
return "\n".join(H) + html_sco_header.sco_footer() 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.""" """Send file to client."""
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
sem_archive_id = sem["scodoc7_id"] or formsemestre_id sem_archive_id = formsemestre_id
return PVArchive.get_archived_file(REQUEST, sem_archive_id, archive_name, filename) return PVArchive.get_archived_file(sem_archive_id, archive_name, filename)
def formsemestre_delete_archive( def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed=False):
REQUEST, formsemestre_id, archive_name, dialog_confirmed=False
):
"""Delete an archive""" """Delete an archive"""
if not sco_permissions_check.can_edit_pv(formsemestre_id): if not sco_permissions_check.can_edit_pv(formsemestre_id):
raise AccessDenied( raise AccessDenied("opération non autorisée pour %s" % str(current_user))
"opération non autorisée pour %s" % str(REQUEST.AUTHENTICATED_USER)
)
sem = sco_formsemestre.get_formsemestre(formsemestre_id) 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) archive_id = PVArchive.get_id_from_name(sem_archive_id, archive_name)
dest_url = "formsemestre_list_archives?formsemestre_id=%s" % (formsemestre_id) dest_url = "formsemestre_list_archives?formsemestre_id=%s" % (formsemestre_id)

View File

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

@ -28,6 +28,7 @@
"""Génération des bulletins de notes """Génération des bulletins de notes
""" """
from app.models import formsemestre
import time import time
import pprint import pprint
import email import email
@ -35,11 +36,10 @@ from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.mime.base import MIMEBase from email.mime.base import MIMEBase
from email.header import Header from email.header import Header
from reportlab.lib.colors import Color 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 import url_for
from flask_login import current_user from flask_login import current_user
from flask_mail import Message from flask_mail import Message
@ -48,7 +48,7 @@ import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app import log from app import log
from app.scodoc.sco_permissions import Permission 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 html_sco_header
from app.scodoc import htmlutils from app.scodoc import htmlutils
from app.scodoc import sco_abs from app.scodoc import sco_abs
@ -121,9 +121,7 @@ def make_context_dict(sem, etud):
return C return C
def formsemestre_bulletinetud_dict( def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
formsemestre_id, etudid, version="long", REQUEST=None
):
"""Collecte informations pour bulletin de notes """Collecte informations pour bulletin de notes
Retourne un dictionnaire (avec valeur par défaut chaine vide). Retourne un dictionnaire (avec valeur par défaut chaine vide).
Le contenu du dictionnaire dépend des options (rangs, ...) Le contenu du dictionnaire dépend des options (rangs, ...)
@ -138,15 +136,13 @@ def formsemestre_bulletinetud_dict(
prefs = sco_preferences.SemPreferences(formsemestre_id) prefs = sco_preferences.SemPreferences(formsemestre_id)
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes 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 = scu.DictDefault(defaultvalue="")
I["etudid"] = etudid I["etudid"] = etudid
I["formsemestre_id"] = formsemestre_id I["formsemestre_id"] = formsemestre_id
I["sem"] = nt.sem I["sem"] = nt.sem
if REQUEST: I["server_name"] = request.url_root
I["server_name"] = REQUEST.BASE0
else:
I["server_name"] = ""
# Formation et parcours # Formation et parcours
I["formation"] = sco_formations.formation_list( I["formation"] = sco_formations.formation_list(
@ -771,14 +767,16 @@ def formsemestre_bulletinetud(
xml_with_decisions=False, xml_with_decisions=False,
force_publishing=False, # force publication meme si semestre non publie sur "portail" force_publishing=False, # force publication meme si semestre non publie sur "portail"
prefer_mail_perso=False, prefer_mail_perso=False,
REQUEST=None,
): ):
"page bulletin de notes" "page bulletin de notes"
try: try:
etud = sco_etud.get_etud_info(filled=True)[0] etud = sco_etud.get_etud_info(filled=True)[0]
etudid = etud["etudid"] etudid = etud["etudid"]
except: 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( bulletin = do_formsemestre_bulletinetud(
formsemestre_id, formsemestre_id,
@ -788,15 +786,15 @@ def formsemestre_bulletinetud(
xml_with_decisions=xml_with_decisions, xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing, force_publishing=force_publishing,
prefer_mail_perso=prefer_mail_perso, prefer_mail_perso=prefer_mail_perso,
REQUEST=REQUEST,
)[0] )[0]
if format not in {"html", "pdfmail"}: 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) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [ H = [
_formsemestre_bulletinetud_header_html( _formsemestre_bulletinetud_header_html(
etud, etudid, sem, formsemestre_id, format, version, REQUEST etud, etudid, sem, formsemestre_id, format, version
), ),
bulletin, bulletin,
] ]
@ -854,7 +852,6 @@ def do_formsemestre_bulletinetud(
etudid, etudid,
version="long", # short, long, selectedevals version="long", # short, long, selectedevals
format="html", format="html",
REQUEST=None,
nohtml=False, nohtml=False,
xml_with_decisions=False, # force decisions dans XML xml_with_decisions=False, # force decisions dans XML
force_publishing=False, # force publication meme si semestre non publie sur "portail" force_publishing=False, # force publication meme si semestre non publie sur "portail"
@ -862,14 +859,13 @@ def do_formsemestre_bulletinetud(
): ):
"""Génère le bulletin au format demandé. """Génère le bulletin au format demandé.
Retourne: (bul, filigranne) 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"). et filigranne est un message à placer en "filigranne" (eg "Provisoire").
""" """
if format == "xml": if format == "xml":
bul = sco_bulletins_xml.make_xml_formsemestre_bulletinetud( bul = sco_bulletins_xml.make_xml_formsemestre_bulletinetud(
formsemestre_id, formsemestre_id,
etudid, etudid,
REQUEST=REQUEST,
xml_with_decisions=xml_with_decisions, xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing, force_publishing=force_publishing,
version=version, version=version,
@ -881,19 +877,18 @@ def do_formsemestre_bulletinetud(
bul = sco_bulletins_json.make_json_formsemestre_bulletinetud( bul = sco_bulletins_json.make_json_formsemestre_bulletinetud(
formsemestre_id, formsemestre_id,
etudid, etudid,
REQUEST=REQUEST,
xml_with_decisions=xml_with_decisions, xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing, force_publishing=force_publishing,
version=version, version=version,
) )
return bul, "" return bul, ""
I = formsemestre_bulletinetud_dict(formsemestre_id, etudid, REQUEST=REQUEST) I = formsemestre_bulletinetud_dict(formsemestre_id, etudid)
etud = I["etud"] etud = I["etud"]
if format == "html": if format == "html":
htm, _ = sco_bulletins_generator.make_formsemestre_bulletinetud( htm, _ = sco_bulletins_generator.make_formsemestre_bulletinetud(
I, version=version, format="html", REQUEST=REQUEST I, version=version, format="html"
) )
return htm, I["filigranne"] return htm, I["filigranne"]
@ -903,11 +898,10 @@ def do_formsemestre_bulletinetud(
version=version, version=version,
format="pdf", format="pdf",
stand_alone=(format != "pdfpart"), stand_alone=(format != "pdfpart"),
REQUEST=REQUEST,
) )
if format == "pdf": if format == "pdf":
return ( return (
scu.sendPDFFile(REQUEST, bul, filename), scu.sendPDFFile(bul, filename),
I["filigranne"], I["filigranne"],
) # unused ret. value ) # unused ret. value
else: else:
@ -923,11 +917,11 @@ def do_formsemestre_bulletinetud(
htm = "" # speed up if html version not needed htm = "" # speed up if html version not needed
else: else:
htm, _ = sco_bulletins_generator.make_formsemestre_bulletinetud( 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( pdfdata, filename = sco_bulletins_generator.make_formsemestre_bulletinetud(
I, version=version, format="pdf", REQUEST=REQUEST I, version=version, format="pdf"
) )
if prefer_mail_perso: if prefer_mail_perso:
@ -993,12 +987,11 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr):
bcc = copy_addr.strip() bcc = copy_addr.strip()
else: else:
bcc = "" bcc = ""
msg = Message(subject, sender=sender, recipients=recipients, bcc=bcc) msg = Message(subject, sender=sender, recipients=recipients, bcc=[bcc])
msg.body = hea msg.body = hea
# Attach pdf # Attach pdf
msg.attach(filename, scu.PDF_MIMETYPE, pdfdata) msg.attach(filename, scu.PDF_MIMETYPE, pdfdata)
log("mail bulletin a %s" % recipient_addr) log("mail bulletin a %s" % recipient_addr)
email.send_message(msg) email.send_message(msg)
@ -1010,7 +1003,6 @@ def _formsemestre_bulletinetud_header_html(
formsemestre_id=None, formsemestre_id=None,
format=None, format=None,
version=None, version=None,
REQUEST=None,
): ):
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
@ -1033,7 +1025,7 @@ def _formsemestre_bulletinetud_header_html(
), ),
""" """
<form name="f" method="GET" action="%s">""" <form name="f" method="GET" action="%s">"""
% REQUEST.URL0, % request.base_url,
f"""Bulletin <span class="bull_liensemestre"><a href="{ f"""Bulletin <span class="bull_liensemestre"><a href="{
url_for("notes.formsemestre_status", url_for("notes.formsemestre_status",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
@ -1063,14 +1055,20 @@ def _formsemestre_bulletinetud_header_html(
H.append("""</select></td>""") H.append("""</select></td>""")
# Menu # Menu
endpoint = "notes.formsemestre_bulletinetud" endpoint = "notes.formsemestre_bulletinetud"
url = REQUEST.URL0
qurl = six.moves.urllib.parse.quote_plus(url + "?" + REQUEST.QUERY_STRING)
menuBul = [ menuBul = [
{ {
"title": "Réglages bulletins", "title": "Réglages bulletins",
"endpoint": "notes.formsemestre_edit_options", "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"]) "enabled": (current_user.id in sem["responsables"])
or current_user.has_permission(Permission.ScoImplement), or current_user.has_permission(Permission.ScoImplement),
}, },
@ -1113,6 +1111,16 @@ def _formsemestre_bulletinetud_header_html(
"enabled": etud["emailperso"] "enabled": etud["emailperso"]
and can_send_bulletin_by_mail(formsemestre_id), 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", "title": "Version XML",
"endpoint": endpoint, "endpoint": endpoint,
@ -1188,9 +1196,14 @@ def _formsemestre_bulletinetud_header_html(
H.append( H.append(
'<td> <a href="%s">%s</a></td>' '<td> <a href="%s">%s</a></td>'
% ( % (
url url_for(
+ "?formsemestre_id=%s&etudid=%s&format=pdf&version=%s" "notes.formsemestre_bulletinetud",
% (formsemestre_id, etudid, version), scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etudid=etudid,
format="pdf",
version=version,
),
scu.ICON_PDF, scu.ICON_PDF,
) )
) )
@ -1201,9 +1214,7 @@ def _formsemestre_bulletinetud_header_html(
""" """
% ( % (
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid), url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
sco_photos.etud_photo_html( sco_photos.etud_photo_html(etud, title="fiche de " + etud["nom"]),
etud, title="fiche de " + etud["nom"], REQUEST=REQUEST
),
) )
) )
H.append( H.append(

View File

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

View File

@ -47,27 +47,22 @@ from app.scodoc import sco_etud
def make_json_formsemestre_bulletinetud( def make_json_formsemestre_bulletinetud(
formsemestre_id, formsemestre_id: int,
etudid, etudid: int,
REQUEST=None,
xml_with_decisions=False, xml_with_decisions=False,
version="long", version="long",
force_publishing=False, # force publication meme si semestre non publie sur "portail" force_publishing=False, # force publication meme si semestre non publie sur "portail"
): ) -> str:
"""Renvoie bulletin en chaine JSON""" """Renvoie bulletin en chaine JSON"""
d = formsemestre_bulletinetud_published_dict( d = formsemestre_bulletinetud_published_dict(
formsemestre_id, formsemestre_id,
etudid, etudid,
force_publishing=force_publishing, force_publishing=force_publishing,
REQUEST=REQUEST,
xml_with_decisions=xml_with_decisions, xml_with_decisions=xml_with_decisions,
version=version, version=version,
) )
if REQUEST:
REQUEST.RESPONSE.setHeader("content-type", scu.JSON_MIMETYPE)
return json.dumps(d, cls=scu.ScoDocJSONEncoder) return json.dumps(d, cls=scu.ScoDocJSONEncoder)
@ -79,7 +74,6 @@ def formsemestre_bulletinetud_published_dict(
etudid, etudid,
force_publishing=False, force_publishing=False,
xml_nodate=False, xml_nodate=False,
REQUEST=None,
xml_with_decisions=False, # inclue les decisions même si non publiées xml_with_decisions=False, # inclue les decisions même si non publiées
version="long", version="long",
): ):
@ -366,7 +360,7 @@ def formsemestre_bulletinetud_published_dict(
"decisions_ue" "decisions_ue"
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee) ]: # 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(): 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]
d["decision_ue"].append( d["decision_ue"].append(
dict( dict(
ue_id=ue["ue_id"], ue_id=ue["ue_id"],

View File

@ -51,23 +51,24 @@ Chaque semestre peut si nécessaire utiliser un type de bulletin différent.
""" """
import io import io
import os
import re import re
import time import time
import traceback import traceback
from pydoc import html
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate 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 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_cache
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_pdf from app.scodoc import sco_pdf
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_etud from app.scodoc import sco_etud
import sco_version import sco_version
from app.scodoc.sco_logos import find_logo
def pdfassemblebulletins( def pdfassemblebulletins(
@ -110,6 +111,17 @@ def pdfassemblebulletins(
return data 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"): def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
"""Process a field given in preferences, returns """Process a field given in preferences, returns
- if format = 'pdf': a list of Platypus objects - 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 return text
# --- PDF format: # --- PDF format:
# handle logos: # 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( text = re.sub(
r"<(\s*)logo(.*?)src\s*=\s*(.*?)>", r"<\1logo\2\3>", text r"<(\s*)logo(.*?)src\s*=\s*(.*?)>", r"<\1logo\2\3>", text
) # remove forbidden src attribute ) # remove forbidden src attribute
if image_dir is not None: text = re.sub(
text = re.sub( r'(<\s*logo(.*?)name\s*=\s*"(\w*?)"(.*?)/?>)',
r'<\s*logo(.*?)name\s*=\s*"(\w*?)"(.*?)/?>', replacement_function,
r'<img\1src="%s/logo_\2.jpg"\3/>' % image_dir, text,
text, )
) # nota: le match sur \w*? donne le nom du logo et interdit les .. et autres
# nota: le match sur \w*? donne le nom du logo et interdit les .. et autres # tentatives d'acceder à d'autres fichiers !
# 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)) # log('field: %s' % (text))
return sco_pdf.makeParas(text, style, suppress_empty=suppress_empty_pars) 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" "document pdf et filename"
from app.scodoc import sco_bulletins from app.scodoc import sco_bulletins
@ -184,7 +190,6 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, REQUEST, version="selectedev
etudid, etudid,
format="pdfpart", format="pdfpart",
version=version, version=version,
REQUEST=REQUEST,
) )
fragments += frag fragments += frag
filigrannes[i] = filigranne filigrannes[i] = filigranne
@ -192,8 +197,8 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, REQUEST, version="selectedev
i = i + 1 i = i + 1
# #
infos = {"DeptName": sco_preferences.get_preference("DeptName", formsemestre_id)} infos = {"DeptName": sco_preferences.get_preference("DeptName", formsemestre_id)}
if REQUEST: if request:
server_name = REQUEST.BASE0 server_name = request.url_root
else: else:
server_name = "" server_name = ""
try: try:
@ -220,7 +225,7 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, REQUEST, version="selectedev
return pdfdoc, filename 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" "Bulletins pdf de tous les semestres de l'étudiant, et filename"
from app.scodoc import sco_bulletins from app.scodoc import sco_bulletins
@ -235,15 +240,14 @@ def get_etud_bulletins_pdf(etudid, REQUEST, version="selectedevals"):
etudid, etudid,
format="pdfpart", format="pdfpart",
version=version, version=version,
REQUEST=REQUEST,
) )
fragments += frag fragments += frag
filigrannes[i] = filigranne filigrannes[i] = filigranne
bookmarks[i] = sem["session_id"] # eg RT-DUT-FI-S1-2015 bookmarks[i] = sem["session_id"] # eg RT-DUT-FI-S1-2015
i = i + 1 i = i + 1
infos = {"DeptName": sco_preferences.get_preference("DeptName")} infos = {"DeptName": sco_preferences.get_preference("DeptName")}
if REQUEST: if request:
server_name = REQUEST.BASE0 server_name = request.url_root
else: else:
server_name = "" server_name = ""
try: try:

View File

@ -56,7 +56,7 @@ et sur page "réglages bulletin" (avec formsemestre_id)
# import os # 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""" # """Change pdf signature"""
# filename = _get_sig_existing_filename( # filename = _get_sig_existing_filename(
# side, formsemestre_id=formsemestre_id # 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") # raise ValueError("invalid value for 'side' parameter")
# signatureloc = get_bul_sig_img() # signatureloc = get_bul_sig_img()
# H = [ # 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> # """<h2>Changement de la signature bulletin de %(sidetxt)s</h2>
# """ # """
# % (sidetxt,), # % (sidetxt,),

View File

@ -69,16 +69,13 @@ def make_xml_formsemestre_bulletinetud(
doc=None, # XML document doc=None, # XML document
force_publishing=False, force_publishing=False,
xml_nodate=False, xml_nodate=False,
REQUEST=None,
xml_with_decisions=False, # inclue les decisions même si non publiées xml_with_decisions=False, # inclue les decisions même si non publiées
version="long", version="long",
): ) -> str:
"bulletin au format XML" "bulletin au format XML"
from app.scodoc import sco_bulletins from app.scodoc import sco_bulletins
log("xml_bulletin( formsemestre_id=%s, etudid=%s )" % (formsemestre_id, etudid)) 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) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if (not sem["bul_hide_xml"]) or force_publishing: if (not sem["bul_hide_xml"]) or force_publishing:
@ -388,7 +385,7 @@ def make_xml_formsemestre_bulletinetud(
"decisions_ue" "decisions_ue"
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee) ]: # 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(): 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( doc.append(
Element( Element(
"decision_ue", "decision_ue",

View File

@ -46,9 +46,9 @@
# sco_cache.NotesTableCache.delete_many(formsemestre_id_list) # sco_cache.NotesTableCache.delete_many(formsemestre_id_list)
# #
# Bulletins PDF: # Bulletins PDF:
# sco_cache.PDFBulCache.get(formsemestre_id, version) # sco_cache.SemBulletinsPDFCache.get(formsemestre_id, version)
# sco_cache.PDFBulCache.set(formsemestre_id, version, filename, pdfdoc) # sco_cache.SemBulletinsPDFCache.set(formsemestre_id, version, filename, pdfdoc)
# sco_cache.PDFBulCache.delete(formsemestre_id) suppr. toutes les versions # sco_cache.SemBulletinsPDFCache.delete(formsemestre_id) suppr. toutes les versions
# Evaluations: # Evaluations:
# sco_cache.EvaluationCache.get(evaluation_id), set(evaluation_id, value), delete(evaluation_id), # sco_cache.EvaluationCache.get(evaluation_id), set(evaluation_id, value), delete(evaluation_id),
@ -157,7 +157,7 @@ class EvaluationCache(ScoDocCache):
class AbsSemEtudCache(ScoDocCache): class AbsSemEtudCache(ScoDocCache):
"""Cache pour les comptes d'absences d'un étudiant dans un semestre. """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. change les dates début/fin d'un semestre.
C'est pourquoi il expire après timeout secondes. C'est pourquoi il expire après timeout secondes.
Le timeout evite aussi d'éliminer explicitement ces éléments cachés lors Le timeout evite aussi d'éliminer explicitement ces éléments cachés lors
@ -292,7 +292,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
class DefferedSemCacheManager: 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 même requete qui invalident le cache. Par exemple, quand on inscrit
des étudiants un par un à un semestre, chaque inscription va invalider 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. le cache, et la suivante va le reconstruire... pour l'invalider juste après.

View File

@ -28,7 +28,7 @@
"""Semestres: Codes gestion parcours (constantes) """Semestres: Codes gestion parcours (constantes)
""" """
import collections import collections
from six.moves import range from app import log
NOTES_TOLERANCE = 0.00499999999999 # si note >= (BARRE-TOLERANCE), considere ok 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) # (permet d'eviter d'afficher 10.00 sous barre alors que la moyenne vaut 9.999)
@ -44,6 +44,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_STAGE_10 = 3 # ue "stage" avec moyenne requise > 10
UE_ELECTIVE = 4 # UE "élective" dans certains parcours (UCAC?, ISCID) UE_ELECTIVE = 4 # UE "élective" dans certains parcours (UCAC?, ISCID)
UE_PROFESSIONNELLE = 5 # UE "professionnelle" (ISCID, ...) UE_PROFESSIONNELLE = 5 # UE "professionnelle" (ISCID, ...)
UE_OPTIONNELLE = 6 # UE non fondamentales (ILEPS, ...)
def UE_is_fondamentale(ue_type): def UE_is_fondamentale(ue_type):
@ -62,7 +63,8 @@ UE_TYPE_NAME = {
UE_STAGE_LP: "Projet tuteuré et stage (Lic. Pro.)", UE_STAGE_LP: "Projet tuteuré et stage (Lic. Pro.)",
UE_STAGE_10: "Stage (moyenne min. 10/20)", UE_STAGE_10: "Stage (moyenne min. 10/20)",
UE_ELECTIVE: "Elective (ISCID)", UE_ELECTIVE: "Elective (ISCID)",
UE_PROFESSIONNELLE: "Professionnelle (ISCID)" UE_PROFESSIONNELLE: "Professionnelle (ISCID)",
UE_OPTIONNELLE: "Optionnelle",
# UE_FONDAMENTALE : '"Fondamentale" (eg UCAC)', # UE_FONDAMENTALE : '"Fondamentale" (eg UCAC)',
# UE_OPTIONNELLE : '"Optionnelle" (UCAC)' # UE_OPTIONNELLE : '"Optionnelle" (UCAC)'
} }
@ -520,6 +522,34 @@ class ParcoursMasterISCID4(ParcoursISCID):
register_parcours(ParcoursMasterISCID4()) 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): class ParcoursUCAC(TypeParcours):
"""Règles de validation UCAC""" """Règles de validation UCAC"""
@ -673,4 +703,9 @@ FORMATION_PARCOURS_TYPES = [p[0] for p in _tp] # codes numeriques (TYPE_PARCOUR
def get_parcours_from_code(code_parcours): 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

@ -27,10 +27,10 @@
"""Calcul des moyennes de module """Calcul des moyennes de module
""" """
import traceback
import pprint import pprint
import traceback
from flask import url_for, g
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc.sco_utils import ( from app.scodoc.sco_utils import (
@ -40,7 +40,7 @@ from app.scodoc.sco_utils import (
EVALUATION_RATTRAPAGE, EVALUATION_RATTRAPAGE,
EVALUATION_SESSION2, EVALUATION_SESSION2,
) )
from app.scodoc.sco_exceptions import ScoException from app.scodoc.sco_exceptions import ScoValueError
from app import log from app import log
from app.scodoc import sco_abs from app.scodoc import sco_abs
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
@ -65,7 +65,8 @@ def moduleimpl_has_expression(mod):
def formsemestre_expressions_use_abscounts(formsemestre_id): def formsemestre_expressions_use_abscounts(formsemestre_id):
"""True si les notes de ce semestre dépendent des compteurs d'absences. """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 # check presence of 'nbabs' in expressions
ab = "nb_abs" # chaine recherchée ab = "nb_abs" # chaine recherchée
@ -79,7 +80,7 @@ def formsemestre_expressions_use_abscounts(formsemestre_id):
if expr and expr[0] != "#" and ab in expr: if expr and expr[0] != "#" and ab in expr:
return True return True
# 2- moyennes de modules # 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"]: if moduleimpl_has_expression(mod) and ab in mod["computation_expr"]:
return True return True
return False return False
@ -128,7 +129,7 @@ def compute_user_formula(
coefs, coefs,
coefs_mask, coefs_mask,
formula, 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, use_abs=True,
): ):
"""Calcul moyenne a partir des notes et coefs, en utilisant la formule utilisateur (une chaine). """Calcul moyenne a partir des notes et coefs, en utilisant la formule utilisateur (une chaine).
@ -159,14 +160,19 @@ def compute_user_formula(
# log('expression : %s\nvariables=%s\n' % (formula, variables)) # debug # log('expression : %s\nvariables=%s\n' % (formula, variables)) # debug
user_moy = sco_formulas.eval_user_expression(formula, variables) user_moy = sco_formulas.eval_user_expression(formula, variables)
# log('user_moy=%s' % user_moy) # log('user_moy=%s' % user_moy)
if user_moy != "NA0" and user_moy != "NA": if user_moy != "NA":
user_moy = float(user_moy) user_moy = float(user_moy)
if (user_moy > 20) or (user_moy < 0): if (user_moy > 20) or (user_moy < 0):
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
raise ScoException( raise ScoValueError(
"""valeur moyenne %s hors limite pour <a href="formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s">%s</a>""" f"""
% (user_moy, sem["formsemestre_id"], etudid, etud["nomprenom"]) 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: except:
log( log(
@ -183,7 +189,7 @@ def compute_user_formula(
return user_moy return user_moy
def do_moduleimpl_moyennes(nt, mod): def compute_moduleimpl_moyennes(nt, modimpl):
"""Retourne dict { etudid : note_moyenne } pour tous les etuds inscrits """Retourne dict { etudid : note_moyenne } pour tous les etuds inscrits
au moduleimpl mod, la liste des evaluations "valides" (toutes notes entrées 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). ou en attente), et att (vrai s'il y a des notes en attente dans ce module).
@ -193,13 +199,13 @@ def do_moduleimpl_moyennes(nt, mod):
S'il manque des notes et que le coef n'est pas nul, S'il manque des notes et que le coef n'est pas nul,
la moyenne n'est pas calculée: NA la moyenne n'est pas calculée: NA
Ne prend en compte que les evaluations toutes les notes sont entrées. 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 diag_info = {} # message d'erreur formule
moduleimpl_id = mod["moduleimpl_id"] moduleimpl_id = modimpl["moduleimpl_id"]
is_malus = mod["module"]["module_type"] == scu.MODULE_MALUS is_malus = modimpl["module"]["module_type"] == scu.MODULE_MALUS
sem = sco_formsemestre.get_formsemestre(mod["formsemestre_id"]) sem = sco_formsemestre.get_formsemestre(modimpl["formsemestre_id"])
etudids = sco_moduleimpl.do_moduleimpl_listeetuds( etudids = sco_moduleimpl.moduleimpl_listeetuds(
moduleimpl_id moduleimpl_id
) # tous, y compris demissions ) # tous, y compris demissions
# Inscrits au semestre (pour traiter les demissions): # Inscrits au semestre (pour traiter les demissions):
@ -207,7 +213,7 @@ def do_moduleimpl_moyennes(nt, mod):
[ [
x["etudid"] x["etudid"]
for x in sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( for x in sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits(
mod["formsemestre_id"] modimpl["formsemestre_id"]
) )
] ]
) )
@ -218,7 +224,7 @@ def do_moduleimpl_moyennes(nt, mod):
key=lambda x: (x["numero"], x["jour"], x["heure_debut"]) key=lambda x: (x["numero"], x["jour"], x["heure_debut"])
) # la plus ancienne en tête ) # la plus ancienne en tête
user_expr = moduleimpl_has_expression(mod) user_expr = moduleimpl_has_expression(modimpl)
attente = False attente = False
# recupere les notes de toutes les evaluations # recupere les notes de toutes les evaluations
eval_rattr = None eval_rattr = None
@ -268,7 +274,7 @@ def do_moduleimpl_moyennes(nt, mod):
] ]
# #
R = {} R = {}
formula = scu.unescape_html(mod["computation_expr"]) formula = scu.unescape_html(modimpl["computation_expr"])
formula_use_abs = "abs" in formula formula_use_abs = "abs" in formula
for etudid in insmod_set: # inscrits au semestre et au module for etudid in insmod_set: # inscrits au semestre et au module
@ -289,15 +295,17 @@ def do_moduleimpl_moyennes(nt, mod):
# il manque une note ! (si publish_incomplete, cela peut arriver, on ignore) # il manque une note ! (si publish_incomplete, cela peut arriver, on ignore)
if e["coefficient"] > 0 and not e["publish_incomplete"]: if e["coefficient"] > 0 and not e["publish_incomplete"]:
nb_missing += 1 nb_missing += 1
# ne devrait pas arriver ?
log("\nXXX SCM298\n")
if nb_missing == 0 and sum_coefs > 0: if nb_missing == 0 and sum_coefs > 0:
if sum_coefs > 0: if sum_coefs > 0:
R[etudid] = sum_notes / sum_coefs R[etudid] = sum_notes / sum_coefs
moy_valid = True moy_valid = True
else: else:
R[etudid] = "na" R[etudid] = "NA"
moy_valid = False moy_valid = False
else: else:
R[etudid] = "NA%d" % nb_missing R[etudid] = "NA"
moy_valid = False moy_valid = False
if user_expr: if user_expr:
@ -348,14 +356,14 @@ def do_moduleimpl_moyennes(nt, mod):
if etudid in eval_rattr["notes"]: if etudid in eval_rattr["notes"]:
note = eval_rattr["notes"][etudid]["value"] note = eval_rattr["notes"][etudid]["value"]
if note != None and note != NOTES_NEUTRALISE and note != NOTES_ATTENTE: 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 R[etudid] = note
else: else:
note_sur_20 = note * 20.0 / eval_rattr["note_max"] note_sur_20 = note * 20.0 / eval_rattr["note_max"]
if eval_rattr["evaluation_type"] == EVALUATION_RATTRAPAGE: if eval_rattr["evaluation_type"] == EVALUATION_RATTRAPAGE:
# rattrapage classique: prend la meilleure note entre moyenne # rattrapage classique: prend la meilleure note entre moyenne
# module et note eval rattrapage # 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) # log('note_sur_20=%s' % note_sur_20)
R[etudid] = note_sur_20 R[etudid] = note_sur_20
elif eval_rattr["evaluation_type"] == EVALUATION_SESSION2: elif eval_rattr["evaluation_type"] == EVALUATION_SESSION2:
@ -365,7 +373,7 @@ def do_moduleimpl_moyennes(nt, mod):
return R, valid_evals, attente, diag_info 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 } }, """retourne dict { moduleimpl_id : { etudid, note_moyenne_dans_ce_module } },
la liste des moduleimpls, la liste des evaluations valides, la liste des moduleimpls, la liste des evaluations valides,
liste des moduleimpls avec notes en attente. liste des moduleimpls avec notes en attente.
@ -375,7 +383,7 @@ def do_formsemestre_moyennes(nt, formsemestre_id):
# args={"formsemestre_id": formsemestre_id} # args={"formsemestre_id": formsemestre_id}
# ) # )
# etudids = [x["etudid"] for x in inscr] # 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 # recupere les moyennes des etudiants de tous les modules
D = {} D = {}
valid_evals = [] valid_evals = []
@ -383,15 +391,16 @@ def do_formsemestre_moyennes(nt, formsemestre_id):
mods_att = [] mods_att = []
expr_diags = [] expr_diags = []
for modimpl in modimpls: for modimpl in modimpls:
mod = sco_edit_module.do_module_list(args={"module_id": modimpl["module_id"]})[ mod = sco_edit_module.module_list(args={"module_id": modimpl["module_id"]})[0]
0
]
modimpl["module"] = mod # add module dict to moduleimpl (used by nt) modimpl["module"] = mod # add module dict to moduleimpl (used by nt)
moduleimpl_id = modimpl["moduleimpl_id"] moduleimpl_id = modimpl["moduleimpl_id"]
assert moduleimpl_id not in D 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_per_mod[moduleimpl_id] = valid_evals_mod
valid_evals += valid_evals_mod valid_evals += valid_evals_mod
if attente: if attente:

View File

@ -0,0 +1,181 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# ScoDoc
#
# Copyright (c) 1999 - 2021 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
#
##############################################################################
"""
Module main: page d'accueil, avec liste des départements
Emmanuel Viennet, 2021
"""
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']}.",
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']}).",
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

@ -0,0 +1,402 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# ScoDoc
#
# Copyright (c) 1999 - 2021 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
#
##############################################################################
"""
Module main: page d'accueil, avec liste des départements
Emmanuel Viennet, 2021
"""
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 BooleanField, 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.required("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.required("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 assute 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

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

View File

@ -29,7 +29,7 @@
Rapport (table) avec dernier semestre fréquenté et débouché de chaque étudiant Rapport (table) avec dernier semestre fréquenté et débouché de chaque étudiant
""" """
import http 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.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -47,10 +47,20 @@ from app.scodoc import sco_etud
import sco_version import sco_version
def report_debouche_date(start_year=None, format="html", REQUEST=None): 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.""" """Rapport (table) pour les débouchés des étudiants sortis
à partir de l'année indiquée.
"""
if not start_year: 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": if format == "xls":
keep_numeric = True # pas de conversion des notes en strings keep_numeric = True # pas de conversion des notes en strings
else: 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() + "" "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.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( return tab.make_page(
title="""<h2 class="formsemestre">Débouchés étudiants </h2>""", title="""<h2 class="formsemestre">Débouchés étudiants </h2>""",
init_qtip=True, init_qtip=True,
javascripts=["js/etud_info.js"], javascripts=["js/etud_info.js"],
format=format, format=format,
REQUEST=REQUEST,
with_html_headers=True, 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 FROM notes_formsemestre_inscription i, notes_formsemestre s, itemsuivi it
WHERE i.etudid = it.etudid WHERE i.etudid = it.etudid
AND i.formsemestre_id = s.id AND s.date_fin >= %(start_date)s 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] return [x["etudid"] for x in r]
@ -194,15 +204,16 @@ def table_debouche_etudids(etudids, keep_numeric=True):
return tab return tab
def report_debouche_ask_date(REQUEST=None): def report_debouche_ask_date(msg: str) -> str:
"""Formulaire demande date départ""" """Formulaire demande date départ"""
return ( return f"""{html_sco_header.sco_header()}
html_sco_header.sco_header() <h2>Table des débouchés des étudiants</h2>
+ """<form method="GET"> <form method="GET">
Date de départ de la recherche: <input type="text" name="start_year" value="" size=10/> {msg}
</form>""" <input type="text" name="start_year" value="" size=10/>
+ html_sco_header.sco_footer() </form>
) {html_sco_header.sco_footer()}
"""
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
@ -249,7 +260,7 @@ def itemsuivi_get(cnx, itemsuivi_id, ignore_errors=False):
return None return None
def itemsuivi_suppress(itemsuivi_id, REQUEST=None): def itemsuivi_suppress(itemsuivi_id):
"""Suppression d'un item""" """Suppression d'un item"""
if not sco_permissions_check.can_edit_suivi(): if not sco_permissions_check.can_edit_suivi():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") 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) _itemsuivi_delete(cnx, itemsuivi_id)
logdb(cnx, method="itemsuivi_suppress", etudid=item["etudid"]) logdb(cnx, method="itemsuivi_suppress", etudid=item["etudid"])
log("suppressed itemsuivi %s" % (itemsuivi_id,)) 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""" """Creation d'un item"""
if not sco_permissions_check.can_edit_suivi(): if not sco_permissions_check.can_edit_suivi():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") 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)) log("created itemsuivi %s for %s" % (itemsuivi_id, etudid))
item = itemsuivi_get(cnx, itemsuivi_id) item = itemsuivi_get(cnx, itemsuivi_id)
if format == "json": if format == "json":
return scu.sendJSON(REQUEST, item) return scu.sendJSON(item)
return item return item
def itemsuivi_set_date(itemsuivi_id, item_date, REQUEST=None): def itemsuivi_set_date(itemsuivi_id, item_date):
"""set item date """set item date
item_date is a string dd/mm/yyyy 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 = itemsuivi_get(cnx, itemsuivi_id)
item["item_date"] = item_date item["item_date"] = item_date
_itemsuivi_edit(cnx, item) _itemsuivi_edit(cnx, item)
return ("", 204)
def itemsuivi_set_situation(object, value, REQUEST=None): def itemsuivi_set_situation(object, value):
"""set situation""" """set situation"""
if not sco_permissions_check.can_edit_suivi(): if not sco_permissions_check.can_edit_suivi():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") 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 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""" """Liste des items pour cet étudiant, avec tags"""
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
items = _itemsuivi_list(cnx, {"etudid": etudid}) items = _itemsuivi_list(cnx, {"etudid": etudid})
for it in items: for it in items:
it["tags"] = ", ".join(itemsuivi_tag_list(it["itemsuivi_id"])) it["tags"] = ", ".join(itemsuivi_tag_list(it["itemsuivi_id"]))
if format == "json": if format == "json":
return scu.sendJSON(REQUEST, items) return scu.sendJSON(items)
return items return items
@ -328,7 +341,7 @@ def itemsuivi_tag_list(itemsuivi_id):
return [x["title"] for x in r] 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)""" """List all used tag names (for auto-completion)"""
# restrict charset to avoid injections # restrict charset to avoid injections
if not scu.ALPHANUM_EXP.match(term): 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] 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: """taglist may either be:
a string with tag names separated by commas ("un;deux") a string with tag names separated by commas ("un;deux")
or a list of strings (["un", "deux"]) or a list of strings (["un", "deux"])

View File

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

View File

@ -51,6 +51,7 @@ import fcntl
import subprocess import subprocess
import requests import requests
from flask_login import current_user
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -64,7 +65,7 @@ from app.scodoc.sco_exceptions import ScoValueError
SCO_DUMP_LOCK = "/tmp/scodump.lock" 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""" """Dump base de données et l'envoie anonymisée pour debug"""
H = [html_sco_header.sco_header(page_title="Assistance technique")] H = [html_sco_header.sco_header(page_title="Assistance technique")]
# get currect (dept) DB name: # get currect (dept) DB name:
@ -93,7 +94,7 @@ def sco_dump_and_send_db(REQUEST=None):
_anonymize_db(ano_db_name) _anonymize_db(ano_db_name)
# Send # Send
r = _send_db(REQUEST, ano_db_name) r = _send_db(ano_db_name)
if ( if (
r.status_code r.status_code
== requests.codes.INSUFFICIENT_STORAGE # pylint: disable=no-member == requests.codes.INSUFFICIENT_STORAGE # pylint: disable=no-member
@ -166,34 +167,33 @@ def _anonymize_db(ano_db_name):
def _get_scodoc_serial(): def _get_scodoc_serial():
try: 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: except:
return 0 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""" """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: try:
data = subprocess.check_output("pg_dump {} | gzip".format(ano_db_name), shell=1) dump = subprocess.check_output(
except subprocess.CalledProcessError as e: f"pg_dump --format=custom {ano_db_name}", shell=1
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)
) )
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...") log("uploading anonymized dump...")
files = {"file": (ano_db_name + ".gz", data)} files = {"file": (ano_db_name + ".dump", dump)}
r = requests.post( r = requests.post(
scu.SCO_DUMP_UP_URL, scu.SCO_DUMP_UP_URL,
files=files, files=files,
data={ data={
"dept_name": sco_preferences.get_preference("DeptName"), "dept_name": sco_preferences.get_preference("DeptName"),
"serial": _get_scodoc_serial(), "serial": _get_scodoc_serial(),
"sco_user": str(REQUEST.AUTHENTICATED_USER), "sco_user": str(current_user),
"sent_by": sco_users.user_info(str(REQUEST.AUTHENTICATED_USER))[ "sent_by": sco_users.user_info(str(current_user))["nomcomplet"],
"nomcomplet"
],
"sco_version": sco_version.SCOVERSION, "sco_version": sco_version.SCOVERSION,
"sco_fullversion": scu.get_scodoc_version(), "sco_fullversion": scu.get_scodoc_version(),
}, },

View File

@ -29,7 +29,7 @@
(portage from DTML) (portage from DTML)
""" """
import flask import flask
from flask import g, url_for from flask import g, url_for, request
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -47,7 +47,7 @@ from app.scodoc import sco_formsemestre
from app.scodoc import sco_news 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""" """Delete a formation"""
F = sco_formations.formation_list(args={"formation_id": formation_id}) F = sco_formations.formation_list(args={"formation_id": formation_id})
if not F: if not F:
@ -104,7 +104,7 @@ def do_formation_delete(oid):
raise ScoLockedFormError() raise ScoLockedFormError()
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
# delete all UE in this formation # 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: for ue in ues:
sco_edit_ue.do_ue_delete(ue["ue_id"], force=True) sco_edit_ue.do_ue_delete(ue["ue_id"], force=True)
@ -119,12 +119,12 @@ def do_formation_delete(oid):
) )
def formation_create(REQUEST=None): def formation_create():
"""Creation d'une formation""" """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""" """Edit or create a formation"""
if create: if create:
H = [ H = [
@ -159,8 +159,8 @@ def formation_edit(formation_id=None, create=False, REQUEST=None):
) )
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
( (
("formation_id", {"default": formation_id, "input_type": "hidden"}), ("formation_id", {"default": formation_id, "input_type": "hidden"}),
( (
@ -252,7 +252,7 @@ def formation_edit(formation_id=None, create=False, REQUEST=None):
do_formation_edit(tf[2]) do_formation_edit(tf[2])
return flask.redirect( return flask.redirect(
url_for( 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
) )
) )
@ -311,15 +311,15 @@ def invalidate_sems_in_formation(formation_id):
) # > formation modif. ) # > formation modif.
def module_move(module_id, after=0, REQUEST=None, redirect=1): def module_move(module_id, after=0, redirect=1):
"""Move before/after previous one (decrement/increment numero)""" """Move before/after previous one (decrement/increment numero)"""
module = sco_edit_module.do_module_list({"module_id": module_id})[0] module = sco_edit_module.module_list({"module_id": module_id})[0]
redirect = int(redirect) redirect = int(redirect)
after = int(after) # 0: deplace avant, 1 deplace apres after = int(after) # 0: deplace avant, 1 deplace apres
if after not in (0, 1): if after not in (0, 1):
raise ValueError('invalid value for "after"') raise ValueError('invalid value for "after"')
formation_id = module["formation_id"] formation_id = module["formation_id"]
others = sco_edit_module.do_module_list({"matiere_id": module["matiere_id"]}) others = sco_edit_module.module_list({"matiere_id": module["matiere_id"]})
# log('others=%s' % others) # log('others=%s' % others)
if len(others) > 1: if len(others) > 1:
idx = [p["module_id"] for p in others].index(module_id) idx = [p["module_id"] for p in others].index(module_id)
@ -343,21 +343,21 @@ def module_move(module_id, after=0, REQUEST=None, redirect=1):
if redirect: if redirect:
return flask.redirect( return flask.redirect(
url_for( 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
) )
) )
def ue_move(ue_id, after=0, redirect=1): def ue_move(ue_id, after=0, redirect=1):
"""Move UE before/after previous one (decrement/increment numero)""" """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)) # log('ue_move %s (#%s) after=%s' % (ue_id, o['numero'], after))
redirect = int(redirect) redirect = int(redirect)
after = int(after) # 0: deplace avant, 1 deplace apres after = int(after) # 0: deplace avant, 1 deplace apres
if after not in (0, 1): if after not in (0, 1):
raise ValueError('invalid value for "after"') raise ValueError('invalid value for "after"')
formation_id = o["formation_id"] 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: if len(others) > 1:
idx = [p["ue_id"] for p in others].index(ue_id) idx = [p["ue_id"] for p in others].index(ue_id)
neigh = None # object to swap with neigh = None # object to swap with
@ -378,7 +378,7 @@ def ue_move(ue_id, after=0, redirect=1):
if redirect: if redirect:
return flask.redirect( return flask.redirect(
url_for( url_for(
"notes.ue_list", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=o["formation_id"], formation_id=o["formation_id"],
) )

View File

@ -29,7 +29,7 @@
(portage from DTML) (portage from DTML)
""" """
import flask import flask
from flask import g, url_for from flask import g, url_for, request
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -47,7 +47,7 @@ _matiereEditor = ndb.EditableTable(
) )
def do_matiere_list(*args, **kw): def matiere_list(*args, **kw):
"list matieres" "list matieres"
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
return _matiereEditor.list(cnx, *args, **kw) return _matiereEditor.list(cnx, *args, **kw)
@ -60,12 +60,12 @@ def do_matiere_edit(*args, **kw):
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
# check # 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"]): if matiere_is_locked(mat["matiere_id"]):
raise ScoLockedFormError() raise ScoLockedFormError()
# edit # edit
_matiereEditor.edit(cnx, *args, **kw) _matiereEditor.edit(cnx, *args, **kw)
formation_id = sco_edit_ue.do_ue_list({"ue_id": mat["ue_id"]})[0]["formation_id"] formation_id = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]["formation_id"]
sco_edit_formation.invalidate_sems_in_formation(formation_id) sco_edit_formation.invalidate_sems_in_formation(formation_id)
@ -77,7 +77,7 @@ def do_matiere_create(args):
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
# check # 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 # create matiere
r = _matiereEditor.create(cnx, args) r = _matiereEditor.create(cnx, args)
@ -92,11 +92,11 @@ def do_matiere_create(args):
return r return r
def matiere_create(ue_id=None, REQUEST=None): def matiere_create(ue_id=None):
"""Creation d'une matiere""" """Creation d'une matiere"""
from app.scodoc import sco_edit_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]
H = [ H = [
html_sco_header.sco_header(page_title="Création d'une matière"), 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, """<h2>Création d'une matière dans l'UE %(titre)s (%(acronyme)s)</h2>""" % UE,
@ -116,8 +116,8 @@ associé.
</p>""", </p>""",
] ]
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
( (
("ue_id", {"input_type": "hidden", "default": ue_id}), ("ue_id", {"input_type": "hidden", "default": ue_id}),
("titre", {"size": 30, "explanation": "nom de la matière."}), ("titre", {"size": 30, "explanation": "nom de la matière."}),
@ -134,7 +134,7 @@ associé.
) )
dest_url = url_for( 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: if tf[0] == 0:
@ -143,7 +143,7 @@ associé.
return flask.redirect(dest_url) return flask.redirect(dest_url)
else: else:
# check unicity # 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: if mats:
return ( return (
"\n".join(H) "\n".join(H)
@ -164,8 +164,8 @@ def do_matiere_delete(oid):
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
# check # check
mat = do_matiere_list({"matiere_id": oid})[0] mat = matiere_list({"matiere_id": oid})[0]
ue = sco_edit_ue.do_ue_list({"ue_id": mat["ue_id"]})[0] ue = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]
locked = matiere_is_locked(mat["matiere_id"]) locked = matiere_is_locked(mat["matiere_id"])
if locked: if locked:
log("do_matiere_delete: mat=%s" % mat) log("do_matiere_delete: mat=%s" % mat)
@ -174,7 +174,7 @@ def do_matiere_delete(oid):
raise ScoLockedFormError() raise ScoLockedFormError()
log("do_matiere_delete: matiere_id=%s" % oid) log("do_matiere_delete: matiere_id=%s" % oid)
# delete all modules in this matiere # delete all modules in this matiere
mods = sco_edit_module.do_module_list({"matiere_id": oid}) mods = sco_edit_module.module_list({"matiere_id": oid})
for mod in mods: for mod in mods:
sco_edit_module.do_module_delete(mod["module_id"]) sco_edit_module.do_module_delete(mod["module_id"])
_matiereEditor.delete(cnx, oid) _matiereEditor.delete(cnx, oid)
@ -189,21 +189,25 @@ def do_matiere_delete(oid):
) )
def matiere_delete(matiere_id=None, REQUEST=None): def matiere_delete(matiere_id=None):
"""Delete an UE""" """Delete matière"""
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
M = do_matiere_list(args={"matiere_id": matiere_id})[0] M = matiere_list(args={"matiere_id": matiere_id})[0]
UE = sco_edit_ue.do_ue_list(args={"ue_id": M["ue_id"]})[0] UE = sco_edit_ue.ue_list(args={"ue_id": M["ue_id"]})[0]
H = [ H = [
html_sco_header.sco_header(page_title="Suppression d'une matière"), 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" % M,
" dans l'UE (%(acronyme)s))</h2>" % UE, " 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( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
(("matiere_id", {"input_type": "hidden"}),), (("matiere_id", {"input_type": "hidden"}),),
initvalues=M, initvalues=M,
submitlabel="Confirmer la suppression", submitlabel="Confirmer la suppression",
@ -218,22 +222,22 @@ def matiere_delete(matiere_id=None, REQUEST=None):
return flask.redirect(dest_url) return flask.redirect(dest_url)
def matiere_edit(matiere_id=None, REQUEST=None): def matiere_edit(matiere_id=None):
"""Edit matiere""" """Edit matiere"""
from app.scodoc import sco_formations from app.scodoc import sco_formations
from app.scodoc import sco_edit_ue 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: if not F:
raise ScoValueError("Matière inexistante !") raise ScoValueError("Matière inexistante !")
F = F[0] F = F[0]
U = sco_edit_ue.do_ue_list(args={"ue_id": F["ue_id"]}) ues = sco_edit_ue.ue_list(args={"ue_id": F["ue_id"]})
if not F: if not ues:
raise ScoValueError("UE inexistante !") raise ScoValueError("UE inexistante !")
U = U[0] ue = ues[0]
Fo = sco_formations.formation_list(args={"formation_id": U["formation_id"]})[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_names = ["%(acronyme)s (%(titre)s)" % u for u in ues]
ue_ids = [u["ue_id"] for u in ues] ue_ids = [u["ue_id"] for u in ues]
H = [ H = [
@ -256,8 +260,8 @@ des notes.</em>
associé. associé.
</p>""" </p>"""
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
( (
("matiere_id", {"input_type": "hidden"}), ("matiere_id", {"input_type": "hidden"}),
( (
@ -278,15 +282,18 @@ associé.
submitlabel="Modifier les valeurs", 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: if tf[0] == 0:
return "\n".join(H) + tf[1] + help + html_sco_header.sco_footer() return "\n".join(H) + tf[1] + help + html_sco_header.sco_footer()
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(dest_url) return flask.redirect(dest_url)
else: else:
# check unicity # 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): if len(mats) > 1 or (len(mats) == 1 and mats[0]["matiere_id"] != matiere_id):
return ( return (
"\n".join(H) "\n".join(H)
@ -323,4 +330,4 @@ def matiere_is_locked(matiere_id):
""", """,
{"matiere_id": matiere_id}, {"matiere_id": matiere_id},
) )
return len(r) > 0 return len(r) > 0

View File

@ -29,7 +29,8 @@
(portage from DTML) (portage from DTML)
""" """
import flask import flask
from flask import url_for, g from flask import url_for, g, request
from flask_login import current_user
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -93,7 +94,7 @@ _moduleEditor = ndb.EditableTable(
) )
def do_module_list(*args, **kw): def module_list(*args, **kw):
"list modules" "list modules"
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
return _moduleEditor.list(cnx, *args, **kw) return _moduleEditor.list(cnx, *args, **kw)
@ -118,15 +119,15 @@ def do_module_create(args) -> int:
return r return r
def module_create(matiere_id=None, REQUEST=None): def module_create(matiere_id=None):
"""Creation d'un module""" """Creation d'un module"""
from app.scodoc import sco_formations from app.scodoc import sco_formations
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
if matiere_id is None: if matiere_id is None:
raise ScoValueError("invalid matiere !") raise ScoValueError("invalid matiere !")
M = sco_edit_matiere.do_matiere_list(args={"matiere_id": matiere_id})[0] M = sco_edit_matiere.matiere_list(args={"matiere_id": matiere_id})[0]
UE = sco_edit_ue.do_ue_list(args={"ue_id": M["ue_id"]})[0] UE = sco_edit_ue.ue_list(args={"ue_id": M["ue_id"]})[0]
Fo = sco_formations.formation_list(args={"formation_id": UE["formation_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"]) parcours = sco_codes_parcours.get_parcours_from_code(Fo["type_parcours"])
semestres_indices = list(range(1, parcours.NB_SEM + 1)) semestres_indices = list(range(1, parcours.NB_SEM + 1))
@ -137,14 +138,14 @@ def module_create(matiere_id=None, REQUEST=None):
_MODULE_HELP, _MODULE_HELP,
] ]
# cherche le numero adequat (pour placer le module en fin de liste) # cherche le numero adequat (pour placer le module en fin de liste)
Mods = do_module_list(args={"matiere_id": matiere_id}) Mods = module_list(args={"matiere_id": matiere_id})
if Mods: if Mods:
default_num = max([m["numero"] for m in Mods]) + 10 default_num = max([m["numero"] for m in Mods]) + 10
else: else:
default_num = 10 default_num = 10
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
( (
( (
"code", "code",
@ -240,7 +241,7 @@ def module_create(matiere_id=None, REQUEST=None):
do_module_create(tf[2]) do_module_create(tf[2])
return flask.redirect( return flask.redirect(
url_for( url_for(
"notes.ue_list", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=UE["formation_id"], formation_id=UE["formation_id"],
) )
@ -251,19 +252,20 @@ def do_module_delete(oid):
"delete module" "delete module"
from app.scodoc import sco_formations from app.scodoc import sco_formations
mod = do_module_list({"module_id": oid})[0] mod = module_list({"module_id": oid})[0]
if module_is_locked(mod["module_id"]): if module_is_locked(mod["module_id"]):
raise ScoLockedFormError() raise ScoLockedFormError()
# S'il y a des moduleimpls, on ne peut pas detruire le module ! # 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: if mods:
err_page = scu.confirm_dialog( err_page = f"""<h3>Destruction du module impossible car il est utilisé dans des semestres existants !</h3>
message="""<h3>Destruction du module impossible car il est utilisé dans des semestres existants !</h3>""", <p class="help">Il faut d'abord supprimer le semestre. Mais il est peut être préférable de
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.""", laisser ce programme intact et d'en créer une nouvelle version pour la modifier.
dest_url="ue_list", </p>
parameters={"formation_id": mod["formation_id"]}, <a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
) formation_id=mod["formation_id"])}">reprendre</a>
"""
raise ScoGenError(err_page) raise ScoGenError(err_page)
# delete # delete
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
@ -279,25 +281,29 @@ def do_module_delete(oid):
) )
def module_delete(module_id=None, REQUEST=None): def module_delete(module_id=None):
"""Delete a module""" """Delete a module"""
if not module_id: if not module_id:
raise ScoValueError("invalid module !") raise ScoValueError("invalid module !")
Mods = do_module_list(args={"module_id": module_id}) modules = module_list(args={"module_id": module_id})
if not Mods: if not modules:
raise ScoValueError("Module inexistant !") raise ScoValueError("Module inexistant !")
Mod = Mods[0] mod = modules[0]
H = [ H = [
html_sco_header.sco_header(page_title="Suppression d'un module"), 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( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
(("module_id", {"input_type": "hidden"}),), (("module_id", {"input_type": "hidden"}),),
initvalues=Mod, initvalues=mod,
submitlabel="Confirmer la suppression", submitlabel="Confirmer la suppression",
cancelbutton="Annuler", cancelbutton="Annuler",
) )
@ -315,7 +321,7 @@ def do_module_edit(val):
from app.scodoc import sco_edit_formation from app.scodoc import sco_edit_formation
# check # check
mod = do_module_list({"module_id": val["module_id"]})[0] mod = module_list({"module_id": val["module_id"]})[0]
if module_is_locked(mod["module_id"]): if module_is_locked(mod["module_id"]):
# formation verrouillée: empeche de modifier certains champs: # formation verrouillée: empeche de modifier certains champs:
protected_fields = ("coefficient", "ue_id", "matiere_id", "semestre_id") protected_fields = ("coefficient", "ue_id", "matiere_id", "semestre_id")
@ -330,21 +336,21 @@ def do_module_edit(val):
def check_module_code_unicity(code, field, formation_id, module_id=None): def check_module_code_unicity(code, field, formation_id, module_id=None):
"true si code module unique dans la formation" "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 if module_id: # edition: supprime le module en cours
Mods = [m for m in Mods if m["module_id"] != module_id] Mods = [m for m in Mods if m["module_id"] != module_id]
return len(Mods) == 0 return len(Mods) == 0
def module_edit(module_id=None, REQUEST=None): def module_edit(module_id=None):
"""Edit a module""" """Edit a module"""
from app.scodoc import sco_formations from app.scodoc import sco_formations
from app.scodoc import sco_tag_module from app.scodoc import sco_tag_module
if not module_id: if not module_id:
raise ScoValueError("invalid module !") raise ScoValueError("invalid module !")
Mod = do_module_list(args={"module_id": module_id}) Mod = module_list(args={"module_id": module_id})
if not Mod: if not Mod:
raise ScoValueError("invalid module !") raise ScoValueError("invalid module !")
Mod = Mod[0] Mod = Mod[0]
@ -365,9 +371,11 @@ def module_edit(module_id=None, REQUEST=None):
Mod["ue_matiere_id"] = "%s!%s" % (Mod["ue_id"], Mod["matiere_id"]) Mod["ue_matiere_id"] = "%s!%s" % (Mod["ue_id"], Mod["matiere_id"])
semestres_indices = list(range(1, parcours.NB_SEM + 1)) semestres_indices = list(range(1, parcours.NB_SEM + 1))
dest_url = url_for(
dest_url = scu.NotesURL() + "/ue_list?formation_id=" + str(Mod["formation_id"]) "notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(Mod["formation_id"]),
)
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title="Modification du module %(titre)s" % Mod, page_title="Modification du module %(titre)s" % Mod,
@ -388,8 +396,8 @@ def module_edit(module_id=None, REQUEST=None):
) )
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
( (
( (
"code", "code",
@ -513,13 +521,13 @@ def module_edit(module_id=None, REQUEST=None):
# Edition en ligne du code Apogee # 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" "Set UE code apogee"
module_id = id module_id = id
value = value.strip("-_ \t") value = value.strip("-_ \t")
log("edit_module_set_code_apogee: module_id=%s code_apogee=%s" % (module_id, value)) 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: if not modules:
return "module invalide" # should not occur return "module invalide" # should not occur
@ -529,7 +537,7 @@ def edit_module_set_code_apogee(id=None, value=None, REQUEST=None):
return value return value
def module_list(formation_id, REQUEST=None): def module_table(formation_id):
"""Liste des modules de la formation """Liste des modules de la formation
(XXX inutile ou a revoir) (XXX inutile ou a revoir)
""" """
@ -544,9 +552,9 @@ def module_list(formation_id, REQUEST=None):
% F, % F,
'<ul class="notes_module_list">', '<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) H.append('<li class="notes_module_list">%s' % Mod)
if editable: if editable:
H.append('<a href="module_edit?module_id=%(module_id)s">modifier</a>' % Mod) H.append('<a href="module_edit?module_id=%(module_id)s">modifier</a>' % Mod)
@ -578,37 +586,41 @@ def module_is_locked(module_id):
def module_count_moduleimpls(module_id): def module_count_moduleimpls(module_id):
"Number of moduleimpls using this module" "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) 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""" """Création d'un module de "malus" dans chaque UE d'une formation"""
from app.scodoc import sco_edit_ue 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: # Un seul module de malus par UE:
nb_mod_malus = len( nb_mod_malus = len(
[ [
mod mod
for mod in do_module_list(args={"ue_id": ue["ue_id"]}) for mod in module_list(args={"ue_id": ue["ue_id"]})
if mod["module_type"] == scu.MODULE_MALUS if mod["module_type"] == scu.MODULE_MALUS
] ]
) )
if nb_mod_malus == 0: 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: if redirect:
return flask.redirect("ue_list?formation_id=" + str(formation_id)) 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""" """Add a malus module in this ue"""
from app.scodoc import sco_edit_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: if titre is None:
titre = "" titre = ""
@ -627,7 +639,7 @@ def ue_add_malus_module(ue_id, titre=None, code=None, REQUEST=None):
) )
# Matiere pour placer le module malus # 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 numero = max([mat["numero"] for mat in Matlist]) + 10
matiere_id = sco_edit_matiere.do_matiere_create( matiere_id = sco_edit_matiere.do_matiere_create(
{"ue_id": ue_id, "titre": "Malus", "numero": numero} {"ue_id": ue_id, "titre": "Malus", "numero": numero}

View File

@ -29,9 +29,10 @@
""" """
import flask import flask
from flask import g, url_for from flask import g, url_for, request
from flask_login import current_user from flask_login import current_user
from app.models.formations import NotesUE
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import log
@ -74,7 +75,7 @@ _ueEditor = ndb.EditableTable(
sortkey="numero", sortkey="numero",
input_formators={ input_formators={
"type": ndb.int_null_is_zero, "type": ndb.int_null_is_zero,
"is_external": bool, "is_external": ndb.bool_or_str,
}, },
output_formators={ output_formators={
"numero": ndb.int_null_is_zero, "numero": ndb.int_null_is_zero,
@ -84,7 +85,7 @@ _ueEditor = ndb.EditableTable(
) )
def do_ue_list(*args, **kw): def ue_list(*args, **kw):
"list UEs" "list UEs"
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
return _ueEditor.list(cnx, *args, **kw) return _ueEditor.list(cnx, *args, **kw)
@ -96,9 +97,7 @@ def do_ue_create(args):
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
# check duplicates # check duplicates
ues = do_ue_list( ues = ue_list({"formation_id": args["formation_id"], "acronyme": args["acronyme"]})
{"formation_id": args["formation_id"], "acronyme": args["acronyme"]}
)
if ues: if ues:
raise ScoValueError('Acronyme d\'UE "%s" déjà utilisé !' % args["acronyme"]) raise ScoValueError('Acronyme d\'UE "%s" déjà utilisé !' % args["acronyme"])
# create # create
@ -123,7 +122,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
log("do_ue_delete: ue_id=%s, delete_validations=%s" % (ue_id, delete_validations)) log("do_ue_delete: ue_id=%s, delete_validations=%s" % (ue_id, delete_validations))
# check # check
ue = do_ue_list({"ue_id": ue_id}) ue = ue_list({"ue_id": ue_id})
if not ue: if not ue:
raise ScoValueError("UE inexistante !") raise ScoValueError("UE inexistante !")
ue = ue[0] ue = ue[0]
@ -140,7 +139,11 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
% (len(validations), ue["acronyme"], ue["titre"]), % (len(validations), ue["acronyme"], ue["titre"]),
dest_url="", dest_url="",
target_variable="delete_validations", target_variable="delete_validations",
cancel_url="ue_list?formation_id=%s" % ue["formation_id"], cancel_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(ue["formation_id"]),
),
parameters={"ue_id": ue_id, "dialog_confirmed": 1}, parameters={"ue_id": ue_id, "dialog_confirmed": 1},
) )
if delete_validations: if delete_validations:
@ -151,7 +154,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
) )
# delete all matiere in this UE # delete all matiere in this UE
mats = sco_edit_matiere.do_matiere_list({"ue_id": ue_id}) mats = sco_edit_matiere.matiere_list({"ue_id": ue_id})
for mat in mats: for mat in mats:
sco_edit_matiere.do_matiere_delete(mat["matiere_id"]) sco_edit_matiere.do_matiere_delete(mat["matiere_id"])
# delete uecoef and events # delete uecoef and events
@ -176,7 +179,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
if not force: if not force:
return flask.redirect( return flask.redirect(
url_for( url_for(
"notes.ue_list", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=ue["formation_id"], formation_id=ue["formation_id"],
) )
@ -185,18 +188,18 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
return None return None
def ue_create(formation_id=None, REQUEST=None): def ue_create(formation_id=None):
"""Creation d'une UE""" """Creation d'une UE"""
return ue_edit(create=True, formation_id=formation_id, REQUEST=REQUEST) return ue_edit(create=True, formation_id=formation_id)
def ue_edit(ue_id=None, create=False, formation_id=None, REQUEST=None): def ue_edit(ue_id=None, create=False, formation_id=None):
"""Modification ou creation d'une UE""" """Modification ou creation d'une UE"""
from app.scodoc import sco_formations from app.scodoc import sco_formations
create = int(create) create = int(create)
if not create: if not create:
U = do_ue_list(args={"ue_id": ue_id}) U = ue_list(args={"ue_id": ue_id})
if not U: if not U:
raise ScoValueError("UE inexistante !") raise ScoValueError("UE inexistante !")
U = U[0] U = U[0]
@ -295,6 +298,14 @@ def ue_edit(ue_id=None, create=False, formation_id=None, REQUEST=None):
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules", "explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
}, },
), ),
(
"is_external",
{
"input_type": "boolcheckbox",
"title": "UE externe",
"explanation": "réservé pour les capitalisations d'UE effectuées à l'extérieur de l'établissement",
},
),
] ]
if parcours.UE_IS_MODULE: if parcours.UE_IS_MODULE:
# demande le semestre pour creer le module immediatement: # demande le semestre pour creer le module immediatement:
@ -326,7 +337,11 @@ def ue_edit(ue_id=None, create=False, formation_id=None, REQUEST=None):
) )
) )
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, REQUEST.form, fw, initvalues=initvalues, submitlabel=submitlabel request.base_url,
scu.get_request_args(),
fw,
initvalues=initvalues,
submitlabel=submitlabel,
) )
if tf[0] == 0: if tf[0] == 0:
X = """<div id="ue_list_code"></div> X = """<div id="ue_list_code"></div>
@ -366,18 +381,18 @@ def ue_edit(ue_id=None, create=False, formation_id=None, REQUEST=None):
do_ue_edit(tf[2]) do_ue_edit(tf[2])
return flask.redirect( return flask.redirect(
url_for( 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
) )
) )
def _add_ue_semestre_id(ue_list): def _add_ue_semestre_id(ues):
"""ajoute semestre_id dans les ue, en regardant le premier module de chacune. """ajoute semestre_id dans les ue, en regardant le premier module de chacune.
Les UE sans modules se voient attribuer le numero UE_SEM_DEFAULT (1000000), Les UE sans modules se voient attribuer le numero UE_SEM_DEFAULT (1000000),
qui les place à la fin de la liste. qui les place à la fin de la liste.
""" """
for ue in ue_list: for ue in ues:
Modlist = sco_edit_module.do_module_list(args={"ue_id": ue["ue_id"]}) Modlist = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
if Modlist: if Modlist:
ue["semestre_id"] = Modlist[0]["semestre_id"] ue["semestre_id"] = Modlist[0]["semestre_id"]
else: else:
@ -388,42 +403,46 @@ def next_ue_numero(formation_id, semestre_id=None):
"""Numero d'une nouvelle UE dans cette formation. """Numero d'une nouvelle UE dans cette formation.
Si le semestre est specifie, cherche les UE ayant des modules de ce semestre Si le semestre est specifie, cherche les UE ayant des modules de ce semestre
""" """
ue_list = do_ue_list(args={"formation_id": formation_id}) ues = ue_list(args={"formation_id": formation_id})
if not ue_list: if not ues:
return 0 return 0
if semestre_id is None: if semestre_id is None:
return ue_list[-1]["numero"] + 1000 return ues[-1]["numero"] + 1000
else: else:
# Avec semestre: (prend le semestre du 1er module de l'UE) # Avec semestre: (prend le semestre du 1er module de l'UE)
_add_ue_semestre_id(ue_list) _add_ue_semestre_id(ues)
ue_list_semestre = [ue for ue in ue_list if ue["semestre_id"] == semestre_id] ue_list_semestre = [ue for ue in ues if ue["semestre_id"] == semestre_id]
if ue_list_semestre: if ue_list_semestre:
return ue_list_semestre[-1]["numero"] + 10 return ue_list_semestre[-1]["numero"] + 10
else: else:
return ue_list[-1]["numero"] + 1000 return ues[-1]["numero"] + 1000
def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False): def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
"""Delete an UE""" """Delete an UE"""
ue = do_ue_list(args={"ue_id": ue_id}) ues = ue_list(args={"ue_id": ue_id})
if not ue: if not ues:
raise ScoValueError("UE inexistante !") raise ScoValueError("UE inexistante !")
ue = ue[0] ue = ues[0]
if not dialog_confirmed: if not dialog_confirmed:
return scu.confirm_dialog( return scu.confirm_dialog(
"<h2>Suppression de l'UE %(titre)s (%(acronyme)s))</h2>" % ue, "<h2>Suppression de l'UE %(titre)s (%(acronyme)s))</h2>" % ue,
dest_url="", dest_url="",
parameters={"ue_id": ue_id}, parameters={"ue_id": ue_id},
cancel_url="ue_list?formation_id=%s" % ue["formation_id"], cancel_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(ue["formation_id"]),
),
) )
return do_ue_delete(ue_id, delete_validations=delete_validations) return do_ue_delete(ue_id, delete_validations=delete_validations)
def ue_list(formation_id=None, msg=""): def ue_table(formation_id=None, msg=""): # was ue_list
"""Liste des matières et modules d'une formation, avec liens pour """Liste des matières et modules d'une formation, avec liens pour
editer (si non verrouillée). éditer (si non verrouillée).
""" """
from app.scodoc import sco_formations from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre_validation from app.scodoc import sco_formsemestre_validation
@ -435,28 +454,31 @@ def ue_list(formation_id=None, msg=""):
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"]) parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
locked = sco_formations.formation_has_locked_sems(formation_id) locked = sco_formations.formation_has_locked_sems(formation_id)
ue_list = do_ue_list(args={"formation_id": formation_id}) ues = ue_list(args={"formation_id": formation_id, "is_external": False})
ues_externes = ue_list(args={"formation_id": formation_id, "is_external": True})
# tri par semestre et numero: # tri par semestre et numero:
_add_ue_semestre_id(ue_list) _add_ue_semestre_id(ues)
ue_list.sort(key=lambda u: (u["semestre_id"], u["numero"])) _add_ue_semestre_id(ues_externes)
has_duplicate_ue_codes = len(set([ue["ue_code"] for ue in ue_list])) != len(ue_list) ues.sort(key=lambda u: (u["semestre_id"], u["numero"]))
ues_externes.sort(key=lambda u: (u["semestre_id"], u["numero"]))
has_duplicate_ue_codes = len(set([ue["ue_code"] for ue in ues])) != len(ues)
perm_change = current_user.has_permission(Permission.ScoChangeFormation) has_perm_change = current_user.has_permission(Permission.ScoChangeFormation)
# editable = (not locked) and perm_change # editable = (not locked) and has_perm_change
# On autorise maintanant la modification des formations qui ont des semestres verrouillés, # On autorise maintanant la modification des formations qui ont des semestres verrouillés,
# sauf si cela affect les notes passées (verrouillées): # sauf si cela affect les notes passées (verrouillées):
# - pas de modif des modules utilisés dans des semestres verrouillés # - pas de modif des modules utilisés dans des semestres verrouillés
# - pas de changement des codes d'UE utilisés dans des semestres verrouillés # - pas de changement des codes d'UE utilisés dans des semestres verrouillés
editable = perm_change editable = has_perm_change
tag_editable = ( tag_editable = (
current_user.has_permission(Permission.ScoEditFormationTags) or perm_change current_user.has_permission(Permission.ScoEditFormationTags) or has_perm_change
) )
if locked: if locked:
lockicon = scu.icontag("lock32_img", title="verrouillé") lockicon = scu.icontag("lock32_img", title="verrouillé")
else: else:
lockicon = "" lockicon = ""
arrow_up, arrow_down, arrow_none = sco_groups.getArrowIconsTags() arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
delete_icon = scu.icontag( delete_icon = scu.icontag(
"delete_small_img", title="Supprimer (module inutilisé)", alt="supprimer" "delete_small_img", title="Supprimer (module inutilisé)", alt="supprimer"
) )
@ -553,213 +575,20 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
H.append( H.append(
'<form><input type="checkbox" class="sco_tag_checkbox">montrer les tags</input></form>' '<form><input type="checkbox" class="sco_tag_checkbox">montrer les tags</input></form>'
) )
H.append(
cur_ue_semestre_id = None _ue_table_ues(
iue = 0 parcours,
for UE in ue_list: ues,
if UE["ects"]: editable,
UE["ects_str"] = ", %g ECTS" % UE["ects"] tag_editable,
else: has_perm_change,
UE["ects_str"] = "" arrow_up,
if editable: arrow_down,
klass = "span_apo_edit" arrow_none,
else: delete_icon,
klass = "" delete_disabled_icon,
UE["code_apogee_str"] = (
""", Apo: <span class="%s" data-url="edit_ue_set_code_apogee" id="%s" data-placeholder="%s">"""
% (klass, UE["ue_id"], scu.APO_MISSING_CODE_STR)
+ (UE["code_apogee"] or "")
+ "</span>"
) )
)
if cur_ue_semestre_id != UE["semestre_id"]:
cur_ue_semestre_id = UE["semestre_id"]
if iue > 0:
H.append("</ul>")
if UE["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT:
lab = "Pas d'indication de semestre:"
else:
lab = "Semestre %s:" % UE["semestre_id"]
H.append('<div class="ue_list_tit_sem">%s</div>' % lab)
H.append('<ul class="notes_ue_list">')
H.append('<li class="notes_ue_list">')
if iue != 0 and editable:
H.append(
'<a href="ue_move?ue_id=%s&after=0" class="aud">%s</a>'
% (UE["ue_id"], arrow_up)
)
else:
H.append(arrow_none)
if iue < len(ue_list) - 1 and editable:
H.append(
'<a href="ue_move?ue_id=%s&after=1" class="aud">%s</a>'
% (UE["ue_id"], arrow_down)
)
else:
H.append(arrow_none)
iue += 1
UE["acro_titre"] = str(UE["acronyme"])
if UE["titre"] != UE["acronyme"]:
UE["acro_titre"] += " " + str(UE["titre"])
H.append(
"""%(acro_titre)s <span class="ue_code">(code %(ue_code)s%(ects_str)s, coef. %(coefficient)3.2f%(code_apogee_str)s)</span>
<span class="ue_coef"></span>
"""
% UE
)
if UE["type"] != sco_codes_parcours.UE_STANDARD:
H.append(
'<span class="ue_type">%s</span>'
% sco_codes_parcours.UE_TYPE_NAME[UE["type"]]
)
ue_editable = editable and not ue_is_locked(UE["ue_id"])
if ue_editable:
H.append(
'<a class="stdlink" href="ue_edit?ue_id=%(ue_id)s">modifier</a>' % UE
)
else:
H.append('<span class="locked">[verrouillé]</span>')
if not parcours.UE_IS_MODULE:
H.append('<ul class="notes_matiere_list">')
Matlist = sco_edit_matiere.do_matiere_list(args={"ue_id": UE["ue_id"]})
for Mat in Matlist:
if not parcours.UE_IS_MODULE:
H.append('<li class="notes_matiere_list">')
if editable and not sco_edit_matiere.matiere_is_locked(
Mat["matiere_id"]
):
H.append(
f"""<a class="stdlink" href="{
url_for("notes.matiere_edit",
scodoc_dept=g.scodoc_dept, matiere_id=Mat["matiere_id"])
}">
"""
)
H.append("%(titre)s" % Mat)
if editable and not sco_edit_matiere.matiere_is_locked(
Mat["matiere_id"]
):
H.append("</a>")
H.append('<ul class="notes_module_list">')
Modlist = sco_edit_module.do_module_list(
args={"matiere_id": Mat["matiere_id"]}
)
im = 0
for Mod in Modlist:
Mod["nb_moduleimpls"] = sco_edit_module.module_count_moduleimpls(
Mod["module_id"]
)
klass = "notes_module_list"
if Mod["module_type"] == scu.MODULE_MALUS:
klass += " module_malus"
H.append('<li class="%s">' % klass)
H.append('<span class="notes_module_list_buts">')
if im != 0 and editable:
H.append(
'<a href="module_move?module_id=%s&after=0" class="aud">%s</a>'
% (Mod["module_id"], arrow_up)
)
else:
H.append(arrow_none)
if im < len(Modlist) - 1 and editable:
H.append(
'<a href="module_move?module_id=%s&after=1" class="aud">%s</a>'
% (Mod["module_id"], arrow_down)
)
else:
H.append(arrow_none)
im += 1
if Mod["nb_moduleimpls"] == 0 and editable:
H.append(
'<a class="smallbutton" href="module_delete?module_id=%s">%s</a>'
% (Mod["module_id"], delete_icon)
)
else:
H.append(delete_disabled_icon)
H.append("</span>")
mod_editable = editable # and not sco_edit_module.module_is_locked( Mod['module_id'])
if mod_editable:
H.append(
'<a class="discretelink" title="Modifier le module numéro %(numero)s, utilisé par %(nb_moduleimpls)d sessions" href="module_edit?module_id=%(module_id)s">'
% Mod
)
H.append(
'<span class="formation_module_tit">%s</span>'
% scu.join_words(Mod["code"], Mod["titre"])
)
if mod_editable:
H.append("</a>")
heurescoef = (
"%(heures_cours)s/%(heures_td)s/%(heures_tp)s, coef. %(coefficient)s"
% Mod
)
if mod_editable:
klass = "span_apo_edit"
else:
klass = ""
heurescoef += (
', Apo: <span class="%s" data-url="edit_module_set_code_apogee" id="%s" data-placeholder="%s">'
% (klass, Mod["module_id"], scu.APO_MISSING_CODE_STR)
+ (Mod["code_apogee"] or "")
+ "</span>"
)
if tag_editable:
tag_cls = "module_tag_editor"
else:
tag_cls = "module_tag_editor_ro"
tag_mk = """<span class="sco_tag_edit"><form><textarea data-module_id="{}" class="{}">{}</textarea></form></span>"""
tag_edit = tag_mk.format(
Mod["module_id"],
tag_cls,
",".join(sco_tag_module.module_tag_list(Mod["module_id"])),
)
H.append(
" %s %s" % (parcours.SESSION_NAME, Mod["semestre_id"])
+ " (%s)" % heurescoef
+ tag_edit
)
H.append("</li>")
if not Modlist:
H.append("<li>Aucun module dans cette matière !")
if editable:
H.append(
f"""<a class="stdlink" href="{
url_for("notes.matiere_delete",
scodoc_dept=g.scodoc_dept, matiere_id=Mat["matiere_id"])}"
>supprimer cette matière</a>
"""
)
H.append("</li>")
if editable: # and ((not parcours.UE_IS_MODULE) or len(Modlist) == 0):
H.append(
f"""<li> <a class="stdlink" href="{
url_for("notes.module_create",
scodoc_dept=g.scodoc_dept, matiere_id=Mat["matiere_id"])}"
>créer un module</a></li>
"""
)
H.append("</ul>")
H.append("</li>")
if not Matlist:
H.append("<li>Aucune matière dans cette UE ! ")
if editable:
H.append(
"""<a class="stdlink" href="ue_delete?ue_id=%(ue_id)s">supprimer l'UE</a>"""
% UE
)
H.append("</li>")
if editable and not parcours.UE_IS_MODULE:
H.append(
'<li><a class="stdlink" href="matiere_create?ue_id=%(ue_id)s">créer une matière</a> </li>'
% UE
)
if not parcours.UE_IS_MODULE:
H.append("</ul>")
H.append("</ul>")
if editable: if editable:
H.append( H.append(
'<ul><li><a class="stdlink" href="ue_create?formation_id=%s">Ajouter une UE</a></li>' '<ul><li><a class="stdlink" href="ue_create?formation_id=%s">Ajouter une UE</a></li>'
@ -771,6 +600,27 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
) )
H.append("</div>") # formation_ue_list H.append("</div>") # formation_ue_list
if ues_externes:
H.append('<div class="formation_ue_list formation_ue_list_externes">')
H.append(
'<div class="ue_list_tit">UE externes déclarées (pour information):</div>'
)
H.append(
_ue_table_ues(
parcours,
ues_externes,
editable,
tag_editable,
has_perm_change,
arrow_up,
arrow_down,
arrow_none,
delete_icon,
delete_disabled_icon,
)
)
H.append("</div>") # formation_ue_list
H.append("<p><ul>") H.append("<p><ul>")
if editable: if editable:
H.append( H.append(
@ -792,7 +642,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
</p>""" </p>"""
% F % F
) )
if perm_change: if has_perm_change:
H.append( H.append(
""" """
<h3> <a name="sems">Semestres ou sessions de cette formation</a></h3> <h3> <a name="sems">Semestres ou sessions de cette formation</a></h3>
@ -833,6 +683,294 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
return "".join(H) return "".join(H)
def _ue_table_ues(
parcours,
ues,
editable,
tag_editable,
has_perm_change,
arrow_up,
arrow_down,
arrow_none,
delete_icon,
delete_disabled_icon,
):
"""Édition de programme: liste des UEs (avec leurs matières et modules)."""
H = []
cur_ue_semestre_id = None
iue = 0
for ue in ues:
if ue["ects"]:
ue["ects_str"] = ", %g ECTS" % ue["ects"]
else:
ue["ects_str"] = ""
if editable:
klass = "span_apo_edit"
else:
klass = ""
ue["code_apogee_str"] = (
""", Apo: <span class="%s" data-url="edit_ue_set_code_apogee" id="%s" data-placeholder="%s">"""
% (klass, ue["ue_id"], scu.APO_MISSING_CODE_STR)
+ (ue["code_apogee"] or "")
+ "</span>"
)
if cur_ue_semestre_id != ue["semestre_id"]:
cur_ue_semestre_id = ue["semestre_id"]
if iue > 0:
H.append("</ul>")
if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT:
lab = "Pas d'indication de semestre:"
else:
lab = "Semestre %s:" % ue["semestre_id"]
H.append('<div class="ue_list_tit_sem">%s</div>' % lab)
H.append('<ul class="notes_ue_list">')
H.append('<li class="notes_ue_list">')
if iue != 0 and editable:
H.append(
'<a href="ue_move?ue_id=%s&after=0" class="aud">%s</a>'
% (ue["ue_id"], arrow_up)
)
else:
H.append(arrow_none)
if iue < len(ues) - 1 and editable:
H.append(
'<a href="ue_move?ue_id=%s&after=1" class="aud">%s</a>'
% (ue["ue_id"], arrow_down)
)
else:
H.append(arrow_none)
iue += 1
ue["acro_titre"] = str(ue["acronyme"])
if ue["titre"] != ue["acronyme"]:
ue["acro_titre"] += " " + str(ue["titre"])
H.append(
"""%(acro_titre)s <span class="ue_code">(code %(ue_code)s%(ects_str)s, coef. %(coefficient)3.2f%(code_apogee_str)s)</span>
<span class="ue_coef"></span>
"""
% ue
)
if ue["type"] != sco_codes_parcours.UE_STANDARD:
H.append(
'<span class="ue_type">%s</span>'
% sco_codes_parcours.UE_TYPE_NAME[ue["type"]]
)
if ue["is_external"]:
# Cas spécial: si l'UE externe a plus d'un module, c'est peut être une UE
# qui a été déclarée externe par erreur (ou suite à un bug d'import/export xml)
# Dans ce cas, propose de changer le type (même si verrouillée)
if len(sco_moduleimpl.moduleimpls_in_external_ue(ue["ue_id"])) > 1:
H.append('<span class="ue_is_external">')
if has_perm_change:
H.append(
f"""<a class="stdlink" href="{
url_for("notes.ue_set_internal", scodoc_dept=g.scodoc_dept, ue_id=ue["ue_id"])
}">transformer en UE ordinaire</a>&nbsp;"""
)
H.append("</span>")
ue_editable = editable and not ue_is_locked(ue["ue_id"])
if ue_editable:
H.append(
'<a class="stdlink" href="ue_edit?ue_id=%(ue_id)s">modifier</a>' % ue
)
else:
H.append('<span class="locked">[verrouillé]</span>')
H.append(
_ue_table_matieres(
parcours,
ue,
editable,
tag_editable,
arrow_up,
arrow_down,
arrow_none,
delete_icon,
delete_disabled_icon,
)
)
return "\n".join(H)
def _ue_table_matieres(
parcours,
ue,
editable,
tag_editable,
arrow_up,
arrow_down,
arrow_none,
delete_icon,
delete_disabled_icon,
):
"""Édition de programme: liste des matières (et leurs modules) d'une UE."""
H = []
if not parcours.UE_IS_MODULE:
H.append('<ul class="notes_matiere_list">')
matieres = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]})
for mat in matieres:
if not parcours.UE_IS_MODULE:
H.append('<li class="notes_matiere_list">')
if editable and not sco_edit_matiere.matiere_is_locked(mat["matiere_id"]):
H.append(
f"""<a class="stdlink" href="{
url_for("notes.matiere_edit",
scodoc_dept=g.scodoc_dept, matiere_id=mat["matiere_id"])
}">
"""
)
H.append("%(titre)s" % mat)
if editable and not sco_edit_matiere.matiere_is_locked(mat["matiere_id"]):
H.append("</a>")
modules = sco_edit_module.module_list(args={"matiere_id": mat["matiere_id"]})
H.append(
_ue_table_modules(
parcours,
mat,
modules,
editable,
tag_editable,
arrow_up,
arrow_down,
arrow_none,
delete_icon,
delete_disabled_icon,
)
)
if not matieres:
H.append("<li>Aucune matière dans cette UE ! ")
if editable:
H.append(
"""<a class="stdlink" href="ue_delete?ue_id=%(ue_id)s">supprimer l'UE</a>"""
% ue
)
H.append("</li>")
if editable and not parcours.UE_IS_MODULE:
H.append(
'<li><a class="stdlink" href="matiere_create?ue_id=%(ue_id)s">créer une matière</a> </li>'
% ue
)
if not parcours.UE_IS_MODULE:
H.append("</ul>")
return "\n".join(H)
def _ue_table_modules(
parcours,
mat,
modules,
editable,
tag_editable,
arrow_up,
arrow_down,
arrow_none,
delete_icon,
delete_disabled_icon,
):
"""Édition de programme: liste des modules d'une matière d'une UE"""
H = ['<ul class="notes_module_list">']
im = 0
for mod in modules:
mod["nb_moduleimpls"] = sco_edit_module.module_count_moduleimpls(
mod["module_id"]
)
klass = "notes_module_list"
if mod["module_type"] == scu.MODULE_MALUS:
klass += " module_malus"
H.append('<li class="%s">' % klass)
H.append('<span class="notes_module_list_buts">')
if im != 0 and editable:
H.append(
'<a href="module_move?module_id=%s&after=0" class="aud">%s</a>'
% (mod["module_id"], arrow_up)
)
else:
H.append(arrow_none)
if im < len(modules) - 1 and editable:
H.append(
'<a href="module_move?module_id=%s&after=1" class="aud">%s</a>'
% (mod["module_id"], arrow_down)
)
else:
H.append(arrow_none)
im += 1
if mod["nb_moduleimpls"] == 0 and editable:
H.append(
'<a class="smallbutton" href="module_delete?module_id=%s">%s</a>'
% (mod["module_id"], delete_icon)
)
else:
H.append(delete_disabled_icon)
H.append("</span>")
mod_editable = (
editable # and not sco_edit_module.module_is_locked( Mod['module_id'])
)
if mod_editable:
H.append(
'<a class="discretelink" title="Modifier le module numéro %(numero)s, utilisé par %(nb_moduleimpls)d sessions" href="module_edit?module_id=%(module_id)s">'
% mod
)
H.append(
'<span class="formation_module_tit">%s</span>'
% scu.join_words(mod["code"], mod["titre"])
)
if mod_editable:
H.append("</a>")
heurescoef = (
"%(heures_cours)s/%(heures_td)s/%(heures_tp)s, coef. %(coefficient)s" % mod
)
if mod_editable:
klass = "span_apo_edit"
else:
klass = ""
heurescoef += (
', Apo: <span class="%s" data-url="edit_module_set_code_apogee" id="%s" data-placeholder="%s">'
% (klass, mod["module_id"], scu.APO_MISSING_CODE_STR)
+ (mod["code_apogee"] or "")
+ "</span>"
)
if tag_editable:
tag_cls = "module_tag_editor"
else:
tag_cls = "module_tag_editor_ro"
tag_mk = """<span class="sco_tag_edit"><form><textarea data-module_id="{}" class="{}">{}</textarea></form></span>"""
tag_edit = tag_mk.format(
mod["module_id"],
tag_cls,
",".join(sco_tag_module.module_tag_list(mod["module_id"])),
)
H.append(
" %s %s" % (parcours.SESSION_NAME, mod["semestre_id"])
+ " (%s)" % heurescoef
+ tag_edit
)
H.append("</li>")
if not modules:
H.append("<li>Aucun module dans cette matière ! ")
if editable:
H.append(
f"""<a class="stdlink" href="{
url_for("notes.matiere_delete",
scodoc_dept=g.scodoc_dept, matiere_id=mat["matiere_id"])}"
>la supprimer</a>
"""
)
H.append("</li>")
if editable: # and ((not parcours.UE_IS_MODULE) or len(Modlist) == 0):
H.append(
f"""<li> <a class="stdlink" href="{
url_for("notes.module_create",
scodoc_dept=g.scodoc_dept, matiere_id=mat["matiere_id"])}"
>créer un module</a></li>
"""
)
H.append("</ul>")
H.append("</li>")
return "\n".join(H)
def ue_sharing_code(ue_code=None, ue_id=None, hide_ue_id=None): def ue_sharing_code(ue_code=None, ue_id=None, hide_ue_id=None):
"""HTML list of UE sharing this code """HTML list of UE sharing this code
Either ue_code or ue_id may be specified. Either ue_code or ue_id may be specified.
@ -840,31 +978,32 @@ def ue_sharing_code(ue_code=None, ue_id=None, hide_ue_id=None):
""" """
from app.scodoc import sco_formations from app.scodoc import sco_formations
ue_code = str(ue_code)
if ue_id: if ue_id:
ue = do_ue_list(args={"ue_id": ue_id})[0] ue = ue_list(args={"ue_id": ue_id})[0]
if not ue_code: if not ue_code:
ue_code = ue["ue_code"] ue_code = ue["ue_code"]
F = sco_formations.formation_list(args={"formation_id": ue["formation_id"]})[0] F = sco_formations.formation_list(args={"formation_id": ue["formation_id"]})[0]
formation_code = F["formation_code"] formation_code = F["formation_code"]
# UE du même code, code formation et departement:
ue_list_all = do_ue_list(args={"ue_code": ue_code}) q_ues = (
if ue_id: NotesUE.query.filter_by(ue_code=ue_code)
# retire les UE d'autres formations: .join(NotesUE.formation, aliased=True)
# log('checking ucode %s formation %s' % (ue_code, formation_code)) .filter_by(dept_id=g.scodoc_dept_id, formation_code=formation_code)
ue_list = [] )
for ue in ue_list_all:
F = sco_formations.formation_list(
args={"formation_id": ue["formation_id"]}
)[0]
if formation_code == F["formation_code"]:
ue_list.append(ue)
else: else:
ue_list = ue_list_all # Toutes les UE du departement avec ce code:
q_ues = (
NotesUE.query.filter_by(ue_code=ue_code)
.join(NotesUE.formation, aliased=True)
.filter_by(dept_id=g.scodoc_dept_id)
)
if hide_ue_id: # enlève l'ue de depart if hide_ue_id: # enlève l'ue de depart
ue_list = [ue for ue in ue_list if ue["ue_id"] != hide_ue_id] q_ues = q_ues.filter(NotesUE.id != hide_ue_id)
if not ue_list: ues = q_ues.all()
if not ues:
if ue_id: if ue_id:
return """<span class="ue_share">Seule UE avec code %s</span>""" % ue_code return """<span class="ue_share">Seule UE avec code %s</span>""" % ue_code
else: else:
@ -875,18 +1014,13 @@ def ue_sharing_code(ue_code=None, ue_id=None, hide_ue_id=None):
else: else:
H.append('<span class="ue_share">UE avec le code %s:</span>' % ue_code) H.append('<span class="ue_share">UE avec le code %s:</span>' % ue_code)
H.append("<ul>") H.append("<ul>")
for ue in ue_list: for ue in ues:
F = sco_formations.formation_list(args={"formation_id": ue["formation_id"]})[0]
H.append( H.append(
'<li>%s (%s) dans <a class="stdlink" href="ue_list?formation_id=%s">%s (%s)</a>, version %s</li>' f"""<li>{ue.acronyme} ({ue.titre}) dans <a class="stdlink"
% ( href="{url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)}"
ue["acronyme"], >{ue.formation.acronyme} ({ue.formation.titre})</a>, version {ue.formation.version}
ue["titre"], </li>
F["formation_id"], """
F["acronyme"],
F["titre"],
F["version"],
)
) )
H.append("</ul>") H.append("</ul>")
return "\n".join(H) return "\n".join(H)
@ -896,13 +1030,13 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
"edit an UE" "edit an UE"
# check # check
ue_id = args["ue_id"] ue_id = args["ue_id"]
ue = do_ue_list({"ue_id": ue_id})[0] ue = ue_list({"ue_id": ue_id})[0]
if (not bypass_lock) and ue_is_locked(ue["ue_id"]): if (not bypass_lock) and ue_is_locked(ue["ue_id"]):
raise ScoLockedFormError() raise ScoLockedFormError()
# check: acronyme unique dans cette formation # check: acronyme unique dans cette formation
if "acronyme" in args: if "acronyme" in args:
new_acro = args["acronyme"] new_acro = args["acronyme"]
ues = do_ue_list({"formation_id": ue["formation_id"], "acronyme": new_acro}) ues = ue_list({"formation_id": ue["formation_id"], "acronyme": new_acro})
if ues and ues[0]["ue_id"] != ue_id: if ues and ues[0]["ue_id"] != ue_id:
raise ScoValueError('Acronyme d\'UE "%s" déjà utilisé !' % args["acronyme"]) raise ScoValueError('Acronyme d\'UE "%s" déjà utilisé !' % args["acronyme"])
@ -925,7 +1059,7 @@ def edit_ue_set_code_apogee(id=None, value=None):
value = value.strip("-_ \t") value = value.strip("-_ \t")
log("edit_ue_set_code_apogee: ue_id=%s code_apogee=%s" % (ue_id, value)) log("edit_ue_set_code_apogee: ue_id=%s code_apogee=%s" % (ue_id, value))
ues = do_ue_list(args={"ue_id": ue_id}) ues = ue_list(args={"ue_id": ue_id})
if not ues: if not ues:
return "ue invalide" return "ue invalide"
@ -956,7 +1090,7 @@ def ue_is_locked(ue_id):
# ---- Table recap formation # ---- Table recap formation
def formation_table_recap(formation_id, format="html", REQUEST=None): def formation_table_recap(formation_id, format="html"):
"""Table recapitulant formation.""" """Table recapitulant formation."""
from app.scodoc import sco_formations from app.scodoc import sco_formations
@ -965,11 +1099,11 @@ def formation_table_recap(formation_id, format="html", REQUEST=None):
raise ScoValueError("invalid formation_id") raise ScoValueError("invalid formation_id")
F = F[0] F = F[0]
T = [] T = []
ue_list = do_ue_list(args={"formation_id": formation_id}) ues = ue_list(args={"formation_id": formation_id})
for UE in ue_list: for ue in ues:
Matlist = sco_edit_matiere.do_matiere_list(args={"ue_id": UE["ue_id"]}) Matlist = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]})
for Mat in Matlist: for Mat in Matlist:
Modlist = sco_edit_module.do_module_list( Modlist = sco_edit_module.module_list(
args={"matiere_id": Mat["matiere_id"]} args={"matiere_id": Mat["matiere_id"]}
) )
for Mod in Modlist: for Mod in Modlist:
@ -979,7 +1113,7 @@ def formation_table_recap(formation_id, format="html", REQUEST=None):
# #
T.append( T.append(
{ {
"UE_acro": UE["acronyme"], "UE_acro": ue["acronyme"],
"Mat_tit": Mat["titre"], "Mat_tit": Mat["titre"],
"Mod_tit": Mod["abbrev"] or Mod["titre"], "Mod_tit": Mod["abbrev"] or Mod["titre"],
"Mod_code": Mod["code"], "Mod_code": Mod["code"],
@ -1033,13 +1167,13 @@ def formation_table_recap(formation_id, format="html", REQUEST=None):
caption=title, caption=title,
html_caption=title, html_caption=title,
html_class="table_leftalign", html_class="table_leftalign",
base_url="%s?formation_id=%s" % (REQUEST.URL0, formation_id), base_url="%s?formation_id=%s" % (request.base_url, formation_id),
page_title=title, page_title=title,
html_title="<h2>" + title + "</h2>", html_title="<h2>" + title + "</h2>",
pdf_title=title, pdf_title=title,
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)
def ue_list_semestre_ids(ue): def ue_list_semestre_ids(ue):
@ -1048,5 +1182,5 @@ def ue_list_semestre_ids(ue):
Mais cela n'a pas toujours été le cas dans les programmes pédagogiques officiels, Mais cela n'a pas toujours été le cas dans les programmes pédagogiques officiels,
aussi ScoDoc laisse le choix. aussi ScoDoc laisse le choix.
""" """
Modlist = sco_edit_module.do_module_list(args={"ue_id": ue["ue_id"]}) Modlist = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
return sorted(list(set([mod["semestre_id"] for mod in Modlist]))) return sorted(list(set([mod["semestre_id"] for mod in Modlist])))

View File

@ -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 icalendar
import pprint import pprint
import traceback
import urllib
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import log
@ -80,7 +80,7 @@ def formsemestre_load_ics(sem):
ics_data = "" ics_data = ""
else: else:
log("Loading edt from %s" % ics_url) log("Loading edt from %s" % ics_url)
f = six.moves.urllib.request.urlopen( f = urllib.request.urlopen(
ics_url, timeout=5 ics_url, timeout=5
) # 5s TODO: add config parameter, eg for slow networks ) # 5s TODO: add config parameter, eg for slow networks
ics_data = f.read() ics_data = f.read()
@ -123,7 +123,7 @@ def get_edt_transcodage_groups(formsemestre_id):
return edt2sco, sco2edt, msg 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 """EDT complet du semestre, au format JSON
TODO: indiquer un groupe TODO: indiquer un groupe
TODO: utiliser start et end (2 dates au format ISO YYYY-MM-DD) 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) J.append(d)
return scu.sendJSON(REQUEST, J) return scu.sendJSON(J)
"""XXX """XXX
@ -159,9 +159,7 @@ for e in events:
""" """
def experimental_calendar( def experimental_calendar(group_id=None, formsemestre_id=None): # inutilisé
group_id=None, formsemestre_id=None, REQUEST=None
): # inutilisé
"""experimental page""" """experimental page"""
return "\n".join( return "\n".join(
[ [

View File

@ -32,11 +32,11 @@
Voir sco_apogee_csv.py pour la structure du fichier Apogée. Voir sco_apogee_csv.py pour la structure du fichier Apogée.
Stockage: utilise sco_archive.py 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 pour une maquette de l'année scolaire 2016, semestre 1, etape V3ASR
ou bien (à partir de ScoDoc 1678) : 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. 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. La version VDI sera ignorée sauf si elle est indiquée dans l'étape du semestre.

View File

@ -32,7 +32,7 @@ import io
from zipfile import ZipFile from zipfile import ZipFile
import flask 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 # from werkzeug.utils import send_file
@ -62,7 +62,6 @@ def apo_semset_maq_status(
block_export_res_ues=False, block_export_res_ues=False,
block_export_res_modules=False, block_export_res_modules=False,
block_export_res_sdj=True, block_export_res_sdj=True,
REQUEST=None,
): ):
"""Page statut / tableau de bord""" """Page statut / tableau de bord"""
if not semset_id: if not semset_id:
@ -83,7 +82,7 @@ def apo_semset_maq_status(
prefs = sco_preferences.SemPreferences() prefs = sco_preferences.SemPreferences()
tab_archives = table_apo_csv_list(semset, REQUEST=REQUEST) tab_archives = table_apo_csv_list(semset)
( (
ok_for_export, ok_for_export,
@ -250,7 +249,7 @@ def apo_semset_maq_status(
"""<form name="f" method="get" action="%s"> """<form name="f" method="get" action="%s">
<input type="hidden" name="semset_id" value="%s"></input> <input type="hidden" name="semset_id" value="%s"></input>
<div><input type="checkbox" name="allow_missing_apo" value="1" onchange="document.f.submit()" """ <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: if allow_missing_apo:
H.append("checked") H.append("checked")
@ -357,7 +356,7 @@ def apo_semset_maq_status(
H.append( H.append(
", ".join( ", ".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 % f
for f in formations for f in formations
] ]
@ -430,7 +429,7 @@ def apo_semset_maq_status(
return "\n".join(H) 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)""" """Table des archives (triée par date d'archivage)"""
annee_scolaire = semset["annee_scolaire"] annee_scolaire = semset["annee_scolaire"]
sem_id = semset["sem_id"] sem_id = semset["sem_id"]
@ -476,7 +475,7 @@ def table_apo_csv_list(semset, REQUEST=None):
rows=T, rows=T,
html_class="table_leftalign apo_maq_list", html_class="table_leftalign apo_maq_list",
html_sortable=True, 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', # caption='Maquettes enregistrées',
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
@ -484,7 +483,7 @@ def table_apo_csv_list(semset, REQUEST=None):
return tab 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 """Table des étudiants Apogée par nips
nip_list est une chaine, codes nip séparés par des , 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()), etuds=list(etuds.values()),
keys=("nip", "etape_apo", "nom", "prenom", "inscriptions_scodoc"), keys=("nip", "etape_apo", "nom", "prenom", "inscriptions_scodoc"),
format=format, 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""" """Table des étudiants ScoDoc par nips ou etudids"""
if not isinstance(nip_list, str): if not isinstance(nip_list, str):
nip_list = str(nip_list) nip_list = str(nip_list)
@ -541,13 +539,10 @@ def view_scodoc_etuds(semset_id, title="", nip_list="", format="html", REQUEST=N
etuds=etuds, etuds=etuds,
keys=("code_nip", "nom", "prenom"), keys=("code_nip", "nom", "prenom"),
format=format, format=format,
REQUEST=REQUEST,
) )
def _view_etuds_page( def _view_etuds_page(semset_id, title="", etuds=[], keys=(), format="html"):
semset_id, title="", etuds=[], keys=(), format="html", REQUEST=None
):
# Tri les étudiants par nom: # Tri les étudiants par nom:
if etuds: if etuds:
etuds.sort(key=lambda x: (x["nom"], x["prenom"])) etuds.sort(key=lambda x: (x["nom"], x["prenom"]))
@ -578,7 +573,7 @@ def _view_etuds_page(
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
if format != "html": if format != "html":
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)
H.append(tab.html()) H.append(tab.html())
@ -590,9 +585,7 @@ def _view_etuds_page(
return "\n".join(H) + html_sco_header.sco_footer() return "\n".join(H) + html_sco_header.sco_footer()
def view_apo_csv_store( def view_apo_csv_store(semset_id="", csvfile=None, data="", autodetect=False):
semset_id="", csvfile=None, data="", autodetect=False, REQUEST=None
):
"""Store CSV data """Store CSV data
Le semset identifie l'annee scolaire et le semestre Le semset identifie l'annee scolaire et le semestre
Si csvfile, lit depuis FILE, sinon utilise data Si csvfile, lit depuis FILE, sinon utilise data
@ -627,7 +620,7 @@ def view_apo_csv_store(
return flask.redirect("apo_semset_maq_status?semset_id=" + semset_id) return flask.redirect("apo_semset_maq_status?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""" """Download maquette and store it"""
if not semset_id: if not semset_id:
raise ValueError("invalid null semset_id") raise ValueError("invalid null semset_id")
@ -639,17 +632,15 @@ def view_apo_csv_download_and_store(etape_apo="", semset_id="", REQUEST=None):
# here, data is utf8 # here, data is utf8
# but we store and generate latin1 files, to ease further import in Apogée # 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 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) return view_apo_csv_store(semset_id, data=data, autodetect=False)
def view_apo_csv_delete( def view_apo_csv_delete(etape_apo="", semset_id="", dialog_confirmed=False):
etape_apo="", semset_id="", dialog_confirmed=False, REQUEST=None
):
"""Delete CSV file""" """Delete CSV file"""
if not semset_id: if not semset_id:
raise ValueError("invalid null semset_id") raise ValueError("invalid null semset_id")
semset = sco_semset.SemSet(semset_id=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: if not dialog_confirmed:
return scu.confirm_dialog( return scu.confirm_dialog(
"""<h2>Confirmer la suppression du fichier étape <tt>%s</tt>?</h2> """<h2>Confirmer la suppression du fichier étape <tt>%s</tt>?</h2>
@ -667,7 +658,7 @@ def view_apo_csv_delete(
return flask.redirect(dest_url + "&head_message=Archive%20supprimée") 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 """Visualise une maquette stockée
Si format="raw", renvoie le fichier maquette tel quel Si format="raw", renvoie le fichier maquette tel quel
""" """
@ -678,7 +669,8 @@ def view_apo_csv(etape_apo="", semset_id="", format="html", REQUEST=None):
sem_id = semset["sem_id"] sem_id = semset["sem_id"]
csv_data = sco_etape_apogee.apo_csv_get(etape_apo, annee_scolaire, sem_id) csv_data = sco_etape_apogee.apo_csv_get(etape_apo, annee_scolaire, sem_id)
if format == "raw": if format == "raw":
return scu.sendCSVFile(REQUEST, csv_data, etape_apo + ".txt") 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"]) apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
( (
@ -746,14 +738,15 @@ def view_apo_csv(etape_apo="", semset_id="", format="html", REQUEST=None):
rows=etuds, rows=etuds,
html_sortable=True, html_sortable=True,
html_class="table_leftalign apo_maq_table", 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, filename="students_" + etape_apo,
caption="Etudiants Apogée en " + etape_apo, caption="Etudiants Apogée en " + etape_apo,
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
if format != "html": if format != "html":
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)
H += [ H += [
tab.html(), tab.html(),
@ -768,7 +761,7 @@ def view_apo_csv(etape_apo="", semset_id="", format="html", REQUEST=None):
return "\n".join(H) return "\n".join(H)
# called from Web # called from Web (GET)
def apo_csv_export_results( def apo_csv_export_results(
semset_id, semset_id,
block_export_res_etape=False, block_export_res_etape=False,

View File

@ -31,27 +31,21 @@
# Ancien module "scolars" # Ancien module "scolars"
import os import os
import time 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 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 import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import SCO_ENCODING
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError 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 safehtml
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc.scolog import logdb from app.scodoc.scolog import logdb
from flask_mail import Message from app.scodoc.TrivialFormulator import TrivialFormulator
from app import mail
MONTH_NAMES_ABBREV = [ MONTH_NAMES_ABBREV = [
"Jan ", "Jan ",
@ -158,7 +152,7 @@ def format_nom(s, uppercase=True):
def input_civilite(s): def input_civilite(s):
"""Converts external representation of civilite to internal: """Converts external representation of civilite to internal:
'M', 'F', or 'X' (and nothing else). 'M', 'F', or 'X' (and nothing else).
Raises valueError if conversion fails. Raises ScoValueError if conversion fails.
""" """
s = s.upper().strip() s = s.upper().strip()
if s in ("M", "M.", "MR", "H"): if s in ("M", "M.", "MR", "H"):
@ -167,12 +161,13 @@ def input_civilite(s):
return "F" return "F"
elif s == "X" or not s: elif s == "X" or not s:
return "X" return "X"
raise ValueError("valeur invalide pour la civilité: %s" % s) raise ScoValueError("valeur invalide pour la civilité: %s" % s)
def format_civilite(civilite): def format_civilite(civilite):
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre, """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: try:
return { return {
@ -181,7 +176,7 @@ def format_civilite(civilite):
"X": "", "X": "",
}[civilite] }[civilite]
except KeyError: except KeyError:
raise ValueError("valeur invalide pour la civilité: %s" % civilite) raise ScoValueError("valeur invalide pour la civilité: %s" % civilite)
def format_lycee(nomlycee): def format_lycee(nomlycee):
@ -256,7 +251,6 @@ _identiteEditor = ndb.EditableTable(
"photo_filename", "photo_filename",
"code_ine", "code_ine",
"code_nip", "code_nip",
"scodoc7_id",
), ),
filter_dept=True, filter_dept=True,
sortkey="nom", sortkey="nom",
@ -321,9 +315,7 @@ def check_nom_prenom(cnx, nom="", prenom="", etudid=None):
return True, len(res) return True, len(res)
def _check_duplicate_code( def _check_duplicate_code(cnx, args, code_name, disable_notify=False, edit=True):
cnx, args, code_name, disable_notify=False, edit=True, REQUEST=None
):
etudid = args.get("etudid", None) etudid = args.get("etudid", None)
if args.get(code_name, None): if args.get(code_name, None):
etuds = identite_list(cnx, {code_name: str(args[code_name])}) etuds = identite_list(cnx, {code_name: str(args[code_name])})
@ -345,31 +337,33 @@ def _check_duplicate_code(
) )
if etudid: if etudid:
OK = "retour à la fiche étudiant" OK = "retour à la fiche étudiant"
dest_url = "ficheEtud" dest_endpoint = "scolar.ficheEtud"
parameters = {"etudid": etudid} parameters = {"etudid": etudid}
else: else:
if "tf_submitted" in args: if "tf_submitted" in args:
del args["tf_submitted"] del args["tf_submitted"]
OK = "Continuer" OK = "Continuer"
dest_url = "etudident_create_form" dest_endpoint = "scolar.etudident_create_form"
parameters = args parameters = args
else: else:
OK = "Annuler" OK = "Annuler"
dest_url = "" dest_endpoint = "notes.index_html"
parameters = {} parameters = {}
if not disable_notify: if not disable_notify:
err_page = scu.confirm_dialog( err_page = f"""<h3><h3>Code étudiant ({code_name}) dupliqué !</h3>
message="""<h3>Code étudiant (%s) dupliqué !</h3>""" % code_name, <p class="help">Le {code_name} {args[code_name]} est déjà utilisé: un seul étudiant peut avoir
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>""" ce code. Vérifier votre valeur ou supprimer l'autre étudiant avec cette valeur.
% (code_name, args[code_name]) </p>
+ "</li><li>".join(listh) <ul><li>
+ "</li></ul><p>", { '</li><li>'.join(listh) }
OK=OK, </li></ul>
dest_url=dest_url, <p>
parameters=parameters, <a href="{ url_for(dest_endpoint, scodoc_dept=g.scodoc_dept, **parameters) }
) ">{OK}</a>
</p>
"""
else: 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])) log("*** error: code %s duplique: %s" % (code_name, args[code_name]))
raise ScoGenError(err_page) raise ScoGenError(err_page)
@ -379,15 +373,15 @@ def _check_civilite(args):
args["civilite"] = input_civilite(civilite) # TODO: A faire valider 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. """Modifie l'identite d'un étudiant.
Si pref notification et difference, envoie message notification, sauf si disable_notify Si pref notification et difference, envoie message notification, sauf si disable_notify
""" """
_check_duplicate_code( _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( _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 notify_to = None
if not disable_notify: if not disable_notify:
@ -415,10 +409,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 unique etudid, then create"
_check_duplicate_code(cnx, args, "code_nip", edit=False, REQUEST=REQUEST) _check_duplicate_code(cnx, args, "code_nip", edit=False)
_check_duplicate_code(cnx, args, "code_ine", edit=False, REQUEST=REQUEST) _check_duplicate_code(cnx, args, "code_ine", edit=False)
_check_civilite(args) _check_civilite(args)
if "etudid" in args: if "etudid" in args:
@ -456,7 +450,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: sending notification to %s" % email_addr)
log("notify_etud_change: subject: %s" % subject) log("notify_etud_change: subject: %s" % subject)
log(txt) log(txt)
mail.send_email( email.send_email(
subject, sco_preferences.get_preference("email_from_addr"), [email_addr], txt subject, sco_preferences.get_preference("email_from_addr"), [email_addr], txt
) )
return txt return txt
@ -559,7 +553,6 @@ _admissionEditor = ndb.EditableTable(
"villelycee", "villelycee",
"codepostallycee", "codepostallycee",
"codelycee", "codelycee",
"debouche",
"type_admission", "type_admission",
"boursier_prec", "boursier_prec",
), ),
@ -583,8 +576,8 @@ admission_edit = _admissionEditor.edit
# Edition simultanee de identite et admission # Edition simultanee de identite et admission
class EtudIdentEditor(object): class EtudIdentEditor(object):
def create(self, cnx, args, REQUEST=None): def create(self, cnx, args):
etudid = identite_create(cnx, args, REQUEST) etudid = identite_create(cnx, args)
args["etudid"] = etudid args["etudid"] = etudid
admission_create(cnx, args) admission_create(cnx, args)
return etudid return etudid
@ -615,8 +608,8 @@ class EtudIdentEditor(object):
res.sort(key=itemgetter("nom", "prenom")) res.sort(key=itemgetter("nom", "prenom"))
return res return res
def edit(self, cnx, args, disable_notify=False, REQUEST=None): def edit(self, cnx, args, disable_notify=False):
identite_edit(cnx, args, disable_notify=disable_notify, REQUEST=REQUEST) identite_edit(cnx, args, disable_notify=disable_notify)
if "adm_id" in args: # safety net if "adm_id" in args: # safety net
admission_edit(cnx, args) admission_edit(cnx, args)
@ -656,11 +649,17 @@ def make_etud_args(etudid=None, code_nip=None, use_request=True, raise_exc=True)
return args 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: def get_etud_info(etudid=False, code_nip=False, filled=False) -> list:
"""infos sur un etudiant (API). If not foud, returns empty list. """infos sur un etudiant (API). If not found, returns empty list.
On peut specifier etudid ou code_nip On peut specifier etudid ou code_nip
ou bien cherche dans REQUEST.form: etudid, code_nip, code_ine ou bien cherche dans les argumenst de la requête courante:
(dans cet ordre). etudid, code_nip, code_ine (dans cet ordre).
""" """
if etudid is None: if etudid is None:
return [] return []
@ -673,7 +672,20 @@ def get_etud_info(etudid=False, code_nip=False, filled=False) -> list:
return etud 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". """Creation d'un étudiant. génère aussi évenement et "news".
Args: Args:
@ -685,7 +697,7 @@ def create_etud(cnx, args={}, REQUEST=None):
from app.scodoc import sco_news from app.scodoc import sco_news
# creation d'un etudiant # 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" !) # crée une adresse vide (chaque etudiant doit etre dans la table "adresse" !)
_ = adresse_create( _ = adresse_create(
cnx, cnx,
@ -819,8 +831,8 @@ appreciations_edit = _appreciationsEditor.edit
def read_etablissements(): def read_etablissements():
filename = os.path.join(scu.SCO_TOOLS_DIR, scu.CONFIG.ETABL_FILENAME) filename = os.path.join(scu.SCO_TOOLS_DIR, scu.CONFIG.ETABL_FILENAME)
log("reading %s" % filename) log("reading %s" % filename)
f = open(filename) with open(filename) as f:
L = [x[:-1].split(";") for x in f] L = [x[:-1].split(";") for x in f]
E = {} E = {}
for l in L[1:]: for l in L[1:]:
E[l[0]] = { E[l[0]] = {

View File

@ -31,7 +31,7 @@ import datetime
import operator import operator
import pprint import pprint
import time import time
import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error import urllib
import flask import flask
from flask import url_for from flask import url_for
@ -179,7 +179,7 @@ def do_evaluation_list(args, sortkey=None):
def do_evaluation_list_in_formsemestre(formsemestre_id): def do_evaluation_list_in_formsemestre(formsemestre_id):
"list evaluations in this formsemestre" "list evaluations in this formsemestre"
mods = sco_moduleimpl.do_moduleimpl_list(formsemestre_id=formsemestre_id) mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
evals = [] evals = []
for mod in mods: for mod in mods:
evals += do_evaluation_list(args={"moduleimpl_id": mod["moduleimpl_id"]}) evals += do_evaluation_list(args={"moduleimpl_id": mod["moduleimpl_id"]})
@ -213,7 +213,7 @@ def _check_evaluation_args(args):
jour = args.get("jour", None) jour = args.get("jour", None)
args["jour"] = jour args["jour"] = jour
if jour: if jour:
M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"]) sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
d, m, y = [int(x) for x in sem["date_debut"].split("/")] d, m, y = [int(x) for x in sem["date_debut"].split("/")]
date_debut = datetime.date(y, m, d) date_debut = datetime.date(y, m, d)
@ -250,7 +250,6 @@ def do_evaluation_create(
publish_incomplete=None, publish_incomplete=None,
evaluation_type=None, evaluation_type=None,
numero=None, numero=None,
REQUEST=None,
**kw, # ceci pour absorber les arguments excedentaires de tf #sco8 **kw, # ceci pour absorber les arguments excedentaires de tf #sco8
): ):
"""Create an evaluation""" """Create an evaluation"""
@ -274,13 +273,13 @@ def do_evaluation_create(
if args["jour"]: if args["jour"]:
next_eval = None next_eval = None
t = ( t = (
ndb.DateDMYtoISO(args["jour"]), ndb.DateDMYtoISO(args["jour"], null_is_empty=True),
ndb.TimetoISO8601(args["heure_debut"]), ndb.TimetoISO8601(args["heure_debut"], null_is_empty=True),
) )
for e in ModEvals: for e in ModEvals:
if ( if (
ndb.DateDMYtoISO(e["jour"]), ndb.DateDMYtoISO(e["jour"], null_is_empty=True),
ndb.TimetoISO8601(e["heure_debut"]), ndb.TimetoISO8601(e["heure_debut"], null_is_empty=True),
) > t: ) > t:
next_eval = e next_eval = e
break break
@ -302,8 +301,8 @@ def do_evaluation_create(
r = _evaluationEditor.create(cnx, args) r = _evaluationEditor.create(cnx, args)
# news # news
M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
mod = sco_edit_module.do_module_list(args={"module_id": M["module_id"]})[0] mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
mod["moduleimpl_id"] = M["moduleimpl_id"] mod["moduleimpl_id"] = M["moduleimpl_id"]
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
sco_news.add( sco_news.add(
@ -333,7 +332,7 @@ def do_evaluation_edit(args):
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
_evaluationEditor.edit(cnx, args) _evaluationEditor.edit(cnx, args)
# inval cache pour ce semestre # inval cache pour ce semestre
M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"]) sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"])
@ -358,10 +357,10 @@ def do_evaluation_delete(evaluation_id):
_evaluationEditor.delete(cnx, evaluation_id) _evaluationEditor.delete(cnx, evaluation_id)
# inval cache pour ce semestre # inval cache pour ce semestre
M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"]) sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"])
# news # news
mod = sco_edit_module.do_module_list(args={"module_id": M["module_id"]})[0] mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
mod["moduleimpl_id"] = M["moduleimpl_id"] mod["moduleimpl_id"] = M["moduleimpl_id"]
mod["url"] = ( mod["url"] = (
scu.NotesURL() + "/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod scu.NotesURL() + "/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
@ -374,9 +373,6 @@ def do_evaluation_delete(evaluation_id):
) )
_DEE_TOT = 0
def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition=False): def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition=False):
"""donne infos sur l'etat du evaluation """donne infos sur l'etat du evaluation
{ nb_inscrits, nb_notes, nb_abs, nb_neutre, nb_att, { nb_inscrits, nb_notes, nb_abs, nb_neutre, nb_att,
@ -413,8 +409,8 @@ def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition=
last_modif = None last_modif = None
# ---- Liste des groupes complets et incomplets # ---- Liste des groupes complets et incomplets
E = do_evaluation_list(args={"evaluation_id": evaluation_id})[0] E = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
Mod = sco_edit_module.do_module_list(args={"module_id": M["module_id"]})[0] Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
is_malus = Mod["module_type"] == scu.MODULE_MALUS # True si module de malus is_malus = Mod["module_type"] == scu.MODULE_MALUS # True si module de malus
formsemestre_id = M["formsemestre_id"] formsemestre_id = M["formsemestre_id"]
# Si partition_id is None, prend 'all' ou bien la premiere: # Si partition_id is None, prend 'all' ou bien la premiere:
@ -713,7 +709,7 @@ def do_evaluation_etat_in_mod(nt, moduleimpl_id):
return etat return etat
def formsemestre_evaluations_cal(formsemestre_id, REQUEST=None): def formsemestre_evaluations_cal(formsemestre_id):
"""Page avec calendrier de toutes les evaluations de ce semestre""" """Page avec calendrier de toutes les evaluations de ce semestre"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > liste evaluations nt = sco_cache.NotesTableCache.get(formsemestre_id) # > liste evaluations
@ -736,7 +732,7 @@ def formsemestre_evaluations_cal(formsemestre_id, REQUEST=None):
if not e["jour"]: if not e["jour"]:
continue continue
day = e["jour"].strftime("%Y-%m-%d") day = e["jour"].strftime("%Y-%m-%d")
mod = sco_moduleimpl.do_moduleimpl_withmodule_list( mod = sco_moduleimpl.moduleimpl_withmodule_list(
moduleimpl_id=e["moduleimpl_id"] moduleimpl_id=e["moduleimpl_id"]
)[0] )[0]
txt = mod["module"]["code"] or mod["module"]["abbrev"] or "eval" txt = mod["module"]["code"] or mod["module"]["abbrev"] or "eval"
@ -780,7 +776,6 @@ def formsemestre_evaluations_cal(formsemestre_id, REQUEST=None):
H = [ H = [
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
REQUEST,
"Evaluations du semestre", "Evaluations du semestre",
sem, sem,
cssstyles=["css/calabs.css"], cssstyles=["css/calabs.css"],
@ -814,7 +809,7 @@ def evaluation_date_first_completion(evaluation_id):
# (pour avoir l'etat et le groupe) et aussi les inscriptions # (pour avoir l'etat et le groupe) et aussi les inscriptions
# au module (pour gerer les modules optionnels correctement) # au module (pour gerer les modules optionnels correctement)
# E = do_evaluation_list(args={"evaluation_id": evaluation_id})[0] # E = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
# M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] # M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
# formsemestre_id = M["formsemestre_id"] # formsemestre_id = M["formsemestre_id"]
# insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( formsemestre_id) # insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( formsemestre_id)
# insmod = sco_moduleimpl.do_moduleimpl_inscription_list(moduleimpl_id=E["moduleimpl_id"]) # insmod = sco_moduleimpl.do_moduleimpl_inscription_list(moduleimpl_id=E["moduleimpl_id"])
@ -844,9 +839,7 @@ def evaluation_date_first_completion(evaluation_id):
return max(date_premiere_note.values()) return max(date_premiere_note.values())
def formsemestre_evaluations_delai_correction( def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
formsemestre_id, format="html", REQUEST=None
):
"""Experimental: un tableau indiquant pour chaque évaluation """Experimental: un tableau indiquant pour chaque évaluation
le nombre de jours avant la publication des notes. le nombre de jours avant la publication des notes.
@ -858,8 +851,8 @@ def formsemestre_evaluations_delai_correction(
evals = nt.get_sem_evaluation_etat_list() evals = nt.get_sem_evaluation_etat_list()
T = [] T = []
for e in evals: for e in evals:
M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=e["moduleimpl_id"])[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=e["moduleimpl_id"])[0]
Mod = sco_edit_module.do_module_list(args={"module_id": M["module_id"]})[0] Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
if (e["evaluation_type"] != scu.EVALUATION_NORMALE) or ( if (e["evaluation_type"] != scu.EVALUATION_NORMALE) or (
Mod["module_type"] == scu.MODULE_MALUS Mod["module_type"] == scu.MODULE_MALUS
): ):
@ -915,13 +908,13 @@ def formsemestre_evaluations_delai_correction(
html_title="<h2>Correction des évaluations du semestre</h2>", html_title="<h2>Correction des évaluations du semestre</h2>",
caption="Correction des évaluations du semestre", caption="Correction des évaluations du semestre",
preferences=sco_preferences.SemPreferences(formsemestre_id), preferences=sco_preferences.SemPreferences(formsemestre_id),
base_url="%s?formsemestre_id=%s" % (REQUEST.URL0, formsemestre_id), base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id),
origin="Généré par %s le " % sco_version.SCONAME origin="Généré par %s le " % sco_version.SCONAME
+ scu.timedate_human_repr() + scu.timedate_human_repr()
+ "", + "",
filename=scu.make_filename("evaluations_delais_" + sem["titreannee"]), filename=scu.make_filename("evaluations_delais_" + sem["titreannee"]),
) )
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)
def module_evaluation_insert_before(ModEvals, next_eval): def module_evaluation_insert_before(ModEvals, next_eval):
@ -1039,8 +1032,8 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
E = do_evaluation_list({"evaluation_id": evaluation_id})[0] E = do_evaluation_list({"evaluation_id": evaluation_id})[0]
moduleimpl_id = E["moduleimpl_id"] moduleimpl_id = E["moduleimpl_id"]
M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
Mod = sco_edit_module.do_module_list(args={"module_id": M["module_id"]})[0] Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
formsemestre_id = M["formsemestre_id"] formsemestre_id = M["formsemestre_id"]
u = sco_users.user_info(M["responsable_id"]) u = sco_users.user_info(M["responsable_id"])
resp = u["prenomnom"] resp = u["prenomnom"]
@ -1069,7 +1062,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
] ]
if Mod["module_type"] == scu.MODULE_MALUS: if Mod["module_type"] == scu.MODULE_MALUS:
# Indique l'UE # Indique l'UE
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]
H.append("<p><b>UE : %(acronyme)s</b></p>" % ue) H.append("<p><b>UE : %(acronyme)s</b></p>" % ue)
# store min/max values used by JS client-side checks: # store min/max values used by JS client-side checks:
H.append( H.append(
@ -1077,21 +1070,34 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
) )
else: else:
# date et absences (pas pour evals de malus) # date et absences (pas pour evals de malus)
jour = E["jour"] or "<em>pas de date</em>"
H.append(
"<p>Réalisée le <b>%s</b> de %s à %s "
% (jour, E["heure_debut"], E["heure_fin"])
)
if E["jour"]: if E["jour"]:
jour = E["jour"]
H.append(
"<p>Réalisée le <b>%s</b> "
% (jour)
)
if E["heure_debut"] != E["heure_fin"]:
H.append(
"de %s à %s "
% (E["heure_debut"], E["heure_fin"])
)
group_id = sco_groups.get_default_group(formsemestre_id) group_id = sco_groups.get_default_group(formsemestre_id)
H.append( H.append(
'<span class="noprint"><a href="%s/Absences/EtatAbsencesDate?group_ids=%s&date=%s">(absences ce jour)</a></span>' f"""<span class="noprint"><a href="{url_for(
% ( 'absences.EtatAbsencesDate',
scu.ScoURL(), scodoc_dept=g.scodoc_dept,
group_id, group_ids=group_id,
six.moves.urllib.parse.quote(E["jour"], safe=""), date=E["jour"]
) )
}">(absences ce jour)</a></span>"""
) )
else:
jour = "<em>pas de date</em>"
H.append(
"<p>Réalisée le <b>%s</b> "
% (jour)
)
H.append( H.append(
'</p><p>Coefficient dans le module: <b>%s</b>, notes sur <span id="eval_note_max">%g</span> ' '</p><p>Coefficient dans le module: <b>%s</b>, notes sur <span id="eval_note_max">%g</span> '
% (E["coefficient"], E["note_max"]) % (E["coefficient"], E["note_max"])
@ -1110,7 +1116,6 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
def evaluation_create_form( def evaluation_create_form(
moduleimpl_id=None, moduleimpl_id=None,
evaluation_id=None, evaluation_id=None,
REQUEST=None,
edit=False, edit=False,
readonly=False, readonly=False,
page_title="Evaluation", page_title="Evaluation",
@ -1120,7 +1125,7 @@ def evaluation_create_form(
the_eval = do_evaluation_list({"evaluation_id": evaluation_id})[0] the_eval = do_evaluation_list({"evaluation_id": evaluation_id})[0]
moduleimpl_id = the_eval["moduleimpl_id"] moduleimpl_id = the_eval["moduleimpl_id"]
# #
M = sco_moduleimpl.do_moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0] M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0]
is_malus = M["module"]["module_type"] == scu.MODULE_MALUS # True si module de malus is_malus = M["module"]["module_type"] == scu.MODULE_MALUS # True si module de malus
formsemestre_id = M["formsemestre_id"] formsemestre_id = M["formsemestre_id"]
min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible
@ -1179,7 +1184,7 @@ def evaluation_create_form(
else: else:
min_note_max_str = "0" min_note_max_str = "0"
# #
Mod = sco_edit_module.do_module_list(args={"module_id": M["module_id"]})[0] Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
# #
help = """<div class="help"><p class="help"> help = """<div class="help"><p class="help">
Le coefficient d'une évaluation n'est utilisé que pour pondérer les évaluations au sein d'un module. Le coefficient d'une évaluation n'est utilisé que pour pondérer les évaluations au sein d'un module.
@ -1232,11 +1237,9 @@ def evaluation_create_form(
initvalues["visibulletinlist"] = ["X"] initvalues["visibulletinlist"] = ["X"]
else: else:
initvalues["visibulletinlist"] = [] initvalues["visibulletinlist"] = []
if ( vals = scu.get_request_args()
REQUEST.form.get("tf_submitted", False) if vals.get("tf_submitted", False) and "visibulletinlist" not in vals:
and "visibulletinlist" not in REQUEST.form vals["visibulletinlist"] = []
):
REQUEST.form["visibulletinlist"] = []
# #
form = [ form = [
("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}), ("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
@ -1346,8 +1349,8 @@ def evaluation_create_form(
), ),
] ]
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, vals,
form, form,
cancelbutton="Annuler", cancelbutton="Annuler",
submitlabel=submitlabel, submitlabel=submitlabel,
@ -1369,7 +1372,7 @@ def evaluation_create_form(
tf[2]["visibulletin"] = False tf[2]["visibulletin"] = False
if not edit: if not edit:
# creation d'une evaluation # creation d'une evaluation
evaluation_id = do_evaluation_create(REQUEST=REQUEST, **tf[2]) evaluation_id = do_evaluation_create(**tf[2])
return flask.redirect(dest_url) return flask.redirect(dest_url)
else: else:
do_evaluation_edit(tf[2]) do_evaluation_edit(tf[2])

View File

@ -35,11 +35,11 @@ from enum import Enum
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
import openpyxl.utils.datetime import openpyxl.utils.datetime
from openpyxl.styles.numbers import FORMAT_NUMBER_00, FORMAT_GENERAL, FORMAT_DATE_DDMMYY
from openpyxl.comments import Comment
from openpyxl import Workbook, load_workbook from openpyxl import Workbook, load_workbook
from openpyxl.cell import WriteOnlyCell from openpyxl.cell import WriteOnlyCell
from openpyxl.styles import Font, Border, Side, Alignment, PatternFill from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
from openpyxl.styles.numbers import FORMAT_NUMBER_00, FORMAT_GENERAL
from openpyxl.comments import Comment
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import notesdb from app.scodoc import notesdb
@ -59,31 +59,33 @@ class COLORS(Enum):
LIGHT_YELLOW = "FFFFFF99" LIGHT_YELLOW = "FFFFFF99"
def send_excel_file(request, data, filename, mime=scu.XLSX_MIMETYPE):
"""publication fichier.
(on ne doit rien avoir émis avant, car ici sont générés les entetes)
"""
filename = (
scu.unescape_html(scu.suppress_accents(filename))
.replace("&", "")
.replace(" ", "_")
)
request.RESPONSE.setHeader("content-type", mime)
request.RESPONSE.setHeader(
"content-disposition", 'attachment; filename="%s"' % filename
)
return data
# Un style est enregistré comme un dictionnaire qui précise la valeur d'un attributdans la liste suivante: # Un style est enregistré comme un dictionnaire qui précise la valeur d'un attributdans la liste suivante:
# font, border, number_format, fill, .. (cf https://openpyxl.readthedocs.io/en/stable/styles.html#working-with-styles) # font, border, number_format, fill,...
# (cf https://openpyxl.readthedocs.io/en/stable/styles.html#working-with-styles)
def xldate_as_datetime(xldate, datemode=0): def xldate_as_datetime(xldate, datemode=0):
"""Conversion d'une date Excel en date """Conversion d'une date Excel en datetime python
Deux formats de chaîne acceptés:
* JJ/MM/YYYY (chaîne naïve)
* Date ISO (valeur de type date lue dans la feuille)
Peut lever une ValueError Peut lever une ValueError
""" """
return openpyxl.utils.datetime.from_ISO8601(xldate) try:
return datetime.datetime.strptime(xldate, "%d/%m/%Y")
except:
return openpyxl.utils.datetime.from_ISO8601(xldate)
def adjust_sheetname(sheet_name):
"""Renvoie un nom convenable pour une feuille excel: < 31 cars, sans caractères spéciaux
Le / n'est pas autorisé par exemple.
Voir https://xlsxwriter.readthedocs.io/workbook.html#add_worksheet
"""
sheet_name = scu.make_filename(sheet_name)
# Le nom de la feuille ne peut faire plus de 31 caractères.
# si la taille du nom de feuille est > 31 on tronque (on pourrait remplacer par 'feuille' ?)
return sheet_name[:31]
class ScoExcelBook: class ScoExcelBook:
@ -98,13 +100,16 @@ class ScoExcelBook:
def __init__(self): def __init__(self):
self.sheets = [] # list of sheets self.sheets = [] # list of sheets
self.wb = Workbook(write_only=True)
def create_sheet(self, sheet_name="feuille", default_style=None): def create_sheet(self, sheet_name="feuille", default_style=None):
"""Crée une nouvelle feuille dans ce classeur """Crée une nouvelle feuille dans ce classeur
sheet_name -- le nom de la feuille sheet_name -- le nom de la feuille
default_style -- le style par défaut default_style -- le style par défaut
""" """
sheet = ScoExcelSheet(sheet_name, default_style) sheet_name = adjust_sheetname(sheet_name)
ws = self.wb.create_sheet(sheet_name)
sheet = ScoExcelSheet(sheet_name, default_style, ws)
self.sheets.append(sheet) self.sheets.append(sheet)
return sheet return sheet
@ -112,12 +117,12 @@ class ScoExcelBook:
"""génération d'un stream binaire représentant la totalité du classeur. """génération d'un stream binaire représentant la totalité du classeur.
retourne le flux retourne le flux
""" """
wb = Workbook(write_only=True)
for sheet in self.sheets: for sheet in self.sheets:
sheet.generate(self) sheet.prepare()
# construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream) # construction d'un flux
# (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream)
with NamedTemporaryFile() as tmp: with NamedTemporaryFile() as tmp:
wb.save(tmp.name) self.wb.save(tmp.name)
tmp.seek(0) tmp.seek(0)
return tmp.read() return tmp.read()
@ -125,6 +130,7 @@ class ScoExcelBook:
def excel_make_style( def excel_make_style(
bold=False, bold=False,
italic=False, italic=False,
outline=False,
color: COLORS = COLORS.BLACK, color: COLORS = COLORS.BLACK,
bgcolor: COLORS = None, bgcolor: COLORS = None,
halign=None, halign=None,
@ -145,7 +151,14 @@ def excel_make_style(
size -- taille de police size -- taille de police
""" """
style = {} style = {}
font = Font(name=font_name, bold=bold, italic=italic, color=color.value, size=size) font = Font(
name=font_name,
bold=bold,
italic=italic,
outline=outline,
color=color.value,
size=size,
)
style["font"] = font style["font"] = font
if bgcolor: if bgcolor:
style["fill"] = PatternFill(fill_type="solid", fgColor=bgcolor.value) style["fill"] = PatternFill(fill_type="solid", fgColor=bgcolor.value)
@ -182,58 +195,125 @@ class ScoExcelSheet:
""" """
def __init__(self, sheet_name="feuille", default_style=None, wb=None): def __init__(self, sheet_name="feuille", default_style=None, wb=None):
"""Création de la feuille. """Création de la feuille. sheet_name
sheet_name -- le nom de la feuille -- le nom de la feuille default_style
default_style -- le style par défaut des cellules -- le style par défaut des cellules ws
wb -- le WorkBook dans laquelle se trouve la feuille. Si wb est None (cas d'un classeur mono-feuille), -- None si la feuille est autonome (dans ce cas ell crée son propre wb), sinon c'est la worksheet
un workbook est crée et associé à cette feuille. créée par le workbook propriétaire un workbook est crée et associé à cette feuille.
""" """
# Le nom de la feuille ne peut faire plus de 31 caractères. # Le nom de la feuille ne peut faire plus de 31 caractères.
# si la taille du nom de feuille est > 31 on tronque (on pourrait remplacer par 'feuille' ?) # si la taille du nom de feuille est > 31 on tronque (on pourrait remplacer par 'feuille' ?)
self.sheet_name = sheet_name[ self.sheet_name = adjust_sheetname(sheet_name)
:31
] # if len(sheet_name) > 31: sheet_name = 'Feuille' ?
self.rows = [] # list of list of cells
# self.cells_styles_lico = {} # { (li,co) : style }
# self.cells_styles_li = {} # { li : style }
# self.cells_styles_co = {} # { co : style }
if default_style is None: if default_style is None:
default_style = excel_make_style() default_style = excel_make_style()
self.default_style = default_style self.default_style = default_style
self.wb = wb or Workbook(write_only=True) # Création de workbook si nécessaire if wb is None:
self.ws = self.wb.create_sheet(title=self.sheet_name) self.wb = Workbook()
self.ws = self.wb.active
self.ws.title = self.sheet_name
else:
self.wb = None
self.ws = wb
# internal data
self.rows = [] # list of list of cells
self.column_dimensions = {} self.column_dimensions = {}
self.row_dimensions = {}
def set_column_dimension_width(self, cle, value): def excel_make_composite_style(
"""Détermine la largeur d'une colonne. self,
cle -- identifie la colonne ("A"n "B", ...) alignment=None,
value -- la dimension (unité : 7 pixels comme affiché dans Excel) border=None,
fill=None,
number_format=None,
font=None,
):
style = {}
if font is not None:
style["font"] = font
if alignment is not None:
style["alignment"] = alignment
if border is not None:
style["border"] = border
if fill is not None:
style["fill"] = fill
if number_format is None:
style["number_format"] = FORMAT_GENERAL
else:
style["number_format"] = number_format
return style
@staticmethod
def i2col(idx):
if idx < 26: # one letter key
return chr(idx + 65)
else: # two letters AA..ZZ
first = (idx // 26) + 66
second = (idx % 26) + 65
return "" + chr(first) + chr(second)
def set_column_dimension_width(self, cle=None, value=21):
"""Détermine la largeur d'une colonne. cle -- identifie la colonne ("A" "B", ... ou 0, 1, 2, ...) si None,
value donne la liste des largeurs de colonnes depuis A, B, C, ... value -- la dimension (unité : 7 pixels
comme affiché dans Excel)
""" """
self.ws.column_dimensions[cle].width = value if cle is None:
for i, val in enumerate(value):
self.ws.column_dimensions[self.i2col(i)].width = val
# No keys: value is a list of widths
elif type(cle) == str: # accepts set_column_with("D", ...)
self.ws.column_dimensions[cle].width = value
else:
self.ws.column_dimensions[self.i2col(cle)].width = value
def set_column_dimension_hidden(self, cle, value): def set_row_dimension_height(self, cle=None, value=21):
"""Masque ou affiche une colonne. """Détermine la hauteur d'une ligne. cle -- identifie la ligne (1, 2, ...) si None,
cle -- identifie la colonne ("A"n "B", ...) value donne la liste des hauteurs de colonnes depuis 1, 2, 3, ... value -- la dimension
"""
if cle is None:
for i, val in enumerate(value, start=1):
self.ws.row_dimensions[i].height = val
# No keys: value is a list of widths
else:
self.ws.row_dimensions[cle].height = value
def set_row_dimension_hidden(self, cle, value):
"""Masque ou affiche une ligne.
cle -- identifie la colonne (1...)
value -- boolean (vrai = colonne cachée) value -- boolean (vrai = colonne cachée)
""" """
self.ws.column_dimensions[cle].hidden = value self.ws.row_dimensions[cle].hidden = value
def make_cell(self, value: any = None, style=None, comment=None): def make_cell(self, value: any = None, style=None, comment=None):
"""Construit une cellule. """Construit une cellule.
value -- contenu de la cellule (texte ou numérique) value -- contenu de la cellule (texte, numérique, booléen ou date)
style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié
""" """
cell = WriteOnlyCell(self.ws, value or "") # adapatation des valeurs si nécessaire
if not (isinstance(value, int) or isinstance(value, float)): if value is None:
cell.data_type = "s" value = ""
# if style is not None and "fill" in style: elif value is True:
# toto() value = 1
elif value is False:
value = 0
elif isinstance(value, datetime.datetime):
value = value.replace(
tzinfo=None
) # make date naive (cf https://openpyxl.readthedocs.io/en/latest/datetime.html#timezones)
# création de la cellule
cell = WriteOnlyCell(self.ws, value)
# recopie des styles
if style is None: if style is None:
style = self.default_style style = self.default_style
if "font" in style: if "font" in style:
cell.font = style["font"] cell.font = style["font"]
if "alignment" in style:
cell.alignment = style["alignment"]
if "border" in style: if "border" in style:
cell.border = style["border"] cell.border = style["border"]
if "fill" in style:
cell.fill = style["fill"]
if "number_format" in style: if "number_format" in style:
cell.number_format = style["number_format"] cell.number_format = style["number_format"]
if "fill" in style: if "fill" in style:
@ -245,6 +325,16 @@ class ScoExcelSheet:
lines = comment.splitlines() lines = comment.splitlines()
cell.comment.width = 7 * max([len(line) for line in lines]) cell.comment.width = 7 * max([len(line) for line in lines])
cell.comment.height = 20 * len(lines) cell.comment.height = 20 * len(lines)
# test datatype to overwrite datetime format
if isinstance(value, datetime.date):
cell.data_type = "d"
cell.number_format = FORMAT_DATE_DDMMYY
elif isinstance(value, int) or isinstance(value, float):
cell.data_type = "n"
else:
cell.data_type = "s"
return cell return cell
def make_row(self, values: list, style=None, comments=None): def make_row(self, values: list, style=None, comments=None):
@ -272,73 +362,31 @@ class ScoExcelSheet:
"""ajoute une ligne déjà construite à la feuille.""" """ajoute une ligne déjà construite à la feuille."""
self.rows.append(row) self.rows.append(row)
# def set_style(self, style=None, li=None, co=None): def prepare(self):
# if li is not None and co is not None:
# self.cells_styles_lico[(li, co)] = style
# elif li is None:
# self.cells_styles_li[li] = style
# elif co is None:
# self.cells_styles_co[co] = style
#
# def get_cell_style(self, li, co):
# """Get style for specified cell"""
# return (
# self.cells_styles_lico.get((li, co), None)
# or self.cells_styles_li.get(li, None)
# or self.cells_styles_co.get(co, None)
# or self.default_style
# )
def _generate_ws(self):
"""génére un flux décrivant la feuille. """génére un flux décrivant la feuille.
Ce flux pourra ensuite être repris dans send_excel_file (classeur mono feille) Ce flux pourra ensuite être repris dans send_excel_file (classeur mono feille)
ou pour la génération d'un classeur multi-feuilles ou pour la génération d'un classeur multi-feuilles
""" """
for col in self.column_dimensions.keys(): for row in self.column_dimensions.keys():
self.ws.column_dimensions[col] = self.column_dimensions[col] self.ws.column_dimensions[row] = self.column_dimensions[row]
for row in self.row_dimensions.keys():
self.ws.row_dimensions[row] = self.row_dimensions[row]
for row in self.rows: for row in self.rows:
self.ws.append(row) self.ws.append(row)
def generate_standalone(self): def generate(self):
"""génération d'un classeur mono-feuille""" """génération d'un classeur mono-feuille"""
self._generate_ws() # this method makes sense only if it is a standalone worksheet (else call workbook.generate()
if self.wb is None: # embeded sheet
raise ScoValueError("can't generate a single sheet from a ScoWorkbook")
# construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream) # construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream)
self.prepare()
with NamedTemporaryFile() as tmp: with NamedTemporaryFile() as tmp:
self.wb.save(tmp.name) self.wb.save(tmp.name)
tmp.seek(0) tmp.seek(0)
return tmp.read() return tmp.read()
def generate_embeded(self):
"""generation d'une feuille include dans un classeur multi-feuilles"""
self._generate_ws()
def gen_workbook(self, wb=None):
"""TODO: à remplacer"""
"""Generates and returns a workbook from stored data.
If wb, add a sheet (tab) to the existing workbook (in this case, returns None).
"""
if wb is None:
wb = Workbook() # Création du fichier
sauvegarde = True
else:
sauvegarde = False
ws0 = wb.add_sheet(self.sheet_name)
li = 0
for row in self.rows:
co = 0
for c in row:
# safety net: allow only str, int and float
# #py3 #sco8 A revoir lors de la ré-écriture de ce module
# XXX if type(c) not in (IntType, FloatType):
# c = str(c).decode(scu.SCO_ENCODING)
ws0.write(li, co, c, self.get_cell_style(li, co))
co += 1
li += 1
if sauvegarde:
return wb.savetostr()
else:
return None
def excel_simple_table( def excel_simple_table(
titles=None, lines=None, sheet_name=b"feuille", titles_styles=None, comments=None titles=None, lines=None, sheet_name=b"feuille", titles_styles=None, comments=None
@ -377,7 +425,7 @@ def excel_simple_table(
cell_style = text_style cell_style = text_style
cells.append(ws.make_cell(it, cell_style)) cells.append(ws.make_cell(it, cell_style))
ws.append_row(cells) ws.append_row(cells)
return ws.generate_standalone() return ws.generate()
def excel_feuille_saisie(e, titreannee, description, lines): def excel_feuille_saisie(e, titreannee, description, lines):
@ -538,19 +586,33 @@ def excel_feuille_saisie(e, titreannee, description, lines):
ws.make_cell("cellule vide -> note non modifiée", style_expl), ws.make_cell("cellule vide -> note non modifiée", style_expl),
] ]
) )
return ws.generate_standalone() return ws.generate()
def excel_bytes_to_list(bytes_content): def excel_bytes_to_list(bytes_content):
filelike = io.BytesIO(bytes_content) try:
return _excel_to_list(filelike) filelike = io.BytesIO(bytes_content)
return _excel_to_list(filelike)
except:
raise ScoValueError(
"""Le fichier xlsx attendu n'est pas lisible !
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ..)
"""
)
def excel_file_to_list(filename): def excel_file_to_list(filename):
return _excel_to_list(filename) try:
return _excel_to_list(filename)
except:
raise ScoValueError(
"""Le fichier xlsx attendu n'est pas lisible !
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...)
"""
)
def _excel_to_list(filelike): # we may need 'encoding' argument ? def _excel_to_list(filelike):
"""returns list of list """returns list of list
convert_to_string is a conversion function applied to all non-string values (ie numbers) convert_to_string is a conversion function applied to all non-string values (ie numbers)
""" """
@ -558,7 +620,8 @@ def _excel_to_list(filelike): # we may need 'encoding' argument ?
wb = load_workbook(filename=filelike, read_only=True, data_only=True) wb = load_workbook(filename=filelike, read_only=True, data_only=True)
except: except:
log("Excel_to_list: failure to import document") log("Excel_to_list: failure to import document")
open("/tmp/last_scodoc_import_failure" + scu.XLSX_SUFFIX, "wb").write(filelike) with open("/tmp/last_scodoc_import_failure" + scu.XLSX_SUFFIX, "wb") as f:
f.write(filelike)
raise ScoValueError( raise ScoValueError(
"Fichier illisible: assurez-vous qu'il s'agit bien d'un document Excel !" "Fichier illisible: assurez-vous qu'il s'agit bien d'un document Excel !"
) )
@ -758,4 +821,4 @@ def excel_feuille_listeappel(
cell_2 = ws.make_cell(("Liste éditée le " + dt), style1i) cell_2 = ws.make_cell(("Liste éditée le " + dt), style1i)
ws.append_row([None, cell_2]) ws.append_row([None, cell_2])
return ws.generate_standalone() return ws.generate()

View File

@ -45,21 +45,15 @@ class InvalidEtudId(NoteProcessError):
pass pass
class AccessDenied(ScoException):
pass
class InvalidNoteValue(ScoException): class InvalidNoteValue(ScoException):
pass pass
# Exception qui stoque dest_url, utilisee dans Zope standard_error_message # Exception qui stoque dest_url, utilisee dans Zope standard_error_message
class ScoValueError(ScoException): class ScoValueError(ScoException):
def __init__(self, msg, dest_url=None, REQUEST=None): def __init__(self, msg, dest_url=None):
ScoException.__init__(self, msg) ScoException.__init__(self, msg)
self.dest_url = dest_url self.dest_url = dest_url
if REQUEST and dest_url:
REQUEST.set("dest_url", dest_url)
class FormatError(ScoValueError): class FormatError(ScoValueError):
@ -79,7 +73,7 @@ class ScoConfigurationError(ScoValueError):
class ScoLockedFormError(ScoException): class ScoLockedFormError(ScoException):
def __init__(self, msg="", REQUEST=None): def __init__(self, msg=""):
msg = ( msg = (
"Cette formation est verrouillée (car il y a un semestre verrouillé qui s'y réfère). " "Cette formation est verrouillée (car il y a un semestre verrouillé qui s'y réfère). "
+ str(msg) + str(msg)
@ -90,10 +84,14 @@ class ScoLockedFormError(ScoException):
class ScoGenError(ScoException): class ScoGenError(ScoException):
"exception avec affichage d'une page explicative ad-hoc" "exception avec affichage d'une page explicative ad-hoc"
def __init__(self, msg="", REQUEST=None): def __init__(self, msg=""):
ScoException.__init__(self, msg) ScoException.__init__(self, msg)
class AccessDenied(ScoGenError):
pass
class ScoInvalidDateError(ScoValueError): class ScoInvalidDateError(ScoValueError):
pass pass

View File

@ -27,7 +27,7 @@
"""Export d'une table avec les résultats de tous les étudiants """Export d'une table avec les résultats de tous les étudiants
""" """
from flask import url_for, g from flask import url_for, g, request
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -216,9 +216,7 @@ def get_set_formsemestre_id_dates(start_date, end_date):
return {x["id"] for x in s} return {x["id"] for x in s}
def scodoc_table_results( def scodoc_table_results(start_date="", end_date="", types_parcours=[], format="html"):
start_date="", end_date="", types_parcours=[], format="html", REQUEST=None
):
"""Page affichant la table des résultats """Page affichant la table des résultats
Les dates sont en dd/mm/yyyy (datepicker javascript) Les dates sont en dd/mm/yyyy (datepicker javascript)
types_parcours est la liste des types de parcours à afficher types_parcours est la liste des types de parcours à afficher
@ -240,15 +238,13 @@ def scodoc_table_results(
start_date_iso, end_date_iso, types_parcours start_date_iso, end_date_iso, types_parcours
) )
tab.base_url = "%s?start_date=%s&end_date=%s&types_parcours=%s" % ( tab.base_url = "%s?start_date=%s&end_date=%s&types_parcours=%s" % (
REQUEST.URL0, request.base_url,
start_date, start_date,
end_date, end_date,
"&types_parcours=".join([str(x) for x in types_parcours]), "&types_parcours=".join([str(x) for x in types_parcours]),
) )
if format != "html": if format != "html":
return tab.make_page( return tab.make_page(format=format, with_html_headers=False)
format=format, with_html_headers=False, REQUEST=REQUEST
)
tab_html = tab.html() tab_html = tab.html()
nb_rows = tab.get_nb_rows() nb_rows = tab.get_nb_rows()
else: else:

View File

@ -136,11 +136,11 @@ def search_etud_in_dept(expnom=""):
vals = {} vals = {}
url_args = {"scodoc_dept": g.scodoc_dept} url_args = {"scodoc_dept": g.scodoc_dept}
if "dest_url" in request.form: if "dest_url" in vals:
endpoint = request.form["dest_url"] endpoint = vals["dest_url"]
else: else:
endpoint = "scolar.ficheEtud" endpoint = "scolar.ficheEtud"
if "parameters_keys" in request.form: if "parameters_keys" in vals:
for key in vals["parameters_keys"].split(","): for key in vals["parameters_keys"].split(","):
url_args[key] = vals[key] url_args[key] = vals[key]
@ -362,7 +362,7 @@ def table_etud_in_accessible_depts(expnom=None):
) )
def search_inscr_etud_by_nip(code_nip, REQUEST=None, format="json"): def search_inscr_etud_by_nip(code_nip, format="json"):
"""Recherche multi-departement d'un étudiant par son code NIP """Recherche multi-departement d'un étudiant par son code NIP
Seuls les départements accessibles par l'utilisateur sont cherchés. Seuls les départements accessibles par l'utilisateur sont cherchés.
@ -404,6 +404,4 @@ def search_inscr_etud_by_nip(code_nip, REQUEST=None, format="json"):
) )
tab = GenTable(columns_ids=columns_ids, rows=T) tab = GenTable(columns_ids=columns_ids, rows=T)
return tab.make_page( return tab.make_page(format=format, with_html_headers=False, publish=True)
format=format, with_html_headers=False, REQUEST=REQUEST, publish=True
)

View File

@ -31,7 +31,8 @@ from operator import itemgetter
import xml.dom.minidom import xml.dom.minidom
import flask import flask
from flask import g, url_for from flask import g, url_for, request
from flask_login import current_user
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -93,13 +94,20 @@ def formation_has_locked_sems(formation_id):
def formation_export( def formation_export(
formation_id, export_ids=False, export_tags=True, format=None, REQUEST=None formation_id,
export_ids=False,
export_tags=True,
export_external_ues=False,
format=None,
): ):
"""Get a formation, with UE, matieres, modules """Get a formation, with UE, matieres, modules
in desired format in desired format
""" """
F = formation_list(args={"formation_id": formation_id})[0] F = formation_list(args={"formation_id": formation_id})[0]
ues = sco_edit_ue.do_ue_list({"formation_id": formation_id}) selector = {"formation_id": formation_id}
if not export_external_ues:
selector["is_external"] = False
ues = sco_edit_ue.ue_list(selector)
F["ue"] = ues F["ue"] = ues
for ue in ues: for ue in ues:
ue_id = ue["ue_id"] ue_id = ue["ue_id"]
@ -108,14 +116,14 @@ def formation_export(
del ue["formation_id"] del ue["formation_id"]
if ue["ects"] is None: if ue["ects"] is None:
del ue["ects"] del ue["ects"]
mats = sco_edit_matiere.do_matiere_list({"ue_id": ue_id}) mats = sco_edit_matiere.matiere_list({"ue_id": ue_id})
ue["matiere"] = mats ue["matiere"] = mats
for mat in mats: for mat in mats:
matiere_id = mat["matiere_id"] matiere_id = mat["matiere_id"]
if not export_ids: if not export_ids:
del mat["matiere_id"] del mat["matiere_id"]
del mat["ue_id"] del mat["ue_id"]
mods = sco_edit_module.do_module_list({"matiere_id": matiere_id}) mods = sco_edit_module.module_list({"matiere_id": matiere_id})
mat["module"] = mods mat["module"] = mods
for mod in mods: for mod in mods:
if export_tags: if export_tags:
@ -132,7 +140,7 @@ def formation_export(
del mod["ects"] del mod["ects"]
return scu.sendResult( return scu.sendResult(
REQUEST, F, name="formation", format=format, force_outer_xml_tag=False F, name="formation", format=format, force_outer_xml_tag=False, attached=True
) )
@ -162,20 +170,18 @@ def formation_import_xml(doc: str, import_tags=True):
D = sco_xml.xml_to_dicts(f) D = sco_xml.xml_to_dicts(f)
assert D[0] == "formation" assert D[0] == "formation"
F = D[1] F = D[1]
F_quoted = F.copy() # F_quoted = F.copy()
log("F=%s" % F) # ndb.quote_dict(F_quoted)
ndb.quote_dict(F_quoted) F["dept_id"] = g.scodoc_dept_id
log("F_quoted=%s" % F_quoted)
# find new version number # find new version number
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
log(
"select max(version) from notes_formations where acronyme=%(acronyme)s and titre=%(titre)s"
% F_quoted
)
cursor.execute( cursor.execute(
"select max(version) from notes_formations where acronyme=%(acronyme)s and titre=%(titre)s", """SELECT max(version)
F_quoted, FROM notes_formations
WHERE acronyme=%(acronyme)s and titre=%(titre)s and dept_id=%(dept_id)s
""",
F,
) )
res = cursor.fetchall() res = cursor.fetchall()
try: try:
@ -196,7 +202,7 @@ def formation_import_xml(doc: str, import_tags=True):
assert ue_info[0] == "ue" assert ue_info[0] == "ue"
ue_info[1]["formation_id"] = formation_id ue_info[1]["formation_id"] = formation_id
if "ue_id" in ue_info[1]: if "ue_id" in ue_info[1]:
xml_ue_id = ue_info[1]["ue_id"] xml_ue_id = int(ue_info[1]["ue_id"])
del ue_info[1]["ue_id"] del ue_info[1]["ue_id"]
else: else:
xml_ue_id = None xml_ue_id = None
@ -212,7 +218,7 @@ def formation_import_xml(doc: str, import_tags=True):
for mod_info in mat_info[2]: for mod_info in mat_info[2]:
assert mod_info[0] == "module" assert mod_info[0] == "module"
if "module_id" in mod_info[1]: if "module_id" in mod_info[1]:
xml_module_id = mod_info[1]["module_id"] xml_module_id = int(mod_info[1]["module_id"])
del mod_info[1]["module_id"] del mod_info[1]["module_id"]
else: else:
xml_module_id = None xml_module_id = None
@ -230,7 +236,7 @@ def formation_import_xml(doc: str, import_tags=True):
return formation_id, modules_old2new, ues_old2new return formation_id, modules_old2new, ues_old2new
def formation_list_table(formation_id=None, args={}, REQUEST=None): def formation_list_table(formation_id=None, args={}):
"""List formation, grouped by titre and sorted by versions """List formation, grouped by titre and sorted by versions
and listing associated semestres and listing associated semestres
returns a table returns a table
@ -247,7 +253,7 @@ def formation_list_table(formation_id=None, args={}, REQUEST=None):
"edit_img", border="0", alt="modifier", title="Modifier titres et code" "edit_img", border="0", alt="modifier", title="Modifier titres et code"
) )
editable = REQUEST.AUTHENTICATED_USER.has_permission(Permission.ScoChangeFormation) editable = current_user.has_permission(Permission.ScoChangeFormation)
# Traduit/ajoute des champs à afficher: # Traduit/ajoute des champs à afficher:
for f in formations: for f in formations:
@ -257,7 +263,11 @@ def formation_list_table(formation_id=None, args={}, REQUEST=None):
).NAME ).NAME
except: except:
f["parcours_name"] = "" f["parcours_name"] = ""
f["_titre_target"] = "ue_list?formation_id=%(formation_id)s" % f f["_titre_target"] = url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(f["formation_id"]),
)
f["_titre_link_class"] = "stdlink" f["_titre_link_class"] = "stdlink"
f["_titre_id"] = "titre-%s" % f["acronyme"].lower().replace(" ", "-") f["_titre_id"] = "titre-%s" % f["acronyme"].lower().replace(" ", "-")
# Ajoute les semestres associés à chaque formation: # Ajoute les semestres associés à chaque formation:
@ -347,17 +357,18 @@ def formation_list_table(formation_id=None, args={}, REQUEST=None):
html_class="formation_list_table table_leftalign", html_class="formation_list_table table_leftalign",
html_with_td_classes=True, html_with_td_classes=True,
html_sortable=True, html_sortable=True,
base_url="%s?formation_id=%s" % (REQUEST.URL0, formation_id), base_url="%s?formation_id=%s" % (request.base_url, formation_id),
page_title=title, page_title=title,
pdf_title=title, pdf_title=title,
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
def formation_create_new_version(formation_id, redirect=True, REQUEST=None): def formation_create_new_version(formation_id, redirect=True):
"duplicate formation, with new version number" "duplicate formation, with new version number"
xml = formation_export(formation_id, export_ids=True, format="xml") resp = formation_export(formation_id, export_ids=True, format="xml")
new_id, modules_old2new, ues_old2new = formation_import_xml(xml) xml_data = resp.get_data(as_text=True)
new_id, modules_old2new, ues_old2new = formation_import_xml(xml_data)
# news # news
F = formation_list(args={"formation_id": new_id})[0] F = formation_list(args={"formation_id": new_id})[0]
sco_news.add( sco_news.add(
@ -368,7 +379,7 @@ def formation_create_new_version(formation_id, redirect=True, REQUEST=None):
if redirect: if redirect:
return flask.redirect( return flask.redirect(
url_for( url_for(
"notes.ue_list", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=new_id, formation_id=new_id,
msg="Nouvelle version !", msg="Nouvelle version !",

View File

@ -31,7 +31,7 @@ from app.scodoc.sco_exceptions import ScoValueError
import time import time
from operator import itemgetter from operator import itemgetter
from flask import g from flask import g, request
import app import app
from app.models import Departement from app.models import Departement
@ -61,6 +61,7 @@ _formsemestreEditor = ndb.EditableTable(
"gestion_semestrielle", "gestion_semestrielle",
"etat", "etat",
"bul_hide_xml", "bul_hide_xml",
"block_moyennes",
"bul_bgcolor", "bul_bgcolor",
"modalite", "modalite",
"resp_can_edit", "resp_can_edit",
@ -68,7 +69,6 @@ _formsemestreEditor = ndb.EditableTable(
"ens_can_edit_eval", "ens_can_edit_eval",
"elt_sem_apo", "elt_sem_apo",
"elt_annee_apo", "elt_annee_apo",
"scodoc7_id",
), ),
filter_dept=True, filter_dept=True,
sortkey="date_debut", sortkey="date_debut",
@ -82,6 +82,7 @@ _formsemestreEditor = ndb.EditableTable(
"etat": bool, "etat": bool,
"gestion_compensation": bool, "gestion_compensation": bool,
"bul_hide_xml": bool, "bul_hide_xml": bool,
"block_moyennes": bool,
"gestion_semestrielle": bool, "gestion_semestrielle": bool,
"gestion_compensation": bool, "gestion_compensation": bool,
"gestion_semestrielle": bool, "gestion_semestrielle": bool,
@ -92,18 +93,21 @@ _formsemestreEditor = ndb.EditableTable(
) )
def get_formsemestre(formsemestre_id): def get_formsemestre(formsemestre_id, raise_soft_exc=False):
"list ONE formsemestre" "list ONE formsemestre"
if formsemestre_id in g.stored_get_formsemestre:
return g.stored_get_formsemestre[formsemestre_id]
if not isinstance(formsemestre_id, int): if not isinstance(formsemestre_id, int):
raise ScoValueError( raise ValueError("formsemestre_id must be an integer !")
"""Semestre invalide, reprenez l'opération au départ ou si le problème persiste signalez l'erreur sur scodoc-devel@listes.univ-paris13.fr""" sems = do_formsemestre_list(args={"formsemestre_id": formsemestre_id})
) if not sems:
try:
sem = do_formsemestre_list(args={"formsemestre_id": formsemestre_id})[0]
return sem
except:
log("get_formsemestre: invalid formsemestre_id (%s)" % formsemestre_id) log("get_formsemestre: invalid formsemestre_id (%s)" % formsemestre_id)
raise if raise_soft_exc:
raise ScoValueError(f"semestre {formsemestre_id} inconnu !")
else:
raise ValueError(f"semestre {formsemestre_id} inconnu !")
g.stored_get_formsemestre[formsemestre_id] = sems[0]
return sems[0]
def do_formsemestre_list(*a, **kw): def do_formsemestre_list(*a, **kw):
@ -242,7 +246,7 @@ def do_formsemestre_create(args, silent=False):
default=True, default=True,
redirect=0, redirect=0,
) )
_group_id = sco_groups.createGroup(partition_id, default=True) _group_id = sco_groups.create_group(partition_id, default=True)
# news # news
if "titre" not in args: if "titre" not in args:
@ -565,7 +569,7 @@ def list_formsemestre_by_etape(etape_apo=False, annee_scolaire=False):
return sems return sems
def view_formsemestre_by_etape(etape_apo=None, format="html", REQUEST=None): def view_formsemestre_by_etape(etape_apo=None, format="html"):
"""Affiche table des semestres correspondants à l'étape""" """Affiche table des semestres correspondants à l'étape"""
if etape_apo: if etape_apo:
html_title = ( html_title = (
@ -582,8 +586,8 @@ def view_formsemestre_by_etape(etape_apo=None, format="html", REQUEST=None):
Etape: <input name="etape_apo" type="text" size="8"></input> Etape: <input name="etape_apo" type="text" size="8"></input>
</form>""", </form>""",
) )
tab.base_url = "%s?etape_apo=%s" % (REQUEST.URL0, etape_apo or "") tab.base_url = "%s?etape_apo=%s" % (request.base_url, etape_apo or "")
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)
def sem_has_etape(sem, code_etape): def sem_has_etape(sem, code_etape):

View File

@ -28,7 +28,7 @@
"""Menu "custom" (défini par l'utilisateur) dans les semestres """Menu "custom" (défini par l'utilisateur) dans les semestres
""" """
import flask import flask
from flask import g, url_for from flask import g, url_for, request
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -77,16 +77,14 @@ def formsemestre_custommenu_html(formsemestre_id):
return htmlutils.make_menu("Liens", menu) return htmlutils.make_menu("Liens", menu)
def formsemestre_custommenu_edit(formsemestre_id, REQUEST=None): def formsemestre_custommenu_edit(formsemestre_id):
"""Dialog to edit the custom menu""" """Dialog to edit the custom menu"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
dest_url = ( dest_url = (
scu.NotesURL() + "/formsemestre_status?formsemestre_id=%s" % formsemestre_id scu.NotesURL() + "/formsemestre_status?formsemestre_id=%s" % formsemestre_id
) )
H = [ H = [
html_sco_header.html_sem_header( html_sco_header.html_sem_header("Modification du menu du semestre ", sem),
REQUEST, "Modification du menu du semestre ", sem
),
"""<p class="help">Ce menu, spécifique à chaque semestre, peut être utilisé pour placer des liens vers vos applications préférées.</p> """<p class="help">Ce menu, spécifique à chaque semestre, peut être utilisé pour placer des liens vers vos applications préférées.</p>
<p class="help">Procédez en plusieurs fois si vous voulez ajouter plusieurs items.</p>""", <p class="help">Procédez en plusieurs fois si vous voulez ajouter plusieurs items.</p>""",
] ]
@ -119,8 +117,8 @@ def formsemestre_custommenu_edit(formsemestre_id, REQUEST=None):
initvalues["title_" + str(item["custommenu_id"])] = item["title"] initvalues["title_" + str(item["custommenu_id"])] = item["title"]
initvalues["url_" + str(item["custommenu_id"])] = item["url"] initvalues["url_" + str(item["custommenu_id"])] = item["url"]
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
descr, descr,
initvalues=initvalues, initvalues=initvalues,
cancelbutton="Annuler", cancelbutton="Annuler",

View File

@ -28,7 +28,7 @@
"""Form choix modules / responsables et creation formsemestre """Form choix modules / responsables et creation formsemestre
""" """
import flask import flask
from flask import url_for, g from flask import url_for, g, request
from flask_login import current_user from flask_login import current_user
from app.auth.models import User from app.auth.models import User
@ -51,6 +51,7 @@ from app.scodoc import sco_etud
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc import sco_formations from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups_copy
from app.scodoc import sco_modalites from app.scodoc import sco_modalites
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_parcours_dut from app.scodoc import sco_parcours_dut
@ -58,7 +59,6 @@ from app.scodoc import sco_permissions_check
from app.scodoc import sco_portal_apogee from app.scodoc import sco_portal_apogee
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_users from app.scodoc import sco_users
import six
def _default_sem_title(F): def _default_sem_title(F):
@ -66,7 +66,7 @@ def _default_sem_title(F):
return F["titre"] return F["titre"]
def formsemestre_createwithmodules(REQUEST=None): def formsemestre_createwithmodules():
"""Page création d'un semestre""" """Page création d'un semestre"""
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
@ -77,7 +77,7 @@ def formsemestre_createwithmodules(REQUEST=None):
), ),
"""<h2>Mise en place d'un semestre de formation</h2>""", """<h2>Mise en place d'un semestre de formation</h2>""",
] ]
r = do_formsemestre_createwithmodules(REQUEST=REQUEST) r = do_formsemestre_createwithmodules()
if isinstance(r, str): if isinstance(r, str):
H.append(r) H.append(r)
else: else:
@ -85,13 +85,12 @@ def formsemestre_createwithmodules(REQUEST=None):
return "\n".join(H) + html_sco_header.sco_footer() return "\n".join(H) + html_sco_header.sco_footer()
def formsemestre_editwithmodules(REQUEST, formsemestre_id): def formsemestre_editwithmodules(formsemestre_id):
"""Page modification semestre""" """Page modification semestre"""
# portage from dtml # portage from dtml
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [ H = [
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
REQUEST,
"Modification du semestre", "Modification du semestre",
sem, sem,
javascripts=["libjs/AutoSuggest.js"], javascripts=["libjs/AutoSuggest.js"],
@ -105,12 +104,13 @@ def formsemestre_editwithmodules(REQUEST, formsemestre_id):
% scu.icontag("lock_img", border="0", title="Semestre verrouillé") % scu.icontag("lock_img", border="0", title="Semestre verrouillé")
) )
else: else:
r = do_formsemestre_createwithmodules(REQUEST=REQUEST, edit=1) r = do_formsemestre_createwithmodules(edit=1)
if isinstance(r, str): if isinstance(r, str):
H.append(r) H.append(r)
else: else:
return r # response redirect return r # response redirect
if not REQUEST.form.get("tf_submitted", False): vals = scu.get_request_args()
if not vals.get("tf_submitted", False):
H.append( H.append(
"""<p class="help">Seuls les modules cochés font partie de ce semestre. Pour les retirer, les décocher et appuyer sur le bouton "modifier". """<p class="help">Seuls les modules cochés font partie de ce semestre. Pour les retirer, les décocher et appuyer sur le bouton "modifier".
</p> </p>
@ -121,7 +121,7 @@ def formsemestre_editwithmodules(REQUEST, formsemestre_id):
return "\n".join(H) + html_sco_header.sco_footer() return "\n".join(H) + html_sco_header.sco_footer()
def can_edit_sem(REQUEST, formsemestre_id="", sem=None): def can_edit_sem(formsemestre_id="", sem=None):
"""Return sem if user can edit it, False otherwise""" """Return sem if user can edit it, False otherwise"""
sem = sem or sco_formsemestre.get_formsemestre(formsemestre_id) sem = sem or sco_formsemestre.get_formsemestre(formsemestre_id)
if not current_user.has_permission(Permission.ScoImplement): # pas chef if not current_user.has_permission(Permission.ScoImplement): # pas chef
@ -130,11 +130,12 @@ def can_edit_sem(REQUEST, formsemestre_id="", sem=None):
return sem return sem
def do_formsemestre_createwithmodules(REQUEST=None, edit=False): def do_formsemestre_createwithmodules(edit=False):
"Form choix modules / responsables et creation formsemestre" "Form choix modules / responsables et creation formsemestre"
# Fonction accessible à tous, controle acces à la main: # Fonction accessible à tous, controle acces à la main:
vals = scu.get_request_args()
if edit: if edit:
formsemestre_id = int(REQUEST.form["formsemestre_id"]) formsemestre_id = int(vals["formsemestre_id"])
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if not current_user.has_permission(Permission.ScoImplement): if not current_user.has_permission(Permission.ScoImplement):
if not edit: if not edit:
@ -156,21 +157,21 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
uid2display[u.id] = u.get_nomplogin() uid2display[u.id] = u.get_nomplogin()
allowed_user_names = list(uid2display.values()) + [""] allowed_user_names = list(uid2display.values()) + [""]
# #
formation_id = int(REQUEST.form["formation_id"]) formation_id = int(vals["formation_id"])
F = sco_formations.formation_list(args={"formation_id": formation_id}) F = sco_formations.formation_list(args={"formation_id": formation_id})
if not F: if not F:
raise ScoValueError("Formation inexistante !") raise ScoValueError("Formation inexistante !")
F = F[0] F = F[0]
if not edit: if not edit:
initvalues = {"titre": _default_sem_title(F)} initvalues = {"titre": _default_sem_title(F)}
semestre_id = int(REQUEST.form["semestre_id"]) semestre_id = int(vals["semestre_id"])
sem_module_ids = set() sem_module_ids = set()
else: else:
# setup form init values # setup form init values
initvalues = sem initvalues = sem
semestre_id = initvalues["semestre_id"] semestre_id = initvalues["semestre_id"]
# add associated modules to tf-checked: # add associated modules to tf-checked:
ams = sco_moduleimpl.do_moduleimpl_list(formsemestre_id=formsemestre_id) ams = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
sem_module_ids = set([x["module_id"] for x in ams]) sem_module_ids = set([x["module_id"] for x in ams])
initvalues["tf-checked"] = ["MI" + str(x["module_id"]) for x in ams] initvalues["tf-checked"] = ["MI" + str(x["module_id"]) for x in ams]
for x in ams: for x in ams:
@ -204,11 +205,11 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
# on pourrait faire un simple module_list( ) # on pourrait faire un simple module_list( )
# mais si on veut l'ordre du PPN (groupe par UE et matieres) il faut: # mais si on veut l'ordre du PPN (groupe par UE et matieres) il faut:
mods = [] # liste de dicts mods = [] # liste de dicts
uelist = sco_edit_ue.do_ue_list({"formation_id": formation_id}) uelist = sco_edit_ue.ue_list({"formation_id": formation_id})
for ue in uelist: for ue in uelist:
matlist = sco_edit_matiere.do_matiere_list({"ue_id": ue["ue_id"]}) matlist = sco_edit_matiere.matiere_list({"ue_id": ue["ue_id"]})
for mat in matlist: for mat in matlist:
modsmat = sco_edit_module.do_module_list({"matiere_id": mat["matiere_id"]}) modsmat = sco_edit_module.module_list({"matiere_id": mat["matiere_id"]})
# XXX debug checks # XXX debug checks
for m in modsmat: for m in modsmat:
if m["ue_id"] != ue["ue_id"]: if m["ue_id"] != ue["ue_id"]:
@ -309,7 +310,9 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
{ {
"size": 40, "size": 40,
"title": "Nom de ce semestre", "title": "Nom de ce semestre",
"explanation": """n'indiquez pas les dates, ni le semestre, ni la modalité dans le titre: ils seront automatiquement ajoutés <input type="button" value="remettre titre par défaut" onClick="document.tf.titre.value='%s';"/>""" "explanation": """n'indiquez pas les dates, ni le semestre, ni la modalité dans
le titre: ils seront automatiquement ajoutés <input type="button"
value="remettre titre par défaut" onClick="document.tf.titre.value='%s';"/>"""
% _default_sem_title(F), % _default_sem_title(F),
}, },
), ),
@ -501,6 +504,14 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
"labels": [""], "labels": [""],
}, },
), ),
(
"block_moyennes",
{
"input_type": "boolcheckbox",
"title": "Bloquer moyennes",
"explanation": "empêcher le calcul des moyennes d'UE et générale.",
},
),
( (
"sep", "sep",
{ {
@ -534,7 +545,7 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
select_name = "%s!group_id" % mod["module_id"] select_name = "%s!group_id" % mod["module_id"]
def opt_selected(gid): def opt_selected(gid):
if gid == REQUEST.form.get(select_name): if gid == vals.get(select_name):
return "selected" return "selected"
else: else:
return "" return ""
@ -623,38 +634,29 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
initvalues["gestion_compensation_lst"] = ["X"] initvalues["gestion_compensation_lst"] = ["X"]
else: else:
initvalues["gestion_compensation_lst"] = [] initvalues["gestion_compensation_lst"] = []
if ( if vals.get("tf_submitted", False) and "gestion_compensation_lst" not in vals:
REQUEST.form.get("tf_submitted", False) vals["gestion_compensation_lst"] = []
and "gestion_compensation_lst" not in REQUEST.form
):
REQUEST.form["gestion_compensation_lst"] = []
initvalues["gestion_semestrielle"] = initvalues.get("gestion_semestrielle", False) initvalues["gestion_semestrielle"] = initvalues.get("gestion_semestrielle", False)
if initvalues["gestion_semestrielle"]: if initvalues["gestion_semestrielle"]:
initvalues["gestion_semestrielle_lst"] = ["X"] initvalues["gestion_semestrielle_lst"] = ["X"]
else: else:
initvalues["gestion_semestrielle_lst"] = [] initvalues["gestion_semestrielle_lst"] = []
if ( if vals.get("tf_submitted", False) and "gestion_semestrielle_lst" not in vals:
REQUEST.form.get("tf_submitted", False) vals["gestion_semestrielle_lst"] = []
and "gestion_semestrielle_lst" not in REQUEST.form
):
REQUEST.form["gestion_semestrielle_lst"] = []
initvalues["bul_hide_xml"] = initvalues.get("bul_hide_xml", False) initvalues["bul_hide_xml"] = initvalues.get("bul_hide_xml", False)
if not initvalues["bul_hide_xml"]: if not initvalues["bul_hide_xml"]:
initvalues["bul_publish_xml_lst"] = ["X"] initvalues["bul_publish_xml_lst"] = ["X"]
else: else:
initvalues["bul_publish_xml_lst"] = [] initvalues["bul_publish_xml_lst"] = []
if ( if vals.get("tf_submitted", False) and "bul_publish_xml_lst" not in vals:
REQUEST.form.get("tf_submitted", False) vals["bul_publish_xml_lst"] = []
and "bul_publish_xml_lst" not in REQUEST.form
):
REQUEST.form["bul_publish_xml_lst"] = []
# #
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, vals,
modform, modform,
submitlabel=submitlabel, submitlabel=submitlabel,
cancelbutton="Annuler", cancelbutton="Annuler",
@ -673,7 +675,7 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
if tf[0] == 0 or msg: if tf[0] == 0 or msg:
return ( return (
'<p>Formation <a class="discretelink" href="ue_list?formation_id=%(formation_id)s"><em>%(titre)s</em> (%(acronyme)s), version %(version)s, code %(formation_code)s</a></p>' '<p>Formation <a class="discretelink" href="ue_table?formation_id=%(formation_id)s"><em>%(titre)s</em> (%(acronyme)s), version %(version)s, code %(formation_code)s</a></p>'
% F % F
+ msg + msg
+ str(tf[1]) + str(tf[1])
@ -693,7 +695,6 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
tf[2]["bul_hide_xml"] = False tf[2]["bul_hide_xml"] = False
else: else:
tf[2]["bul_hide_xml"] = True tf[2]["bul_hide_xml"] = True
# remap les identifiants de responsables: # remap les identifiants de responsables:
tf[2]["responsable_id"] = User.get_user_id_from_nomplogin( tf[2]["responsable_id"] = User.get_user_id_from_nomplogin(
tf[2]["responsable_id"] tf[2]["responsable_id"]
@ -750,7 +751,7 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
# (retire le "MI" du début du nom de champs) # (retire le "MI" du début du nom de champs)
checkedmods = [int(x[2:]) for x in tf[2]["tf-checked"]] checkedmods = [int(x[2:]) for x in tf[2]["tf-checked"]]
sco_formsemestre.do_formsemestre_edit(tf[2]) sco_formsemestre.do_formsemestre_edit(tf[2])
ams = sco_moduleimpl.do_moduleimpl_list(formsemestre_id=formsemestre_id) ams = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
existingmods = [x["module_id"] for x in ams] existingmods = [x["module_id"] for x in ams]
mods_tocreate = [x for x in checkedmods if not x in existingmods] mods_tocreate = [x for x in checkedmods if not x in existingmods]
# modules a existants a modifier # modules a existants a modifier
@ -766,7 +767,7 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
"responsable_id": tf[2]["MI" + str(module_id)], "responsable_id": tf[2]["MI" + str(module_id)],
} }
moduleimpl_id = sco_moduleimpl.do_moduleimpl_create(modargs) moduleimpl_id = sco_moduleimpl.do_moduleimpl_create(modargs)
mod = sco_edit_module.do_module_list({"module_id": module_id})[0] mod = sco_edit_module.module_list({"module_id": module_id})[0]
msg += ["création de %s (%s)" % (mod["code"], mod["titre"])] msg += ["création de %s (%s)" % (mod["code"], mod["titre"])]
# INSCRIPTIONS DES ETUDIANTS # INSCRIPTIONS DES ETUDIANTS
log( log(
@ -786,7 +787,6 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
moduleimpl_id, moduleimpl_id,
formsemestre_id, formsemestre_id,
etudids, etudids,
REQUEST=REQUEST,
) )
msg += [ msg += [
"inscription de %d étudiants au module %s" "inscription de %d étudiants au module %s"
@ -801,7 +801,7 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
ok, diag = formsemestre_delete_moduleimpls(formsemestre_id, mods_todelete) ok, diag = formsemestre_delete_moduleimpls(formsemestre_id, mods_todelete)
msg += diag msg += diag
for module_id in mods_toedit: for module_id in mods_toedit:
moduleimpl_id = sco_moduleimpl.do_moduleimpl_list( moduleimpl_id = sco_moduleimpl.moduleimpl_list(
formsemestre_id=formsemestre_id, module_id=module_id formsemestre_id=formsemestre_id, module_id=module_id
)[0]["moduleimpl_id"] )[0]["moduleimpl_id"]
modargs = { modargs = {
@ -813,7 +813,7 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
sco_moduleimpl.do_moduleimpl_edit( sco_moduleimpl.do_moduleimpl_edit(
modargs, formsemestre_id=formsemestre_id modargs, formsemestre_id=formsemestre_id
) )
mod = sco_edit_module.do_module_list({"module_id": module_id})[0] mod = sco_edit_module.module_list({"module_id": module_id})[0]
if msg: if msg:
msg_html = ( msg_html = (
@ -846,10 +846,10 @@ def formsemestre_delete_moduleimpls(formsemestre_id, module_ids_to_del):
msg = [] msg = []
for module_id in module_ids_to_del: for module_id in module_ids_to_del:
# get id # get id
moduleimpl_id = sco_moduleimpl.do_moduleimpl_list( moduleimpl_id = sco_moduleimpl.moduleimpl_list(
formsemestre_id=formsemestre_id, module_id=module_id formsemestre_id=formsemestre_id, module_id=module_id
)[0]["moduleimpl_id"] )[0]["moduleimpl_id"]
mod = sco_edit_module.do_module_list({"module_id": module_id})[0] mod = sco_edit_module.module_list({"module_id": module_id})[0]
# Evaluations dans ce module ? # Evaluations dans ce module ?
evals = sco_evaluations.do_evaluation_list({"moduleimpl_id": moduleimpl_id}) evals = sco_evaluations.do_evaluation_list({"moduleimpl_id": moduleimpl_id})
if evals: if evals:
@ -867,7 +867,7 @@ def formsemestre_delete_moduleimpls(formsemestre_id, module_ids_to_del):
return ok, msg return ok, msg
def formsemestre_clone(formsemestre_id, REQUEST=None): def formsemestre_clone(formsemestre_id):
""" """
Formulaire clonage d'un semestre Formulaire clonage d'un semestre
""" """
@ -888,7 +888,6 @@ def formsemestre_clone(formsemestre_id, REQUEST=None):
H = [ H = [
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
REQUEST,
"Copie du semestre", "Copie du semestre",
sem, sem,
javascripts=["libjs/AutoSuggest.js"], javascripts=["libjs/AutoSuggest.js"],
@ -959,8 +958,8 @@ def formsemestre_clone(formsemestre_id, REQUEST=None):
), ),
] ]
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
descr, descr,
submitlabel="Dupliquer ce semestre", submitlabel="Dupliquer ce semestre",
cancelbutton="Annuler", cancelbutton="Annuler",
@ -985,7 +984,6 @@ def formsemestre_clone(formsemestre_id, REQUEST=None):
tf[2]["date_fin"], tf[2]["date_fin"],
clone_evaluations=tf[2]["clone_evaluations"], clone_evaluations=tf[2]["clone_evaluations"],
clone_partitions=tf[2]["clone_partitions"], clone_partitions=tf[2]["clone_partitions"],
REQUEST=REQUEST,
) )
return flask.redirect( return flask.redirect(
"formsemestre_status?formsemestre_id=%s&head_message=Nouveau%%20semestre%%20créé" "formsemestre_status?formsemestre_id=%s&head_message=Nouveau%%20semestre%%20créé"
@ -1000,7 +998,6 @@ def do_formsemestre_clone(
date_fin, # 'dd/mm/yyyy' date_fin, # 'dd/mm/yyyy'
clone_evaluations=False, clone_evaluations=False,
clone_partitions=False, clone_partitions=False,
REQUEST=None,
): ):
"""Clone a semestre: make copy, same modules, same options, same resps, same partitions. """Clone a semestre: make copy, same modules, same options, same resps, same partitions.
New dates, responsable_id New dates, responsable_id
@ -1018,7 +1015,7 @@ def do_formsemestre_clone(
formsemestre_id = sco_formsemestre.do_formsemestre_create(args) formsemestre_id = sco_formsemestre.do_formsemestre_create(args)
log("created formsemestre %s" % formsemestre_id) log("created formsemestre %s" % formsemestre_id)
# 2- create moduleimpls # 2- create moduleimpls
mods_orig = sco_moduleimpl.do_moduleimpl_list(formsemestre_id=orig_formsemestre_id) mods_orig = sco_moduleimpl.moduleimpl_list(formsemestre_id=orig_formsemestre_id)
for mod_orig in mods_orig: for mod_orig in mods_orig:
args = mod_orig.copy() args = mod_orig.copy()
args["formsemestre_id"] = formsemestre_id args["formsemestre_id"] = formsemestre_id
@ -1040,7 +1037,7 @@ def do_formsemestre_clone(
args = e.copy() args = e.copy()
del args["jour"] # erase date del args["jour"] # erase date
args["moduleimpl_id"] = mid args["moduleimpl_id"] = mid
_ = sco_evaluations.do_evaluation_create(REQUEST=REQUEST, **args) _ = sco_evaluations.do_evaluation_create(**args)
# 3- copy uecoefs # 3- copy uecoefs
objs = sco_formsemestre.formsemestre_uecoef_list( objs = sco_formsemestre.formsemestre_uecoef_list(
@ -1077,32 +1074,11 @@ def do_formsemestre_clone(
args["formsemestre_id"] = formsemestre_id args["formsemestre_id"] = formsemestre_id
_ = sco_compute_moy.formsemestre_ue_computation_expr_create(cnx, args) _ = sco_compute_moy.formsemestre_ue_computation_expr_create(cnx, args)
# 5- Copy partitions # 5- Copy partitions and groups
if clone_partitions: if clone_partitions:
listgroups = [] sco_groups_copy.clone_partitions_and_groups(
listnamegroups = [] orig_formsemestre_id, formsemestre_id
# Création des partitions: )
for part in sco_groups.get_partitions_list(orig_formsemestre_id):
if part["partition_name"] != None:
partname = part["partition_name"]
new_partition_id = sco_groups.partition_create(
formsemestre_id,
partition_name=partname,
redirect=0,
)
for g in sco_groups.get_partition_groups(part):
if g["group_name"] != None:
listnamegroups.append(g["group_name"])
listgroups.append([new_partition_id, listnamegroups])
listnamegroups = []
# Création des groupes dans les nouvelles partitions:
for newpart in sco_groups.get_partitions_list(formsemestre_id):
for g in listgroups:
if newpart["partition_id"] == g[0]:
part_id = g[0]
for group_name in g[1]:
_ = sco_groups.createGroup(part_id, group_name=group_name)
return formsemestre_id return formsemestre_id
@ -1113,10 +1089,11 @@ def do_formsemestre_clone(
def formsemestre_associate_new_version( def formsemestre_associate_new_version(
formsemestre_id, formsemestre_id,
other_formsemestre_ids=[], other_formsemestre_ids=[],
REQUEST=None,
dialog_confirmed=False, dialog_confirmed=False,
): ):
"""Formulaire changement formation d'un semestre""" """Formulaire changement formation d'un semestre"""
formsemestre_id = int(formsemestre_id)
other_formsemestre_ids = [int(x) for x in other_formsemestre_ids]
if not dialog_confirmed: if not dialog_confirmed:
# dresse le liste des semestres de la meme formation et version # dresse le liste des semestres de la meme formation et version
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
@ -1161,15 +1138,19 @@ def formsemestre_associate_new_version(
) )
else: else:
do_formsemestres_associate_new_version( do_formsemestres_associate_new_version(
[formsemestre_id] + other_formsemestre_ids, REQUEST=REQUEST [formsemestre_id] + other_formsemestre_ids
) )
return flask.redirect( return flask.redirect(
"formsemestre_status?formsemestre_id=%s&head_message=Formation%%20dupliquée" url_for(
% formsemestre_id "notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
head_message="Formation dupliquée",
)
) )
def do_formsemestres_associate_new_version(formsemestre_ids, REQUEST=None): def do_formsemestres_associate_new_version(formsemestre_ids):
"""Cree une nouvelle version de la formation du semestre, et y rattache les semestres. """Cree une nouvelle version de la formation du semestre, et y rattache les semestres.
Tous les moduleimpl sont -associés à la nouvelle formation, ainsi que les decisions de jury Tous les moduleimpl sont -associés à la nouvelle formation, ainsi que les decisions de jury
si elles existent (codes d'UE validées). si elles existent (codes d'UE validées).
@ -1179,9 +1160,11 @@ def do_formsemestres_associate_new_version(formsemestre_ids, REQUEST=None):
if not formsemestre_ids: if not formsemestre_ids:
return return
# Check: tous de la même formation # Check: tous de la même formation
assert isinstance(formsemestre_ids[0], int)
sem = sco_formsemestre.get_formsemestre(formsemestre_ids[0]) sem = sco_formsemestre.get_formsemestre(formsemestre_ids[0])
formation_id = sem["formation_id"] formation_id = sem["formation_id"]
for formsemestre_id in formsemestre_ids[1:]: for formsemestre_id in formsemestre_ids[1:]:
assert isinstance(formsemestre_id, int)
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if formation_id != sem["formation_id"]: if formation_id != sem["formation_id"]:
raise ScoValueError("les semestres ne sont pas tous de la même formation !") raise ScoValueError("les semestres ne sont pas tous de la même formation !")
@ -1192,9 +1175,7 @@ def do_formsemestres_associate_new_version(formsemestre_ids, REQUEST=None):
formation_id, formation_id,
modules_old2new, modules_old2new,
ues_old2new, ues_old2new,
) = sco_formations.formation_create_new_version( ) = sco_formations.formation_create_new_version(formation_id, redirect=False)
formation_id, redirect=False, REQUEST=REQUEST
)
for formsemestre_id in formsemestre_ids: for formsemestre_id in formsemestre_ids:
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
@ -1210,7 +1191,7 @@ def _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new)
et met à jour les décisions de jury (validations d'UE). et met à jour les décisions de jury (validations d'UE).
""" """
# re-associate moduleimpls to new modules: # re-associate moduleimpls to new modules:
modimpls = sco_moduleimpl.do_moduleimpl_list(formsemestre_id=formsemestre_id) modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
for mod in modimpls: for mod in modimpls:
mod["module_id"] = modules_old2new[mod["module_id"]] mod["module_id"] = modules_old2new[mod["module_id"]]
sco_moduleimpl.do_moduleimpl_edit(mod, formsemestre_id=formsemestre_id) sco_moduleimpl.do_moduleimpl_edit(mod, formsemestre_id=formsemestre_id)
@ -1230,12 +1211,12 @@ def _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new)
sco_parcours_dut.scolar_formsemestre_validation_edit(cnx, e) sco_parcours_dut.scolar_formsemestre_validation_edit(cnx, e)
def formsemestre_delete(formsemestre_id, REQUEST=None): def formsemestre_delete(formsemestre_id):
"""Delete a formsemestre (affiche avertissements)""" """Delete a formsemestre (affiche avertissements)"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
H = [ H = [
html_sco_header.html_sem_header(REQUEST, "Suppression du semestre", sem), html_sco_header.html_sem_header("Suppression du semestre", sem),
"""<div class="ue_warning"><span>Attention !</span> """<div class="ue_warning"><span>Attention !</span>
<p class="help">A n'utiliser qu'en cas d'erreur lors de la saisie d'une formation. Normalement, <p class="help">A n'utiliser qu'en cas d'erreur lors de la saisie d'une formation. Normalement,
<b>un semestre ne doit jamais être supprimé</b> (on perd la mémoire des notes et de tous les événements liés à ce semestre !).</p> <b>un semestre ne doit jamais être supprimé</b> (on perd la mémoire des notes et de tous les événements liés à ce semestre !).</p>
@ -1261,8 +1242,8 @@ def formsemestre_delete(formsemestre_id, REQUEST=None):
else: else:
submit_label = "Confirmer la suppression du semestre" submit_label = "Confirmer la suppression du semestre"
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
(("formsemestre_id", {"input_type": "hidden"}),), (("formsemestre_id", {"input_type": "hidden"}),),
initvalues=F, initvalues=F,
submitlabel=submit_label, submitlabel=submit_label,
@ -1327,7 +1308,7 @@ def do_formsemestre_delete(formsemestre_id):
sco_cache.EvaluationCache.invalidate_sem(formsemestre_id) sco_cache.EvaluationCache.invalidate_sem(formsemestre_id)
# --- Destruction des modules de ce semestre # --- Destruction des modules de ce semestre
mods = sco_moduleimpl.do_moduleimpl_list(formsemestre_id=formsemestre_id) mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
for mod in mods: for mod in mods:
# evaluations # evaluations
evals = sco_evaluations.do_evaluation_list( evals = sco_evaluations.do_evaluation_list(
@ -1421,7 +1402,7 @@ def do_formsemestre_delete(formsemestre_id):
# --------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------
def formsemestre_edit_options(formsemestre_id, target_url=None, REQUEST=None): def formsemestre_edit_options(formsemestre_id):
"""dialog to change formsemestre options """dialog to change formsemestre options
(accessible par ScoImplement ou dir. etudes) (accessible par ScoImplement ou dir. etudes)
""" """
@ -1429,12 +1410,10 @@ def formsemestre_edit_options(formsemestre_id, target_url=None, REQUEST=None):
ok, err = sco_permissions_check.check_access_diretud(formsemestre_id) ok, err = sco_permissions_check.check_access_diretud(formsemestre_id)
if not ok: if not ok:
return err return err
return sco_preferences.SemPreferences(formsemestre_id).edit( return sco_preferences.SemPreferences(formsemestre_id).edit(categories=["bul"])
REQUEST=REQUEST, categories=["bul"]
)
def formsemestre_change_lock(formsemestre_id, REQUEST=None, dialog_confirmed=False): def formsemestre_change_lock(formsemestre_id) -> None:
"""Change etat (verrouille si ouvert, déverrouille si fermé) """Change etat (verrouille si ouvert, déverrouille si fermé)
nota: etat (1 ouvert, 0 fermé) nota: etat (1 ouvert, 0 fermé)
""" """
@ -1444,34 +1423,12 @@ def formsemestre_change_lock(formsemestre_id, REQUEST=None, dialog_confirmed=Fal
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
etat = not sem["etat"] etat = not sem["etat"]
if REQUEST and not dialog_confirmed:
if etat:
msg = "déverrouillage"
else:
msg = "verrouillage"
return scu.confirm_dialog(
"<h2>Confirmer le %s du semestre ?</h2>" % msg,
helpmsg="""Les notes d'un semestre verrouillé ne peuvent plus être modifiées.
Un semestre verrouillé peut cependant être déverrouillé facilement à tout moment
(par son responsable ou un administrateur).
<br/>
Le programme d'une formation qui a un semestre verrouillé ne peut plus être modifié.
""",
dest_url="",
cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id,
parameters={"formsemestre_id": formsemestre_id},
)
args = {"formsemestre_id": formsemestre_id, "etat": etat} args = {"formsemestre_id": formsemestre_id, "etat": etat}
sco_formsemestre.do_formsemestre_edit(args) sco_formsemestre.do_formsemestre_edit(args)
if REQUEST:
return flask.redirect(
"formsemestre_status?formsemestre_id=%s" % formsemestre_id
)
def formsemestre_change_publication_bul( def formsemestre_change_publication_bul(
formsemestre_id, REQUEST=None, dialog_confirmed=False formsemestre_id, dialog_confirmed=False, redirect=True
): ):
"""Change etat publication bulletins sur portail""" """Change etat publication bulletins sur portail"""
ok, err = sco_permissions_check.check_access_diretud(formsemestre_id) ok, err = sco_permissions_check.check_access_diretud(formsemestre_id)
@ -1480,7 +1437,7 @@ def formsemestre_change_publication_bul(
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
etat = not sem["bul_hide_xml"] etat = not sem["bul_hide_xml"]
if REQUEST and not dialog_confirmed: if not dialog_confirmed:
if etat: if etat:
msg = "non" msg = "non"
else: else:
@ -1499,14 +1456,14 @@ def formsemestre_change_publication_bul(
args = {"formsemestre_id": formsemestre_id, "bul_hide_xml": etat} args = {"formsemestre_id": formsemestre_id, "bul_hide_xml": etat}
sco_formsemestre.do_formsemestre_edit(args) sco_formsemestre.do_formsemestre_edit(args)
if REQUEST: if redirect:
return flask.redirect( return flask.redirect(
"formsemestre_status?formsemestre_id=%s" % formsemestre_id "formsemestre_status?formsemestre_id=%s" % formsemestre_id
) )
return None return None
def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None, REQUEST=None): def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
"""Changement manuel des coefficients des UE capitalisées.""" """Changement manuel des coefficients des UE capitalisées."""
from app.scodoc import notes_table from app.scodoc import notes_table
@ -1538,9 +1495,7 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None, REQUEST=None):
</p> </p>
""" """
H = [ H = [
html_sco_header.html_sem_header( html_sco_header.html_sem_header("Coefficients des UE du semestre", sem),
REQUEST, "Coefficients des UE du semestre", sem
),
help, help,
] ]
# #
@ -1576,8 +1531,8 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None, REQUEST=None):
form.append(("ue_" + str(ue["ue_id"]), descr)) form.append(("ue_" + str(ue["ue_id"]), descr))
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
form, form,
submitlabel="Changer les coefficients", submitlabel="Changer les coefficients",
cancelbutton="Annuler", cancelbutton="Annuler",
@ -1652,9 +1607,7 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None, REQUEST=None):
formsemestre_id=formsemestre_id formsemestre_id=formsemestre_id
) # > modif coef UE cap (modifs notes de _certains_ etudiants) ) # > modif coef UE cap (modifs notes de _certains_ etudiants)
header = html_sco_header.html_sem_header( header = html_sco_header.html_sem_header("Coefficients des UE du semestre", sem)
REQUEST, "Coefficients des UE du semestre", sem
)
return ( return (
header header
+ "\n".join(z) + "\n".join(z)

View File

@ -34,7 +34,7 @@ Ces semestres n'auront qu'un seul inscrit !
import time import time
import flask import flask
from flask import url_for, g from flask import url_for, g, request
from flask_login import current_user from flask_login import current_user
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -52,7 +52,7 @@ from app.scodoc import sco_parcours_dut
from app.scodoc import sco_etud from app.scodoc import sco_etud
def formsemestre_ext_create(etudid, sem_params, REQUEST=None): def formsemestre_ext_create(etudid, sem_params):
"""Crée un formsemestre exterieur et y inscrit l'étudiant. """Crée un formsemestre exterieur et y inscrit l'étudiant.
sem_params: dict nécessaire à la création du formsemestre sem_params: dict nécessaire à la création du formsemestre
""" """
@ -79,7 +79,7 @@ def formsemestre_ext_create(etudid, sem_params, REQUEST=None):
return formsemestre_id return formsemestre_id
def formsemestre_ext_create_form(etudid, formsemestre_id, REQUEST=None): def formsemestre_ext_create_form(etudid, formsemestre_id):
"""Formulaire creation/inscription à un semestre extérieur""" """Formulaire creation/inscription à un semestre extérieur"""
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
H = [ H = [
@ -181,8 +181,8 @@ def formsemestre_ext_create_form(etudid, formsemestre_id, REQUEST=None):
] ]
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
descr, descr,
cancelbutton="Annuler", cancelbutton="Annuler",
method="post", method="post",
@ -204,13 +204,13 @@ def formsemestre_ext_create_form(etudid, formsemestre_id, REQUEST=None):
) )
else: else:
tf[2]["formation_id"] = orig_sem["formation_id"] tf[2]["formation_id"] = orig_sem["formation_id"]
formsemestre_ext_create(etudid, tf[2], REQUEST=REQUEST) formsemestre_ext_create(etudid, tf[2])
return flask.redirect( return flask.redirect(
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
) )
def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid, REQUEST=None): def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid):
"""Edition des validations d'UE et de semestre (jury) """Edition des validations d'UE et de semestre (jury)
pour un semestre extérieur. pour un semestre extérieur.
On peut saisir pour chaque UE du programme de formation On peut saisir pour chaque UE du programme de formation
@ -221,18 +221,17 @@ def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid, REQUEST=None):
""" """
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
ue_list = _list_ue_with_coef_and_validations(sem, etudid) ues = _list_ue_with_coef_and_validations(sem, etudid)
descr = _ue_form_description(ue_list, REQUEST.form) descr = _ue_form_description(ues, scu.get_request_args())
if REQUEST and REQUEST.method == "GET": if request.method == "GET":
initvalues = { initvalues = {
"note_" + str(ue["ue_id"]): ue["validation"].get("moy_ue", "") "note_" + str(ue["ue_id"]): ue["validation"].get("moy_ue", "") for ue in ues
for ue in ue_list
} }
else: else:
initvalues = {} initvalues = {}
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
descr, descr,
cssclass="tf_ext_edit_ue_validations", cssclass="tf_ext_edit_ue_validations",
submitlabel="Enregistrer ces validations", submitlabel="Enregistrer ces validations",
@ -242,27 +241,25 @@ def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid, REQUEST=None):
if tf[0] == -1: if tf[0] == -1:
return "<h4>annulation</h4>" return "<h4>annulation</h4>"
else: else:
H = _make_page(etud, sem, tf, REQUEST=REQUEST) H = _make_page(etud, sem, tf)
if tf[0] == 0: # premier affichage if tf[0] == 0: # premier affichage
return "\n".join(H) return "\n".join(H)
else: # soumission else: # soumission
# simule erreur # simule erreur
ok, message = _check_values(ue_list, tf[2]) ok, message = _check_values(ues, tf[2])
if not ok: if not ok:
H = _make_page(etud, sem, tf, message=message, REQUEST=REQUEST) H = _make_page(etud, sem, tf, message=message)
return "\n".join(H) return "\n".join(H)
else: else:
# Submit # Submit
_record_ue_validations_and_coefs( _record_ue_validations_and_coefs(formsemestre_id, etudid, ues, tf[2])
formsemestre_id, etudid, ue_list, tf[2], REQUEST=REQUEST
)
return flask.redirect( return flask.redirect(
"formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" "formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s"
% (formsemestre_id, etudid) % (formsemestre_id, etudid)
) )
def _make_page(etud, sem, tf, message="", REQUEST=None): def _make_page(etud, sem, tf, message=""):
nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"]) nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"])
moy_gen = nt.get_etud_moy_gen(etud["etudid"]) moy_gen = nt.get_etud_moy_gen(etud["etudid"])
H = [ H = [
@ -303,7 +300,7 @@ _UE_VALID_CODES = {
} }
def _ue_form_description(ue_list, values): def _ue_form_description(ues, values):
"""Description du formulaire de saisie des UE / validations """Description du formulaire de saisie des UE / validations
Pour chaque UE, on peut saisir: son code jury, sa note, son coefficient. Pour chaque UE, on peut saisir: son code jury, sa note, son coefficient.
""" """
@ -320,7 +317,7 @@ def _ue_form_description(ue_list, values):
("formsemestre_id", {"input_type": "hidden"}), ("formsemestre_id", {"input_type": "hidden"}),
("etudid", {"input_type": "hidden"}), ("etudid", {"input_type": "hidden"}),
] ]
for ue in ue_list: for ue in ues:
# Menu pour code validation UE: # Menu pour code validation UE:
# Ne propose que ADM, CMP et "Non inscrit" # Ne propose que ADM, CMP et "Non inscrit"
select_name = "valid_" + str(ue["ue_id"]) select_name = "valid_" + str(ue["ue_id"])
@ -439,8 +436,8 @@ def _list_ue_with_coef_and_validations(sem, etudid):
""" """
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
formsemestre_id = sem["formsemestre_id"] formsemestre_id = sem["formsemestre_id"]
ue_list = sco_edit_ue.do_ue_list({"formation_id": sem["formation_id"]}) ues = sco_edit_ue.ue_list({"formation_id": sem["formation_id"]})
for ue in ue_list: for ue in ues:
# add coefficient # add coefficient
uecoef = sco_formsemestre.formsemestre_uecoef_list( uecoef = sco_formsemestre.formsemestre_uecoef_list(
cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue["ue_id"]} cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue["ue_id"]}
@ -462,13 +459,11 @@ def _list_ue_with_coef_and_validations(sem, etudid):
ue["validation"] = validation[0] ue["validation"] = validation[0]
else: else:
ue["validation"] = {} ue["validation"] = {}
return ue_list return ues
def _record_ue_validations_and_coefs( def _record_ue_validations_and_coefs(formsemestre_id, etudid, ues, values):
formsemestre_id, etudid, ue_list, values, REQUEST=None for ue in ues:
):
for ue in ue_list:
code = values.get("valid_" + str(ue["ue_id"]), False) code = values.get("valid_" + str(ue["ue_id"]), False)
if code == "None": if code == "None":
code = None code = None
@ -492,5 +487,4 @@ def _record_ue_validations_and_coefs(
now_dmy, now_dmy,
code=code, code=code,
ue_coefficient=coef, ue_coefficient=coef,
REQUEST=REQUEST,
) )

View File

@ -30,12 +30,12 @@
import time import time
import flask import flask
from flask import url_for, g from flask import url_for, g, request
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import log
from app.scodoc.scolog import logdb from app.scodoc.scolog import logdb
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoException, ScoValueError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_codes_parcours import UE_STANDARD, UE_SPORT, UE_TYPE_NAME from app.scodoc.sco_codes_parcours import UE_STANDARD, UE_SPORT, UE_TYPE_NAME
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -55,6 +55,7 @@ _formsemestre_inscriptionEditor = ndb.EditableTable(
"formsemestre_inscription_id", "formsemestre_inscription_id",
("formsemestre_inscription_id", "etudid", "formsemestre_id", "etat", "etape"), ("formsemestre_inscription_id", "etudid", "formsemestre_id", "etat", "etape"),
sortkey="formsemestre_id", sortkey="formsemestre_id",
insert_ignore_conflicts=True,
) )
@ -126,6 +127,42 @@ def do_formsemestre_inscription_delete(oid, formsemestre_id=None):
) # > desinscription du semestre ) # > desinscription du semestre
def do_formsemestre_demission(
etudid,
formsemestre_id,
event_date=None,
etat_new="D", # 'D' or DEF
operation_method="demEtudiant",
event_type="DEMISSION",
):
"Démission ou défaillance d'un étudiant"
# marque 'D' ou DEF dans l'inscription au semestre et ajoute
# un "evenement" scolarite
cnx = ndb.GetDBConnexion()
# check lock
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if not sem["etat"]:
raise ScoValueError("Modification impossible: semestre verrouille")
#
ins = do_formsemestre_inscription_list(
{"etudid": etudid, "formsemestre_id": formsemestre_id}
)[0]
if not ins:
raise ScoException("etudiant non inscrit ?!")
ins["etat"] = etat_new
do_formsemestre_inscription_edit(args=ins, formsemestre_id=formsemestre_id)
logdb(cnx, method=operation_method, etudid=etudid)
sco_etud.scolar_events_create(
cnx,
args={
"etudid": etudid,
"event_date": event_date,
"formsemestre_id": formsemestre_id,
"event_type": event_type,
},
)
def do_formsemestre_inscription_edit(args=None, formsemestre_id=None): def do_formsemestre_inscription_edit(args=None, formsemestre_id=None):
"edit a formsemestre_inscription" "edit a formsemestre_inscription"
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
@ -236,7 +273,7 @@ def do_formsemestre_inscription_with_modules(
gdone[group_id] = 1 gdone[group_id] = 1
# inscription a tous les modules de ce semestre # inscription a tous les modules de ce semestre
modimpls = sco_moduleimpl.do_moduleimpl_withmodule_list( modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id formsemestre_id=formsemestre_id
) )
for mod in modimpls: for mod in modimpls:
@ -248,7 +285,7 @@ def do_formsemestre_inscription_with_modules(
def formsemestre_inscription_with_modules_etud( def formsemestre_inscription_with_modules_etud(
formsemestre_id, etudid=None, group_ids=None, REQUEST=None formsemestre_id, etudid=None, group_ids=None
): ):
"""Form. inscription d'un étudiant au semestre. """Form. inscription d'un étudiant au semestre.
Si etudid n'est pas specifié, form. choix etudiant. Si etudid n'est pas specifié, form. choix etudiant.
@ -263,7 +300,7 @@ def formsemestre_inscription_with_modules_etud(
) )
return formsemestre_inscription_with_modules( return formsemestre_inscription_with_modules(
etudid, formsemestre_id, REQUEST=REQUEST, group_ids=group_ids etudid, formsemestre_id, group_ids=group_ids
) )
@ -318,7 +355,7 @@ def formsemestre_inscription_with_modules_form(etudid, only_ext=False):
def formsemestre_inscription_with_modules( def formsemestre_inscription_with_modules(
etudid, formsemestre_id, group_ids=None, multiple_ok=False, REQUEST=None etudid, formsemestre_id, group_ids=None, multiple_ok=False
): ):
""" """
Inscription de l'etud dans ce semestre. Inscription de l'etud dans ce semestre.
@ -334,7 +371,6 @@ def formsemestre_inscription_with_modules(
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
H = [ H = [
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
REQUEST,
"Inscription de %s dans ce semestre" % etud["nomprenom"], "Inscription de %s dans ce semestre" % etud["nomprenom"],
sem, sem,
) )
@ -415,7 +451,7 @@ def formsemestre_inscription_with_modules(
<input type="hidden" name="etudid" value="%s"> <input type="hidden" name="etudid" value="%s">
<input type="hidden" name="formsemestre_id" value="%s"> <input type="hidden" name="formsemestre_id" value="%s">
""" """
% (REQUEST.URL0, etudid, formsemestre_id) % (request.base_url, etudid, formsemestre_id)
) )
H.append(sco_groups.form_group_choice(formsemestre_id, allow_none=True)) H.append(sco_groups.form_group_choice(formsemestre_id, allow_none=True))
@ -431,7 +467,7 @@ def formsemestre_inscription_with_modules(
return "\n".join(H) + F return "\n".join(H) + F
def formsemestre_inscription_option(etudid, formsemestre_id, REQUEST=None): def formsemestre_inscription_option(etudid, formsemestre_id):
"""Dialogue pour (dés)inscription à des modules optionnels.""" """Dialogue pour (dés)inscription à des modules optionnels."""
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if not sem["etat"]: if not sem["etat"]:
@ -448,7 +484,7 @@ def formsemestre_inscription_option(etudid, formsemestre_id, REQUEST=None):
] ]
# Cherche les moduleimpls et les inscriptions # Cherche les moduleimpls et les inscriptions
mods = sco_moduleimpl.do_moduleimpl_withmodule_list(formsemestre_id=formsemestre_id) mods = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
inscr = sco_moduleimpl.do_moduleimpl_inscription_list(etudid=etudid) inscr = sco_moduleimpl.do_moduleimpl_inscription_list(etudid=etudid)
# Formulaire # Formulaire
modimpls_by_ue_ids = scu.DictDefault(defaultvalue=[]) # ue_id : [ moduleimpl_id ] modimpls_by_ue_ids = scu.DictDefault(defaultvalue=[]) # ue_id : [ moduleimpl_id ]
@ -468,7 +504,8 @@ def formsemestre_inscription_option(etudid, formsemestre_id, REQUEST=None):
modimpls_by_ue_names[ue_id].append( modimpls_by_ue_names[ue_id].append(
"%s %s" % (mod["module"]["code"], mod["module"]["titre"]) "%s %s" % (mod["module"]["code"], mod["module"]["titre"])
) )
if not REQUEST.form.get("tf_submitted", False): vals = scu.get_request_args()
if not vals.get("tf_submitted", False):
# inscrit ? # inscrit ?
for ins in inscr: for ins in inscr:
if ins["moduleimpl_id"] == mod["moduleimpl_id"]: if ins["moduleimpl_id"] == mod["moduleimpl_id"]:
@ -533,8 +570,8 @@ function chkbx_select(field_id, state) {
""" """
) )
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
descr, descr,
initvalues, initvalues,
cancelbutton="Annuler", cancelbutton="Annuler",
@ -659,7 +696,7 @@ function chkbx_select(field_id, state) {
def do_moduleimpl_incription_options( def do_moduleimpl_incription_options(
etudid, modulesimpls_ainscrire, modulesimpls_adesinscrire, REQUEST=None etudid, modulesimpls_ainscrire, modulesimpls_adesinscrire
): ):
""" """
Effectue l'inscription et la description aux modules optionnels Effectue l'inscription et la description aux modules optionnels
@ -679,7 +716,7 @@ def do_moduleimpl_incription_options(
# inscriptions # inscriptions
for moduleimpl_id in a_inscrire: for moduleimpl_id in a_inscrire:
# verifie que ce module existe bien # verifie que ce module existe bien
mods = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id) mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)
if len(mods) != 1: if len(mods) != 1:
raise ScoValueError( raise ScoValueError(
"inscription: invalid moduleimpl_id: %s" % moduleimpl_id "inscription: invalid moduleimpl_id: %s" % moduleimpl_id
@ -692,7 +729,7 @@ def do_moduleimpl_incription_options(
# desinscriptions # desinscriptions
for moduleimpl_id in a_desinscrire: for moduleimpl_id in a_desinscrire:
# verifie que ce module existe bien # verifie que ce module existe bien
mods = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id) mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)
if len(mods) != 1: if len(mods) != 1:
raise ScoValueError( raise ScoValueError(
"desinscription: invalid moduleimpl_id: %s" % moduleimpl_id "desinscription: invalid moduleimpl_id: %s" % moduleimpl_id
@ -711,17 +748,16 @@ def do_moduleimpl_incription_options(
oid, formsemestre_id=mod["formsemestre_id"] oid, formsemestre_id=mod["formsemestre_id"]
) )
if REQUEST: H = [
H = [ html_sco_header.sco_header(),
html_sco_header.sco_header(), """<h3>Modifications effectuées</h3>
"""<h3>Modifications effectuées</h3> <p><a class="stdlink" href="%s">
<p><a class="stdlink" href="%s"> Retour à la fiche étudiant</a></p>
Retour à la fiche étudiant</a></p> """
""" % url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
% url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid), html_sco_header.sco_footer(),
html_sco_header.sco_footer(), ]
] return "\n".join(H)
return "\n".join(H)
def est_inscrit_ailleurs(etudid, formsemestre_id): def est_inscrit_ailleurs(etudid, formsemestre_id):
@ -756,14 +792,13 @@ def list_inscrits_ailleurs(formsemestre_id):
return d return d
def formsemestre_inscrits_ailleurs(formsemestre_id, REQUEST=None): def formsemestre_inscrits_ailleurs(formsemestre_id):
"""Page listant les étudiants inscrits dans un autre semestre """Page listant les étudiants inscrits dans un autre semestre
dont les dates recouvrent le semestre indiqué. dont les dates recouvrent le semestre indiqué.
""" """
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [ H = [
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
REQUEST,
"Inscriptions multiples parmi les étudiants du semestre ", "Inscriptions multiples parmi les étudiants du semestre ",
sem, sem,
) )

View File

@ -105,7 +105,7 @@ def _build_menu_stats(formsemestre_id):
"title": "Documents Avis Poursuite Etudes", "title": "Documents Avis Poursuite Etudes",
"endpoint": "notes.pe_view_sem_recap", "endpoint": "notes.pe_view_sem_recap",
"args": {"formsemestre_id": formsemestre_id}, "args": {"formsemestre_id": formsemestre_id},
"enabled": True, "enabled": current_app.config["TESTING"] or current_app.config["DEBUG"],
}, },
{ {
"title": 'Table "débouchés"', "title": 'Table "débouchés"',
@ -141,7 +141,7 @@ def formsemestre_status_menubar(sem):
}, },
{ {
"title": "Voir la formation %(acronyme)s (v%(version)s)" % F, "title": "Voir la formation %(acronyme)s (v%(version)s)" % F,
"endpoint": "notes.ue_list", "endpoint": "notes.ue_table",
"args": {"formation_id": sem["formation_id"]}, "args": {"formation_id": sem["formation_id"]},
"enabled": True, "enabled": True,
"helpmsg": "Tableau de bord du semestre", "helpmsg": "Tableau de bord du semestre",
@ -337,7 +337,7 @@ def formsemestre_status_menubar(sem):
submenu.append( submenu.append(
{ {
"title": "%s" % partition["partition_name"], "title": "%s" % partition["partition_name"],
"endpoint": "scolar.affectGroups", "endpoint": "scolar.affect_groups",
"args": {"partition_id": partition["partition_id"]}, "args": {"partition_id": partition["partition_id"]},
"enabled": enabled, "enabled": enabled,
} }
@ -436,7 +436,7 @@ def formsemestre_status_menubar(sem):
return "\n".join(H) return "\n".join(H)
def retreive_formsemestre_from_request(): def retreive_formsemestre_from_request() -> int:
"""Cherche si on a de quoi déduire le semestre affiché à partir des """Cherche si on a de quoi déduire le semestre affiché à partir des
arguments de la requête: arguments de la requête:
formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id
@ -447,12 +447,13 @@ def retreive_formsemestre_from_request():
args = request.form args = request.form
else: else:
return None return None
formsemestre_id = None
# Search formsemestre # Search formsemestre
group_ids = args.get("group_ids", []) group_ids = args.get("group_ids", [])
if "formsemestre_id" in args: if "formsemestre_id" in args:
formsemestre_id = args["formsemestre_id"] formsemestre_id = args["formsemestre_id"]
elif "moduleimpl_id" in args and args["moduleimpl_id"]: elif "moduleimpl_id" in args and args["moduleimpl_id"]:
modimpl = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=args["moduleimpl_id"]) modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=args["moduleimpl_id"])
if not modimpl: if not modimpl:
return None # suppressed ? return None # suppressed ?
modimpl = modimpl[0] modimpl = modimpl[0]
@ -462,7 +463,7 @@ def retreive_formsemestre_from_request():
if not E: if not E:
return None # evaluation suppressed ? return None # evaluation suppressed ?
E = E[0] E = E[0]
modimpl = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
formsemestre_id = modimpl["formsemestre_id"] formsemestre_id = modimpl["formsemestre_id"]
elif "group_id" in args: elif "group_id" in args:
group = sco_groups.get_group(args["group_id"]) group = sco_groups.get_group(args["group_id"])
@ -479,16 +480,17 @@ def retreive_formsemestre_from_request():
elif "partition_id" in args: elif "partition_id" in args:
partition = sco_groups.get_partition(args["partition_id"]) partition = sco_groups.get_partition(args["partition_id"])
formsemestre_id = partition["formsemestre_id"] formsemestre_id = partition["formsemestre_id"]
else:
if not formsemestre_id:
return None # no current formsemestre return None # no current formsemestre
return formsemestre_id return int(formsemestre_id)
# Element HTML decrivant un semestre (barre de menu et infos) # Element HTML decrivant un semestre (barre de menu et infos)
def formsemestre_page_title(): def formsemestre_page_title():
"""Element HTML decrivant un semestre (barre de menu et infos) """Element HTML decrivant un semestre (barre de menu et infos)
Cherche dans REQUEST si un semestre est défini (formsemestre_id ou moduleimpl ou evaluation ou group) Cherche dans la requete si un semestre est défini (formsemestre_id ou moduleimpl ou evaluation ou group)
""" """
formsemestre_id = retreive_formsemestre_from_request() formsemestre_id = retreive_formsemestre_from_request()
# #
@ -503,15 +505,29 @@ def formsemestre_page_title():
fill_formsemestre(sem) fill_formsemestre(sem)
H = [ h = f"""<div class="formsemestre_page_title">
"""<div class="formsemestre_page_title">""", <div class="infos">
"""<div class="infos"> <span class="semtitle"><a class="stdlink" title="{sem['session_id']}"
<span class="semtitle"><a class="stdlink" title="%(session_id)s" href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre)s</a><a title="%(etape_apo_str)s">%(num_sem)s</a>%(modalitestr)s</span><span class="dates"><a title="du %(date_debut)s au %(date_fin)s ">%(mois_debut)s - %(mois_fin)s</a></span><span class="resp"><a title="%(nomcomplet)s">%(resp)s</a></span><span class="nbinscrits"><a class="discretelink" href="%(notes_url)s/formsemestre_lists?formsemestre_id=%(formsemestre_id)s">%(nbinscrits)d inscrits</a></span><span class="lock">%(locklink)s</span><span class="eye">%(eyelink)s</span></div>""" href="{url_for('notes.formsemestre_status',
% sem, scodoc_dept=g.scodoc_dept, formsemestre_id=sem['formsemestre_id'])}"
formsemestre_status_menubar(sem), >{sem['titre']}</a><a
"""</div>""", title="{sem['etape_apo_str']}">{sem['num_sem']}</a>{sem['modalitestr']}</span><span
] class="dates"><a
return "\n".join(H) title="du {sem['date_debut']} au {sem['date_fin']} "
>{sem['mois_debut']} - {sem['mois_fin']}</a></span><span
class="resp"><a title="{sem['nomcomplet']}">{sem['resp']}</a></span><span
class="nbinscrits"><a class="discretelink"
href="{url_for("scolar.groups_view",
scodoc_dept=g.scodoc_dept, formsemestre_id=sem['formsemestre_id'])}"
>{sem['nbinscrits']} inscrits</a></span><span
class="lock">{sem['locklink']}</span><span
class="eye">{sem['eyelink']}</span>
</div>
{formsemestre_status_menubar(sem)}
</div>
"""
return h
def fill_formsemestre(sem): def fill_formsemestre(sem):
@ -568,7 +584,7 @@ def fill_formsemestre(sem):
# Description du semestre sous forme de table exportable # Description du semestre sous forme de table exportable
def formsemestre_description_table(formsemestre_id, REQUEST=None, with_evals=False): def formsemestre_description_table(formsemestre_id, with_evals=False):
"""Description du semestre sous forme de table exportable """Description du semestre sous forme de table exportable
Liste des modules et de leurs coefficients Liste des modules et de leurs coefficients
""" """
@ -577,9 +593,7 @@ def formsemestre_description_table(formsemestre_id, REQUEST=None, with_evals=Fal
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id) use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"]) parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list( Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
formsemestre_id=formsemestre_id
)
R = [] R = []
sum_coef = 0 sum_coef = 0
@ -610,7 +624,7 @@ def formsemestre_description_table(formsemestre_id, REQUEST=None, with_evals=Fal
moduleimpl_id=M["moduleimpl_id"] moduleimpl_id=M["moduleimpl_id"]
) )
enseignants = ", ".join( enseignants = ", ".join(
[sco_users.user_info(m["ens_id"], REQUEST)["nomprenom"] for m in M["ens"]] [sco_users.user_info(m["ens_id"])["nomprenom"] for m in M["ens"]]
) )
l = { l = {
"UE": M["ue"]["acronyme"], "UE": M["ue"]["acronyme"],
@ -698,41 +712,37 @@ def formsemestre_description_table(formsemestre_id, REQUEST=None, with_evals=Fal
html_caption=title, html_caption=title,
html_class="table_leftalign formsemestre_description", html_class="table_leftalign formsemestre_description",
base_url="%s?formsemestre_id=%s&with_evals=%s" base_url="%s?formsemestre_id=%s&with_evals=%s"
% (REQUEST.URL0, formsemestre_id, with_evals), % (request.base_url, formsemestre_id, with_evals),
page_title=title, page_title=title,
html_title=html_sco_header.html_sem_header( html_title=html_sco_header.html_sem_header(
REQUEST, "Description du semestre", sem, with_page_header=False "Description du semestre", sem, with_page_header=False
), ),
pdf_title=title, pdf_title=title,
preferences=sco_preferences.SemPreferences(formsemestre_id), preferences=sco_preferences.SemPreferences(formsemestre_id),
) )
def formsemestre_description( def formsemestre_description(formsemestre_id, format="html", with_evals=False):
formsemestre_id, format="html", with_evals=False, REQUEST=None
):
"""Description du semestre sous forme de table exportable """Description du semestre sous forme de table exportable
Liste des modules et de leurs coefficients Liste des modules et de leurs coefficients
""" """
with_evals = int(with_evals) with_evals = int(with_evals)
tab = formsemestre_description_table( tab = formsemestre_description_table(formsemestre_id, with_evals=with_evals)
formsemestre_id, REQUEST, with_evals=with_evals
)
tab.html_before_table = """<form name="f" method="get" action="%s"> tab.html_before_table = """<form name="f" method="get" action="%s">
<input type="hidden" name="formsemestre_id" value="%s"></input> <input type="hidden" name="formsemestre_id" value="%s"></input>
<input type="checkbox" name="with_evals" value="1" onchange="document.f.submit()" """ % ( <input type="checkbox" name="with_evals" value="1" onchange="document.f.submit()" """ % (
REQUEST.URL0, request.base_url,
formsemestre_id, formsemestre_id,
) )
if with_evals: if with_evals:
tab.html_before_table += "checked" tab.html_before_table += "checked"
tab.html_before_table += ">indiquer les évaluations</input></form>" tab.html_before_table += ">indiquer les évaluations</input></form>"
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)
# genere liste html pour accès aux groupes de ce semestre # genere liste html pour accès aux groupes de ce semestre
def _make_listes_sem(sem, REQUEST=None, with_absences=True): def _make_listes_sem(sem, with_absences=True):
# construit l'URL "destination" # construit l'URL "destination"
# (a laquelle on revient apres saisie absences) # (a laquelle on revient apres saisie absences)
destination = url_for( destination = url_for(
@ -827,7 +837,6 @@ def _make_listes_sem(sem, REQUEST=None, with_absences=True):
url_for("scolar.groups_view", url_for("scolar.groups_view",
curtab="tab-photos", curtab="tab-photos",
group_ids=group["group_id"], group_ids=group["group_id"],
etat="I",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
) )
}">Photos</a> }">Photos</a>
@ -845,7 +854,7 @@ def _make_listes_sem(sem, REQUEST=None, with_absences=True):
H.append('<p class="help indent">Aucun groupe dans cette partition') H.append('<p class="help indent">Aucun groupe dans cette partition')
if sco_groups.sco_permissions_check.can_change_groups(formsemestre_id): if sco_groups.sco_permissions_check.can_change_groups(formsemestre_id):
H.append( H.append(
f""" (<a href="{url_for("scolar.affectGroups", f""" (<a href="{url_for("scolar.affect_groups",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
partition_id=partition["partition_id"]) partition_id=partition["partition_id"])
}" class="stdlink">créer</a>)""" }" class="stdlink">créer</a>)"""
@ -873,7 +882,7 @@ def html_expr_diagnostic(diagnostics):
last_id, last_msg = None, None last_id, last_msg = None, None
for diag in diagnostics: for diag in diagnostics:
if "moduleimpl_id" in diag: if "moduleimpl_id" in diag:
mod = sco_moduleimpl.do_moduleimpl_withmodule_list( mod = sco_moduleimpl.moduleimpl_withmodule_list(
moduleimpl_id=diag["moduleimpl_id"] moduleimpl_id=diag["moduleimpl_id"]
)[0] )[0]
H.append( H.append(
@ -886,7 +895,7 @@ def html_expr_diagnostic(diagnostics):
) )
else: else:
if diag["ue_id"] != last_id or diag["msg"] != last_msg: if diag["ue_id"] != last_id or diag["msg"] != last_msg:
ue = sco_edit_ue.do_ue_list({"ue_id": diag["ue_id"]})[0] ue = sco_edit_ue.ue_list({"ue_id": diag["ue_id"]})[0]
H.append( H.append(
'<li>UE "%s": %s</li>' '<li>UE "%s": %s</li>'
% (ue["acronyme"] or ue["titre"] or "?", diag["msg"]) % (ue["acronyme"] or ue["titre"] or "?", diag["msg"])
@ -897,7 +906,7 @@ def html_expr_diagnostic(diagnostics):
return "".join(H) return "".join(H)
def formsemestre_status_head(formsemestre_id=None, REQUEST=None, page_title=None): def formsemestre_status_head(formsemestre_id=None, page_title=None):
"""En-tête HTML des pages "semestre" """ """En-tête HTML des pages "semestre" """
semlist = sco_formsemestre.do_formsemestre_list( semlist = sco_formsemestre.do_formsemestre_list(
args={"formsemestre_id": formsemestre_id} args={"formsemestre_id": formsemestre_id}
@ -912,12 +921,12 @@ def formsemestre_status_head(formsemestre_id=None, REQUEST=None, page_title=None
H = [ H = [
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
REQUEST, page_title, sem, with_page_header=False, with_h2=False page_title, sem, with_page_header=False, with_h2=False
), ),
"""<table> f"""<table>
<tr><td class="fichetitre2">Formation: </td><td> <tr><td class="fichetitre2">Formation: </td><td>
<a href="Notes/ue_list?formation_id=%(formation_id)s" class="discretelink" title="Formation %(acronyme)s, v%(version)s">%(titre)s</a>""" <a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=F['formation_id'])}"
% F, class="discretelink" title="Formation {F['acronyme']}, v{F['version']}">{F['titre']}</a>""",
] ]
if sem["semestre_id"] >= 0: if sem["semestre_id"] >= 0:
H.append(", %s %s" % (parcours.SESSION_NAME, sem["semestre_id"])) H.append(", %s %s" % (parcours.SESSION_NAME, sem["semestre_id"]))
@ -948,10 +957,13 @@ Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur ind
</td></tr>""" </td></tr>"""
) )
H.append("</table>") H.append("</table>")
sem_warning = ""
if sem["bul_hide_xml"]: if sem["bul_hide_xml"]:
H.append( sem_warning += "Bulletins non publiés sur le portail. "
'<p class="fontorange"><em>Bulletins non publiés sur le portail</em></p>' if sem["block_moyennes"]:
) sem_warning += "Calcul des moyennes bloqué !"
if sem_warning:
H.append('<p class="fontorange"><em>' + sem_warning + "</em></p>")
if sem["semestre_id"] >= 0 and not sco_formsemestre.sem_une_annee(sem): if sem["semestre_id"] >= 0 and not sco_formsemestre.sem_une_annee(sem):
H.append( H.append(
'<p class="fontorange"><em>Attention: ce semestre couvre plusieurs années scolaires !</em></p>' '<p class="fontorange"><em>Attention: ce semestre couvre plusieurs années scolaires !</em></p>'
@ -962,20 +974,18 @@ Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur ind
return "".join(H) return "".join(H)
def formsemestre_status(formsemestre_id=None, REQUEST=None): def formsemestre_status(formsemestre_id=None):
"""Tableau de bord semestre HTML""" """Tableau de bord semestre HTML"""
# porté du DTML # porté du DTML
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list( Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
formsemestre_id=formsemestre_id
)
# inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( # inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
# args={"formsemestre_id": formsemestre_id} # args={"formsemestre_id": formsemestre_id}
# ) # )
prev_ue_id = None prev_ue_id = None
can_edit = sco_formsemestre_edit.can_edit_sem(REQUEST, formsemestre_id, sem=sem) can_edit = sco_formsemestre_edit.can_edit_sem(formsemestre_id, sem=sem)
H = [ H = [
html_sco_header.sco_header(page_title="Semestre %s" % sem["titreannee"]), html_sco_header.sco_header(page_title="Semestre %s" % sem["titreannee"]),
@ -1018,11 +1028,9 @@ def formsemestre_status(formsemestre_id=None, REQUEST=None):
ModInscrits = sco_moduleimpl.do_moduleimpl_inscription_list( ModInscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
moduleimpl_id=M["moduleimpl_id"] moduleimpl_id=M["moduleimpl_id"]
) )
mails_enseignants.add( mails_enseignants.add(sco_users.user_info(M["responsable_id"])["email"])
sco_users.user_info(M["responsable_id"], REQUEST)["email"]
)
mails_enseignants |= set( mails_enseignants |= set(
[sco_users.user_info(m["ens_id"], REQUEST)["email"] for m in M["ens"]] [sco_users.user_info(m["ens_id"])["email"] for m in M["ens"]]
) )
ue = M["ue"] ue = M["ue"]
if prev_ue_id != ue["ue_id"]: if prev_ue_id != ue["ue_id"]:
@ -1147,7 +1155,7 @@ def formsemestre_status(formsemestre_id=None, REQUEST=None):
# --- LISTE DES ETUDIANTS # --- LISTE DES ETUDIANTS
H += [ H += [
'<div id="groupes">', '<div id="groupes">',
_make_listes_sem(sem, REQUEST), _make_listes_sem(sem),
"</div>", "</div>",
] ]
# --- Lien mail enseignants: # --- Lien mail enseignants:

View File

@ -27,10 +27,10 @@
"""Semestres: validation semestre et UE dans parcours """Semestres: validation semestre et UE dans parcours
""" """
import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error, time, datetime import time
import flask import flask
from flask import url_for, g from flask import url_for, g, request
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -64,7 +64,6 @@ def formsemestre_validation_etud_form(
desturl=None, desturl=None,
sortcol=None, sortcol=None,
readonly=True, readonly=True,
REQUEST=None,
): ):
nt = sco_cache.NotesTableCache.get( nt = sco_cache.NotesTableCache.get(
formsemestre_id formsemestre_id
@ -149,9 +148,7 @@ def formsemestre_validation_etud_form(
'</td><td style="text-align: right;"><a href="%s">%s</a></td></tr></table>' '</td><td style="text-align: right;"><a href="%s">%s</a></td></tr></table>'
% ( % (
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid), url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
sco_photos.etud_photo_html( sco_photos.etud_photo_html(etud, title="fiche de %s" % etud["nom"]),
etud, title="fiche de %s" % etud["nom"], REQUEST=REQUEST
),
) )
) )
@ -163,10 +160,11 @@ def formsemestre_validation_etud_form(
if etud_etat != "I": if etud_etat != "I":
H.append( H.append(
tf_error_message( tf_error_message(
"""Impossible de statuer sur cet étudiant: f"""Impossible de statuer sur cet étudiant:
il est démissionnaire ou défaillant (voir <a href="%s">sa fiche</a>) il est démissionnaire ou défaillant (voir <a href="{
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
}">sa fiche</a>)
""" """
% url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
) )
) )
return "\n".join(H + Footer) return "\n".join(H + Footer)
@ -178,16 +176,19 @@ def formsemestre_validation_etud_form(
) )
if check: if check:
if not desturl: if not desturl:
desturl = ( desturl = url_for(
"formsemestre_recapcomplet?modejury=1&hidemodules=1&hidebac=1&pref_override=0&formsemestre_id=" "notes.formsemestre_recapcomplet",
+ str(formsemestre_id) scodoc_dept=g.scodoc_dept,
modejury=1,
hidemodules=1,
hidebac=1,
pref_override=0,
formsemestre_id=formsemestre_id,
sortcol=sortcol
or None, # pour refaire tri sorttable du tableau de notes
_anchor="etudid%s" % etudid, # va a la bonne ligne
) )
if sortcol: H.append(f'<ul><li><a href="{desturl}">Continuer</a></li></ul>')
desturl += (
"&sortcol=" + sortcol
) # pour refaire tri sorttable du tableau de notes
desturl += "#etudid%s" % etudid # va a la bonne ligne
H.append('<ul><li><a href="%s">Continuer</a></li></ul>' % desturl)
return "\n".join(H + Footer) return "\n".join(H + Footer)
@ -197,8 +198,12 @@ def formsemestre_validation_etud_form(
if nt.etud_has_notes_attente(etudid): if nt.etud_has_notes_attente(etudid):
H.append( H.append(
tf_error_message( tf_error_message(
"""Impossible de statuer sur cet étudiant: il a des notes en attente dans des évaluations de ce semestre (voir <a href="formsemestre_status?formsemestre_id=%s">tableau de bord</a>)""" f"""Impossible de statuer sur cet étudiant: il a des notes en
% formsemestre_id attente dans des évaluations de ce semestre (voir <a href="{
url_for( "notes.formsemestre_status",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
}">tableau de bord</a>)
"""
) )
) )
return "\n".join(H + Footer) return "\n".join(H + Footer)
@ -213,14 +218,24 @@ def formsemestre_validation_etud_form(
if not Se.prev_decision: if not Se.prev_decision:
H.append( H.append(
tf_error_message( tf_error_message(
"""Le jury n\'a pas statué sur le semestre précédent ! (<a href="formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s">le faire maintenant</a>)""" f"""Le jury n'a pas statué sur le semestre précédent ! (<a href="{
% (Se.prev["formsemestre_id"], etudid) url_for("notes.formsemestre_validation_etud_form",
scodoc_dept=g.scodoc_dept,
formsemestre_id=Se.prev["formsemestre_id"],
etudid=etudid)
}">le faire maintenant</a>)
"""
) )
) )
if decision_jury: if decision_jury:
H.append( H.append(
'<a href="formsemestre_validation_suppress_etud?etudid=%s&formsemestre_id=%s" class="stdlink">Supprimer décision existante</a>' f"""<a href="{
% (etudid, formsemestre_id) url_for("notes.formsemestre_validation_suppress_etud",
scodoc_dept=g.scodoc_dept,
etudid=etudid, formsemestre_id=formsemestre_id
)
}" class="stdlink">Supprimer décision existante</a>
"""
) )
H.append(html_sco_header.sco_footer()) H.append(html_sco_header.sco_footer())
return "\n".join(H) return "\n".join(H)
@ -338,7 +353,6 @@ def formsemestre_validation_etud(
codechoice=None, # required codechoice=None, # required
desturl="", desturl="",
sortcol=None, sortcol=None,
REQUEST=None,
): ):
"""Enregistre validation""" """Enregistre validation"""
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
@ -354,9 +368,9 @@ def formsemestre_validation_etud(
if not selected_choice: if not selected_choice:
raise ValueError("code choix invalide ! (%s)" % codechoice) raise ValueError("code choix invalide ! (%s)" % codechoice)
# #
Se.valide_decision(selected_choice, REQUEST) # enregistre Se.valide_decision(selected_choice) # enregistre
return _redirect_valid_choice( return _redirect_valid_choice(
formsemestre_id, etudid, Se, selected_choice, desturl, sortcol, REQUEST formsemestre_id, etudid, Se, selected_choice, desturl, sortcol
) )
@ -369,7 +383,6 @@ def formsemestre_validation_etud_manu(
assidu=False, assidu=False,
desturl="", desturl="",
sortcol=None, sortcol=None,
REQUEST=None,
redirect=True, redirect=True,
): ):
"""Enregistre validation""" """Enregistre validation"""
@ -399,22 +412,20 @@ def formsemestre_validation_etud_manu(
formsemestre_id_utilise_pour_compenser=formsemestre_id_utilise_pour_compenser, formsemestre_id_utilise_pour_compenser=formsemestre_id_utilise_pour_compenser,
) )
# #
Se.valide_decision(choice, REQUEST) # enregistre Se.valide_decision(choice) # enregistre
if redirect: if redirect:
return _redirect_valid_choice( return _redirect_valid_choice(
formsemestre_id, etudid, Se, choice, desturl, sortcol, REQUEST formsemestre_id, etudid, Se, choice, desturl, sortcol
) )
def _redirect_valid_choice( def _redirect_valid_choice(formsemestre_id, etudid, Se, choice, desturl, sortcol):
formsemestre_id, etudid, Se, choice, desturl, sortcol, REQUEST
):
adr = "formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s&check=1" % ( adr = "formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s&check=1" % (
formsemestre_id, formsemestre_id,
etudid, etudid,
) )
if sortcol: if sortcol:
adr += "&sortcol=" + sortcol adr += "&sortcol=" + str(sortcol)
# if desturl: # if desturl:
# desturl += "&desturl=" + desturl # desturl += "&desturl=" + desturl
return flask.redirect(adr) return flask.redirect(adr)
@ -483,7 +494,7 @@ def formsemestre_recap_parcours_table(
with_links=False, with_links=False,
with_all_columns=True, with_all_columns=True,
a_url="", a_url="",
sem_info={}, sem_info=None,
show_details=False, show_details=False,
): ):
"""Tableau HTML recap parcours """Tableau HTML recap parcours
@ -491,6 +502,7 @@ def formsemestre_recap_parcours_table(
sem_info = { formsemestre_id : txt } permet d'ajouter des informations associées à chaque semestre sem_info = { formsemestre_id : txt } permet d'ajouter des informations associées à chaque semestre
with_all_columns: si faux, pas de colonne "assiduité". with_all_columns: si faux, pas de colonne "assiduité".
""" """
sem_info = sem_info or {}
H = [] H = []
linktmpl = '<span onclick="toggle_vis(this);" class="toggle_sem sem_%%s">%s</span>' linktmpl = '<span onclick="toggle_vis(this);" class="toggle_sem sem_%%s">%s</span>'
minuslink = linktmpl % scu.icontag("minus_img", border="0", alt="-") minuslink = linktmpl % scu.icontag("minus_img", border="0", alt="-")
@ -821,12 +833,12 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None
# ----------- # -----------
def formsemestre_validation_auto(formsemestre_id, REQUEST): def formsemestre_validation_auto(formsemestre_id):
"Formulaire saisie automatisee des decisions d'un semestre" "Formulaire saisie automatisee des decisions d'un semestre"
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [ H = [
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
REQUEST, "Saisie automatique des décisions du semestre", sem "Saisie automatique des décisions du semestre", sem
), ),
""" """
<ul> <ul>
@ -851,7 +863,7 @@ def formsemestre_validation_auto(formsemestre_id, REQUEST):
return "\n".join(H) return "\n".join(H)
def do_formsemestre_validation_auto(formsemestre_id, REQUEST): def do_formsemestre_validation_auto(formsemestre_id):
"Saisie automatisee des decisions d'un semestre" "Saisie automatisee des decisions d'un semestre"
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
next_semestre_id = sem["semestre_id"] + 1 next_semestre_id = sem["semestre_id"] + 1
@ -907,7 +919,6 @@ def do_formsemestre_validation_auto(formsemestre_id, REQUEST):
code_etat=ADM, code_etat=ADM,
devenir="NEXT", devenir="NEXT",
assidu=True, assidu=True,
REQUEST=REQUEST,
redirect=False, redirect=False,
) )
nb_valid += 1 nb_valid += 1
@ -972,7 +983,7 @@ def formsemestre_validation_suppress_etud(formsemestre_id, etudid):
) # > suppr. decision jury (peut affecter de plusieurs semestres utilisant UE capitalisée) ) # > suppr. decision jury (peut affecter de plusieurs semestres utilisant UE capitalisée)
def formsemestre_validate_previous_ue(formsemestre_id, etudid, REQUEST=None): def formsemestre_validate_previous_ue(formsemestre_id, etudid):
"""Form. saisie UE validée hors ScoDoc """Form. saisie UE validée hors ScoDoc
(pour étudiants arrivant avec un UE antérieurement validée). (pour étudiants arrivant avec un UE antérieurement validée).
""" """
@ -994,9 +1005,7 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid, REQUEST=None):
'</td><td style="text-align: right;"><a href="%s">%s</a></td></tr></table>' '</td><td style="text-align: right;"><a href="%s">%s</a></td></tr></table>'
% ( % (
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid), url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
sco_photos.etud_photo_html( sco_photos.etud_photo_html(etud, title="fiche de %s" % etud["nom"]),
etud, title="fiche de %s" % etud["nom"], REQUEST=REQUEST
),
) )
), ),
"""<p class="help">Utiliser cette page pour enregistrer une UE validée antérieurement, """<p class="help">Utiliser cette page pour enregistrer une UE validée antérieurement,
@ -1013,12 +1022,12 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid, REQUEST=None):
] ]
# Toutes les UE de cette formation sont présentées (même celles des autres semestres) # Toutes les UE de cette formation sont présentées (même celles des autres semestres)
ues = sco_edit_ue.do_ue_list({"formation_id": Fo["formation_id"]}) ues = sco_edit_ue.ue_list({"formation_id": Fo["formation_id"]})
ue_names = ["Choisir..."] + ["%(acronyme)s %(titre)s" % ue for ue in ues] ue_names = ["Choisir..."] + ["%(acronyme)s %(titre)s" % ue for ue in ues]
ue_ids = [""] + [ue["ue_id"] for ue in ues] ue_ids = [""] + [ue["ue_id"] for ue in ues]
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
( (
("etudid", {"input_type": "hidden"}), ("etudid", {"input_type": "hidden"}),
("formsemestre_id", {"input_type": "hidden"}), ("formsemestre_id", {"input_type": "hidden"}),
@ -1091,7 +1100,6 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid, REQUEST=None):
tf[2]["moy_ue"], tf[2]["moy_ue"],
tf[2]["date"], tf[2]["date"],
semestre_id=semestre_id, semestre_id=semestre_id,
REQUEST=REQUEST,
) )
return flask.redirect( return flask.redirect(
scu.ScoURL() scu.ScoURL()
@ -1109,7 +1117,6 @@ def do_formsemestre_validate_previous_ue(
code=ADM, code=ADM,
semestre_id=None, semestre_id=None,
ue_coefficient=None, ue_coefficient=None,
REQUEST=None,
): ):
"""Enregistre (ou modifie) validation d'UE (obtenue hors ScoDoc). """Enregistre (ou modifie) validation d'UE (obtenue hors ScoDoc).
Si le coefficient est spécifié, modifie le coefficient de Si le coefficient est spécifié, modifie le coefficient de
@ -1165,7 +1172,7 @@ def _invalidate_etud_formation_caches(etudid, formation_id):
) # > modif decision UE (inval tous semestres avec cet etudiant, ok mais conservatif) ) # > modif decision UE (inval tous semestres avec cet etudiant, ok mais conservatif)
def get_etud_ue_cap_html(etudid, formsemestre_id, ue_id, REQUEST=None): def get_etud_ue_cap_html(etudid, formsemestre_id, ue_id):
"""Ramene bout de HTML pour pouvoir supprimer une validation de cette UE""" """Ramene bout de HTML pour pouvoir supprimer une validation de cette UE"""
valids = ndb.SimpleDictFetch( valids = ndb.SimpleDictFetch(
"""SELECT SFV.* """SELECT SFV.*
@ -1201,7 +1208,7 @@ def get_etud_ue_cap_html(etudid, formsemestre_id, ue_id, REQUEST=None):
return "\n".join(H) return "\n".join(H)
def etud_ue_suppress_validation(etudid, formsemestre_id, ue_id, REQUEST=None): def etud_ue_suppress_validation(etudid, formsemestre_id, ue_id):
"""Suppress a validation (ue_id, etudid) and redirect to formsemestre""" """Suppress a validation (ue_id, etudid) and redirect to formsemestre"""
log("etud_ue_suppress_validation( %s, %s, %s)" % (etudid, formsemestre_id, ue_id)) log("etud_ue_suppress_validation( %s, %s, %s)" % (etudid, formsemestre_id, ue_id))
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
@ -1227,7 +1234,7 @@ def check_formation_ues(formation_id):
définition du programme: cette fonction retourne un bout de HTML définition du programme: cette fonction retourne un bout de HTML
à afficher pour prévenir l'utilisateur, ou '' si tout est ok. à afficher pour prévenir l'utilisateur, ou '' si tout est ok.
""" """
ues = sco_edit_ue.do_ue_list({"formation_id": formation_id}) ues = sco_edit_ue.ue_list({"formation_id": formation_id})
ue_multiples = {} # { ue_id : [ liste des formsemestre ] } ue_multiples = {} # { ue_id : [ liste des formsemestre ] }
for ue in ues: for ue in ues:
# formsemestres utilisant cette ue ? # formsemestres utilisant cette ue ?

View File

@ -42,12 +42,12 @@ from xml.etree import ElementTree
from xml.etree.ElementTree import Element from xml.etree.ElementTree import Element
import flask import flask
from flask import g from flask import g, request
from flask import url_for from flask import url_for, make_response
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app import log from app import log, cache
from app.scodoc.scolog import logdb from app.scodoc.scolog import logdb
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
@ -59,20 +59,6 @@ from app.scodoc.sco_exceptions import ScoException, AccessDenied, ScoValueError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
import six
def checkGroupName(
groupName,
): # XXX unused: now allow any string as a group or partition name
"Raises exception if not a valid group name"
if groupName and (
not re.match(r"^\w+$", groupName)
or (scu.simplesqlquote(groupName) != groupName)
):
log("!!! invalid group name: " + groupName)
raise ValueError("invalid group name: " + groupName)
partitionEditor = ndb.EditableTable( partitionEditor = ndb.EditableTable(
"partition", "partition",
@ -103,8 +89,8 @@ def get_group(group_id):
"""Returns group object, with partition""" """Returns group object, with partition"""
r = ndb.SimpleDictFetch( r = ndb.SimpleDictFetch(
"""SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.* """SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.*
FROM group_descr gd, partition p FROM group_descr gd, partition p
WHERE gd.id=%(group_id)s WHERE gd.id=%(group_id)s
AND p.id = gd.partition_id AND p.id = gd.partition_id
""", """,
{"group_id": group_id}, {"group_id": group_id},
@ -126,8 +112,8 @@ def group_delete(group, force=False):
def get_partition(partition_id): def get_partition(partition_id):
r = ndb.SimpleDictFetch( r = ndb.SimpleDictFetch(
"""SELECT p.id AS partition_id, p.* """SELECT p.id AS partition_id, p.*
FROM partition p FROM partition p
WHERE p.id = %(partition_id)s WHERE p.id = %(partition_id)s
""", """,
{"partition_id": partition_id}, {"partition_id": partition_id},
@ -140,7 +126,7 @@ def get_partition(partition_id):
def get_partitions_list(formsemestre_id, with_default=True): def get_partitions_list(formsemestre_id, with_default=True):
"""Liste des partitions pour ce semestre (list of dicts)""" """Liste des partitions pour ce semestre (list of dicts)"""
partitions = ndb.SimpleDictFetch( partitions = ndb.SimpleDictFetch(
"""SELECT p.id AS partition_id, p.* """SELECT p.id AS partition_id, p.*
FROM partition p FROM partition p
WHERE formsemestre_id=%(formsemestre_id)s WHERE formsemestre_id=%(formsemestre_id)s
ORDER BY numero""", ORDER BY numero""",
@ -157,7 +143,7 @@ def get_default_partition(formsemestre_id):
"""Get partition for 'all' students (this one always exists, with NULL name)""" """Get partition for 'all' students (this one always exists, with NULL name)"""
r = ndb.SimpleDictFetch( r = ndb.SimpleDictFetch(
"""SELECT p.id AS partition_id, p.* FROM partition p """SELECT p.id AS partition_id, p.* FROM partition p
WHERE formsemestre_id=%(formsemestre_id)s WHERE formsemestre_id=%(formsemestre_id)s
AND partition_name is NULL AND partition_name is NULL
""", """,
{"formsemestre_id": formsemestre_id}, {"formsemestre_id": formsemestre_id},
@ -184,10 +170,10 @@ def get_partition_groups(partition):
"""List of groups in this partition (list of dicts). """List of groups in this partition (list of dicts).
Some groups may be empty.""" Some groups may be empty."""
return ndb.SimpleDictFetch( return ndb.SimpleDictFetch(
"""SELECT gd.id AS group_id, p.id AS partition_id, gd.*, p.* """SELECT gd.id AS group_id, p.id AS partition_id, gd.*, p.*
FROM group_descr gd, partition p FROM group_descr gd, partition p
WHERE gd.partition_id=%(partition_id)s WHERE gd.partition_id=%(partition_id)s
AND gd.partition_id=p.id AND gd.partition_id=p.id
ORDER BY group_name ORDER BY group_name
""", """,
partition, partition,
@ -198,9 +184,9 @@ def get_default_group(formsemestre_id, fix_if_missing=False):
"""Returns group_id for default ('tous') group""" """Returns group_id for default ('tous') group"""
r = ndb.SimpleDictFetch( r = ndb.SimpleDictFetch(
"""SELECT gd.id AS group_id """SELECT gd.id AS group_id
FROM group_descr gd, partition p FROM group_descr gd, partition p
WHERE p.formsemestre_id=%(formsemestre_id)s WHERE p.formsemestre_id=%(formsemestre_id)s
AND p.partition_name is NULL AND p.partition_name is NULL
AND p.id = gd.partition_id AND p.id = gd.partition_id
""", """,
{"formsemestre_id": formsemestre_id}, {"formsemestre_id": formsemestre_id},
@ -219,7 +205,7 @@ def get_default_group(formsemestre_id, fix_if_missing=False):
partition_id = partition_create( partition_id = partition_create(
formsemestre_id, default=True, redirect=False formsemestre_id, default=True, redirect=False
) )
group_id = createGroup(partition_id, default=True) group_id = create_group(partition_id, default=True)
return group_id return group_id
# debug check # debug check
if len(r) != 1: if len(r) != 1:
@ -232,8 +218,8 @@ def get_sem_groups(formsemestre_id):
"""Returns groups for this sem (in all partitions).""" """Returns groups for this sem (in all partitions)."""
return ndb.SimpleDictFetch( return ndb.SimpleDictFetch(
"""SELECT gd.id AS group_id, p.id AS partition_id, gd.*, p.* """SELECT gd.id AS group_id, p.id AS partition_id, gd.*, p.*
FROM group_descr gd, partition p FROM group_descr gd, partition p
WHERE p.formsemestre_id=%(formsemestre_id)s WHERE p.formsemestre_id=%(formsemestre_id)s
AND p.id = gd.partition_id AND p.id = gd.partition_id
""", """,
{"formsemestre_id": formsemestre_id}, {"formsemestre_id": formsemestre_id},
@ -354,7 +340,7 @@ def get_etud_groups(etudid, sem, exclude_default=False):
"""Infos sur groupes de l'etudiant dans ce semestre """Infos sur groupes de l'etudiant dans ce semestre
[ group + partition_name ] [ group + partition_name ]
""" """
req = """SELECT p.id AS partition_id, p.*, g.id AS group_id, g.* req = """SELECT p.id AS partition_id, p.*, g.id AS group_id, g.*
FROM group_descr g, partition p, group_membership gm FROM group_descr g, partition p, group_membership gm
WHERE gm.etudid=%(etudid)s WHERE gm.etudid=%(etudid)s
and gm.group_id = g.id and gm.group_id = g.id
@ -391,11 +377,18 @@ def formsemestre_get_etud_groupnames(formsemestre_id, attr="group_name"):
{ etudid : { partition_id : group_name }} (attr=group_name or group_id) { etudid : { partition_id : group_name }} (attr=group_name or group_id)
""" """
infos = ndb.SimpleDictFetch( infos = ndb.SimpleDictFetch(
"""SELECT i.id AS etudid, p.id AS partition_id, """SELECT
gd.group_name, gd.id AS group_id i.etudid AS etudid,
FROM notes_formsemestre_inscription i, partition p, p.id AS partition_id,
group_descr gd, group_membership gm gd.group_name,
WHERE i.formsemestre_id=%(formsemestre_id)s gd.id AS group_id
FROM
notes_formsemestre_inscription i,
partition p,
group_descr gd,
group_membership gm
WHERE
i.formsemestre_id=%(formsemestre_id)s
and i.formsemestre_id = p.formsemestre_id and i.formsemestre_id = p.formsemestre_id
and p.id = gd.partition_id and p.id = gd.partition_id
and gm.etudid = i.etudid and gm.etudid = i.etudid
@ -427,7 +420,7 @@ def etud_add_group_infos(etud, sem, sep=" "):
FROM group_descr g, partition p, group_membership gm WHERE gm.etudid=%(etudid)s FROM group_descr g, partition p, group_membership gm WHERE gm.etudid=%(etudid)s
and gm.group_id = g.id and gm.group_id = g.id
and g.partition_id = p.id and g.partition_id = p.id
and p.formsemestre_id = %(formsemestre_id)s and p.formsemestre_id = %(formsemestre_id)s
ORDER BY p.numero ORDER BY p.numero
""", """,
{"etudid": etud["etudid"], "formsemestre_id": sem["formsemestre_id"]}, {"etudid": etud["etudid"], "formsemestre_id": sem["formsemestre_id"]},
@ -452,6 +445,7 @@ def etud_add_group_infos(etud, sem, sep=" "):
return etud return etud
@cache.memoize(timeout=50) # seconds
def get_etud_groups_in_partition(partition_id): def get_etud_groups_in_partition(partition_id):
"""Returns { etudid : group }, with all students in this partition""" """Returns { etudid : group }, with all students in this partition"""
infos = ndb.SimpleDictFetch( infos = ndb.SimpleDictFetch(
@ -468,7 +462,7 @@ def get_etud_groups_in_partition(partition_id):
return R return R
def formsemestre_partition_list(formsemestre_id, format="xml", REQUEST=None): def formsemestre_partition_list(formsemestre_id, format="xml"):
"""Get partitions and groups in this semestre """Get partitions and groups in this semestre
Supported formats: xml, json Supported formats: xml, json
""" """
@ -476,11 +470,11 @@ def formsemestre_partition_list(formsemestre_id, format="xml", REQUEST=None):
# Ajoute les groupes # Ajoute les groupes
for p in partitions: for p in partitions:
p["group"] = get_partition_groups(p) p["group"] = get_partition_groups(p)
return scu.sendResult(REQUEST, partitions, name="partition", format=format) return scu.sendResult(partitions, name="partition", format=format)
# Encore utilisé par groupmgr.js # Encore utilisé par groupmgr.js
def XMLgetGroupsInPartition(partition_id, REQUEST=None): # was XMLgetGroupesTD def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
""" """
Deprecated: use group_list Deprecated: use group_list
Liste des étudiants dans chaque groupe de cette partition. Liste des étudiants dans chaque groupe de cette partition.
@ -492,6 +486,8 @@ def XMLgetGroupsInPartition(partition_id, REQUEST=None): # was XMLgetGroupesTD
""" """
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
cnx = ndb.GetDBConnexion()
t0 = time.time() t0 = time.time()
partition = get_partition(partition_id) partition = get_partition(partition_id)
formsemestre_id = partition["formsemestre_id"] formsemestre_id = partition["formsemestre_id"]
@ -499,8 +495,8 @@ def XMLgetGroupsInPartition(partition_id, REQUEST=None): # was XMLgetGroupesTD
groups = get_partition_groups(partition) groups = get_partition_groups(partition)
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > inscrdict nt = sco_cache.NotesTableCache.get(formsemestre_id) # > inscrdict
etuds_set = set(nt.inscrdict) etuds_set = set(nt.inscrdict)
# XML response: # Build XML:
REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE) t1 = time.time()
doc = Element("ajax-response") doc = Element("ajax-response")
x_response = Element("response", type="object", id="MyUpdater") x_response = Element("response", type="object", id="MyUpdater")
doc.append(x_response) doc.append(x_response)
@ -514,7 +510,8 @@ def XMLgetGroupsInPartition(partition_id, REQUEST=None): # was XMLgetGroupesTD
) )
x_response.append(x_group) x_response.append(x_group)
for e in get_group_members(group["group_id"]): for e in get_group_members(group["group_id"]):
etud = sco_etud.get_etud_info(etudid=e["etudid"], filled=1)[0] etud = sco_etud.get_etud_info(etudid=e["etudid"], filled=True)[0]
# etud = sco_etud.get_etud_info_filled_by_etudid(e["etudid"], cnx)
x_group.append( x_group.append(
Element( Element(
"etud", "etud",
@ -541,6 +538,7 @@ def XMLgetGroupsInPartition(partition_id, REQUEST=None): # was XMLgetGroupesTD
doc.append(x_group) doc.append(x_group)
for etudid in etuds_set: for etudid in etuds_set:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
# etud = sco_etud.get_etud_info_filled_by_etudid(etudid, cnx)
x_group.append( x_group.append(
Element( Element(
"etud", "etud",
@ -551,8 +549,13 @@ def XMLgetGroupsInPartition(partition_id, REQUEST=None): # was XMLgetGroupesTD
origin=comp_origin(etud, sem), origin=comp_origin(etud, sem),
) )
) )
log("XMLgetGroupsInPartition: %s seconds" % (time.time() - t0)) t2 = time.time()
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING) log(f"XMLgetGroupsInPartition: {t2-t0} seconds ({t1-t0}+{t2-t1})")
# XML response:
data = sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING)
response = make_response(data)
response.headers["Content-Type"] = scu.XML_MIMETYPE
return response
def comp_origin(etud, cur_sem): def comp_origin(etud, cur_sem):
@ -652,7 +655,6 @@ def setGroups(
groupsLists="", # members of each existing group groupsLists="", # members of each existing group
groupsToCreate="", # name and members of new groups groupsToCreate="", # name and members of new groups
groupsToDelete="", # groups to delete groupsToDelete="", # groups to delete
REQUEST=None,
): ):
"""Affect groups (Ajax request) """Affect groups (Ajax request)
groupsLists: lignes de la forme "group_id;etudid;...\n" groupsLists: lignes de la forme "group_id;etudid;...\n"
@ -716,7 +718,7 @@ def setGroups(
# Supprime les groupes indiqués comme supprimés: # Supprime les groupes indiqués comme supprimés:
for group_id in groupsToDelete: for group_id in groupsToDelete:
suppressGroup(group_id, partition_id=partition_id, REQUEST=REQUEST) delete_group(group_id, partition_id=partition_id)
# Crée les nouveaux groupes # Crée les nouveaux groupes
for line in groupsToCreate.split("\n"): # for each group_name (one per line) for line in groupsToCreate.split("\n"): # for each group_name (one per line)
@ -724,22 +726,20 @@ def setGroups(
group_name = fs[0].strip() group_name = fs[0].strip()
if not group_name: if not group_name:
continue continue
# ajax arguments are encoded in utf-8: group_id = create_group(partition_id, group_name)
# group_name = six.text_type(group_name, "utf-8").encode(
# scu.SCO_ENCODING
# ) # #py3 #sco8
group_id = createGroup(partition_id, group_name)
# Place dans ce groupe les etudiants indiqués: # Place dans ce groupe les etudiants indiqués:
for etudid in fs[1:-1]: for etudid in fs[1:-1]:
change_etud_group_in_partition(etudid, group_id, partition) change_etud_group_in_partition(etudid, group_id, partition)
REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE) data = (
return (
'<?xml version="1.0" encoding="utf-8"?><response>Groupes enregistrés</response>' '<?xml version="1.0" encoding="utf-8"?><response>Groupes enregistrés</response>'
) )
response = make_response(data)
response.headers["Content-Type"] = scu.XML_MIMETYPE
return response
def createGroup(partition_id, group_name="", default=False): def create_group(partition_id, group_name="", default=False) -> int:
"""Create a new group in this partition""" """Create a new group in this partition"""
partition = get_partition(partition_id) partition = get_partition(partition_id)
formsemestre_id = partition["formsemestre_id"] formsemestre_id = partition["formsemestre_id"]
@ -759,12 +759,12 @@ def createGroup(partition_id, group_name="", default=False):
group_id = groupEditor.create( group_id = groupEditor.create(
cnx, {"partition_id": partition_id, "group_name": group_name} cnx, {"partition_id": partition_id, "group_name": group_name}
) )
log("createGroup: created group_id=%s" % group_id) log("create_group: created group_id=%s" % group_id)
# #
return group_id return group_id
def suppressGroup(group_id, partition_id=None, REQUEST=None): def delete_group(group_id, partition_id=None):
"""form suppression d'un groupe. """form suppression d'un groupe.
(ne desinscrit pas les etudiants, change juste leur (ne desinscrit pas les etudiants, change juste leur
affectation aux groupes) affectation aux groupes)
@ -781,7 +781,7 @@ def suppressGroup(group_id, partition_id=None, REQUEST=None):
if not sco_permissions_check.can_change_groups(partition["formsemestre_id"]): if not sco_permissions_check.can_change_groups(partition["formsemestre_id"]):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
log( log(
"suppressGroup: group_id=%s group_name=%s partition_name=%s" "delete_group: group_id=%s group_name=%s partition_name=%s"
% (group_id, group["group_name"], partition["partition_name"]) % (group_id, group["group_name"], partition["partition_name"])
) )
group_delete(group) group_delete(group)
@ -798,7 +798,7 @@ def partition_create(
if not sco_permissions_check.can_change_groups(formsemestre_id): if not sco_permissions_check.can_change_groups(formsemestre_id):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
if partition_name: if partition_name:
partition_name = partition_name.strip() partition_name = str(partition_name).strip()
if default: if default:
partition_name = None partition_name = None
if not partition_name and not default: if not partition_name and not default:
@ -813,8 +813,21 @@ def partition_create(
) )
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
if numero is None:
numero = (
ndb.SimpleQuery(
"SELECT MAX(id) FROM partition WHERE formsemestre_id=%(formsemestre_id)s",
{"formsemestre_id": formsemestre_id},
).fetchone()[0]
or 0
)
partition_id = partitionEditor.create( partition_id = partitionEditor.create(
cnx, {"formsemestre_id": formsemestre_id, "partition_name": partition_name} cnx,
{
"formsemestre_id": formsemestre_id,
"partition_name": partition_name,
"numero": numero,
},
) )
log("createPartition: created partition_id=%s" % partition_id) log("createPartition: created partition_id=%s" % partition_id)
# #
@ -830,7 +843,7 @@ def partition_create(
return partition_id return partition_id
def getArrowIconsTags(): def get_arrow_icons_tags():
"""returns html tags for arrows""" """returns html tags for arrows"""
# #
arrow_up = scu.icontag("arrow_up", title="remonter") arrow_up = scu.icontag("arrow_up", title="remonter")
@ -840,13 +853,13 @@ def getArrowIconsTags():
return arrow_up, arrow_down, arrow_none return arrow_up, arrow_down, arrow_none
def editPartitionForm(formsemestre_id=None, REQUEST=None): def editPartitionForm(formsemestre_id=None):
"""Form to create/suppress partitions""" """Form to create/suppress partitions"""
# ad-hoc form # ad-hoc form
if not sco_permissions_check.can_change_groups(formsemestre_id): if not sco_permissions_check.can_change_groups(formsemestre_id):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
partitions = get_partitions_list(formsemestre_id) partitions = get_partitions_list(formsemestre_id)
arrow_up, arrow_down, arrow_none = getArrowIconsTags() arrow_up, arrow_down, arrow_none = get_arrow_icons_tags()
suppricon = scu.icontag( suppricon = scu.icontag(
"delete_small_img", border="0", alt="supprimer", title="Supprimer" "delete_small_img", border="0", alt="supprimer", title="Supprimer"
) )
@ -907,7 +920,7 @@ def editPartitionForm(formsemestre_id=None, REQUEST=None):
H.append(", ".join(lg)) H.append(", ".join(lg))
H.append( H.append(
f"""</td><td><a class="stdlink" href="{ f"""</td><td><a class="stdlink" href="{
url_for("scolar.affectGroups", url_for("scolar.affect_groups",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
partition_id=p["partition_id"]) partition_id=p["partition_id"])
}">répartir</a></td> }">répartir</a></td>
@ -968,7 +981,7 @@ def editPartitionForm(formsemestre_id=None, REQUEST=None):
return "\n".join(H) + html_sco_header.sco_footer() return "\n".join(H) + html_sco_header.sco_footer()
def partition_set_attr(partition_id, attr, value, REQUEST=None): def partition_set_attr(partition_id, attr, value):
"""Set partition attribute: bul_show_rank or show_in_lists""" """Set partition attribute: bul_show_rank or show_in_lists"""
if attr not in {"bul_show_rank", "show_in_lists"}: if attr not in {"bul_show_rank", "show_in_lists"}:
raise ValueError("invalid partition attribute: %s" % attr) raise ValueError("invalid partition attribute: %s" % attr)
@ -991,9 +1004,7 @@ def partition_set_attr(partition_id, attr, value, REQUEST=None):
return "enregistré" return "enregistré"
def partition_delete( def partition_delete(partition_id, force=False, redirect=1, dialog_confirmed=False):
partition_id, REQUEST=None, force=False, redirect=1, dialog_confirmed=False
):
"""Suppress a partition (and all groups within). """Suppress a partition (and all groups within).
default partition cannot be suppressed (unless force)""" default partition cannot be suppressed (unless force)"""
partition = get_partition(partition_id) partition = get_partition(partition_id)
@ -1036,7 +1047,7 @@ def partition_delete(
) )
def partition_move(partition_id, after=0, REQUEST=None, redirect=1): def partition_move(partition_id, after=0, redirect=1):
"""Move before/after previous one (decrement/increment numero)""" """Move before/after previous one (decrement/increment numero)"""
partition = get_partition(partition_id) partition = get_partition(partition_id)
formsemestre_id = partition["formsemestre_id"] formsemestre_id = partition["formsemestre_id"]
@ -1050,7 +1061,7 @@ def partition_move(partition_id, after=0, REQUEST=None, redirect=1):
others = get_partitions_list(formsemestre_id) others = get_partitions_list(formsemestre_id)
if len(others) > 1: if len(others) > 1:
pidx = [p["partition_id"] for p in others].index(partition_id) pidx = [p["partition_id"] for p in others].index(partition_id)
log("partition_move: after=%s pidx=%s" % (after, pidx)) # log("partition_move: after=%s pidx=%s" % (after, pidx))
neigh = None # partition to swap with neigh = None # partition to swap with
if after == 0 and pidx > 0: if after == 0 and pidx > 0:
neigh = others[pidx - 1] neigh = others[pidx - 1]
@ -1058,8 +1069,20 @@ def partition_move(partition_id, after=0, REQUEST=None, redirect=1):
neigh = others[pidx + 1] neigh = others[pidx + 1]
if neigh: # if neigh: #
# swap numero between partition and its neighbor # swap numero between partition and its neighbor
log("moving partition %s" % partition_id) # log("moving partition %s" % partition_id)
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
# Si aucun numéro n'a été affecté, le met au minimum
min_numero = (
ndb.SimpleQuery(
"SELECT MIN(numero) FROM partition WHERE formsemestre_id=%(formsemestre_id)s",
{"formsemestre_id": formsemestre_id},
).fetchone()[0]
or 0
)
if neigh["numero"] is None:
neigh["numero"] = min_numero - 1
if partition["numero"] is None:
partition["numero"] = min_numero - 1 - after
partition["numero"], neigh["numero"] = neigh["numero"], partition["numero"] partition["numero"], neigh["numero"] = neigh["numero"], partition["numero"]
partitionEditor.edit(cnx, partition) partitionEditor.edit(cnx, partition)
partitionEditor.edit(cnx, neigh) partitionEditor.edit(cnx, neigh)
@ -1071,7 +1094,7 @@ def partition_move(partition_id, after=0, REQUEST=None, redirect=1):
) )
def partition_rename(partition_id, REQUEST=None): def partition_rename(partition_id):
"""Form to rename a partition""" """Form to rename a partition"""
partition = get_partition(partition_id) partition = get_partition(partition_id)
formsemestre_id = partition["formsemestre_id"] formsemestre_id = partition["formsemestre_id"]
@ -1079,8 +1102,8 @@ def partition_rename(partition_id, REQUEST=None):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
H = ["<h2>Renommer une partition</h2>"] H = ["<h2>Renommer une partition</h2>"]
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
( (
("partition_id", {"default": partition_id, "input_type": "hidden"}), ("partition_id", {"default": partition_id, "input_type": "hidden"}),
( (
@ -1110,14 +1133,12 @@ def partition_rename(partition_id, REQUEST=None):
) )
else: else:
# form submission # form submission
return partition_set_name( return partition_set_name(partition_id, tf[2]["partition_name"])
partition_id, tf[2]["partition_name"], REQUEST=REQUEST, redirect=1
)
def partition_set_name(partition_id, partition_name, REQUEST=None, redirect=1): def partition_set_name(partition_id, partition_name, redirect=1):
"""Set partition name""" """Set partition name"""
partition_name = partition_name.strip() partition_name = str(partition_name).strip()
if not partition_name: if not partition_name:
raise ValueError("partition name must be non empty") raise ValueError("partition name must be non empty")
partition = get_partition(partition_id) partition = get_partition(partition_id)
@ -1127,13 +1148,13 @@ def partition_set_name(partition_id, partition_name, REQUEST=None, redirect=1):
# check unicity # check unicity
r = ndb.SimpleDictFetch( r = ndb.SimpleDictFetch(
"""SELECT p.* FROM partition p """SELECT p.* FROM partition p
WHERE p.partition_name = %(partition_name)s WHERE p.partition_name = %(partition_name)s
AND formsemestre_id = %(formsemestre_id)s AND formsemestre_id = %(formsemestre_id)s
""", """,
{"partition_name": partition_name, "formsemestre_id": formsemestre_id}, {"partition_name": partition_name, "formsemestre_id": formsemestre_id},
) )
if len(r) > 1 or (len(r) == 1 and r[0]["partition_id"] != partition_id): if len(r) > 1 or (len(r) == 1 and r[0]["id"] != partition_id):
raise ScoValueError( raise ScoValueError(
"Partition %s déjà existante dans ce semestre !" % partition_name "Partition %s déjà existante dans ce semestre !" % partition_name
) )
@ -1153,7 +1174,7 @@ def partition_set_name(partition_id, partition_name, REQUEST=None, redirect=1):
) )
def group_set_name(group_id, group_name, REQUEST=None, redirect=1): def group_set_name(group_id, group_name, redirect=True):
"""Set group name""" """Set group name"""
if group_name: if group_name:
group_name = group_name.strip() group_name = group_name.strip()
@ -1173,14 +1194,14 @@ def group_set_name(group_id, group_name, REQUEST=None, redirect=1):
if redirect: if redirect:
return flask.redirect( return flask.redirect(
url_for( url_for(
"scolar.affectGroups", "scolar.affect_groups",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
partition_id=group["partition_id"], partition_id=group["partition_id"],
) )
) )
def group_rename(group_id, REQUEST=None): def group_rename(group_id):
"""Form to rename a group""" """Form to rename a group"""
group = get_group(group_id) group = get_group(group_id)
formsemestre_id = group["formsemestre_id"] formsemestre_id = group["formsemestre_id"]
@ -1188,8 +1209,8 @@ def group_rename(group_id, REQUEST=None):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
H = ["<h2>Renommer un groupe de %s</h2>" % group["partition_name"]] H = ["<h2>Renommer un groupe de %s</h2>" % group["partition_name"]]
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
( (
("group_id", {"default": group_id, "input_type": "hidden"}), ("group_id", {"default": group_id, "input_type": "hidden"}),
( (
@ -1216,19 +1237,17 @@ def group_rename(group_id, REQUEST=None):
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect( return flask.redirect(
url_for( url_for(
"scolar.affectGroups", "scolar.affect_groups",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
partition_id=group["partition_id"], partition_id=group["partition_id"],
) )
) )
else: else:
# form submission # form submission
return group_set_name( return group_set_name(group_id, tf[2]["group_name"])
group_id, tf[2]["group_name"], REQUEST=REQUEST, redirect=1
)
def groups_auto_repartition(partition_id=None, REQUEST=None): def groups_auto_repartition(partition_id=None):
"""Reparti les etudiants dans des groupes dans une partition, en respectant le niveau """Reparti les etudiants dans des groupes dans une partition, en respectant le niveau
et la mixité. et la mixité.
""" """
@ -1238,7 +1257,7 @@ def groups_auto_repartition(partition_id=None, REQUEST=None):
formsemestre_id = partition["formsemestre_id"] formsemestre_id = partition["formsemestre_id"]
# renvoie sur page édition groupes # renvoie sur page édition groupes
dest_url = url_for( dest_url = url_for(
"scolar.affectGroups", scodoc_dept=g.scodoc_dept, partition_id=partition_id "scolar.affect_groups", scodoc_dept=g.scodoc_dept, partition_id=partition_id
) )
if not sco_permissions_check.can_change_groups(formsemestre_id): if not sco_permissions_check.can_change_groups(formsemestre_id):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
@ -1268,8 +1287,8 @@ def groups_auto_repartition(partition_id=None, REQUEST=None):
] ]
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
descr, descr,
{}, {},
cancelbutton="Annuler", cancelbutton="Annuler",
@ -1299,8 +1318,8 @@ def groups_auto_repartition(partition_id=None, REQUEST=None):
# checkGroupName(group_name) # checkGroupName(group_name)
# except: # except:
# H.append('<p class="warning">Nom de groupe invalide: %s</p>'%group_name) # H.append('<p class="warning">Nom de groupe invalide: %s</p>'%group_name)
# return '\n'.join(H) + tf[1] + html_sco_header.sco_footer( REQUEST) # return '\n'.join(H) + tf[1] + html_sco_header.sco_footer()
group_ids.append(createGroup(partition_id, group_name)) group_ids.append(create_group(partition_id, group_name))
# #
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > identdict nt = sco_cache.NotesTableCache.get(formsemestre_id) # > identdict
identdict = nt.identdict identdict = nt.identdict
@ -1360,6 +1379,7 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"):
""" """
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
partition_name = str(partition_name)
log("create_etapes_partition(%s)" % formsemestre_id) log("create_etapes_partition(%s)" % formsemestre_id)
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
args={"formsemestre_id": formsemestre_id} args={"formsemestre_id": formsemestre_id}
@ -1384,7 +1404,7 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"):
groups_by_names = {g["group_name"]: g for g in groups} groups_by_names = {g["group_name"]: g for g in groups}
for etape in etapes: for etape in etapes:
if not (etape in groups_by_names): if not (etape in groups_by_names):
gid = createGroup(pid, etape) gid = create_group(pid, etape)
g = get_group(gid) g = get_group(gid)
groups_by_names[etape] = g groups_by_names[etape] = g
# Place les etudiants dans les groupes # Place les etudiants dans les groupes
@ -1405,6 +1425,7 @@ def do_evaluation_listeetuds_groups(
Si include_dems, compte aussi les etudiants démissionnaires Si include_dems, compte aussi les etudiants démissionnaires
(sinon, par défaut, seulement les 'I') (sinon, par défaut, seulement les 'I')
""" """
# nb: pour notes_table / do_evaluation_etat, getallstudents est vrai et include_dems faux
fromtables = [ fromtables = [
"notes_moduleimpl_inscription Im", "notes_moduleimpl_inscription Im",
"notes_formsemestre_inscription Isem", "notes_formsemestre_inscription Isem",
@ -1416,7 +1437,7 @@ def do_evaluation_listeetuds_groups(
if not groups: if not groups:
return [] # no groups, so no students return [] # no groups, so no students
rg = ["gm.group_id = '%(group_id)s'" % g for g in groups] rg = ["gm.group_id = '%(group_id)s'" % g for g in groups]
rq = """and Isem.etudid = gm.etudid rq = """and Isem.etudid = gm.etudid
and gd.partition_id = p.id and gd.partition_id = p.id
and p.formsemestre_id = Isem.formsemestre_id and p.formsemestre_id = Isem.formsemestre_id
""" """
@ -1429,7 +1450,7 @@ def do_evaluation_listeetuds_groups(
req = ( req = (
"SELECT distinct Im.etudid FROM " "SELECT distinct Im.etudid FROM "
+ ", ".join(fromtables) + ", ".join(fromtables)
+ """ WHERE Isem.etudid = Im.etudid + """ WHERE Isem.etudid = Im.etudid
and Im.moduleimpl_id = M.id and Im.moduleimpl_id = M.id
and Isem.formsemestre_id = M.formsemestre_id and Isem.formsemestre_id = M.formsemestre_id
and E.moduleimpl_id = M.id and E.moduleimpl_id = M.id
@ -1440,10 +1461,9 @@ def do_evaluation_listeetuds_groups(
req += " and Isem.etat='I'" req += " and Isem.etat='I'"
req += r req += r
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor()
cursor.execute(req, {"evaluation_id": evaluation_id}) cursor.execute(req, {"evaluation_id": evaluation_id})
res = cursor.fetchall() return [x[0] for x in cursor]
return [x[0] for x in res]
def do_evaluation_listegroupes(evaluation_id, include_default=False): def do_evaluation_listegroupes(evaluation_id, include_default=False):
@ -1457,7 +1477,7 @@ def do_evaluation_listegroupes(evaluation_id, include_default=False):
else: else:
c = " AND p.partition_name is not NULL" c = " AND p.partition_name is not NULL"
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor()
cursor.execute( cursor.execute(
"""SELECT DISTINCT gd.id AS group_id """SELECT DISTINCT gd.id AS group_id
FROM group_descr gd, group_membership gm, partition p, FROM group_descr gd, group_membership gm, partition p,
@ -1471,8 +1491,7 @@ def do_evaluation_listegroupes(evaluation_id, include_default=False):
+ c, + c,
{"evaluation_id": evaluation_id}, {"evaluation_id": evaluation_id},
) )
res = cursor.fetchall() group_ids = [x[0] for x in cursor]
group_ids = [x[0] for x in res]
return listgroups(group_ids) return listgroups(group_ids)
@ -1482,9 +1501,9 @@ def listgroups(group_ids):
groups = [] groups = []
for group_id in group_ids: for group_id in group_ids:
cursor.execute( cursor.execute(
"""SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.* """SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.*
FROM group_descr gd, partition p FROM group_descr gd, partition p
WHERE p.id = gd.partition_id WHERE p.id = gd.partition_id
AND gd.id = %(group_id)s AND gd.id = %(group_id)s
""", """,
{"group_id": group_id}, {"group_id": group_id},
@ -1499,7 +1518,7 @@ def _sortgroups(groups):
# Tri: place 'all' en tête, puis groupe par partition / nom de groupe # Tri: place 'all' en tête, puis groupe par partition / nom de groupe
R = [g for g in groups if g["partition_name"] is None] R = [g for g in groups if g["partition_name"] is None]
o = [g for g in groups if g["partition_name"] != None] o = [g for g in groups if g["partition_name"] != None]
o.sort(key=lambda x: (x["numero"], x["group_name"])) o.sort(key=lambda x: (x["numero"] or 0, x["group_name"]))
return R + o return R + o

View File

@ -0,0 +1,66 @@
from app import db
from app.scodoc import sco_groups
import app.scodoc.notesdb as ndb
def clone_partitions_and_groups(
orig_formsemestre_id: int, formsemestre_id: int, inscrit_etuds=False
):
"""Crée dans le semestre formsemestre_id les mêmes partitions et groupes que ceux
de orig_formsemestre_id.
Si inscrit_etuds, inscrit les mêmes étudiants (rarement souhaité).
"""
list_groups_per_part = []
list_groups = []
groups_old2new = {} # old group_id : new_group_id
# Création des partitions:
for part in sco_groups.get_partitions_list(orig_formsemestre_id):
if part["partition_name"] is not None:
partname = part["partition_name"]
new_partition_id = sco_groups.partition_create(
formsemestre_id,
partition_name=partname,
numero=part["numero"],
redirect=False,
)
for group in sco_groups.get_partition_groups(part):
if group["group_name"] != None:
list_groups.append(group)
list_groups_per_part.append([new_partition_id, list_groups])
list_groups = []
# Création des groupes dans les nouvelles partitions:
for newpart in sco_groups.get_partitions_list(formsemestre_id):
for (new_partition_id, list_groups) in list_groups_per_part:
if newpart["partition_id"] == new_partition_id:
for group in list_groups:
new_group_id = sco_groups.create_group(
new_partition_id, group_name=group["group_name"]
)
groups_old2new[group["group_id"]] = new_group_id
#
if inscrit_etuds:
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor()
for old_group_id, new_group_id in groups_old2new.items():
cursor.execute(
"""
WITH etuds AS (
SELECT gm.etudid
FROM group_membership gm, notes_formsemestre_inscription ins
WHERE ins.etudid = gm.etudid
AND ins.formsemestre_id = %(orig_formsemestre_id)s
AND gm.group_id=%(old_group_id)s
)
INSERT INTO group_membership (etudid, group_id)
SELECT *, %(new_group_id)s FROM etuds
ON CONFLICT DO NOTHING
""",
{
"orig_formsemestre_id": orig_formsemestre_id,
"old_group_id": old_group_id,
"new_group_id": new_group_id,
},
)
cnx.commit()

View File

@ -27,70 +27,33 @@
"""Formulaires gestion des groupes """Formulaires gestion des groupes
""" """
from flask import render_template
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc.sco_exceptions import AccessDenied from app.scodoc.sco_exceptions import AccessDenied
def affectGroups(partition_id, REQUEST=None): def affect_groups(partition_id):
"""Formulaire affectation des etudiants aux groupes de la partition. """Formulaire affectation des etudiants aux groupes de la partition.
Permet aussi la creation et la suppression de groupes. Permet aussi la creation et la suppression de groupes.
""" """
# Ported from DTML and adapted to new group management (nov 2009) # réécrit pour 9.0.47 avec un template
partition = sco_groups.get_partition(partition_id) partition = sco_groups.get_partition(partition_id)
formsemestre_id = partition["formsemestre_id"] formsemestre_id = partition["formsemestre_id"]
if not sco_groups.sco_permissions_check.can_change_groups(formsemestre_id): if not sco_groups.sco_permissions_check.can_change_groups(formsemestre_id):
raise AccessDenied("vous n'avez pas la permission d'effectuer cette opération") raise AccessDenied("vous n'avez pas la permission de modifier les groupes")
return render_template(
H = [ "scolar/affect_groups.html",
html_sco_header.sco_header( sco_header=html_sco_header.sco_header(
page_title="Affectation aux groupes", page_title="Affectation aux groupes",
javascripts=["js/groupmgr.js"], javascripts=["js/groupmgr.js"],
cssstyles=["css/groups.css"], cssstyles=["css/groups.css"],
), ),
"""<h2 class="formsemestre">Affectation aux groupes de %s</h2><form id="sp">""" sco_footer=html_sco_header.sco_footer(),
% partition["partition_name"], partition=partition,
] partitions_list=sco_groups.get_partitions_list(
formsemestre_id, with_default=False
H += [ ),
"""</select></form>""", formsemestre_id=formsemestre_id,
"""<p>Faites glisser les étudiants d'un groupe à l'autre. Les modifications ne sont enregistrées que lorsque vous cliquez sur le bouton "<em>Enregistrer ces groupes</em>". Vous pouvez créer de nouveaux groupes. Pour <em>supprimer</em> un groupe, utiliser le lien "suppr." en haut à droite de sa boite. Vous pouvez aussi <a class="stdlink" href="groups_auto_repartition?partition_id=%(partition_id)s">répartir automatiquement les groupes</a>. )
</p>"""
% partition,
"""<div id="gmsg" class="head_message"></div>""",
"""<div id="ginfo"></div>""",
"""<div id="savedinfo"></div>""",
"""<form name="formGroup" id="formGroup" onSubmit="return false;">""",
"""<input type="hidden" name="partition_id" value="%s"/>""" % partition_id,
"""<input name="groupName" size="6"/>
<input type="button" onClick="createGroup();" value="Créer groupe"/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<input type="button" onClick="submitGroups( target='gmsg' );" value="Enregistrer ces groupes" />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<input type="button" onClick="document.location = 'formsemestre_status?formsemestre_id=%s'" value="Annuler" />&nbsp;&nbsp;&nbsp;&nbsp;
Editer groupes de
<select name="other_partition_id" onchange="GotoAnother();">"""
% formsemestre_id,
]
for p in sco_groups.get_partitions_list(formsemestre_id, with_default=False):
H.append('<option value="%s"' % p["partition_id"])
if p["partition_id"] == partition_id:
H.append(" selected")
H.append(">%s</option>" % p["partition_name"])
H += [
"""</select>
</form>
<div id="groups">
</div>
<div style="clear: left; margin-top: 15px;">
<p class="help"></p>
</div>
</div>
""",
html_sco_header.sco_footer(),
]
return "\n".join(H)

File diff suppressed because it is too large Load Diff

View File

@ -25,16 +25,16 @@
# #
############################################################################## ##############################################################################
""" Importation des etudiants à partir de fichiers CSV """ Importation des étudiants à partir de fichiers CSV
""" """
import collections import collections
import io
import os import os
import re import re
import time import time
from datetime import date from datetime import date
import flask
from flask import g, url_for from flask import g, url_for
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -219,21 +219,20 @@ def sco_import_generate_excel_sample(
def students_import_excel( def students_import_excel(
csvfile, csvfile,
REQUEST=None,
formsemestre_id=None, formsemestre_id=None,
check_homonyms=True, check_homonyms=True,
require_ine=False, require_ine=False,
return_html=True,
): ):
"import students from Excel file" "import students from Excel file"
diag = scolars_import_excel_file( diag = scolars_import_excel_file(
csvfile, csvfile,
REQUEST,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
check_homonyms=check_homonyms, check_homonyms=check_homonyms,
require_ine=require_ine, require_ine=require_ine,
exclude_cols=["photo_filename"], exclude_cols=["photo_filename"],
) )
if REQUEST: if return_html:
if formsemestre_id: if formsemestre_id:
dest = url_for( dest = url_for(
"notes.formsemestre_status", "notes.formsemestre_status",
@ -253,8 +252,7 @@ def students_import_excel(
def scolars_import_excel_file( def scolars_import_excel_file(
datafile, datafile: io.BytesIO,
REQUEST,
formsemestre_id=None, formsemestre_id=None,
check_homonyms=True, check_homonyms=True,
require_ine=False, require_ine=False,
@ -416,17 +414,14 @@ def scolars_import_excel_file(
if NbHomonyms: if NbHomonyms:
NbImportedHomonyms += 1 NbImportedHomonyms += 1
# Insert in DB tables # Insert in DB tables
formsemestre_to_invalidate.add( formsemestre_id_etud = _import_one_student(
_import_one_student( cnx,
cnx, formsemestre_id,
REQUEST, values,
formsemestre_id, GroupIdInferers,
values, annee_courante,
GroupIdInferers, created_etudids,
annee_courante, linenum,
created_etudids,
linenum,
)
) )
# Verification proportion d'homonymes: si > 10%, abandonne # Verification proportion d'homonymes: si > 10%, abandonne
@ -492,16 +487,15 @@ def scolars_import_excel_file(
def students_import_admission( def students_import_admission(
csvfile, type_admission="", REQUEST=None, formsemestre_id=None csvfile, type_admission="", formsemestre_id=None, return_html=True
): ):
"import donnees admission from Excel file (v2016)" "import donnees admission from Excel file (v2016)"
diag = scolars_import_admission( diag = scolars_import_admission(
csvfile, csvfile,
REQUEST,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
type_admission=type_admission, type_admission=type_admission,
) )
if REQUEST: if return_html:
H = [html_sco_header.sco_header(page_title="Import données admissions")] H = [html_sco_header.sco_header(page_title="Import données admissions")]
H.append("<p>Import terminé !</p>") H.append("<p>Import terminé !</p>")
H.append( H.append(
@ -520,14 +514,13 @@ def students_import_admission(
def _import_one_student( def _import_one_student(
cnx, cnx,
REQUEST,
formsemestre_id, formsemestre_id,
values, values,
GroupIdInferers, GroupIdInferers,
annee_courante, annee_courante,
created_etudids, created_etudids,
linenum, linenum,
): ) -> int:
""" """
Import d'un étudiant et inscription dans le semestre. Import d'un étudiant et inscription dans le semestre.
Return: id du semestre dans lequel il a été inscrit. Return: id du semestre dans lequel il a été inscrit.
@ -538,7 +531,7 @@ def _import_one_student(
) )
# Identite # Identite
args = values.copy() args = values.copy()
etudid = sco_etud.identite_create(cnx, args, REQUEST=REQUEST) etudid = sco_etud.identite_create(cnx, args)
created_etudids.append(etudid) created_etudids.append(etudid)
# Admissions # Admissions
args["etudid"] = etudid args["etudid"] = etudid
@ -555,6 +548,12 @@ def _import_one_student(
else: else:
args["formsemestre_id"] = values["codesemestre"] args["formsemestre_id"] = values["codesemestre"]
formsemestre_id = values["codesemestre"] formsemestre_id = values["codesemestre"]
try:
formsemestre_id = int(formsemestre_id)
except ValueError as exc:
raise ScoValueError(
f"valeur invalide dans la colonne codesemestre, ligne {linenum+1}"
) from exc
# recupere liste des groupes: # recupere liste des groupes:
if formsemestre_id not in GroupIdInferers: if formsemestre_id not in GroupIdInferers:
GroupIdInferers[formsemestre_id] = sco_groups.GroupIdInferer(formsemestre_id) GroupIdInferers[formsemestre_id] = sco_groups.GroupIdInferer(formsemestre_id)
@ -571,7 +570,7 @@ def _import_one_student(
) )
do_formsemestre_inscription_with_modules( do_formsemestre_inscription_with_modules(
args["formsemestre_id"], int(args["formsemestre_id"]),
etudid, etudid,
group_ids, group_ids,
etat="I", etat="I",
@ -587,9 +586,7 @@ def _is_new_ine(cnx, code_ine):
# ------ Fonction ré-écrite en nov 2016 pour lire des fichiers sans etudid (fichiers APB) # ------ Fonction ré-écrite en nov 2016 pour lire des fichiers sans etudid (fichiers APB)
def scolars_import_admission( def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None):
datafile, REQUEST, formsemestre_id=None, type_admission=None
):
"""Importe données admission depuis un fichier Excel quelconque """Importe données admission depuis un fichier Excel quelconque
par exemple ceux utilisés avec APB par exemple ceux utilisés avec APB
@ -646,7 +643,7 @@ def scolars_import_admission(
raise FormatError( raise FormatError(
"scolars_import_admission: colonnes nom et prenom requises", "scolars_import_admission: colonnes nom et prenom requises",
dest_url=url_for( dest_url=url_for(
"notes.form_students_import_infos_admissions", "scolar.form_students_import_infos_admissions",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
), ),
@ -679,7 +676,7 @@ def scolars_import_admission(
'scolars_import_admission: valeur invalide, ligne %d colonne %s: "%s"' 'scolars_import_admission: valeur invalide, ligne %d colonne %s: "%s"'
% (nline, field_name, line[idx]), % (nline, field_name, line[idx]),
dest_url=url_for( dest_url=url_for(
"notes.form_students_import_infos_admissions", "scolar.form_students_import_infos_admissions",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
), ),
@ -765,7 +762,7 @@ def adm_get_fields(titles, formsemestre_id):
'scolars_import_admission: titre "%s" en double (ligne 1)' 'scolars_import_admission: titre "%s" en double (ligne 1)'
% (title), % (title),
dest_url=url_for( dest_url=url_for(
"notes.form_students_import_infos_admissions_apb", "scolar.form_students_import_infos_admissions_apb",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
), ),

View File

@ -27,27 +27,23 @@
"""Import d'utilisateurs via fichier Excel """Import d'utilisateurs via fichier Excel
""" """
import random, time import random
import re import time
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from flask import g, url_for
from email.header import Header from flask_login import current_user
from app import db, Departement from app import db
from app import email
from app.auth.models import User, UserRole
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import log
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError, ScoException from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc import sco_excel from app.scodoc import sco_excel
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_users from app.scodoc import sco_users
from flask import g
from flask_login import current_user
from app.auth.models import User, UserRole
from app import email
TITLES = ("user_name", "nom", "prenom", "email", "roles", "dept") TITLES = ("user_name", "nom", "prenom", "email", "roles", "dept")
COMMENTS = ( COMMENTS = (
@ -86,11 +82,11 @@ def generate_excel_sample():
) )
def import_excel_file(datafile): def import_excel_file(datafile, force=""):
""" """
Import scodoc users from Excel file. Import scodoc users from Excel file.
This method: This method:
* checks that the current_user has the ability to do so (at the moment only a SuperAdmin). He may thereoff import users with any well formed role into any deprtment (or all) * checks that the current_user has the ability to do so (at the moment only a SuperAdmin). He may thereoff import users with any well formed role into any department (or all)
* Once the check is done ans successfull, build the list of users (does not check the data) * Once the check is done ans successfull, build the list of users (does not check the data)
* call :func:`import_users` to actually do the job * call :func:`import_users` to actually do the job
history: scodoc7 with no SuperAdmin every Admin_XXX could import users. history: scodoc7 with no SuperAdmin every Admin_XXX could import users.
@ -98,7 +94,6 @@ def import_excel_file(datafile):
:return: same as import users :return: same as import users
""" """
# Check current user privilege # Check current user privilege
auth_dept = current_user.dept
auth_name = str(current_user) auth_name = str(current_user)
if not current_user.is_administrator(): if not current_user.is_administrator():
raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name) raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name)
@ -109,8 +104,11 @@ def import_excel_file(datafile):
if not exceldata: if not exceldata:
raise ScoValueError("Ficher excel vide ou invalide") raise ScoValueError("Ficher excel vide ou invalide")
_, data = sco_excel.excel_bytes_to_list(exceldata) _, data = sco_excel.excel_bytes_to_list(exceldata)
if not data: # probably a bug if not data:
raise ScoException("import_excel_file: empty file !") raise ScoValueError(
"""Le fichier xlsx attendu semble vide !
"""
)
# 1- --- check title line # 1- --- check title line
fs = [scu.stripquotes(s).lower() for s in data[0]] fs = [scu.stripquotes(s).lower() for s in data[0]]
log("excel: fs='%s'\ndata=%s" % (str(fs), str(data))) log("excel: fs='%s'\ndata=%s" % (str(fs), str(data)))
@ -124,7 +122,8 @@ def import_excel_file(datafile):
del cols[tit] del cols[tit]
if cols or unknown: if cols or unknown:
raise ScoValueError( raise ScoValueError(
"colonnes incorrectes (on attend %d, et non %d) <br/> (colonnes manquantes: %s, colonnes invalides: %s)" """colonnes incorrectes (on attend %d, et non %d) <br/>
(colonnes manquantes: %s, colonnes invalides: %s)"""
% (len(TITLES), len(fs), list(cols.keys()), unknown) % (len(TITLES), len(fs), list(cols.keys()), unknown)
) )
# ok, same titles... : build the list of dictionaries # ok, same titles... : build the list of dictionaries
@ -135,10 +134,10 @@ def import_excel_file(datafile):
d[fs[i]] = line[i] d[fs[i]] = line[i]
users.append(d) users.append(d)
return import_users(users) return import_users(users=users, force=force)
def import_users(users): def import_users(users, force=""):
""" """
Import users from a list of users_descriptors. Import users from a list of users_descriptors.
@ -179,60 +178,35 @@ def import_users(users):
line = line + 1 line = line + 1
user_ok, msg = sco_users.check_modif_user( user_ok, msg = sco_users.check_modif_user(
0, 0,
enforce_optionals=not force,
user_name=u["user_name"], user_name=u["user_name"],
nom=u["nom"], nom=u["nom"],
prenom=u["prenom"], prenom=u["prenom"],
email=u["email"], email=u["email"],
roles=u["roles"].split(","), roles=[r for r in u["roles"].split(",") if r],
dept=u["dept"],
) )
if not user_ok: if not user_ok:
append_msg("identifiant '%s' %s" % (u["user_name"], msg)) append_msg("identifiant '%s' %s" % (u["user_name"], msg))
# raise ScoValueError(
# "données invalides pour %s: %s" % (u["user_name"], msg)
# )
u["passwd"] = generate_password() u["passwd"] = generate_password()
# #
# check identifiant # check identifiant
if not re.match(r"^[a-zA-Z0-9@\\\-_\\\.]*$", u["user_name"]):
user_ok = False
append_msg(
"identifiant '%s' invalide (pas d'accents ni de caractères spéciaux)"
% u["user_name"]
)
if len(u["user_name"]) > 64:
user_ok = False
append_msg(
"identifiant '%s' trop long (64 caractères)" % u["user_name"]
)
if len(u["nom"]) > 64:
user_ok = False
append_msg("nom '%s' trop long (64 caractères)" % u["nom"])
if len(u["prenom"]) > 64:
user_ok = False
append_msg("prenom '%s' trop long (64 caractères)" % u["prenom"])
if len(u["email"]) > 120:
user_ok = False
append_msg("email '%s' trop long (120 caractères)" % u["email"])
# check that tha same user_name has not already been described in this import
if u["user_name"] in created.keys(): if u["user_name"] in created.keys():
user_ok = False user_ok = False
append_msg( append_msg(
"l'utilisateur '%s' a déjà été décrit ligne %s" "l'utilisateur '%s' a déjà été décrit ligne %s"
% (u["user_name"], created[u["user_name"]]["line"]) % (u["user_name"], created[u["user_name"]]["line"])
) )
# check département
if u["dept"] != "":
dept = Departement.query.filter_by(acronym=u["dept"]).first()
if dept is None:
user_ok = False
append_msg("département '%s' inexistant" % u["dept"])
# check roles / ignore whitespaces around roles / build roles_string # check roles / ignore whitespaces around roles / build roles_string
# roles_string (expected by User) appears as column 'roles' in excel file # roles_string (expected by User) appears as column 'roles' in excel file
roles_list = [] roles_list = []
for role in u["roles"].split(","): for role in u["roles"].split(","):
try: try:
_, _ = UserRole.role_dept_from_string(role.strip()) role = role.strip()
roles_list.append(role.strip()) if role:
_, _ = UserRole.role_dept_from_string(role)
roles_list.append(role)
except ScoValueError as value_error: except ScoValueError as value_error:
user_ok = False user_ok = False
append_msg("role %s : %s" % (role, value_error)) append_msg("role %s : %s" % (role, value_error))
@ -244,7 +218,7 @@ def import_users(users):
import_ok = False import_ok = False
except ScoValueError as value_error: except ScoValueError as value_error:
log("import_users: exception: abort create %s" % str(created.keys())) log("import_users: exception: abort create %s" % str(created.keys()))
raise ScoValueError(msg) # re-raise exception raise ScoValueError(msg) from value_error
if import_ok: if import_ok:
for u in created.values(): for u in created.values():
# Création de l'utilisateur (via SQLAlchemy) # Création de l'utilisateur (via SQLAlchemy)
@ -264,7 +238,7 @@ def import_users(users):
ALPHABET = r"""ABCDEFGHIJKLMNPQRSTUVWXYZ123456789123456789AEIOU""" ALPHABET = r"""ABCDEFGHIJKLMNPQRSTUVWXYZ123456789123456789AEIOU"""
PASSLEN = 6 PASSLEN = 8
RNG = random.Random(time.time()) RNG = random.Random(time.time())
@ -279,23 +253,18 @@ def generate_password():
return "".join(RNG.sample(l, PASSLEN)) return "".join(RNG.sample(l, PASSLEN))
def mail_password(u, context=None, reset=False): def mail_password(user: dict, reset=False) -> None:
"Send password by email" "Send password by email"
if not u["email"]: if not user["email"]:
return return
u[ user["url"] = url_for("scodoc.index", _external=True)
"url"
] = (
scu.ScoURL()
) # TODO set auth page URL ? (shared by all departments) ../auth/login
txt = ( txt = (
""" """
Bonjour %(prenom)s %(nom)s, Bonjour %(prenom)s %(nom)s,
""" """
% u % user
) )
if reset: if reset:
txt += ( txt += (
@ -305,10 +274,10 @@ votre mot de passe ScoDoc a été ré-initialisé.
Le nouveau mot de passe est: %(passwd)s Le nouveau mot de passe est: %(passwd)s
Votre nom d'utilisateur est %(user_name)s Votre nom d'utilisateur est %(user_name)s
Vous devrez changer ce mot de passe lors de votre première connexion Vous devrez changer ce mot de passe lors de votre première connexion
sur %(url)s sur %(url)s
""" """
% u % user
) )
else: else:
txt += ( txt += (
@ -320,16 +289,15 @@ Votre mot de passe est: %(passwd)s
Le logiciel est accessible sur: %(url)s Le logiciel est accessible sur: %(url)s
Vous êtes invité à changer ce mot de passe au plus vite (cliquez sur Vous êtes invité à changer ce mot de passe au plus vite (cliquez sur votre nom en haut à gauche de la page d'accueil).
votre nom en haut à gauche de la page d'accueil).
""" """
% u % user
) )
txt += ( txt += (
""" """
_______
ScoDoc est un logiciel libre développé à l'Université Paris 13 par Emmanuel Viennet. ScoDoc est un logiciel libre développé par Emmanuel Viennet et l'association ScoDoc.
Pour plus d'informations sur ce logiciel, voir %s Pour plus d'informations sur ce logiciel, voir %s
""" """
@ -341,4 +309,4 @@ Pour plus d'informations sur ce logiciel, voir %s
else: else:
subject = "Votre accès ScoDoc" subject = "Votre accès ScoDoc"
sender = sco_preferences.get_preference("email_from_addr") sender = sco_preferences.get_preference("email_from_addr")
email.send_email(subject, sender, [u["email"]], txt) email.send_email(subject, sender, [user["email"]], txt)

View File

@ -31,7 +31,7 @@
import datetime import datetime
from operator import itemgetter from operator import itemgetter
from flask import url_for, g from flask import url_for, g, request
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -81,6 +81,7 @@ def list_authorized_etuds_by_sem(sem, delai=274):
"title": src["titreannee"], "title": src["titreannee"],
"title_target": "formsemestre_status?formsemestre_id=%s" "title_target": "formsemestre_status?formsemestre_id=%s"
% src["formsemestre_id"], % src["formsemestre_id"],
"filename": "etud_autorises",
}, },
} }
# ajoute attribut inscrit qui indique si l'étudiant est déjà inscrit dans le semestre dest. # ajoute attribut inscrit qui indique si l'étudiant est déjà inscrit dans le semestre dest.
@ -99,6 +100,7 @@ def list_authorized_etuds_by_sem(sem, delai=274):
% sem["formsemestre_id"], % sem["formsemestre_id"],
"comment": " actuellement inscrits dans ce semestre", "comment": " actuellement inscrits dans ce semestre",
"help": "Ces étudiants sont actuellement inscrits dans ce semestre. Si vous les décochez, il seront désinscrits.", "help": "Ces étudiants sont actuellement inscrits dans ce semestre. Si vous les décochez, il seront désinscrits.",
"filename": "etud_inscrits",
}, },
} }
@ -146,16 +148,15 @@ def list_inscrits_date(sem):
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
sem["date_debut_iso"] = ndb.DateDMYtoISO(sem["date_debut"]) sem["date_debut_iso"] = ndb.DateDMYtoISO(sem["date_debut"])
cursor.execute( cursor.execute(
"""SELECT I.etudid """SELECT ins.etudid
FROM FROM
notes_formsemestre_inscription ins, notes_formsemestre_inscription ins,
notes_formsemestre S, notes_formsemestre S
identite i
WHERE ins.formsemestre_id = S.id WHERE ins.formsemestre_id = S.id
AND S.id != %(formsemestre_id)s AND S.id != %(formsemestre_id)s
AND S.date_debut <= %(date_debut_iso)s AND S.date_debut <= %(date_debut_iso)s
AND S.date_fin >= %(date_debut_iso)s AND S.date_fin >= %(date_debut_iso)s
AND ins.dept_id = %(dept_id) AND S.dept_id = %(dept_id)s
""", """,
sem, sem,
) )
@ -264,7 +265,6 @@ def formsemestre_inscr_passage(
inscrit_groupes=False, inscrit_groupes=False,
submitted=False, submitted=False,
dialog_confirmed=False, dialog_confirmed=False,
REQUEST=None,
): ):
"""Form. pour inscription des etudiants d'un semestre dans un autre """Form. pour inscription des etudiants d'un semestre dans un autre
(donné par formsemestre_id). (donné par formsemestre_id).
@ -287,8 +287,11 @@ def formsemestre_inscr_passage(
header = html_sco_header.sco_header(page_title="Passage des étudiants") header = html_sco_header.sco_header(page_title="Passage des étudiants")
footer = html_sco_header.sco_footer() footer = html_sco_header.sco_footer()
H = [header] H = [header]
if type(etuds) == type(""): if isinstance(etuds, str):
etuds = etuds.split(",") # vient du form de confirmation # list de strings, vient du form de confirmation
etuds = [int(x) for x in etuds.split(",") if x]
elif isinstance(etuds, int):
etuds = [etuds]
auth_etuds_by_sem, inscrits, candidats = list_authorized_etuds_by_sem(sem) auth_etuds_by_sem, inscrits, candidats = list_authorized_etuds_by_sem(sem)
etuds_set = set(etuds) etuds_set = set(etuds)
@ -312,7 +315,6 @@ def formsemestre_inscr_passage(
if not submitted: if not submitted:
H += build_page( H += build_page(
REQUEST,
sem, sem,
auth_etuds_by_sem, auth_etuds_by_sem,
inscrits, inscrits,
@ -342,18 +344,22 @@ def formsemestre_inscr_passage(
% inscrits[etudid] % inscrits[etudid]
) )
H.append("</ol>") H.append("</ol>")
if not a_inscrire and not a_desinscrire: todo = a_inscrire or a_desinscrire
if not todo:
H.append("""<h3>Il n'y a rien à modifier !</h3>""") H.append("""<h3>Il n'y a rien à modifier !</h3>""")
H.append( H.append(
scu.confirm_dialog( scu.confirm_dialog(
dest_url="formsemestre_inscr_passage", dest_url="formsemestre_inscr_passage"
if todo
else "formsemestre_status",
message="<p>Confirmer ?</p>" if todo else "",
add_headers=False, add_headers=False,
cancel_url="formsemestre_inscr_passage?formsemestre_id=" cancel_url="formsemestre_inscr_passage?formsemestre_id="
+ str(formsemestre_id), + str(formsemestre_id),
OK="Effectuer l'opération", OK="Effectuer l'opération" if todo else "",
parameters={ parameters={
"formsemestre_id": formsemestre_id, "formsemestre_id": formsemestre_id,
"etuds": ",".join(etuds), "etuds": ",".join([str(x) for x in etuds]),
"inscrit_groupes": inscrit_groupes, "inscrit_groupes": inscrit_groupes,
"submitted": 1, "submitted": 1,
}, },
@ -385,7 +391,7 @@ def formsemestre_inscr_passage(
): # il y a au moins une vraie partition ): # il y a au moins une vraie partition
H.append( H.append(
f"""<li><a class="stdlink" href="{ f"""<li><a class="stdlink" href="{
url_for("scolar.affectGroups", url_for("scolar.affect_groups",
scodoc_dept=g.scodoc_dept, partition_id=partition["partition_id"]) scodoc_dept=g.scodoc_dept, partition_id=partition["partition_id"])
}">Répartir les groupes de {partition["partition_name"]}</a></li> }">Répartir les groupes de {partition["partition_name"]}</a></li>
""" """
@ -397,7 +403,6 @@ def formsemestre_inscr_passage(
def build_page( def build_page(
REQUEST,
sem, sem,
auth_etuds_by_sem, auth_etuds_by_sem,
inscrits, inscrits,
@ -413,9 +418,9 @@ def build_page(
H = [ H = [
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
REQUEST, "Passages dans le semestre", sem, with_page_header=False "Passages dans le semestre", sem, with_page_header=False
), ),
"""<form method="post" action="%s">""" % REQUEST.URL0, """<form method="post" action="%s">""" % request.base_url,
"""<input type="hidden" name="formsemestre_id" value="%(formsemestre_id)s"/> """<input type="hidden" name="formsemestre_id" value="%(formsemestre_id)s"/>
<input type="submit" name="submitted" value="Appliquer les modifications"/> <input type="submit" name="submitted" value="Appliquer les modifications"/>
&nbsp;<a href="#help">aide</a> &nbsp;<a href="#help">aide</a>
@ -507,7 +512,12 @@ def etuds_select_boxes(
</script> </script>
<div class="etuds_select_boxes">""" <div class="etuds_select_boxes">"""
] # " ] # "
# Élimine les boites vides:
auth_etuds_by_cat = {
k: auth_etuds_by_cat[k]
for k in auth_etuds_by_cat
if auth_etuds_by_cat[k]["etuds"]
}
for src_cat in auth_etuds_by_cat.keys(): for src_cat in auth_etuds_by_cat.keys():
infos = auth_etuds_by_cat[src_cat]["infos"] infos = auth_etuds_by_cat[src_cat]["infos"]
infos["comment"] = infos.get("comment", "") # commentaire dans sous-titre boite infos["comment"] = infos.get("comment", "") # commentaire dans sous-titre boite
@ -550,10 +560,8 @@ def etuds_select_boxes(
if with_checkbox or sel_inscrits: if with_checkbox or sel_inscrits:
H.append(")") H.append(")")
if base_url and etuds: if base_url and etuds:
H.append( url = scu.build_url_query(base_url, export_cat_xls=src_cat)
'<a href="%s&export_cat_xls=%s">%s</a>&nbsp;' H.append(f'<a href="{url}">{scu.ICON_XLS}</a>&nbsp;')
% (base_url, src_cat, scu.ICON_XLS)
)
H.append("</div>") H.append("</div>")
for etud in etuds: for etud in etuds:
if etud.get("inscrit", False): if etud.get("inscrit", False):
@ -633,4 +641,4 @@ def etuds_select_box_xls(src_cat):
caption="%(title)s. %(help)s" % src_cat["infos"], caption="%(title)s. %(help)s" % src_cat["infos"],
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
return tab.excel() return tab.excel() # tab.make_page(filename=src_cat["infos"]["filename"])

View File

@ -1,4 +1,4 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
############################################################################## ##############################################################################
@ -27,11 +27,11 @@
"""Liste des notes d'une évaluation """Liste des notes d'une évaluation
""" """
import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error
from operator import itemgetter from operator import itemgetter
import urllib
import flask import flask
from flask import url_for, g from flask import url_for, g, request
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -56,20 +56,21 @@ from app.scodoc.gen_tables import GenTable
from app.scodoc.htmlutils import histogram_notes from app.scodoc.htmlutils import histogram_notes
def do_evaluation_listenotes(REQUEST): def do_evaluation_listenotes():
""" """
Affichage des notes d'une évaluation Affichage des notes d'une évaluation
args: evaluation_id ou moduleimpl_id args: evaluation_id ou moduleimpl_id
(si moduleimpl_id, affiche toutes les évaluatons du module) (si moduleimpl_id, affiche toutes les évaluations du module)
""" """
mode = None mode = None
if "evaluation_id" in REQUEST.form: vals = scu.get_request_args()
evaluation_id = int(REQUEST.form["evaluation_id"]) if "evaluation_id" in vals:
evaluation_id = int(vals["evaluation_id"])
mode = "eval" mode = "eval"
evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id}) evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})
if "moduleimpl_id" in REQUEST.form: if "moduleimpl_id" in vals and vals["moduleimpl_id"]:
moduleimpl_id = int(REQUEST.form["moduleimpl_id"]) moduleimpl_id = int(vals["moduleimpl_id"])
mode = "module" mode = "module"
evals = sco_evaluations.do_evaluation_list({"moduleimpl_id": moduleimpl_id}) evals = sco_evaluations.do_evaluation_list({"moduleimpl_id": moduleimpl_id})
if not mode: if not mode:
@ -77,7 +78,7 @@ def do_evaluation_listenotes(REQUEST):
if not evals: if not evals:
return "<p>Aucune évaluation !</p>" return "<p>Aucune évaluation !</p>"
format = REQUEST.form.get("format", "html") format = vals.get("format", "html")
E = evals[0] # il y a au moins une evaluation E = evals[0] # il y a au moins une evaluation
# description de l'evaluation # description de l'evaluation
if mode == "eval": if mode == "eval":
@ -177,8 +178,8 @@ def do_evaluation_listenotes(REQUEST):
), ),
] ]
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
descr, descr,
cancelbutton=None, cancelbutton=None,
submitbutton=None, submitbutton=None,
@ -201,7 +202,6 @@ def do_evaluation_listenotes(REQUEST):
hide_groups = tf[2]["hide_groups"] hide_groups = tf[2]["hide_groups"]
with_emails = tf[2]["with_emails"] with_emails = tf[2]["with_emails"]
return _make_table_notes( return _make_table_notes(
REQUEST,
tf[1], tf[1],
evals, evals,
format=format, format=format,
@ -214,7 +214,6 @@ def do_evaluation_listenotes(REQUEST):
def _make_table_notes( def _make_table_notes(
REQUEST,
html_form, html_form,
evals, evals,
format="", format="",
@ -229,8 +228,8 @@ def _make_table_notes(
return "<p>Aucune évaluation !</p>" return "<p>Aucune évaluation !</p>"
E = evals[0] E = evals[0]
moduleimpl_id = E["moduleimpl_id"] moduleimpl_id = E["moduleimpl_id"]
M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
Mod = sco_edit_module.do_module_list(args={"module_id": M["module_id"]})[0] Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"]) sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
# (debug) check that all evals are in same module: # (debug) check that all evals are in same module:
for e in evals: for e in evals:
@ -272,8 +271,8 @@ def _make_table_notes(
"expl_key": "Rem.", "expl_key": "Rem.",
"email": "e-mail", "email": "e-mail",
"emailperso": "e-mail perso", "emailperso": "e-mail perso",
"signatures": "Signatures",
} }
rows = [] rows = []
class keymgr(dict): # comment : key (pour regrouper les comments a la fin) class keymgr(dict): # comment : key (pour regrouper les comments a la fin)
@ -316,7 +315,7 @@ def _make_table_notes(
rows.append( rows.append(
{ {
"code": code, "code": str(code), # INE, NIP ou etudid
"_code_td_attrs": 'style="padding-left: 1em; padding-right: 2em;"', "_code_td_attrs": 'style="padding-left: 1em; padding-right: 2em;"',
"etudid": etudid, "etudid": etudid,
"nom": etud["nom"].upper(), "nom": etud["nom"].upper(),
@ -375,9 +374,11 @@ def _make_table_notes(
columns_ids.append(e["evaluation_id"]) columns_ids.append(e["evaluation_id"])
# #
if anonymous_listing: if anonymous_listing:
rows.sort(key=lambda x: x["code"]) rows.sort(key=lambda x: x["code"] or "")
else: else:
rows.sort(key=lambda x: (x["nom"], x["prenom"])) # sort by nom, prenom rows.sort(
key=lambda x: (x["nom"] or "", x["prenom"] or "")
) # sort by nom, prenom
# Si module, ajoute moyenne du module: # Si module, ajoute moyenne du module:
if len(evals) > 1: if len(evals) > 1:
@ -398,7 +399,7 @@ def _make_table_notes(
if with_emails: if with_emails:
columns_ids += ["email", "emailperso"] columns_ids += ["email", "emailperso"]
# Ajoute lignes en tête et moyennes # Ajoute lignes en tête et moyennes
if len(evals) > 0: if len(evals) > 0 and format != 'bordereau':
rows = [coefs, note_max] + rows rows = [coefs, note_max] + rows
rows.append(moys) rows.append(moys)
# ajout liens HTMl vers affichage une evaluation: # ajout liens HTMl vers affichage une evaluation:
@ -422,6 +423,8 @@ def _make_table_notes(
columns_ids.append("expl_key") columns_ids.append("expl_key")
elif format == "xls" or format == "xml": elif format == "xls" or format == "xml":
columns_ids.append("comment") columns_ids.append("comment")
elif format == "bordereau":
columns_ids.append("signatures")
# titres divers: # titres divers:
gl = "".join(["&group_ids%3Alist=" + str(g) for g in group_ids]) gl = "".join(["&group_ids%3Alist=" + str(g) for g in group_ids])
@ -435,10 +438,33 @@ def _make_table_notes(
gl = "&with_emails%3Alist=yes" + gl gl = "&with_emails%3Alist=yes" + gl
if len(evals) == 1: if len(evals) == 1:
evalname = "%s-%s" % (Mod["code"], ndb.DateDMYtoISO(E["jour"])) evalname = "%s-%s" % (Mod["code"], ndb.DateDMYtoISO(E["jour"]))
hh = "%s, %s (%d étudiants)" % (E["description"], gr_title, len(etudids))
filename = scu.make_filename("notes_%s_%s" % (evalname, gr_title_filename)) filename = scu.make_filename("notes_%s_%s" % (evalname, gr_title_filename))
if format == 'bordereau':
hh = " %d étudiants" % (len(etudids))
hh += " %d absent" % (nb_abs)
if nb_abs > 1:
hh += "s"
hh += ", %d en attente." % ( nb_att)
pdf_title = '<br/> BORDEREAU DE SIGNATURES'
pdf_title += '<br/><br/>%(titre)s' % sem
pdf_title += '<br/>(%(mois_debut)s - %(mois_fin)s)' % sem
pdf_title += ' semestre %s %s' % (sem['semestre_id'],sem.get('modalite',''))
pdf_title += '<br/>Notes du module %(code)s - %(titre)s' % Mod
pdf_title += '<br/>Evaluation : %(description)s ' % e
if len(e["jour"]) > 0 :
pdf_title += " (%(jour)s)" % e
pdf_title += '(noté sur %(note_max)s )<br/><br/>' % e
else:
hh = " %s, %s (%d étudiants)" % (E["description"], gr_title, len(etudids))
if len(e["jour"]) > 0 :
pdf_title = "%(description)s (%(jour)s)" % e
else:
pdf_title = "%(description)s " % e
caption = hh caption = hh
pdf_title = "%(description)s (%(jour)s)" % e
html_title = "" html_title = ""
base_url = "evaluation_listenotes?evaluation_id=%s" % E["evaluation_id"] + gl base_url = "evaluation_listenotes?evaluation_id=%s" % E["evaluation_id"] + gl
html_next_section = ( html_next_section = (
@ -453,7 +479,7 @@ def _make_table_notes(
title += " %s" % gr_title title += " %s" % gr_title
caption = title caption = title
html_next_section = "" html_next_section = ""
if format == "pdf": if format == "pdf" or format == "bordereau":
caption = "" # same as pdf_title caption = "" # same as pdf_title
pdf_title = title pdf_title = title
html_title = ( html_title = (
@ -481,8 +507,9 @@ def _make_table_notes(
preferences=sco_preferences.SemPreferences(M["formsemestre_id"]), preferences=sco_preferences.SemPreferences(M["formsemestre_id"]),
# html_generate_cells=False # la derniere ligne (moyennes) est incomplete # html_generate_cells=False # la derniere ligne (moyennes) est incomplete
) )
if format == "bordereau":
t = tab.make_page(format=format, with_html_headers=False, REQUEST=REQUEST) format = "pdf"
t = tab.make_page(format=format, with_html_headers=False)
if format != "html": if format != "html":
return t return t
@ -500,12 +527,12 @@ def _make_table_notes(
# Une seule evaluation: ajoute histogramme # Une seule evaluation: ajoute histogramme
histo = histogram_notes(notes) histo = histogram_notes(notes)
# 2 colonnes: histo, comments # 2 colonnes: histo, comments
C = [ C = []
"<table><tr><td><div><h4>Répartition des notes:</h4>" C.append('<br><b><i>Bordereau de Signatures (version PDF)</i> </b><a href="%s&format=bordereau">%s</a>'%(base_url, scu.ICON_PDF))
+ histo C.append("<br><table><tr><td><div><h4>Répartition des notes:</h4>")
+ "</div></td>\n", C.append( histo + "</div></td>\n")
'<td style="padding-left: 50px; vertical-align: top;"><p>', C.append('<td style="padding-left: 50px; vertical-align: top;"><p>')
]
commentkeys = list(K.items()) # [ (comment, key), ... ] commentkeys = list(K.items()) # [ (comment, key), ... ]
commentkeys.sort(key=lambda x: int(x[1])) commentkeys.sort(key=lambda x: int(x[1]))
for (comment, key) in commentkeys: for (comment, key) in commentkeys:
@ -625,8 +652,11 @@ def _add_eval_columns(
else: else:
moys[evaluation_id] = "" moys[evaluation_id] = ""
titles[evaluation_id] = "%(description)s (%(jour)s)" % e if len(e["jour"]) > 0:
titles[evaluation_id] = "%(description)s (%(jour)s)" % e
else:
titles[evaluation_id] = "%(description)s " % e
if e["eval_state"]["evalcomplete"]: if e["eval_state"]["evalcomplete"]:
titles["_" + str(evaluation_id) + "_td_attrs"] = 'class="eval_complete"' titles["_" + str(evaluation_id) + "_td_attrs"] = 'class="eval_complete"'
elif e["eval_state"]["evalattente"]: elif e["eval_state"]["evalattente"]:
@ -665,7 +695,7 @@ def _add_moymod_column(
notes.append(val) notes.append(val)
nb_notes = nb_notes + 1 nb_notes = nb_notes + 1
sum_notes += val sum_notes += val
coefs[col_id] = "" coefs[col_id] = "(avec abs)"
if keep_numeric: if keep_numeric:
note_max[col_id] = 20.0 note_max[col_id] = 20.0
else: else:
@ -760,9 +790,7 @@ def evaluation_check_absences(evaluation_id):
return ValButAbs, AbsNonSignalee, ExcNonSignalee, ExcNonJust, AbsButExc return ValButAbs, AbsNonSignalee, ExcNonSignalee, ExcNonJust, AbsButExc
def evaluation_check_absences_html( def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True):
evaluation_id, with_header=True, show_ok=True, REQUEST=None
):
"""Affiche etat verification absences d'une evaluation""" """Affiche etat verification absences d'une evaluation"""
E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0] E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0]
@ -778,9 +806,7 @@ def evaluation_check_absences_html(
if with_header: if with_header:
H = [ H = [
html_sco_header.html_sem_header( html_sco_header.html_sem_header("Vérification absences à l'évaluation"),
REQUEST, "Vérification absences à l'évaluation"
),
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id), 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>""", """<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.</p>""",
] ]
@ -814,14 +840,16 @@ def evaluation_check_absences_html(
) )
if linkabs: if linkabs:
H.append( H.append(
'<a class="stdlink" href="Absences/doSignaleAbsence?etudid=%s&datedebut=%s&datefin=%s&demijournee=%s&moduleimpl_id=%s">signaler cette absence</a>' f"""<a class="stdlink" href="{url_for(
% ( 'absences.doSignaleAbsence',
etud["etudid"], scodoc_dept=g.scodoc_dept,
six.moves.urllib.parse.quote(E["jour"]), etudid=etud["etudid"],
six.moves.urllib.parse.quote(E["jour"]), datedebut=E["jour"],
demijournee, datefin=E["jour"],
E["moduleimpl_id"], demijournee=demijournee,
moduleimpl_id=E["moduleimpl_id"],
) )
}">signaler cette absence</a>"""
) )
H.append("</li>") H.append("</li>")
H.append("</ul>") H.append("</ul>")
@ -861,12 +889,11 @@ def evaluation_check_absences_html(
return "\n".join(H) return "\n".join(H)
def formsemestre_check_absences_html(formsemestre_id, REQUEST=None): def formsemestre_check_absences_html(formsemestre_id):
"""Affiche etat verification absences pour toutes les evaluations du semestre !""" """Affiche etat verification absences pour toutes les evaluations du semestre !"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [ H = [
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
REQUEST,
"Vérification absences aux évaluations de ce semestre", "Vérification absences aux évaluations de ce semestre",
sem, sem,
), ),
@ -876,9 +903,7 @@ def formsemestre_check_absences_html(formsemestre_id, REQUEST=None):
</p>""", </p>""",
] ]
# Modules, dans l'ordre # Modules, dans l'ordre
Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list( Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
formsemestre_id=formsemestre_id
)
for M in Mlist: for M in Mlist:
evals = sco_evaluations.do_evaluation_list( evals = sco_evaluations.do_evaluation_list(
{"moduleimpl_id": M["moduleimpl_id"]} {"moduleimpl_id": M["moduleimpl_id"]}
@ -894,7 +919,6 @@ def formsemestre_check_absences_html(formsemestre_id, REQUEST=None):
E["evaluation_id"], E["evaluation_id"],
with_header=False, with_header=False,
show_ok=False, show_ok=False,
REQUEST=REQUEST,
) )
) )
if evals: if evals:

View File

@ -32,32 +32,242 @@ avec `ext` membre de LOGOS_IMAGES_ALLOWED_TYPES (= jpg, png)
SCODOC_LOGOS_DIR /opt/scodoc-data/config/logos SCODOC_LOGOS_DIR /opt/scodoc-data/config/logos
""" """
import glob
import imghdr import imghdr
import os import os
import re
import shutil
from pathlib import Path
from flask import abort, current_app from flask import abort, current_app, url_for
from werkzeug.utils import secure_filename
from app import Departement, ScoValueError
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from PIL import Image as PILImage
GLOBAL = "_" # category for server level logos
def get_logo_filename(logo_type: str, scodoc_dept: str) -> str: def find_logo(logoname, dept_id=None, strict=False, prefix=scu.LOGO_FILE_PREFIX):
"""return full filename for this logo, or "" if not found
an existing file with extension.
logo_type: "header" or "footer"
scodoc-dept: acronym
""" """
# Search logos in dept specific dir (/opt/scodoc-data/config/logos/logos_<dept>), "Recherche un logo 'name' existant.
# then in config dir /opt/scodoc-data/config/logos/ Deux strategies:
for image_dir in ( si strict:
scu.SCODOC_LOGOS_DIR + "/logos_" + scodoc_dept, reherche uniquement dans le département puis si non trouvé au niveau global
scu.SCODOC_LOGOS_DIR, # global logos sinon
): On recherche en local au dept d'abord puis si pas trouvé recherche globale
for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES: quelque soit la stratégie, retourne None si pas trouvé
filename = os.path.join(image_dir, f"logo_{logo_type}.{suffix}") :param logoname: le nom recherche
if os.path.isfile(filename) and os.access(filename, os.R_OK): :param dept_id: l'id du département dans lequel se fait la recherche (None si global)
return filename :param strict: stratégie de recherche (strict = False => dept ou global)
:param prefix: le prefix utilisé (parmi scu.LOGO_FILE_PREFIX / scu.BACKGROUND_FILE_PREFIX)
:return: un objet Logo désignant le fichier image trouvé (ou None)
"""
logo = Logo(logoname, dept_id, prefix).select()
if logo is None and not strict:
logo = Logo(logoname=logoname, dept_id=None, prefix=prefix).select()
return logo
return ""
def delete_logo(name, dept_id=None):
"""Delete all files matching logo (dept_id, name) (including all allowed extensions)
Args:
name: The name of the logo
dept_id: the dept_id (if local). Use None to destroy globals logos
"""
logo = find_logo(logoname=name, dept_id=dept_id)
while logo is not None:
os.unlink(logo.select().filepath)
logo = find_logo(logoname=name, dept_id=dept_id)
def write_logo(stream, name, dept_id=None):
"""Crée le fichier logo sur le serveur.
Le suffixe du fichier (parmi LOGO_IMAGES_ALLOWED_TYPES) est déduit du contenu du stream"""
Logo(logoname=name, dept_id=dept_id).create(stream)
def list_logos():
"""Crée l'inventaire de tous les logos existants.
L'inventaire se présente comme un dictionnaire de dictionnaire de Logo:
[None][name] pour les logos globaux
[dept_id][name] pour les logos propres à un département (attention id numérique du dept)
Les départements sans logos sont absents du résultat
"""
inventory = {None: _list_dept_logos()} # logos globaux (header / footer)
for dept in Departement.query.filter_by(visible=True).all():
logos_dept = _list_dept_logos(dept_id=dept.id)
if logos_dept:
inventory[dept.id] = _list_dept_logos(dept.id)
return inventory
def _list_dept_logos(dept_id=None, prefix=scu.LOGO_FILE_PREFIX):
"""nventorie toutes les images existantes pour un niveau (GLOBAL ou un département).
retourne un dictionnaire de Logo [logoname] -> Logo
les noms des fichiers concernés doivent être de la forme: <rep>/<prefix><name>.<suffixe>
<rep> : répertoire de recherche (déduit du dept_id)
<prefix>: le prefix (LOGO_FILE_PREFIX pour les logos)
<suffix>: un des suffixes autorisés
:param dept_id: l'id du departement concerné (si None -> global)
:param prefix: le préfixe utilisé
:return: le résultat de la recherche ou None si aucune image trouvée
"""
allowed_ext = "|".join(scu.LOGOS_IMAGES_ALLOWED_TYPES)
filename_parser = re.compile(f"{prefix}([^.]*).({allowed_ext})")
logos = {}
path_dir = Path(scu.SCODOC_LOGOS_DIR)
if dept_id:
path_dir = Path(
os.path.sep.join(
[scu.SCODOC_LOGOS_DIR, scu.LOGOS_DIR_PREFIX + str(dept_id)]
)
)
if path_dir.exists():
for entry in path_dir.iterdir():
if os.access(path_dir.joinpath(entry).absolute(), os.R_OK):
result = filename_parser.match(entry.name)
if result:
logoname = result.group(1)
logos[logoname] = Logo(logoname=logoname, dept_id=dept_id).select()
return logos if len(logos.keys()) > 0 else None
class Logo:
"""Responsable des opérations (select, create), du calcul des chemins et url
ainsi que de la récupération des informations sur un logp.
Usage:
logo existant: Logo(<name>, <dept_id>, ...).select() (retourne None si fichier non trouvé)
logo en création: Logo(<name>, <dept_id>, ...).create(stream)
Les attributs filename, filepath, get_url() ne devraient pas être utilisés avant les opérations
select ou save (le format n'est pas encore connu à ce moement là)
"""
def __init__(self, logoname, dept_id=None, prefix=scu.LOGO_FILE_PREFIX):
"""Initialisation des noms et département des logos.
if prefix = None on recherche simplement une image 'logoname.*'
Le format est renseigné au moment de la lecture (select) ou de la création (create) de l'objet
"""
self.logoname = secure_filename(logoname)
self.scodoc_dept_id = dept_id
self.prefix = prefix or ""
if self.scodoc_dept_id:
self.dirpath = os.path.sep.join(
[
scu.SCODOC_LOGOS_DIR,
scu.LOGOS_DIR_PREFIX + secure_filename(str(dept_id)),
]
)
else:
self.dirpath = scu.SCODOC_LOGOS_DIR
self.basepath = os.path.sep.join(
[self.dirpath, self.prefix + secure_filename(self.logoname)]
)
# next attributes are computer by the select function
self.suffix = "Not inited: call the select or create function before access"
self.filepath = "Not inited: call the select or create function before access"
self.filename = "Not inited: call the select or create function before access"
self.size = "Not inited: call the select or create function before access"
self.aspect_ratio = (
"Not inited: call the select or create function before access"
)
self.density = "Not inited: call the select or create function before access"
self.mm = "Not inited: call the select or create function before access"
def _set_format(self, fmt):
self.suffix = fmt
self.filepath = self.basepath + "." + fmt
self.filename = self.logoname + "." + fmt
def _ensure_directory_exists(self):
"create enclosing directory if necessary"
if not Path(self.dirpath).exists():
current_app.logger.info(f"sco_logos creating directory %s", self.dirpath)
os.mkdir(self.dirpath)
def create(self, stream):
img_type = guess_image_type(stream)
if img_type not in scu.LOGOS_IMAGES_ALLOWED_TYPES:
abort(400, "type d'image invalide")
self._set_format(img_type)
self._ensure_directory_exists()
filename = self.basepath + "." + self.suffix
with open(filename, "wb") as f:
f.write(stream.read())
current_app.logger.info(f"sco_logos.store_image %s", self.filename)
# erase other formats if they exists
for suffix in set(scu.LOGOS_IMAGES_ALLOWED_TYPES) - set([img_type]):
try:
os.unlink(self.basepath + "." + suffix)
except IOError:
pass
def _read_info(self, img):
"""computes some properties from the real image
aspect_ratio assumes that x_density and y_density are equals
"""
x_size, y_size = img.size
self.density = img.info.get("dpi", None)
unit = 1
if self.density is None: # no dpi found try jfif infos
self.density = img.info.get("jfif_density", None)
unit = img.info.get("jfif_unit", 0) # 0 = no unit ; 1 = inch ; 2 = mm
if self.density is not None:
x_density, y_density = self.density
if unit != 0:
unit2mm = [0, 1 / 0.254, 0.1][unit]
x_mm = round(x_size * unit2mm / x_density, 2)
y_mm = round(y_size * unit2mm / y_density, 2)
self.mm = (x_mm, y_mm)
else:
self.mm = None
else:
self.mm = None
self.size = (x_size, y_size)
self.aspect_ratio = round(float(x_size) / y_size, 2)
def select(self):
"""
Récupération des données pour un logo existant
il doit exister un et un seul fichier image parmi de suffixe/types autorisés
(sinon on prend le premier trouvé)
cette opération permet d'affiner le format d'un logo de format inconnu
"""
for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES:
path = Path(self.basepath + "." + suffix)
if path.exists():
self._set_format(suffix)
with open(self.filepath, "rb") as f:
img = PILImage.open(f)
self._read_info(img)
return self
return None
def get_url(self):
"""Retourne l'URL permettant d'obtenir l'image du logo"""
return url_for(
"scodoc.get_logo",
dept_id=self.scodoc_dept_id,
name=self.logoname,
global_if_not_found=False,
)
def get_url_small(self):
"""Retourne l'URL permettant d'obtenir l'image du logo sous forme de miniature"""
return url_for(
"scodoc.get_logo_small",
dept_id=self.scodoc_dept_id,
name=self.logoname,
global_if_not_found=False,
)
def get_usage(self):
if self.mm is None:
return f'<logo name="{self.logoname}" width="?? mm" height="?? mm">'
else:
return f'<logo name="{self.logoname}" width="{self.mm[0]}mm"">'
def guess_image_type(stream) -> str: def guess_image_type(stream) -> str:
@ -70,26 +280,33 @@ def guess_image_type(stream) -> str:
return fmt if fmt != "jpeg" else "jpg" return fmt if fmt != "jpeg" else "jpg"
def _ensure_directory_exists(filename): def make_logo_local(logoname, dept_name):
"create enclosing directory if necessary" depts = Departement.query.filter_by(acronym=dept_name).all()
directory = os.path.split(filename)[0] if len(depts) == 0:
if not os.path.exists(directory): print(f"no dept {dept_name} found. aborting")
current_app.logger.info(f"sco_logos creating directory %s", directory) return
os.mkdir(directory) if len(depts) > 1:
print(f"several depts {dept_name} found. aborting")
return
def store_image(stream, basename): dept = depts[0]
img_type = guess_image_type(stream) print(f"Move logo {logoname}' from global to {dept.acronym}")
if img_type not in scu.LOGOS_IMAGES_ALLOWED_TYPES: old_path_wild = f"/opt/scodoc-data/config/logos/logo_{logoname}.*"
abort(400, "type d'image invalide") new_dir = f"/opt/scodoc-data/config/logos/logos_{dept.id}"
filename = basename + "." + img_type logos = glob.glob(old_path_wild)
_ensure_directory_exists(filename) # checks that there is non local already present
with open(filename, "wb") as f: for logo in logos:
f.write(stream.read()) filename = os.path.split(logo)[1]
current_app.logger.info(f"sco_logos.store_image %s", filename) new_name = os.path.sep.join([new_dir, filename])
# erase other formats if they exists if os.path.exists(new_name):
for extension in set(scu.LOGOS_IMAGES_ALLOWED_TYPES) - set([img_type]): print("local version of global logo already exists. aborting")
try: return
os.unlink(basename + "." + extension) # create new__dir if necessary
except IOError: if not os.path.exists(new_dir):
pass print(f"- create {new_dir} directory")
os.mkdir(new_dir)
# move global logo (all suffixes) to local dir note: pre existent file (logo_XXX.*) in local dir does not
# prevent operation if there is no conflict with moved files
# At this point everything is ok so we can do files manipulation
for logo in logos:
shutil.move(logo, new_dir)
# print(f"moved {n_moves}/{n} etuds")

View File

@ -31,7 +31,7 @@
""" """
from operator import itemgetter from operator import itemgetter
from flask import url_for, g from flask import url_for, g, request
import app import app
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -63,7 +63,7 @@ def formsemestre_table_etuds_lycees(
) )
def scodoc_table_etuds_lycees(format="html", REQUEST=None): def scodoc_table_etuds_lycees(format="html"):
"""Table avec _tous_ les étudiants des semestres non verrouillés """Table avec _tous_ les étudiants des semestres non verrouillés
de _tous_ les départements. de _tous_ les départements.
""" """
@ -84,8 +84,8 @@ def scodoc_table_etuds_lycees(format="html", REQUEST=None):
sco_preferences.SemPreferences(), sco_preferences.SemPreferences(),
no_links=True, no_links=True,
) )
tab.base_url = REQUEST.URL0 tab.base_url = request.base_url
t = tab.make_page(format=format, with_html_headers=False, REQUEST=REQUEST) t = tab.make_page(format=format, with_html_headers=False)
if format != "html": if format != "html":
return t return t
H = [ H = [
@ -181,23 +181,22 @@ def formsemestre_etuds_lycees(
format="html", format="html",
only_primo=False, only_primo=False,
no_grouping=False, no_grouping=False,
REQUEST=None,
): ):
"""Table des lycées d'origine""" """Table des lycées d'origine"""
tab, etuds_by_lycee = formsemestre_table_etuds_lycees( tab, etuds_by_lycee = formsemestre_table_etuds_lycees(
formsemestre_id, only_primo=only_primo, group_lycees=not no_grouping formsemestre_id, only_primo=only_primo, group_lycees=not no_grouping
) )
tab.base_url = "%s?formsemestre_id=%s" % (REQUEST.URL0, formsemestre_id) tab.base_url = "%s?formsemestre_id=%s" % (request.base_url, formsemestre_id)
if only_primo: if only_primo:
tab.base_url += "&only_primo=1" tab.base_url += "&only_primo=1"
if no_grouping: if no_grouping:
tab.base_url += "&no_grouping=1" tab.base_url += "&no_grouping=1"
t = tab.make_page(format=format, with_html_headers=False, REQUEST=REQUEST) t = tab.make_page(format=format, with_html_headers=False)
if format != "html": if format != "html":
return t return t
F = [ F = [
sco_report.tsp_form_primo_group( sco_report.tsp_form_primo_group(
REQUEST, only_primo, no_grouping, formsemestre_id, format only_primo, no_grouping, formsemestre_id, format
) )
] ]
H = [ H = [

View File

@ -88,14 +88,14 @@ def do_modalite_list(*args, **kw):
return _modaliteEditor.list(cnx, *args, **kw) return _modaliteEditor.list(cnx, *args, **kw)
def do_modalite_create(args, REQUEST=None): def do_modalite_create(args):
"create a modalite" "create a modalite"
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
r = _modaliteEditor.create(cnx, args) r = _modaliteEditor.create(cnx, args)
return r return r
def do_modalite_delete(oid, REQUEST=None): def do_modalite_delete(oid):
"delete a modalite" "delete a modalite"
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
log("do_modalite_delete: form_modalite_id=%s" % oid) log("do_modalite_delete: form_modalite_id=%s" % oid)

View File

@ -100,9 +100,7 @@ def do_moduleimpl_delete(oid, formsemestre_id=None):
) # > moduleimpl_delete ) # > moduleimpl_delete
def do_moduleimpl_list( def moduleimpl_list(moduleimpl_id=None, formsemestre_id=None, module_id=None):
moduleimpl_id=None, formsemestre_id=None, module_id=None, REQUEST=None
):
"list moduleimpls" "list moduleimpls"
args = locals() args = locals()
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
@ -110,7 +108,7 @@ def do_moduleimpl_list(
# Ajoute la liste des enseignants # Ajoute la liste des enseignants
for mo in modimpls: for mo in modimpls:
mo["ens"] = do_ens_list(args={"moduleimpl_id": mo["moduleimpl_id"]}) mo["ens"] = do_ens_list(args={"moduleimpl_id": mo["moduleimpl_id"]})
return scu.return_text_if_published(modimpls, REQUEST) return modimpls
def do_moduleimpl_edit(args, formsemestre_id=None, cnx=None): def do_moduleimpl_edit(args, formsemestre_id=None, cnx=None):
@ -124,10 +122,11 @@ def do_moduleimpl_edit(args, formsemestre_id=None, cnx=None):
) # > modif moduleimpl ) # > modif moduleimpl
def do_moduleimpl_withmodule_list( def moduleimpl_withmodule_list(
moduleimpl_id=None, formsemestre_id=None, module_id=None, REQUEST=None moduleimpl_id=None, formsemestre_id=None, module_id=None
): ):
"""Liste les moduleimpls et ajoute dans chacun le module correspondant """Liste les moduleimpls et ajoute dans chacun
l'UE, la matière et le module auxquels ils appartiennent.
Tri la liste par semestre/UE/numero_matiere/numero_module. Tri la liste par semestre/UE/numero_matiere/numero_module.
Attention: Cette fonction fait partie de l'API ScoDoc 7 et est publiée. Attention: Cette fonction fait partie de l'API ScoDoc 7 et est publiée.
@ -136,23 +135,33 @@ def do_moduleimpl_withmodule_list(
from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
args = locals() modimpls = moduleimpl_list(
del args["REQUEST"]
modimpls = do_moduleimpl_list(
**{ **{
"moduleimpl_id": moduleimpl_id, "moduleimpl_id": moduleimpl_id,
"formsemestre_id": formsemestre_id, "formsemestre_id": formsemestre_id,
"module_id": module_id, "module_id": module_id,
} }
) )
for mo in modimpls: ues = {}
mo["module"] = sco_edit_module.do_module_list( matieres = {}
args={"module_id": mo["module_id"]} modules = {}
)[0] for mi in modimpls:
mo["ue"] = sco_edit_ue.do_ue_list(args={"ue_id": mo["module"]["ue_id"]})[0] module_id = mi["module_id"]
mo["matiere"] = sco_edit_matiere.do_matiere_list( if not mi["module_id"] in modules:
args={"matiere_id": mo["module"]["matiere_id"]} modules[module_id] = sco_edit_module.module_list(
)[0] args={"module_id": module_id}
)[0]
mi["module"] = modules[module_id]
ue_id = mi["module"]["ue_id"]
if not ue_id in ues:
ues[ue_id] = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
mi["ue"] = ues[ue_id]
matiere_id = mi["module"]["matiere_id"]
if not matiere_id in matieres:
matieres[matiere_id] = sco_edit_matiere.matiere_list(
args={"matiere_id": matiere_id}
)[0]
mi["matiere"] = matieres[matiere_id]
# tri par semestre/UE/numero_matiere/numero_module # tri par semestre/UE/numero_matiere/numero_module
modimpls.sort( modimpls.sort(
@ -166,19 +175,30 @@ def do_moduleimpl_withmodule_list(
) )
) )
return scu.return_text_if_published(modimpls, REQUEST) return modimpls
def do_moduleimpl_inscription_list(moduleimpl_id=None, etudid=None, REQUEST=None): def moduleimpls_in_external_ue(ue_id):
"""List of modimpls in this ue"""
cursor = ndb.SimpleQuery(
"""SELECT DISTINCT mi.*
FROM notes_ue u, notes_moduleimpl mi, notes_modules m
WHERE u.is_external is true
AND mi.module_id = m.id AND m.ue_id = %(ue_id)s
""",
{"ue_id": ue_id},
)
return cursor.dictfetchall()
def do_moduleimpl_inscription_list(moduleimpl_id=None, etudid=None):
"list moduleimpl_inscriptions" "list moduleimpl_inscriptions"
args = locals() args = locals()
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
return scu.return_text_if_published( return _moduleimpl_inscriptionEditor.list(cnx, args)
_moduleimpl_inscriptionEditor.list(cnx, args), REQUEST
)
def do_moduleimpl_listeetuds(moduleimpl_id): def moduleimpl_listeetuds(moduleimpl_id):
"retourne liste des etudids inscrits a ce module" "retourne liste des etudids inscrits a ce module"
req = """SELECT DISTINCT Im.etudid req = """SELECT DISTINCT Im.etudid
FROM notes_moduleimpl_inscription Im, FROM notes_moduleimpl_inscription Im,
@ -244,9 +264,7 @@ def do_moduleimpl_inscription_delete(oid, formsemestre_id=None):
) # > moduleimpl_inscription ) # > moduleimpl_inscription
def do_moduleimpl_inscrit_etuds( def do_moduleimpl_inscrit_etuds(moduleimpl_id, formsemestre_id, etudids, reset=False):
moduleimpl_id, formsemestre_id, etudids, reset=False, REQUEST=None
):
"""Inscrit les etudiants (liste d'etudids) a ce module. """Inscrit les etudiants (liste d'etudids) a ce module.
Si reset, desinscrit tous les autres. Si reset, desinscrit tous les autres.
""" """
@ -309,11 +327,11 @@ def do_ens_create(args):
return r return r
def can_change_module_resp(REQUEST, moduleimpl_id): def can_change_module_resp(moduleimpl_id):
"""Check if current user can modify module resp. (raise exception if not). """Check if current user can modify module resp. (raise exception if not).
= Admin, et dir des etud. (si option l'y autorise) = Admin, et dir des etud. (si option l'y autorise)
""" """
M = do_moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0] M = moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0]
# -- check lock # -- check lock
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"]) sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
if not sem["etat"]: if not sem["etat"]:
@ -329,7 +347,7 @@ def can_change_module_resp(REQUEST, moduleimpl_id):
def can_change_ens(moduleimpl_id, raise_exc=True): def can_change_ens(moduleimpl_id, raise_exc=True):
"check if current user can modify ens list (raise exception if not)" "check if current user can modify ens list (raise exception if not)"
M = do_moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0] M = moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0]
# -- check lock # -- check lock
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"]) sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
if not sem["etat"]: if not sem["etat"]:

View File

@ -30,7 +30,8 @@
from operator import itemgetter from operator import itemgetter
import flask import flask
from flask import url_for, g from flask import url_for, g, request
from flask_login import current_user
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -50,9 +51,7 @@ from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
def moduleimpl_inscriptions_edit( def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
moduleimpl_id, etuds=[], submitted=False, REQUEST=None
):
"""Formulaire inscription des etudiants a ce module """Formulaire inscription des etudiants a ce module
* Gestion des inscriptions * Gestion des inscriptions
Nom TD TA TP (triable) Nom TD TA TP (triable)
@ -64,9 +63,9 @@ def moduleimpl_inscriptions_edit(
* Si pas les droits: idem en readonly * Si pas les droits: idem en readonly
""" """
M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
formsemestre_id = M["formsemestre_id"] formsemestre_id = M["formsemestre_id"]
mod = sco_edit_module.do_module_list(args={"module_id": M["module_id"]})[0] mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
# -- check lock # -- check lock
if not sem["etat"]: if not sem["etat"]:
@ -137,7 +136,7 @@ def moduleimpl_inscriptions_edit(
</script>""" </script>"""
) )
H.append("""<form method="post" id="mi_form" action="%s">""" % REQUEST.URL0) H.append("""<form method="post" id="mi_form" action="%s">""" % request.base_url)
H.append( H.append(
""" """
<input type="hidden" name="moduleimpl_id" value="%(moduleimpl_id)s"/> <input type="hidden" name="moduleimpl_id" value="%(moduleimpl_id)s"/>
@ -199,7 +198,7 @@ def moduleimpl_inscriptions_edit(
else: # SUBMISSION else: # SUBMISSION
# inscrit a ce module tous les etuds selectionnes # inscrit a ce module tous les etuds selectionnes
sco_moduleimpl.do_moduleimpl_inscrit_etuds( sco_moduleimpl.do_moduleimpl_inscrit_etuds(
moduleimpl_id, formsemestre_id, etuds, reset=True, REQUEST=REQUEST moduleimpl_id, formsemestre_id, etuds, reset=True
) )
return flask.redirect("moduleimpl_status?moduleimpl_id=%s" % (moduleimpl_id)) return flask.redirect("moduleimpl_status?moduleimpl_id=%s" % (moduleimpl_id))
# #
@ -230,7 +229,7 @@ def _make_menu(partitions, title="", check="true"):
) )
def moduleimpl_inscriptions_stats(formsemestre_id, REQUEST=None): def moduleimpl_inscriptions_stats(formsemestre_id):
"""Affiche quelques informations sur les inscriptions """Affiche quelques informations sur les inscriptions
aux modules de ce semestre. aux modules de ce semestre.
@ -250,7 +249,7 @@ def moduleimpl_inscriptions_stats(formsemestre_id, REQUEST=None):
tous sauf <liste d'au plus 7 noms> tous sauf <liste d'au plus 7 noms>
""" """
authuser = REQUEST.AUTHENTICATED_USER authuser = current_user
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
@ -264,9 +263,7 @@ def moduleimpl_inscriptions_stats(formsemestre_id, REQUEST=None):
can_change = authuser.has_permission(Permission.ScoEtudInscrit) and sem["etat"] can_change = authuser.has_permission(Permission.ScoEtudInscrit) and sem["etat"]
# Liste des modules # Liste des modules
Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list( Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
formsemestre_id=formsemestre_id
)
# Decrit les inscriptions aux modules: # Decrit les inscriptions aux modules:
commons = [] # modules communs a tous les etuds du semestre commons = [] # modules communs a tous les etuds du semestre
options = [] # modules ou seuls quelques etudiants sont inscrits options = [] # modules ou seuls quelques etudiants sont inscrits
@ -285,9 +282,7 @@ def moduleimpl_inscriptions_stats(formsemestre_id, REQUEST=None):
mod["nb_inscrits"] = nb_inscrits mod["nb_inscrits"] = nb_inscrits
options.append(mod) options.append(mod)
# Page HTML: # Page HTML:
H = [ H = [html_sco_header.html_sem_header("Inscriptions aux modules du semestre")]
html_sco_header.html_sem_header(REQUEST, "Inscriptions aux modules du semestre")
]
H.append("<h3>Inscrits au semestre: %d étudiants</h3>" % len(inscrits)) H.append("<h3>Inscrits au semestre: %d étudiants</h3>" % len(inscrits))
@ -344,7 +339,7 @@ def moduleimpl_inscriptions_stats(formsemestre_id, REQUEST=None):
UECaps = get_etuds_with_capitalized_ue(formsemestre_id) UECaps = get_etuds_with_capitalized_ue(formsemestre_id)
if UECaps: if UECaps:
H.append('<h3>Etudiants avec UEs capitalisées:</h3><ul class="ue_inscr_list">') H.append('<h3>Etudiants avec UEs capitalisées:</h3><ul class="ue_inscr_list">')
ues = [sco_edit_ue.do_ue_list({"ue_id": ue_id})[0] for ue_id in UECaps.keys()] ues = [sco_edit_ue.ue_list({"ue_id": ue_id})[0] for ue_id in UECaps.keys()]
ues.sort(key=lambda u: u["numero"]) ues.sort(key=lambda u: u["numero"])
for ue in ues: for ue in ues:
H.append( H.append(
@ -524,40 +519,39 @@ def is_inscrit_ue(etudid, formsemestre_id, ue_id):
return r return r
def do_etud_desinscrit_ue(etudid, formsemestre_id, ue_id, REQUEST=None): def do_etud_desinscrit_ue(etudid, formsemestre_id, ue_id):
"""Desincrit l'etudiant de tous les modules de cette UE dans ce semestre.""" """Desincrit l'etudiant de tous les modules de cette UE dans ce semestre."""
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute( cursor.execute(
"""DELETE FROM notes_moduleimpl_inscription """DELETE FROM notes_moduleimpl_inscription
WHERE moduleimpl_inscription_id IN ( WHERE id IN (
SELECT i.moduleimpl_inscription_id FROM SELECT i.id FROM
notes_moduleimpl mi, notes_modules mod, notes_moduleimpl mi, notes_modules mod,
notes_formsemestre sem, notes_moduleimpl_inscription i notes_formsemestre sem, notes_moduleimpl_inscription i
WHERE sem.formsemestre_id = %(formsemestre_id)s WHERE sem.id = %(formsemestre_id)s
AND mi.formsemestre_id = sem.formsemestre_id AND mi.formsemestre_id = sem.id
AND mod.module_id = mi.module_id AND mod.id = mi.module_id
AND mod.ue_id = %(ue_id)s AND mod.ue_id = %(ue_id)s
AND i.moduleimpl_id = mi.moduleimpl_id AND i.moduleimpl_id = mi.id
AND i.etudid = %(etudid)s AND i.etudid = %(etudid)s
) )
""", """,
{"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id}, {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id},
) )
if REQUEST: logdb(
logdb( cnx,
cnx, method="etud_desinscrit_ue",
method="etud_desinscrit_ue", etudid=etudid,
etudid=etudid, msg="desinscription UE %s" % ue_id,
msg="desinscription UE %s" % ue_id, commit=False,
commit=False, )
)
sco_cache.invalidate_formsemestre( sco_cache.invalidate_formsemestre(
formsemestre_id=formsemestre_id formsemestre_id=formsemestre_id
) # > desinscription etudiant des modules ) # > desinscription etudiant des modules
def do_etud_inscrit_ue(etudid, formsemestre_id, ue_id, REQUEST=None): def do_etud_inscrit_ue(etudid, formsemestre_id, ue_id):
"""Incrit l'etudiant de tous les modules de cette UE dans ce semestre.""" """Incrit l'etudiant de tous les modules de cette UE dans ce semestre."""
# Verifie qu'il est bien inscrit au semestre # Verifie qu'il est bien inscrit au semestre
insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(

View File

@ -28,7 +28,7 @@
"""Tableau de bord module """Tableau de bord module
""" """
import time import time
import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error import urllib
from flask import g, url_for from flask import g, url_for
from flask_login import current_user from flask_login import current_user
@ -55,16 +55,16 @@ from app.scodoc import sco_users
# ported from old DTML code in oct 2009 # ported from old DTML code in oct 2009
# menu evaluation dans moduleimpl # menu evaluation dans moduleimpl
def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0, REQUEST=None): def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0):
"Menu avec actions sur une evaluation" "Menu avec actions sur une evaluation"
E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0] E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0]
modimpl = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
group_id = sco_groups.get_default_group(modimpl["formsemestre_id"]) group_id = sco_groups.get_default_group(modimpl["formsemestre_id"])
if ( if (
sco_permissions_check.can_edit_notes( sco_permissions_check.can_edit_notes(
REQUEST.AUTHENTICATED_USER, E["moduleimpl_id"], allow_ens=False current_user, E["moduleimpl_id"], allow_ens=False
) )
and nbnotes != 0 and nbnotes != 0
): ):
@ -80,7 +80,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0, REQUEST=None):
"evaluation_id": evaluation_id, "evaluation_id": evaluation_id,
}, },
"enabled": sco_permissions_check.can_edit_notes( "enabled": sco_permissions_check.can_edit_notes(
REQUEST.AUTHENTICATED_USER, E["moduleimpl_id"] current_user, E["moduleimpl_id"]
), ),
}, },
{ {
@ -90,7 +90,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0, REQUEST=None):
"evaluation_id": evaluation_id, "evaluation_id": evaluation_id,
}, },
"enabled": sco_permissions_check.can_edit_notes( "enabled": sco_permissions_check.can_edit_notes(
REQUEST.AUTHENTICATED_USER, E["moduleimpl_id"], allow_ens=False current_user, E["moduleimpl_id"], allow_ens=False
), ),
}, },
{ {
@ -101,7 +101,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0, REQUEST=None):
}, },
"enabled": nbnotes == 0 "enabled": nbnotes == 0
and sco_permissions_check.can_edit_notes( and sco_permissions_check.can_edit_notes(
REQUEST.AUTHENTICATED_USER, E["moduleimpl_id"], allow_ens=False current_user, E["moduleimpl_id"], allow_ens=False
), ),
}, },
{ {
@ -111,7 +111,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0, REQUEST=None):
"evaluation_id": evaluation_id, "evaluation_id": evaluation_id,
}, },
"enabled": sco_permissions_check.can_edit_notes( "enabled": sco_permissions_check.can_edit_notes(
REQUEST.AUTHENTICATED_USER, E["moduleimpl_id"], allow_ens=False current_user, E["moduleimpl_id"], allow_ens=False
), ),
}, },
{ {
@ -128,16 +128,15 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0, REQUEST=None):
"args": { "args": {
"evaluation_id": evaluation_id, "evaluation_id": evaluation_id,
}, },
"enabled": nbnotes == 0 "enabled": sco_permissions_check.can_edit_notes(
and sco_permissions_check.can_edit_notes( current_user, E["moduleimpl_id"]
REQUEST.AUTHENTICATED_USER, E["moduleimpl_id"]
), ),
}, },
{ {
"title": "Absences ce jour", "title": "Absences ce jour",
"endpoint": "absences.EtatAbsencesDate", "endpoint": "absences.EtatAbsencesDate",
"args": { "args": {
"date": six.moves.urllib.parse.quote(E["jour"], safe=""), "date": E["jour"],
"group_ids": group_id, "group_ids": group_id,
}, },
"enabled": E["jour"], "enabled": E["jour"],
@ -155,11 +154,11 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0, REQUEST=None):
return htmlutils.make_menu("actions", menuEval, alone=True) return htmlutils.make_menu("actions", menuEval, alone=True)
def moduleimpl_status(moduleimpl_id=None, partition_id=None, REQUEST=None): def moduleimpl_status(moduleimpl_id=None, partition_id=None):
"""Tableau de bord module (liste des evaluations etc)""" """Tableau de bord module (liste des evaluations etc)"""
M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
formsemestre_id = M["formsemestre_id"] formsemestre_id = M["formsemestre_id"]
Mod = sco_edit_module.do_module_list(args={"module_id": M["module_id"]})[0] Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
ModInscrits = sco_moduleimpl.do_moduleimpl_inscription_list( ModInscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
@ -177,7 +176,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None, REQUEST=None):
current_user, moduleimpl_id, allow_ens=sem["ens_can_edit_eval"] current_user, moduleimpl_id, allow_ens=sem["ens_can_edit_eval"]
) )
caneditnotes = sco_permissions_check.can_edit_notes(current_user, moduleimpl_id) caneditnotes = sco_permissions_check.can_edit_notes(current_user, moduleimpl_id)
arrow_up, arrow_down, arrow_none = sco_groups.getArrowIconsTags() arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
# #
module_resp = User.query.get(M["responsable_id"]) module_resp = User.query.get(M["responsable_id"])
H = [ H = [
@ -191,7 +190,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None, REQUEST=None):
f"""<span class="blacktt">({module_resp.user_name})</span>""", f"""<span class="blacktt">({module_resp.user_name})</span>""",
] ]
try: try:
sco_moduleimpl.can_change_module_resp(REQUEST, moduleimpl_id) sco_moduleimpl.can_change_module_resp(moduleimpl_id)
H.append( H.append(
"""<a class="stdlink" href="edit_moduleimpl_resp?moduleimpl_id=%s">modifier</a>""" """<a class="stdlink" href="edit_moduleimpl_resp?moduleimpl_id=%s">modifier</a>"""
% moduleimpl_id % moduleimpl_id
@ -515,7 +514,6 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None, REQUEST=None):
moduleimpl_evaluation_menu( moduleimpl_evaluation_menu(
eval["evaluation_id"], eval["evaluation_id"],
nbnotes=etat["nb_notes"], nbnotes=etat["nb_notes"],
REQUEST=REQUEST,
) )
) )
H.append("</td>") H.append("</td>")

View File

@ -174,7 +174,7 @@ def _get_formsemestre_infos_from_news(n):
elif n["type"] == NEWS_NOTE: elif n["type"] == NEWS_NOTE:
moduleimpl_id = n["object"] moduleimpl_id = n["object"]
if n["object"]: if n["object"]:
mods = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id) mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)
if not mods: if not mods:
return {} # module does not exists anymore return {} # module does not exists anymore
return {} # pas d'indication du module return {} # pas d'indication du module

View File

@ -30,7 +30,8 @@
Fiche description d'un étudiant et de son parcours Fiche description d'un étudiant et de son parcours
""" """
from flask import url_for, g from flask import url_for, g, request
from flask_login import current_user
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -46,6 +47,7 @@ from app.scodoc import sco_groups
from app.scodoc import sco_parcours_dut from app.scodoc import sco_parcours_dut
from app.scodoc import sco_permissions_check from app.scodoc import sco_permissions_check
from app.scodoc import sco_photos from app.scodoc import sco_photos
from app.scodoc import sco_users
from app.scodoc import sco_report from app.scodoc import sco_report
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc.sco_bulletins import etud_descr_situation_semestre from app.scodoc.sco_bulletins import etud_descr_situation_semestre
@ -142,18 +144,22 @@ def _menuScolarite(authuser, sem, etudid):
) )
def ficheEtud(etudid=None, REQUEST=None): def ficheEtud(etudid=None):
"fiche d'informations sur un etudiant" "fiche d'informations sur un etudiant"
authuser = REQUEST.AUTHENTICATED_USER authuser = current_user
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
if etudid and REQUEST: if etudid:
try: # pour les bookmarks avec d'anciens ids...
etudid = int(etudid)
except ValueError:
raise ScoValueError("id invalide !")
# la sidebar est differente s'il y a ou pas un etudid # la sidebar est differente s'il y a ou pas un etudid
# voir html_sidebar.sidebar() # voir html_sidebar.sidebar()
REQUEST.form["etudid"] = etudid g.etudid = etudid
args = sco_etud.make_etud_args(etudid=etudid) args = sco_etud.make_etud_args(etudid=etudid)
etuds = sco_etud.etudident_list(cnx, args) etuds = sco_etud.etudident_list(cnx, args)
if not etuds: if not etuds:
log("ficheEtud: etudid=%s REQUEST.form=%s" % (etudid, REQUEST.form)) log("ficheEtud: etudid=%s request.args=%s" % (etudid, request.args))
raise ScoValueError("Etudiant inexistant !") raise ScoValueError("Etudiant inexistant !")
etud = etuds[0] etud = etuds[0]
etudid = etud["etudid"] etudid = etud["etudid"]
@ -167,7 +173,7 @@ def ficheEtud(etudid=None, REQUEST=None):
info["info_naissance"] += " à " + info["lieu_naissance"] info["info_naissance"] += " à " + info["lieu_naissance"]
if info["dept_naissance"]: if info["dept_naissance"]:
info["info_naissance"] += " (%s)" % info["dept_naissance"] info["info_naissance"] += " (%s)" % info["dept_naissance"]
info["etudfoto"] = sco_photos.etud_photo_html(etud, REQUEST=REQUEST) info["etudfoto"] = sco_photos.etud_photo_html(etud)
if ( if (
(not info["domicile"]) (not info["domicile"])
and (not info["codepostaldomicile"]) and (not info["codepostaldomicile"])
@ -254,10 +260,19 @@ def ficheEtud(etudid=None, REQUEST=None):
with_all_columns=False, with_all_columns=False,
a_url="Notes/", a_url="Notes/",
) )
info["link_bul_pdf"] = ( info[
'<span class="link_bul_pdf"><a class="stdlink" href="Notes/etud_bulletins_pdf?etudid=%(etudid)s">tous les bulletins</a></span>' "link_bul_pdf"
% etud ] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
) url_for("notes.etud_bulletins_pdf", scodoc_dept=g.scodoc_dept, etudid=etudid)
}">tous les bulletins</a></span>"""
if authuser.has_permission(Permission.ScoEtudInscrit):
info[
"link_inscrire_ailleurs"
] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
url_for("notes.formsemestre_inscription_with_modules_form", scodoc_dept=g.scodoc_dept, etudid=etudid)
}">inscrire à un autre semestre</a></span>"""
else:
info["link_inscrire_ailleurs"] = ""
else: else:
# non inscrit # non inscrit
l = ["<p><b>Etudiant%s non inscrit%s" % (info["ne"], info["ne"])] l = ["<p><b>Etudiant%s non inscrit%s" % (info["ne"], info["ne"])]
@ -269,6 +284,7 @@ def ficheEtud(etudid=None, REQUEST=None):
l.append("</b></b>") l.append("</b></b>")
info["liste_inscriptions"] = "\n".join(l) info["liste_inscriptions"] = "\n".join(l)
info["link_bul_pdf"] = "" info["link_bul_pdf"] = ""
info["link_inscrire_ailleurs"] = ""
# Liste des annotations # Liste des annotations
alist = [] alist = []
@ -289,9 +305,11 @@ def ficheEtud(etudid=None, REQUEST=None):
title="Supprimer cette annotation", title="Supprimer cette annotation",
), ),
) )
author = sco_users.user_info(a["author"])
alist.append( alist.append(
'<tr><td><span class="annodate">Le %(date)s par %(author)s : </span><span class="annoc">%(comment)s</span></td>%(dellink)s</tr>' f"""<tr><td><span class="annodate">Le {a['date']} par {author['prenomnom']} :
% a </span><span class="annoc">{a['comment']}</span></td>{a['dellink']}</tr>
"""
) )
info["liste_annotations"] = "\n".join(alist) info["liste_annotations"] = "\n".join(alist)
# fiche admission # fiche admission
@ -345,7 +363,7 @@ def ficheEtud(etudid=None, REQUEST=None):
# Fichiers archivés: # Fichiers archivés:
info["fichiers_archive_htm"] = ( info["fichiers_archive_htm"] = (
'<div class="fichetitre">Fichiers associés</div>' '<div class="fichetitre">Fichiers associés</div>'
+ sco_archives_etud.etud_list_archives_html(REQUEST, etudid) + sco_archives_etud.etud_list_archives_html(etudid)
) )
# Devenir de l'étudiant: # Devenir de l'étudiant:
@ -392,10 +410,11 @@ def ficheEtud(etudid=None, REQUEST=None):
"inscriptions_mkup" "inscriptions_mkup"
] = """<div class="ficheinscriptions" id="ficheinscriptions"> ] = """<div class="ficheinscriptions" id="ficheinscriptions">
<div class="fichetitre">Parcours</div>%s <div class="fichetitre">Parcours</div>%s
%s %s %s
</div>""" % ( </div>""" % (
info["liste_inscriptions"], info["liste_inscriptions"],
info["link_bul_pdf"], info["link_bul_pdf"],
info["link_inscrire_ailleurs"],
) )
# #
@ -405,7 +424,7 @@ def ficheEtud(etudid=None, REQUEST=None):
) )
else: else:
info["groupes_row"] = "" info["groupes_row"] = ""
info["menus_etud"] = menus_etud(REQUEST) info["menus_etud"] = menus_etud(etudid)
tmpl = """<div class="menus_etud">%(menus_etud)s</div> tmpl = """<div class="menus_etud">%(menus_etud)s</div>
<div class="ficheEtud" id="ficheEtud"><table> <div class="ficheEtud" id="ficheEtud"><table>
<tr><td> <tr><td>
@ -487,13 +506,11 @@ def ficheEtud(etudid=None, REQUEST=None):
return header + tmpl % info + html_sco_header.sco_footer() return header + tmpl % info + html_sco_header.sco_footer()
def menus_etud(REQUEST=None): def menus_etud(etudid):
"""Menu etudiant (operations sur l'etudiant)""" """Menu etudiant (operations sur l'etudiant)"""
if "etudid" not in REQUEST.form: authuser = current_user
return ""
authuser = REQUEST.AUTHENTICATED_USER
etud = sco_etud.get_etud_info(filled=True)[0] etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
menuEtud = [ menuEtud = [
{ {
@ -532,16 +549,14 @@ def menus_etud(REQUEST=None):
return htmlutils.make_menu("Etudiant", menuEtud, alone=True) return htmlutils.make_menu("Etudiant", menuEtud, alone=True)
def etud_info_html(etudid, with_photo="1", REQUEST=None, debug=False): def etud_info_html(etudid, with_photo="1", debug=False):
"""An HTML div with basic information and links about this etud. """An HTML div with basic information and links about this etud.
Used for popups information windows. Used for popups information windows.
""" """
formsemestre_id = sco_formsemestre_status.retreive_formsemestre_from_request() formsemestre_id = sco_formsemestre_status.retreive_formsemestre_from_request()
with_photo = int(with_photo) with_photo = int(with_photo)
etud = sco_etud.get_etud_info(filled=True)[0] etud = sco_etud.get_etud_info(filled=True)[0]
photo_html = sco_photos.etud_photo_html( photo_html = sco_photos.etud_photo_html(etud, title="fiche de " + etud["nom"])
etud, title="fiche de " + etud["nom"], REQUEST=REQUEST
)
# experimental: may be too slow to be here # experimental: may be too slow to be here
etud["codeparcours"], etud["decisions_jury"] = sco_report.get_codeparcoursetud( etud["codeparcours"], etud["decisions_jury"] = sco_report.get_codeparcoursetud(
etud, prefix="S", separator=", " etud, prefix="S", separator=", "

View File

@ -535,7 +535,7 @@ class SituationEtudParcoursGeneric(object):
validated = True validated = True
return s return s
def valide_decision(self, decision, REQUEST): def valide_decision(self, decision):
"""Enregistre la decision (instance de DecisionSem) """Enregistre la decision (instance de DecisionSem)
Enregistre codes semestre et UE, et autorisations inscription. Enregistre codes semestre et UE, et autorisations inscription.
""" """
@ -588,7 +588,6 @@ class SituationEtudParcoursGeneric(object):
self.etudid, self.etudid,
decision.code_etat, decision.code_etat,
decision.assiduite, decision.assiduite,
REQUEST=REQUEST,
) )
# -- modification du code du semestre precedent # -- modification du code du semestre precedent
if self.prev and decision.new_code_prev: if self.prev and decision.new_code_prev:
@ -619,7 +618,6 @@ class SituationEtudParcoursGeneric(object):
self.etudid, self.etudid,
decision.new_code_prev, decision.new_code_prev,
decision.assiduite, # attention: en toute rigueur il faudrait utiliser une indication de l'assiduite au sem. precedent, que nous n'avons pas... decision.assiduite, # attention: en toute rigueur il faudrait utiliser une indication de l'assiduite au sem. precedent, que nous n'avons pas...
REQUEST=REQUEST,
) )
sco_cache.invalidate_formsemestre( sco_cache.invalidate_formsemestre(
@ -897,9 +895,7 @@ def formsemestre_update_validation_sem(
return to_invalidate return to_invalidate
def formsemestre_validate_ues( def formsemestre_validate_ues(formsemestre_id, etudid, code_etat_sem, assiduite):
formsemestre_id, etudid, code_etat_sem, assiduite, REQUEST=None
):
"""Enregistre codes UE, selon état semestre. """Enregistre codes UE, selon état semestre.
Les codes UE sont toujours calculés ici, et non passés en paramètres Les codes UE sont toujours calculés ici, et non passés en paramètres
car ils ne dépendent que de la note d'UE et de la validation ou non du semestre. car ils ne dépendent que de la note d'UE et de la validation ou non du semestre.
@ -920,7 +916,7 @@ def formsemestre_validate_ues(
and ue_status["moy"] >= nt.parcours.NOTES_BARRE_VALID_UE and ue_status["moy"] >= nt.parcours.NOTES_BARRE_VALID_UE
): ):
code_ue = ADM code_ue = ADM
elif isinstance(ue_status["moy"], float): elif not isinstance(ue_status["moy"], float):
# aucune note (pas de moyenne) dans l'UE: ne la valide pas # aucune note (pas de moyenne) dans l'UE: ne la valide pas
code_ue = None code_ue = None
elif valid_semestre: elif valid_semestre:
@ -933,14 +929,13 @@ def formsemestre_validate_ues(
cnx, nt, formsemestre_id, etudid, ue_id, code_ue cnx, nt, formsemestre_id, etudid, ue_id, code_ue
) )
if REQUEST: logdb(
logdb( cnx,
cnx, method="validate_ue",
method="validate_ue", etudid=etudid,
etudid=etudid, msg="ue_id=%s code=%s" % (ue_id, code_ue),
msg="ue_id=%s code=%s" % (ue_id, code_ue), commit=False,
commit=False, )
)
cnx.commit() cnx.commit()

View File

@ -60,13 +60,10 @@ from reportlab.lib.pagesizes import letter, A4, landscape
from flask import g from flask import g
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ( from app.scodoc.sco_logos import find_logo
CONFIG, from app.scodoc.sco_utils import CONFIG
SCODOC_LOGOS_DIR,
LOGOS_IMAGES_ALLOWED_TYPES,
)
from app import log from app import log
from app.scodoc.sco_exceptions import ScoGenError from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
import sco_version import sco_version
PAGE_HEIGHT = defaultPageSize[1] PAGE_HEIGHT = defaultPageSize[1]
@ -121,6 +118,7 @@ def makeParas(txt, style, suppress_empty=False):
"""Returns a list of Paragraph instances from a text """Returns a list of Paragraph instances from a text
with one or more <para> ... </para> with one or more <para> ... </para>
""" """
result = []
try: try:
paras = _splitPara(txt) paras = _splitPara(txt)
if suppress_empty: if suppress_empty:
@ -133,21 +131,30 @@ def makeParas(txt, style, suppress_empty=False):
if m.group(1): # non empty paragraph if m.group(1): # non empty paragraph
r.append(para) r.append(para)
paras = r paras = r
return [Paragraph(SU(s), style) for s in paras] result = [Paragraph(SU(s), style) for s in paras]
except OSError as e:
msg = str(e)
# If a file is missing, try to display the invalid name
m = re.match(r".*\sfilename=\'(.*?)\'.*", msg, re.DOTALL)
if m:
filename = os.path.split(m.group(1))[1]
if filename.startswith("logo_"):
filename = filename[len("logo_") :]
raise ScoValueError(
f"Erreur dans le format PDF paramétré: fichier logo <b>{filename}</b> non trouvé"
) from e
else:
raise e
except Exception as e: except Exception as e:
detail = " " + str(e)
log(traceback.format_exc()) log(traceback.format_exc())
log("Invalid pdf para format: %s" % txt) log("Invalid pdf para format: %s" % txt)
return [ result = [
Paragraph( Paragraph(
SU( SU('<font color="red"><i>Erreur: format invalide</i></font>'),
'<font color="red"><i>Erreur: format invalide{}</i></font>'.format(
detail
)
),
style, style,
) )
] ]
return result
def bold_paras(L, tag="b", close=None): def bold_paras(L, tag="b", close=None):
@ -209,20 +216,16 @@ class ScolarsPageTemplate(PageTemplate):
) )
PageTemplate.__init__(self, "ScolarsPageTemplate", [content]) PageTemplate.__init__(self, "ScolarsPageTemplate", [content])
self.logo = None self.logo = None
# XXX COPIED from sco_pvpdf, to be refactored (no time now) logo = find_logo(
# Search background in dept specific dir, then in global config dir logoname="bul_pdf_background", dept_id=g.scodoc_dept_id, prefix=None
for image_dir in ( )
SCODOC_LOGOS_DIR + "/logos_" + g.scodoc_dept + "/", if logo is None:
SCODOC_LOGOS_DIR + "/", # global logos # Also try to use PV background
): logo = find_logo(
for suffix in LOGOS_IMAGES_ALLOWED_TYPES: logoname="letter_background", dept_id=g.scodoc_dept_id, prefix=None
fn = image_dir + "/bul_pdf_background" + "." + suffix )
if not self.background_image_filename and os.path.exists(fn): if logo is not None:
self.background_image_filename = fn self.background_image_filename = logo.filepath
# Also try to use PV background
fn = image_dir + "/letter_background" + "." + suffix
if not self.background_image_filename and os.path.exists(fn):
self.background_image_filename = fn
def beforeDrawPage(self, canvas, doc): def beforeDrawPage(self, canvas, doc):
"""Draws (optional) background, logo and contribution message on each page. """Draws (optional) background, logo and contribution message on each page.
@ -340,7 +343,6 @@ def pdf_basic_page(
# Gestion du lock pdf # Gestion du lock pdf
import threading, time, six.moves.queue, six.moves._thread
class PDFLock(object): class PDFLock(object):

View File

@ -26,7 +26,7 @@ def can_edit_notes(authuser, moduleimpl_id, allow_ens=True):
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_parcours_dut from app.scodoc import sco_parcours_dut
M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"]) sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
if not sem["etat"]: if not sem["etat"]:
return False # semestre verrouillé return False # semestre verrouillé
@ -64,7 +64,7 @@ def can_edit_evaluation(moduleimpl_id=None):
# acces pour resp. moduleimpl et resp. form semestre (dir etud) # acces pour resp. moduleimpl et resp. form semestre (dir etud)
if moduleimpl_id is None: if moduleimpl_id is None:
raise ValueError("no moduleimpl specified") # bug raise ValueError("no moduleimpl specified") # bug
M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"]) sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
if ( if (
@ -200,4 +200,4 @@ def can_handle_passwd(user, allow_admindepts=False):
if (current_user.dept == user.dept) or allow_admindepts: if (current_user.dept == user.dept) or allow_admindepts:
return True return True
else: else:
return False return False

View File

@ -43,6 +43,8 @@ Les images sont servies par ScoDoc, via la méthode getphotofile?etudid=xxx
""" """
from flask.helpers import make_response
from app.scodoc.sco_exceptions import ScoGenError
import datetime import datetime
import glob import glob
import io import io
@ -52,6 +54,7 @@ import requests
import time import time
import traceback import traceback
import PIL
from PIL import Image as PILImage from PIL import Image as PILImage
from flask import request, g from flask import request, g
@ -118,7 +121,7 @@ def etud_photo_url(etud, size="small", fast=False):
return photo_url return photo_url
def get_photo_image(etudid=None, size="small", REQUEST=None): def get_photo_image(etudid=None, size="small"):
"""Returns photo image (HTTP response) """Returns photo image (HTTP response)
If not etudid, use "unknown" image If not etudid, use "unknown" image
""" """
@ -129,24 +132,14 @@ def get_photo_image(etudid=None, size="small", REQUEST=None):
filename = photo_pathname(etud, size=size) filename = photo_pathname(etud, size=size)
if not filename: if not filename:
filename = UNKNOWN_IMAGE_PATH filename = UNKNOWN_IMAGE_PATH
return _http_jpeg_file(filename, REQUEST=REQUEST) return _http_jpeg_file(filename)
def _http_jpeg_file(filename, REQUEST=None): def _http_jpeg_file(filename):
"""returns an image. """returns an image as a Flask response"""
This function will be modified when we kill #zope
"""
st = os.stat(filename) st = os.stat(filename)
last_modified = st.st_mtime # float timestamp last_modified = st.st_mtime # float timestamp
last_modified_str = time.strftime(
"%a, %d %b %Y %H:%M:%S GMT", time.gmtime(last_modified)
)
file_size = st.st_size file_size = st.st_size
RESPONSE = REQUEST.RESPONSE
RESPONSE.setHeader("Content-Type", "image/jpeg")
RESPONSE.setHeader("Last-Modified", last_modified_str)
RESPONSE.setHeader("Cache-Control", "max-age=3600")
RESPONSE.setHeader("Content-Length", str(file_size))
header = request.headers.get("If-Modified-Since") header = request.headers.get("If-Modified-Since")
if header is not None: if header is not None:
header = header.split(";")[0] header = header.split(";")[0]
@ -159,20 +152,27 @@ def _http_jpeg_file(filename, REQUEST=None):
try: try:
dt = datetime.datetime.strptime(header, "%a, %d %b %Y %H:%M:%S GMT") dt = datetime.datetime.strptime(header, "%a, %d %b %Y %H:%M:%S GMT")
mod_since = dt.timestamp() mod_since = dt.timestamp()
except: except ValueError:
mod_since = None mod_since = None
if (mod_since is not None) and last_modified <= mod_since: if (mod_since is not None) and last_modified <= mod_since:
RESPONSE.setStatus(304) # not modified return "", 304 # not modified
return "" #
last_modified_str = time.strftime(
return open(filename, mode="rb").read() "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(last_modified)
)
response = make_response(open(filename, mode="rb").read())
response.headers["Content-Type"] = "image/jpeg"
response.headers["Last-Modified"] = last_modified_str
response.headers["Cache-Control"] = "max-age=3600"
response.headers["Content-Length"] = str(file_size)
return response
def etud_photo_is_local(etud, size="small"): def etud_photo_is_local(etud, size="small"):
return photo_pathname(etud, size=size) return photo_pathname(etud, size=size)
def etud_photo_html(etud=None, etudid=None, title=None, size="small", REQUEST=None): def etud_photo_html(etud=None, etudid=None, title=None, size="small"):
"""HTML img tag for the photo, either in small size (h90) """HTML img tag for the photo, either in small size (h90)
or original size (size=="orig") or original size (size=="orig")
""" """
@ -204,14 +204,12 @@ def etud_photo_html(etud=None, etudid=None, title=None, size="small", REQUEST=No
) )
def etud_photo_orig_html(etud=None, etudid=None, title=None, REQUEST=None): def etud_photo_orig_html(etud=None, etudid=None, title=None):
"""HTML img tag for the photo, in full size. """HTML img tag for the photo, in full size.
Full-size images are always stored locally in the filesystem. Full-size images are always stored locally in the filesystem.
They are the original uploaded images, converted in jpeg. They are the original uploaded images, converted in jpeg.
""" """
return etud_photo_html( return etud_photo_html(etud=etud, etudid=etudid, title=title, size="orig")
etud=etud, etudid=etudid, title=title, size="orig", REQUEST=REQUEST
)
def photo_pathname(etud, size="orig"): def photo_pathname(etud, size="orig"):
@ -246,7 +244,10 @@ def store_photo(etud, data):
filesize = len(data) filesize = len(data)
if filesize < 10 or filesize > MAX_FILE_SIZE: if filesize < 10 or filesize > MAX_FILE_SIZE:
return 0, "Fichier image de taille invalide ! (%d)" % filesize return 0, "Fichier image de taille invalide ! (%d)" % filesize
filename = save_image(etud["etudid"], data) try:
filename = save_image(etud["etudid"], data)
except PIL.UnidentifiedImageError:
raise ScoGenError(msg="Fichier d'image invalide ou non format non supporté")
# update database: # update database:
etud["photo_filename"] = filename etud["photo_filename"] = filename
etud["foto"] = None etud["foto"] = None
@ -260,7 +261,7 @@ def store_photo(etud, data):
return 1, "ok" return 1, "ok"
def suppress_photo(etud, REQUEST=None): def suppress_photo(etud):
"""Suppress a photo""" """Suppress a photo"""
log("suppress_photo etudid=%s" % etud["etudid"]) log("suppress_photo etudid=%s" % etud["etudid"])
rel_path = photo_pathname(etud) rel_path = photo_pathname(etud)
@ -278,8 +279,7 @@ def suppress_photo(etud, REQUEST=None):
log("removing file %s" % filename) log("removing file %s" % filename)
os.remove(filename) os.remove(filename)
# 3- log # 3- log
if REQUEST: logdb(cnx, method="changePhoto", msg="suppression", etudid=etud["etudid"])
logdb(cnx, method="changePhoto", msg="suppression", etudid=etud["etudid"])
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -298,6 +298,7 @@ def save_image(etudid, data):
filename = get_new_filename(etudid) filename = get_new_filename(etudid)
path = os.path.join(PHOTO_DIR, filename) path = os.path.join(PHOTO_DIR, filename)
log("saving %dx%d jpeg to %s" % (img.size[0], img.size[1], path)) log("saving %dx%d jpeg to %s" % (img.size[0], img.size[1], path))
img = img.convert("RGB")
img.save(path + IMAGE_EXT, format="JPEG", quality=92) img.save(path + IMAGE_EXT, format="JPEG", quality=92)
# resize: # resize:
img = scale_height(img) img = scale_height(img)
@ -341,7 +342,7 @@ def find_new_dir():
def copy_portal_photo_to_fs(etud): def copy_portal_photo_to_fs(etud):
"""Copy the photo from portal (distant website) to local fs. """Copy the photo from portal (distant website) to local fs.
Returns rel. path or None if copy failed, with a diagnotic message Returns rel. path or None if copy failed, with a diagnostic message
""" """
sco_etud.format_etud_ident(etud) sco_etud.format_etud_ident(etud)
url = photo_portal_url(etud) url = photo_portal_url(etud)
@ -353,11 +354,12 @@ def copy_portal_photo_to_fs(etud):
log("copy_portal_photo_to_fs: getting %s" % url) log("copy_portal_photo_to_fs: getting %s" % url)
r = requests.get(url, timeout=portal_timeout) r = requests.get(url, timeout=portal_timeout)
except: except:
log("download failed: exception:\n%s" % traceback.format_exc()) # log("download failed: exception:\n%s" % traceback.format_exc())
log("called from:\n" + "".join(traceback.format_stack())) # log("called from:\n" + "".join(traceback.format_stack()))
log("copy_portal_photo_to_fs: error.")
return None, "%s: erreur chargement de %s" % (etud["nomprenom"], url) return None, "%s: erreur chargement de %s" % (etud["nomprenom"], url)
if r.status_code != 200: if r.status_code != 200:
log("download failed") log(f"copy_portal_photo_to_fs: download failed {r.status_code }")
return None, "%s: erreur chargement de %s" % (etud["nomprenom"], url) return None, "%s: erreur chargement de %s" % (etud["nomprenom"], url)
data = r.content # image bytes data = r.content # image bytes
try: try:

File diff suppressed because it is too large Load Diff

View File

@ -39,7 +39,6 @@ import app.scodoc.sco_utils as scu
from app import log from app import log
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
import six
SCO_CACHE_ETAPE_FILENAME = os.path.join(scu.SCO_TMP_DIR, "last_etapes.xml") SCO_CACHE_ETAPE_FILENAME = os.path.join(scu.SCO_TMP_DIR, "last_etapes.xml")
@ -386,7 +385,8 @@ def get_etapes_apogee():
# cache le resultat (utile si le portail repond de façon intermitente) # cache le resultat (utile si le portail repond de façon intermitente)
if infos: if infos:
log("get_etapes_apogee: caching result") log("get_etapes_apogee: caching result")
open(SCO_CACHE_ETAPE_FILENAME, "w").write(doc) with open(SCO_CACHE_ETAPE_FILENAME, "w") as f:
f.write(doc)
except: except:
log("invalid XML response from getEtapes Web Service\n%s" % etapes_url) log("invalid XML response from getEtapes Web Service\n%s" % etapes_url)
# Avons nous la copie d'une réponse récente ? # Avons nous la copie d'une réponse récente ?

View File

@ -31,7 +31,7 @@ Recapitule tous les semestres validés dans une feuille excel.
""" """
import collections import collections
from flask import url_for, g from flask import url_for, g, request
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import sco_abs from app.scodoc import sco_abs
@ -164,7 +164,7 @@ def _getEtudInfoGroupes(group_ids, etat=None):
return etuds return etuds
def formsemestre_poursuite_report(formsemestre_id, format="html", REQUEST=None): def formsemestre_poursuite_report(formsemestre_id, format="html"):
"""Table avec informations "poursuite" """ """Table avec informations "poursuite" """
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
etuds = _getEtudInfoGroupes([sco_groups.get_default_group(formsemestre_id)]) etuds = _getEtudInfoGroupes([sco_groups.get_default_group(formsemestre_id)])
@ -211,12 +211,11 @@ def formsemestre_poursuite_report(formsemestre_id, format="html", REQUEST=None):
) )
tab.caption = "Récapitulatif %s." % sem["titreannee"] tab.caption = "Récapitulatif %s." % sem["titreannee"]
tab.html_caption = "Récapitulatif %s." % sem["titreannee"] tab.html_caption = "Récapitulatif %s." % sem["titreannee"]
tab.base_url = "%s?formsemestre_id=%s" % (REQUEST.URL0, formsemestre_id) tab.base_url = "%s?formsemestre_id=%s" % (request.base_url, formsemestre_id)
return tab.make_page( return tab.make_page(
title="""<h2 class="formsemestre">Poursuite d'études</h2>""", title="""<h2 class="formsemestre">Poursuite d'études</h2>""",
init_qtip=True, init_qtip=True,
javascripts=["js/etud_info.js"], javascripts=["js/etud_info.js"],
format=format, format=format,
REQUEST=REQUEST,
with_html_headers=True, with_html_headers=True,
) )

View File

@ -77,7 +77,7 @@ sinon, elle ne concerne que le semestre indiqué.
- avoir un mapping (read only) de toutes les valeurs: - avoir un mapping (read only) de toutes les valeurs:
sco_preferences.SemPreferences(formsemestre_id) sco_preferences.SemPreferences(formsemestre_id)
- editer les preferences globales: - editer les preferences globales:
sco_preferences.get_base_preferences(self).edit(REQUEST=REQUEST) sco_preferences.get_base_preferences(self).edit()
- editer les preferences d'un semestre: - editer les preferences d'un semestre:
SemPreferences(formsemestre_id).edit() SemPreferences(formsemestre_id).edit()
@ -111,7 +111,8 @@ get_base_preferences(formsemestre_id)
""" """
import flask import flask
from flask import g, url_for from flask import g, url_for, request
from flask_login import current_user
from app.models import Departement from app.models import Departement
from app.scodoc import sco_cache from app.scodoc import sco_cache
@ -180,7 +181,7 @@ def _convert_pref_type(p, pref_spec):
def _get_pref_default_value_from_config(name, pref_spec): def _get_pref_default_value_from_config(name, pref_spec):
"""get default value store in application level config. """get default value store in application level config.
If not found, use defalut value hardcoded in pref_spec. If not found, use default value hardcoded in pref_spec.
""" """
# XXX va changer avec la nouvelle base # XXX va changer avec la nouvelle base
# search in scu.CONFIG # search in scu.CONFIG
@ -1408,7 +1409,7 @@ class BasePreferences(object):
{ {
"initvalue": 1, "initvalue": 1,
"title": "Indique si les bulletins sont publiés", "title": "Indique si les bulletins sont publiés",
"explanation": "décocher si vous n'avez pas de portal étudiant publiant les bulletins", "explanation": "décocher si vous n'avez pas de portail étudiant publiant les bulletins",
"input_type": "boolcheckbox", "input_type": "boolcheckbox",
"labels": ["non", "oui"], "labels": ["non", "oui"],
"category": "bul", "category": "bul",
@ -1891,23 +1892,11 @@ class BasePreferences(object):
def get(self, formsemestre_id, name): def get(self, formsemestre_id, name):
"""Returns preference value. """Returns preference value.
If global_lookup, when no value defined for this semestre, returns global value. when no value defined for this semestre, returns global value.
""" """
params = { if formsemestre_id in self.prefs:
"dept_id": self.dept_id, return self.prefs[formsemestre_id].get(name, self.prefs[None][name])
"name": name, return self.prefs[None][name]
"formsemestre_id": formsemestre_id,
}
cnx = ndb.GetDBConnexion()
plist = self._editor.list(cnx, params)
if not plist:
del params["formsemestre_id"]
plist = self._editor.list(cnx, params)
if not plist:
return self.default[name]
p = plist[0]
_convert_pref_type(p, self.prefs_dict[name])
return p["value"]
def __contains__(self, item): def __contains__(self, item):
return item in self.prefs[None] return item in self.prefs[None]
@ -1948,6 +1937,15 @@ class BasePreferences(object):
"name": name, "name": name,
}, },
) )
if len(pdb) > 1:
# suppress buggy duplicates (may come from corrupted database for ice ages)
log(
f"**oups** detected duplicated preference !\n({self.dept_id}, {formsemestre_id}, {name}, {value})"
)
for obj in pdb[1:]:
self._editor.delete(cnx, obj["id"])
pdb = [pdb[0]]
if not pdb: if not pdb:
# crée préférence # crée préférence
log("create pref sem=%s %s=%s" % (formsemestre_id, name, value)) log("create pref sem=%s %s=%s" % (formsemestre_id, name, value))
@ -1961,10 +1959,8 @@ class BasePreferences(object):
}, },
) )
modif = True modif = True
log("create pref sem=%s %s=%s" % (formsemestre_id, name, value))
else: else:
# edit existing value # edit existing value
existing_value = pdb[0]["value"] # old stored value existing_value = pdb[0]["value"] # old stored value
if ( if (
(existing_value != value) (existing_value != value)
@ -2013,7 +2009,7 @@ class BasePreferences(object):
self._editor.delete(cnx, pdb[0]["pref_id"]) self._editor.delete(cnx, pdb[0]["pref_id"])
sco_cache.invalidate_formsemestre() # > modif preferences sco_cache.invalidate_formsemestre() # > modif preferences
def edit(self, REQUEST): def edit(self):
"""HTML dialog: edit global preferences""" """HTML dialog: edit global preferences"""
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
@ -2021,16 +2017,18 @@ class BasePreferences(object):
H = [ H = [
html_sco_header.sco_header(page_title="Préférences"), html_sco_header.sco_header(page_title="Préférences"),
"<h2>Préférences globales pour %s</h2>" % scu.ScoURL(), "<h2>Préférences globales pour %s</h2>" % scu.ScoURL(),
f"""<p><a href="{url_for("scolar.config_logos", scodoc_dept=g.scodoc_dept) # f"""<p><a href="{url_for("scolar.config_logos", scodoc_dept=g.scodoc_dept)
}">modification des logos du département (pour documents pdf)</a></p>""", # }">modification des logos du département (pour documents pdf)</a></p>"""
# if current_user.is_administrator()
# else "",
"""<p class="help">Ces paramètres s'appliquent par défaut à tous les semestres, sauf si ceux-ci définissent des valeurs spécifiques.</p> """<p class="help">Ces paramètres s'appliquent par défaut à tous les semestres, sauf si ceux-ci définissent des valeurs spécifiques.</p>
<p class="msg">Attention: cliquez sur "Enregistrer les modifications" en bas de page pour appliquer vos changements !</p> <p class="msg">Attention: cliquez sur "Enregistrer les modifications" en bas de page pour appliquer vos changements !</p>
""", """,
] ]
form = self.build_tf_form() form = self.build_tf_form()
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
form, form,
initvalues=self.prefs[None], initvalues=self.prefs[None],
submitlabel="Enregistrer les modifications", submitlabel="Enregistrer les modifications",
@ -2140,7 +2138,7 @@ class SemPreferences(object):
return self.base_prefs.is_global(self.formsemestre_id, name) return self.base_prefs.is_global(self.formsemestre_id, name)
# The dialog # The dialog
def edit(self, categories=[], REQUEST=None): def edit(self, categories=[]):
"""Dialog to edit semestre preferences in given categories""" """Dialog to edit semestre preferences in given categories"""
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
@ -2151,7 +2149,7 @@ class SemPreferences(object):
) # a bug ! ) # a bug !
sem = sco_formsemestre.get_formsemestre(self.formsemestre_id) sem = sco_formsemestre.get_formsemestre(self.formsemestre_id)
H = [ H = [
html_sco_header.html_sem_header(REQUEST, "Préférences du semestre", sem), html_sco_header.html_sem_header("Préférences du semestre", sem),
""" """
<p class="help">Les paramètres définis ici ne s'appliqueront qu'à ce semestre.</p> <p class="help">Les paramètres définis ici ne s'appliqueront qu'à ce semestre.</p>
<p class="msg">Attention: cliquez sur "Enregistrer les modifications" en bas de page pour appliquer vos changements !</p> <p class="msg">Attention: cliquez sur "Enregistrer les modifications" en bas de page pour appliquer vos changements !</p>
@ -2194,8 +2192,8 @@ function set_global_pref(el, pref_name) {
form.append(("destination", {"input_type": "hidden"})) form.append(("destination", {"input_type": "hidden"}))
form.append(("formsemestre_id", {"input_type": "hidden"})) form.append(("formsemestre_id", {"input_type": "hidden"}))
tf = TrivialFormulator( tf = TrivialFormulator(
REQUEST.URL0, request.base_url,
REQUEST.form, scu.get_request_args(),
form, form,
initvalues=self, initvalues=self,
cssclass="sco_pref", cssclass="sco_pref",
@ -2245,7 +2243,7 @@ function set_global_pref(el, pref_name) {
return flask.redirect(dest_url + "&head_message=Préférences modifiées") return flask.redirect(dest_url + "&head_message=Préférences modifiées")
elif destination == "again": elif destination == "again":
return flask.redirect( return flask.redirect(
REQUEST.URL0 + "?formsemestre_id=" + str(self.formsemestre_id) request.base_url + "?formsemestre_id=" + str(self.formsemestre_id)
) )
elif destination == "global": elif destination == "global":
return flask.redirect(scu.ScoURL() + "/edit_preferences") return flask.redirect(scu.ScoURL() + "/edit_preferences")
@ -2253,7 +2251,7 @@ function set_global_pref(el, pref_name) {
# #
def doc_preferences(): def doc_preferences():
""" Liste les preferences en MarkDown, pour la documentation""" """Liste les preferences en MarkDown, pour la documentation"""
L = [] L = []
for cat, cat_descr in PREF_CATEGORIES: for cat, cat_descr in PREF_CATEGORIES:
L.append([""]) L.append([""])

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