Compare commits

...

276 Commits

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

1
.gitignore vendored
View File

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

View File

@ -3,16 +3,12 @@
(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>
Documentation utilisateur: <https://scodoc.org>
## Version ScoDoc 9
N'utiliser que pour les développements et tests.
La version ScoDoc 9 est basée sur Flask (au lieu de Zope) et sur
**python 3.9+**.
@ -22,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:**
- feuille "placement" (en cours)
- génération LaTeX des avis de poursuite d'études
- ancien module "Entreprises" (obsolete)
@ -110,13 +106,15 @@ Ou avec couverture (`pip install pytest-cov`)
#### Utilisation des tests unitaires pour initialiser la base de dev
On peut aussi utiliser les tests unitaires pour mettre la base
de données de développement dans un état connu, par exemple pour éviter de recréer à la main étudianst et semestres quand on développe.
de données de développement dans un état connu, par exemple pour éviter de
recréer à la main étudiants et semestres quand on développe.
Il suffit de positionner une variable d'environnement indiquant la BD utilisée par les tests:
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
puis de les lancer normalement, par exemple:
(si elle n'existe pas, voir plus loin pour la créer) puis de les lancer
normalement, par exemple:
pytest tests/unit/test_sco_basic.py
@ -132,12 +130,13 @@ base de données (tous les départements, et les utilisateurs) avant de commence
On utilise SQLAlchemy avec Alembic et Flask-Migrate.
flask db migrate -m "ScoDoc 9.0.x: ..." # ajuster le message !
flask db migrate -m "message explicatif....."
flask db upgrade
Ne pas oublier de commiter les migrations (`git add migrations` ...).
Ne pas oublier de d'ajouter le script de migration à git (`git add migrations/...`).
Mémo pour développeurs: séquence re-création d'une base:
**Mémo**: séquence re-création d'une base (vérifiez votre `.env`
ou variables d'environnement pour interroger la bonne base !).
dropdb SCODOC_DEV
tools/create_database.sh SCODOC_DEV # créé base SQL
@ -152,7 +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
positionner à la bonne étape.
# Paquet debian 11
### Profiling
Sur une machine de DEV, lancer
flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data
le fichier `.prof` sera alors écrit dans `/opt/scodoc-data` (on peut aussi utiliser `/tmp`).
Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bien:
pip install snakeviz
puis
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
# Paquet Debian 11
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
important est `postinst`qui se charge de configurer le système (install ou

View File

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

View File

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

View File

@ -33,7 +33,8 @@ token_auth = HTTPTokenAuth()
@basic_auth.verify_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):
return user
@ -51,3 +52,17 @@ def verify_token(token):
@token_auth.error_handler
def token_auth_error(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
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.from flask import jsonify
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from flask import jsonify
from werkzeug.http import HTTP_STATUS_CODES

View File

@ -29,7 +29,7 @@
"""
# PAS ENCORE IMPLEMENTEE, juste un essai
# Pour P. Bouron, il faudrait en priorité l'équivalent de
# Scolarite/Notes/do_moduleimpl_withmodule_list
# Scolarite/Notes/moduleimpl_withmodule_list (alias scodoc7 do_moduleimpl_withmodule_list)
# Scolarite/Notes/evaluation_create
# Scolarite/Notes/evaluation_delete
# Scolarite/Notes/formation_list
@ -48,9 +48,9 @@ from app.api.errors import bad_request
from app import models
@bp.route("/ScoDoc/api/list_depts", methods=["GET"])
@bp.route("list_depts", methods=["GET"])
@token_auth.login_required
def list_depts():
depts = models.Departement.query.filter_by(visible=True).all()
data = {"items": [d.to_dict() for d in depts]}
data = [d.to_dict() for d in depts]
return jsonify(data)

View File

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

View File

@ -10,6 +10,7 @@ import re
from time import time
from typing import Optional
import cracklib # pylint: disable=import-error
from flask import current_app, url_for, g
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
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):
@ -177,8 +195,9 @@ class User(UserMixin, db.Model):
if "roles_string" in data:
self.user_roles = []
for r_d in data["roles_string"].split(","):
role, dept = UserRole.role_dept_from_string(r_d)
self.add_role(role, dept)
if r_d:
role, dept = UserRole.role_dept_from_string(r_d)
self.add_role(role, dept)
def get_token(self, expires_in=3600):
now = datetime.utcnow()
@ -194,6 +213,9 @@ class User(UserMixin, db.Model):
@staticmethod
def check_token(token):
"""Retreive user for given token, chek token's validity
and returns the user object.
"""
user = User.query.filter_by(token=token).first()
if user is None or user.token_expiration < datetime.utcnow():
return None
@ -329,7 +351,7 @@ class Role(db.Model):
"""Roles for ScoDoc"""
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
name = db.Column(db.String(64), unique=True) # TODO: , nullable=False))
default = db.Column(db.Boolean, default=False, index=True)
permissions = db.Column(db.BigInteger) # 64 bits
users = db.relationship("User", secondary="user_role", viewonly=True)
@ -388,7 +410,7 @@ class UserRole(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
role_id = db.Column(db.Integer, db.ForeignKey("role.id"))
dept = db.Column(db.String(64)) # dept acronym
dept = db.Column(db.String(64)) # dept acronym ou NULL
user = db.relationship(
User, backref=db.backref("user_roles", cascade="all, delete-orphan")
)
@ -407,6 +429,9 @@ class UserRole(db.Model):
"""
fields = role_dept.split("_", 1) # maxsplit=1, le dept peut contenir un "_"
if len(fields) != 2:
current_app.logger.warning(
f"role_dept_from_string: Invalid role_dept '{role_dept}'"
)
raise ScoValueError("Invalid role_dept")
role_name, dept = fields
if dept == "":
@ -418,7 +443,7 @@ class UserRole(db.Model):
def get_super_admin():
"""L'utilisateur admin (où le premier, s'il y en a plusieurs).
"""L'utilisateur admin (ou le premier, s'il y en a plusieurs).
Utilisé par les tests unitaires et le script de migration.
"""
admin_role = Role.query.filter_by(name="SuperAdmin").first()

View File

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

View File

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

View File

@ -73,3 +73,17 @@ class BilletAbsence(db.Model):
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
# true si l'absence _pourrait_ etre justifiée
justified = db.Column(db.Boolean(), default=False, server_default="false")
def to_dict(self):
data = {
"id": self.id,
"billet_id": self.id,
"etudid": self.etudid,
"abs_begin": self.abs_begin,
"abs_end": self.abs_begin,
"description": self.description,
"etat": self.etat,
"entry_date": self.entry_date,
"justified": self.justified,
}
return data

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")
def __repr__(self):
return f"<Departement {self.acronym}>"
return f"<{self.__class__.__name__}(id={self.id}, acronym='{self.acronym}')>"
def to_dict(self):
data = {

View File

@ -41,7 +41,10 @@ class Identite(db.Model):
code_nip = db.Column(db.Text())
code_ine = db.Column(db.Text())
# 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)
#
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
class Adresse(db.Model):

View File

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

View File

@ -41,6 +41,10 @@ class FormSemestre(db.Model):
bul_hide_xml = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
)
# Bloque le calcul des moyennes (générale et d'UE)
block_moyennes = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
)
# semestres decales (pour gestion jurys):
gestion_semestrielle = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
@ -66,10 +70,16 @@ class FormSemestre(db.Model):
# code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'
elt_annee_apo = db.Column(db.Text())
# Relations:
etapes = db.relationship(
"NotesFormsemestreEtape", cascade="all,delete", backref="notes_formsemestre"
"NotesFormsemestreEtape", cascade="all,delete", backref="formsemestre"
)
formsemestres = db.relationship(
"NotesModuleImpl", backref="formsemestre", lazy="dynamic"
)
# Ancien id ScoDoc7 pour les migrations de bases anciennes
# ne pas utiliser après migrate_scodoc7_dept_archives
scodoc7_id = db.Column(db.Text(), nullable=True)
def __init__(self, **kwargs):
@ -268,6 +278,7 @@ class NotesModuleImplInscription(db.Model):
"""Inscription à un module (etudiants,moduleimpl)"""
__tablename__ = "notes_moduleimpl_inscription"
__table_args__ = (db.UniqueConstraint("moduleimpl_id", "etudid"),)
id = db.Column(db.Integer, primary_key=True)
moduleimpl_inscription_id = db.synonym("id")

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

View File

@ -52,10 +52,10 @@ from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours # sco_codes_parcours.NEXT -> sem suivant
from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre
from app.scodoc import pe_tagtable
from app.scodoc import pe_tools
from app.scodoc import pe_semestretag
from app.scodoc import pe_settag
from app.pe import pe_tagtable
from app.pe import pe_tools
from app.pe import pe_semestretag
from app.pe import pe_settag
# ----------------------------------------------------------------------------------------
def comp_nom_semestre_dans_parcours(sem):
@ -946,7 +946,7 @@ class JuryPE(object):
return list(taglist)
def get_allTagInSyntheseJury(self):
"""Extrait tous les tags du dictionnaire syntheseJury trié par ordre alphabétique. [] si aucun tag """
"""Extrait tous les tags du dictionnaire syntheseJury trié par ordre alphabétique. [] si aucun tag"""
allTags = set()
for nom in JuryPE.PARCOURS.keys():
allTags = allTags.union(set(self.get_allTagForAggregat(nom)))

View File

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

View File

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

View File

@ -44,7 +44,6 @@ import unicodedata
import app.scodoc.sco_utils as scu
from app import log
import six
PE_DEBUG = 0
@ -145,7 +144,7 @@ def escape_for_latex(s):
}
exp = re.compile(
"|".join(
re.escape(six.text_type(key))
re.escape(key)
for key in sorted(list(conv.keys()), key=lambda item: -len(item))
)
)
@ -167,8 +166,19 @@ def list_directory_filenames(path):
def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip):
"""Read pathname server file and add content to zip under path_in_zip"""
rooted_path_in_zip = os.path.join(ziproot, path_in_zip)
data = open(pathname).read()
zipfile.writestr(rooted_path_in_zip, data)
zipfile.write(filename=pathname, arcname=rooted_path_in_zip)
# data = open(pathname).read()
# zipfile.writestr(rooted_path_in_zip, data)
def add_refs_to_register(register, directory):
"""Ajoute les fichiers trouvés dans directory au registre (dictionaire) sous la forme
filename => pathname
"""
length = len(directory)
for pathname in list_directory_filenames(directory):
filename = pathname[length + 1 :]
register[filename] = pathname
def add_pe_stuff_to_zip(zipfile, ziproot):
@ -179,30 +189,16 @@ def add_pe_stuff_to_zip(zipfile, ziproot):
Also copy logos
"""
register = {}
# first add standard (distrib references)
distrib_dir = os.path.join(REP_DEFAULT_AVIS, "distrib")
distrib_pathnames = list_directory_filenames(
distrib_dir
) # eg /opt/scodoc/tools/doc_poursuites_etudes/distrib/modeles/toto.tex
l = len(distrib_dir)
distrib_filenames = {x[l + 1 :] for x in distrib_pathnames} # eg modeles/toto.tex
add_refs_to_register(register=register, directory=distrib_dir)
# then add local references (some oh them may overwrite distrib refs)
local_dir = os.path.join(REP_LOCAL_AVIS, "local")
local_pathnames = list_directory_filenames(local_dir)
l = len(local_dir)
local_filenames = {x[l + 1 :] for x in local_pathnames}
for filename in distrib_filenames | local_filenames:
if filename in local_filenames:
add_local_file_to_zip(
zipfile, ziproot, os.path.join(local_dir, filename), "avis/" + filename
)
else:
add_local_file_to_zip(
zipfile,
ziproot,
os.path.join(distrib_dir, filename),
"avis/" + filename,
)
add_refs_to_register(register=register, directory=local_dir)
# at this point register contains all refs (filename, pathname) to be saved
for filename, pathname in register.items():
add_local_file_to_zip(zipfile, ziproot, pathname, "avis/" + filename)
# Logos: (add to logos/ directory in zip)
logos_names = ["logo_header.jpg", "logo_footer.jpg"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -58,7 +58,7 @@ import traceback
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
from flask import g, url_for
from flask import g, url_for, request
import app.scodoc.sco_utils as scu
from app import log
@ -164,7 +164,7 @@ def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
return sco_pdf.makeParas(text, style, suppress_empty=suppress_empty_pars)
def get_formsemestre_bulletins_pdf(formsemestre_id, REQUEST, version="selectedevals"):
def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
"document pdf et filename"
from app.scodoc import sco_bulletins
@ -184,7 +184,6 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, REQUEST, version="selectedev
etudid,
format="pdfpart",
version=version,
REQUEST=REQUEST,
)
fragments += frag
filigrannes[i] = filigranne
@ -192,8 +191,8 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, REQUEST, version="selectedev
i = i + 1
#
infos = {"DeptName": sco_preferences.get_preference("DeptName", formsemestre_id)}
if REQUEST:
server_name = REQUEST.BASE0
if request:
server_name = request.url_root
else:
server_name = ""
try:
@ -220,7 +219,7 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, REQUEST, version="selectedev
return pdfdoc, filename
def get_etud_bulletins_pdf(etudid, REQUEST, version="selectedevals"):
def get_etud_bulletins_pdf(etudid, version="selectedevals"):
"Bulletins pdf de tous les semestres de l'étudiant, et filename"
from app.scodoc import sco_bulletins
@ -235,15 +234,14 @@ def get_etud_bulletins_pdf(etudid, REQUEST, version="selectedevals"):
etudid,
format="pdfpart",
version=version,
REQUEST=REQUEST,
)
fragments += frag
filigrannes[i] = filigranne
bookmarks[i] = sem["session_id"] # eg RT-DUT-FI-S1-2015
i = i + 1
infos = {"DeptName": sco_preferences.get_preference("DeptName")}
if REQUEST:
server_name = REQUEST.BASE0
if request:
server_name = request.url_root
else:
server_name = ""
try:

View File

@ -56,7 +56,7 @@ et sur page "réglages bulletin" (avec formsemestre_id)
# import os
# def form_change_bul_sig(side, formsemestre_id=None, REQUEST=None):
# def form_change_bul_sig(side, formsemestre_id=None):
# """Change pdf signature"""
# filename = _get_sig_existing_filename(
# side, formsemestre_id=formsemestre_id
@ -69,7 +69,7 @@ et sur page "réglages bulletin" (avec formsemestre_id)
# raise ValueError("invalid value for 'side' parameter")
# signatureloc = get_bul_sig_img()
# H = [
# self.sco_header(REQUEST, page_title="Changement de signature"),
# self.sco_header(page_title="Changement de signature"),
# """<h2>Changement de la signature bulletin de %(sidetxt)s</h2>
# """
# % (sidetxt,),

View File

@ -69,16 +69,13 @@ def make_xml_formsemestre_bulletinetud(
doc=None, # XML document
force_publishing=False,
xml_nodate=False,
REQUEST=None,
xml_with_decisions=False, # inclue les decisions même si non publiées
version="long",
):
) -> str:
"bulletin au format XML"
from app.scodoc import sco_bulletins
log("xml_bulletin( formsemestre_id=%s, etudid=%s )" % (formsemestre_id, etudid))
if REQUEST:
REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE)
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if (not sem["bul_hide_xml"]) or force_publishing:
@ -388,7 +385,7 @@ def make_xml_formsemestre_bulletinetud(
"decisions_ue"
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
for ue_id in decision["decisions_ue"].keys():
ue = sco_edit_ue.do_ue_list({"ue_id": ue_id})[0]
ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
doc.append(
Element(
"decision_ue",

View File

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

View File

@ -28,7 +28,6 @@
"""Semestres: Codes gestion parcours (constantes)
"""
import collections
from six.moves import range
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)

View File

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

View File

@ -0,0 +1,144 @@
# -*- 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
#
##############################################################################
from flask import flash, redirect, url_for, render_template
from werkzeug.utils import secure_filename
import app.scodoc.sco_utils as scu
import app
from app import Departement, ScoValueError
from app.models import ScoDocSiteConfig
from app.scodoc import sco_logos
import flask
from flask import jsonify, url_for, flash, redirect, render_template, make_response
from wtforms import SelectField, FileField, FormField, HiddenField
from flask import current_app, g, request
from flask_login import current_user
from flask_wtf import FlaskForm
from wtforms import SubmitField
from flask_wtf.file import FileField, FileAllowed
# ---- CONFIGURATION
from app.scodoc.sco_logos import list_logos
class SimpleForm(FlaskForm):
key = HiddenField("form_id")
logo = FileField(
label="Modifier l'image:",
description="logo",
validators=[
FileAllowed(
scu.LOGOS_IMAGES_ALLOWED_TYPES,
f"n'accepte que les fichiers image <tt>{','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}</tt>",
)
],
)
submit = SubmitField("submit")
# def __init__(self, **kwargs):
# super().__init__(**kwargs)
class LogoForm(FlaskForm):
header = FormField(SimpleForm, label="header")
footer = FormField(SimpleForm, label="footer")
logo = FileField(
label="Modifier l'image:",
description="logo",
validators=[
FileAllowed(
scu.LOGOS_IMAGES_ALLOWED_TYPES,
f"n'accepte que les fichiers image <tt>{','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}</tt>",
)
],
)
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()
],
)
# logo_header = FileField(
# label="Modifier l'image:",
# description="logo placé en haut des documents PDF",
# validators=[
# FileAllowed(
# scu.LOGOS_IMAGES_ALLOWED_TYPES,
# f"n'accepte que les fichiers image <tt>{','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}</tt>",
# )
# ],
# )
# logo_footer = FileField(
# label="Modifier l'image:",
# description="logo placé en pied des documents PDF",
# validators=[
# FileAllowed(
# scu.LOGOS_IMAGES_ALLOWED_TYPES,
# f"n'accepte que les fichiers image <tt>{','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}</tt>",
# )
# ],
# )
submit = SubmitField("Enregistrer")
def configuration():
"""Panneau de configuration général"""
inventory = list_logos()
form = LogoForm()
# form = ScoDocConfigurationForm(
# bonus_sport_func_name=ScoDocSiteConfig.get_bonus_sport_func_name(),
# )
if form.validate_on_submit():
# ScoDocSiteConfig.set_bonus_sport_func(form.bonus_sport_func_name.data)
# if form.logo_header.data:
# sco_logos.store_image(
# form.logo_header.data, os.path.join(scu.SCODOC_LOGOS_DIR, "logo_header")
# )
# if form.logo_footer.data:
# sco_logos.store_image(
# form.logo_footer.data, os.path.join(scu.SCODOC_LOGOS_DIR, "logo_footer")
# )
# app.clear_scodoc_cache()
# flash(f"Configuration enregistrée")
return redirect(url_for("scodoc.index"))
return render_template(
"configuration.html",
title="Configuration ScoDoc",
form=form,
scodoc_dept=None,
inventory=inventory,
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,9 +29,10 @@
"""
import flask
from flask import g, url_for
from flask import g, url_for, request
from flask_login import current_user
from app.models.formations import NotesUE
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app import log
@ -74,7 +75,7 @@ _ueEditor = ndb.EditableTable(
sortkey="numero",
input_formators={
"type": ndb.int_null_is_zero,
"is_external": bool,
"is_external": ndb.bool_or_str,
},
output_formators={
"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"
cnx = ndb.GetDBConnexion()
return _ueEditor.list(cnx, *args, **kw)
@ -96,9 +97,7 @@ def do_ue_create(args):
cnx = ndb.GetDBConnexion()
# check duplicates
ues = do_ue_list(
{"formation_id": args["formation_id"], "acronyme": args["acronyme"]}
)
ues = ue_list({"formation_id": args["formation_id"], "acronyme": args["acronyme"]})
if ues:
raise ScoValueError('Acronyme d\'UE "%s" déjà utilisé !' % args["acronyme"])
# create
@ -123,7 +122,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
cnx = ndb.GetDBConnexion()
log("do_ue_delete: ue_id=%s, delete_validations=%s" % (ue_id, delete_validations))
# check
ue = do_ue_list({"ue_id": ue_id})
ue = ue_list({"ue_id": ue_id})
if not ue:
raise ScoValueError("UE inexistante !")
ue = ue[0]
@ -140,7 +139,11 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
% (len(validations), ue["acronyme"], ue["titre"]),
dest_url="",
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},
)
if delete_validations:
@ -151,7 +154,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
)
# 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:
sco_edit_matiere.do_matiere_delete(mat["matiere_id"])
# delete uecoef and events
@ -176,7 +179,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
if not force:
return flask.redirect(
url_for(
"notes.ue_list",
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=ue["formation_id"],
)
@ -185,18 +188,18 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
return None
def ue_create(formation_id=None, REQUEST=None):
def ue_create(formation_id=None):
"""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"""
from app.scodoc import sco_formations
create = int(create)
if not create:
U = do_ue_list(args={"ue_id": ue_id})
U = ue_list(args={"ue_id": ue_id})
if not U:
raise ScoValueError("UE inexistante !")
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",
},
),
(
"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:
# 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(
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:
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])
return flask.redirect(
url_for(
"notes.ue_list", scodoc_dept=g.scodoc_dept, formation_id=formation_id
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id
)
)
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.
Les UE sans modules se voient attribuer le numero UE_SEM_DEFAULT (1000000),
qui les place à la fin de la liste.
"""
for ue in ue_list:
Modlist = sco_edit_module.do_module_list(args={"ue_id": ue["ue_id"]})
for ue in ues:
Modlist = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
if Modlist:
ue["semestre_id"] = Modlist[0]["semestre_id"]
else:
@ -388,42 +403,46 @@ def next_ue_numero(formation_id, semestre_id=None):
"""Numero d'une nouvelle UE dans cette formation.
Si le semestre est specifie, cherche les UE ayant des modules de ce semestre
"""
ue_list = do_ue_list(args={"formation_id": formation_id})
if not ue_list:
ues = ue_list(args={"formation_id": formation_id})
if not ues:
return 0
if semestre_id is None:
return ue_list[-1]["numero"] + 1000
return ues[-1]["numero"] + 1000
else:
# Avec semestre: (prend le semestre du 1er module de l'UE)
_add_ue_semestre_id(ue_list)
ue_list_semestre = [ue for ue in ue_list if ue["semestre_id"] == semestre_id]
_add_ue_semestre_id(ues)
ue_list_semestre = [ue for ue in ues if ue["semestre_id"] == semestre_id]
if ue_list_semestre:
return ue_list_semestre[-1]["numero"] + 10
else:
return ue_list[-1]["numero"] + 1000
return ues[-1]["numero"] + 1000
def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
"""Delete an UE"""
ue = do_ue_list(args={"ue_id": ue_id})
if not ue:
ues = ue_list(args={"ue_id": ue_id})
if not ues:
raise ScoValueError("UE inexistante !")
ue = ue[0]
ue = ues[0]
if not dialog_confirmed:
return scu.confirm_dialog(
"<h2>Suppression de l'UE %(titre)s (%(acronyme)s))</h2>" % ue,
dest_url="",
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)
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
editer (si non verrouillée).
éditer (si non verrouillée).
"""
from app.scodoc import sco_formations
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"])
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:
_add_ue_semestre_id(ue_list)
ue_list.sort(key=lambda u: (u["semestre_id"], u["numero"]))
has_duplicate_ue_codes = len(set([ue["ue_code"] for ue in ue_list])) != len(ue_list)
_add_ue_semestre_id(ues)
_add_ue_semestre_id(ues_externes)
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)
# editable = (not locked) and perm_change
has_perm_change = current_user.has_permission(Permission.ScoChangeFormation)
# editable = (not locked) and has_perm_change
# On autorise maintanant la modification des formations qui ont des semestres verrouillés,
# sauf si cela affect les notes passées (verrouillées):
# - 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
editable = perm_change
editable = has_perm_change
tag_editable = (
current_user.has_permission(Permission.ScoEditFormationTags) or perm_change
current_user.has_permission(Permission.ScoEditFormationTags) or has_perm_change
)
if locked:
lockicon = scu.icontag("lock32_img", title="verrouillé")
else:
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_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(
'<form><input type="checkbox" class="sco_tag_checkbox">montrer les tags</input></form>'
)
cur_ue_semestre_id = None
iue = 0
for UE in ue_list:
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>"
H.append(
_ue_table_ues(
parcours,
ues,
editable,
tag_editable,
has_perm_change,
arrow_up,
arrow_down,
arrow_none,
delete_icon,
delete_disabled_icon,
)
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:
H.append(
'<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
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>")
if editable:
H.append(
@ -792,7 +642,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
</p>"""
% F
)
if perm_change:
if has_perm_change:
H.append(
"""
<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)
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):
"""HTML list of UE sharing this code
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
ue_code = str(ue_code)
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:
ue_code = ue["ue_code"]
F = sco_formations.formation_list(args={"formation_id": ue["formation_id"]})[0]
formation_code = F["formation_code"]
ue_list_all = do_ue_list(args={"ue_code": ue_code})
if ue_id:
# retire les UE d'autres formations:
# log('checking ucode %s formation %s' % (ue_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)
# UE du même code, code formation et departement:
q_ues = (
NotesUE.query.filter_by(ue_code=ue_code)
.join(NotesUE.formation, aliased=True)
.filter_by(dept_id=g.scodoc_dept_id, formation_code=formation_code)
)
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
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:
return """<span class="ue_share">Seule UE avec code %s</span>""" % ue_code
else:
@ -875,18 +1014,13 @@ def ue_sharing_code(ue_code=None, ue_id=None, hide_ue_id=None):
else:
H.append('<span class="ue_share">UE avec le code %s:</span>' % ue_code)
H.append("<ul>")
for ue in ue_list:
F = sco_formations.formation_list(args={"formation_id": ue["formation_id"]})[0]
for ue in ues:
H.append(
'<li>%s (%s) dans <a class="stdlink" href="ue_list?formation_id=%s">%s (%s)</a>, version %s</li>'
% (
ue["acronyme"],
ue["titre"],
F["formation_id"],
F["acronyme"],
F["titre"],
F["version"],
)
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.formation.acronyme} ({ue.formation.titre})</a>, version {ue.formation.version}
</li>
"""
)
H.append("</ul>")
return "\n".join(H)
@ -896,13 +1030,13 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
"edit an UE"
# check
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"]):
raise ScoLockedFormError()
# check: acronyme unique dans cette formation
if "acronyme" in args:
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:
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")
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:
return "ue invalide"
@ -956,7 +1090,7 @@ def ue_is_locked(ue_id):
# ---- Table recap formation
def formation_table_recap(formation_id, format="html", REQUEST=None):
def formation_table_recap(formation_id, format="html"):
"""Table recapitulant formation."""
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")
F = F[0]
T = []
ue_list = do_ue_list(args={"formation_id": formation_id})
for UE in ue_list:
Matlist = sco_edit_matiere.do_matiere_list(args={"ue_id": UE["ue_id"]})
ues = ue_list(args={"formation_id": formation_id})
for ue in ues:
Matlist = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]})
for Mat in Matlist:
Modlist = sco_edit_module.do_module_list(
Modlist = sco_edit_module.module_list(
args={"matiere_id": Mat["matiere_id"]}
)
for Mod in Modlist:
@ -979,7 +1113,7 @@ def formation_table_recap(formation_id, format="html", REQUEST=None):
#
T.append(
{
"UE_acro": UE["acronyme"],
"UE_acro": ue["acronyme"],
"Mat_tit": Mat["titre"],
"Mod_tit": Mod["abbrev"] or Mod["titre"],
"Mod_code": Mod["code"],
@ -1033,13 +1167,13 @@ def formation_table_recap(formation_id, format="html", REQUEST=None):
caption=title,
html_caption=title,
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,
html_title="<h2>" + title + "</h2>",
pdf_title=title,
preferences=sco_preferences.SemPreferences(),
)
return tab.make_page(format=format, REQUEST=REQUEST)
return tab.make_page(format=format)
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,
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])))

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

View File

@ -32,11 +32,11 @@
Voir sco_apogee_csv.py pour la structure du fichier Apogée.
Stockage: utilise sco_archive.py
=> /opt/scodoc/var/scodoc/archives/apo_csv/RT/2016-1/2016-07-03-16-12-19/V3ASR.csv
=> /opt/scodoc/var/scodoc/archives/apo_csv/<dept_id>/2016-1/2016-07-03-16-12-19/V3ASR.csv
pour une maquette de l'année scolaire 2016, semestre 1, etape V3ASR
ou bien (à partir de ScoDoc 1678) :
/opt/scodoc/var/scodoc/archives/apo_csv/RT/2016-1/2016-07-03-16-12-19/V3ASR!111.csv
/opt/scodoc/var/scodoc/archives/apo_csv/<dept_id>/2016-1/2016-07-03-16-12-19/V3ASR!111.csv
pour une maquette de l'étape V3ASR version VDI 111.
La version VDI sera ignorée sauf si elle est indiquée dans l'étape du semestre.

View File

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

View File

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

View File

@ -31,7 +31,7 @@ import datetime
import operator
import pprint
import time
import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error
import urllib
import flask
from flask import url_for
@ -179,7 +179,7 @@ def do_evaluation_list(args, sortkey=None):
def do_evaluation_list_in_formsemestre(formsemestre_id):
"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 = []
for mod in mods:
evals += do_evaluation_list(args={"moduleimpl_id": mod["moduleimpl_id"]})
@ -213,7 +213,7 @@ def _check_evaluation_args(args):
jour = args.get("jour", None)
args["jour"] = 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"])
d, m, y = [int(x) for x in sem["date_debut"].split("/")]
date_debut = datetime.date(y, m, d)
@ -250,7 +250,6 @@ def do_evaluation_create(
publish_incomplete=None,
evaluation_type=None,
numero=None,
REQUEST=None,
**kw, # ceci pour absorber les arguments excedentaires de tf #sco8
):
"""Create an evaluation"""
@ -274,13 +273,13 @@ def do_evaluation_create(
if args["jour"]:
next_eval = None
t = (
ndb.DateDMYtoISO(args["jour"]),
ndb.TimetoISO8601(args["heure_debut"]),
ndb.DateDMYtoISO(args["jour"], null_is_empty=True),
ndb.TimetoISO8601(args["heure_debut"], null_is_empty=True),
)
for e in ModEvals:
if (
ndb.DateDMYtoISO(e["jour"]),
ndb.TimetoISO8601(e["heure_debut"]),
ndb.DateDMYtoISO(e["jour"], null_is_empty=True),
ndb.TimetoISO8601(e["heure_debut"], null_is_empty=True),
) > t:
next_eval = e
break
@ -302,8 +301,8 @@ def do_evaluation_create(
r = _evaluationEditor.create(cnx, args)
# news
M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
mod = sco_edit_module.do_module_list(args={"module_id": M["module_id"]})[0]
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
mod["moduleimpl_id"] = M["moduleimpl_id"]
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
sco_news.add(
@ -333,7 +332,7 @@ def do_evaluation_edit(args):
cnx = ndb.GetDBConnexion()
_evaluationEditor.edit(cnx, args)
# 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"])
@ -358,10 +357,10 @@ def do_evaluation_delete(evaluation_id):
_evaluationEditor.delete(cnx, evaluation_id)
# 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"])
# 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["url"] = (
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):
"""donne infos sur l'etat du evaluation
{ 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
# ---- Liste des groupes complets et incomplets
E = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
Mod = sco_edit_module.do_module_list(args={"module_id": M["module_id"]})[0]
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_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
formsemestre_id = M["formsemestre_id"]
# 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
def formsemestre_evaluations_cal(formsemestre_id, REQUEST=None):
def formsemestre_evaluations_cal(formsemestre_id):
"""Page avec calendrier de toutes les evaluations de ce semestre"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
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"]:
continue
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"]
)[0]
txt = mod["module"]["code"] or mod["module"]["abbrev"] or "eval"
@ -780,7 +776,6 @@ def formsemestre_evaluations_cal(formsemestre_id, REQUEST=None):
H = [
html_sco_header.html_sem_header(
REQUEST,
"Evaluations du semestre",
sem,
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
# au module (pour gerer les modules optionnels correctement)
# 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"]
# insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( formsemestre_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())
def formsemestre_evaluations_delai_correction(
formsemestre_id, format="html", REQUEST=None
):
def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
"""Experimental: un tableau indiquant pour chaque évaluation
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()
T = []
for e in evals:
M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=e["moduleimpl_id"])[0]
Mod = sco_edit_module.do_module_list(args={"module_id": M["module_id"]})[0]
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=e["moduleimpl_id"])[0]
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
if (e["evaluation_type"] != scu.EVALUATION_NORMALE) or (
Mod["module_type"] == scu.MODULE_MALUS
):
@ -915,13 +908,13 @@ def formsemestre_evaluations_delai_correction(
html_title="<h2>Correction des évaluations du semestre</h2>",
caption="Correction des évaluations du semestre",
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
+ scu.timedate_human_repr()
+ "",
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):
@ -1039,8 +1032,8 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
E = do_evaluation_list({"evaluation_id": evaluation_id})[0]
moduleimpl_id = E["moduleimpl_id"]
M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
Mod = sco_edit_module.do_module_list(args={"module_id": M["module_id"]})[0]
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
formsemestre_id = M["formsemestre_id"]
u = sco_users.user_info(M["responsable_id"])
resp = u["prenomnom"]
@ -1069,7 +1062,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
]
if Mod["module_type"] == scu.MODULE_MALUS:
# 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)
# store min/max values used by JS client-side checks:
H.append(
@ -1089,7 +1082,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
% (
scu.ScoURL(),
group_id,
six.moves.urllib.parse.quote(E["jour"], safe=""),
urllib.parse.quote(E["jour"], safe=""),
)
)
H.append(
@ -1110,7 +1103,6 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
def evaluation_create_form(
moduleimpl_id=None,
evaluation_id=None,
REQUEST=None,
edit=False,
readonly=False,
page_title="Evaluation",
@ -1120,7 +1112,7 @@ def evaluation_create_form(
the_eval = do_evaluation_list({"evaluation_id": evaluation_id})[0]
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
formsemestre_id = M["formsemestre_id"]
min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible
@ -1179,7 +1171,7 @@ def evaluation_create_form(
else:
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">
Le coefficient d'une évaluation n'est utilisé que pour pondérer les évaluations au sein d'un module.
@ -1232,11 +1224,9 @@ def evaluation_create_form(
initvalues["visibulletinlist"] = ["X"]
else:
initvalues["visibulletinlist"] = []
if (
REQUEST.form.get("tf_submitted", False)
and "visibulletinlist" not in REQUEST.form
):
REQUEST.form["visibulletinlist"] = []
vals = scu.get_request_args()
if vals.get("tf_submitted", False) and "visibulletinlist" not in vals:
vals["visibulletinlist"] = []
#
form = [
("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
@ -1346,8 +1336,8 @@ def evaluation_create_form(
),
]
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
vals,
form,
cancelbutton="Annuler",
submitlabel=submitlabel,
@ -1369,7 +1359,7 @@ def evaluation_create_form(
tf[2]["visibulletin"] = False
if not edit:
# 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)
else:
do_evaluation_edit(tf[2])

View File

@ -35,11 +35,11 @@ from enum import Enum
from tempfile import NamedTemporaryFile
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.cell import WriteOnlyCell
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
from app.scodoc import notesdb
@ -59,31 +59,33 @@ class COLORS(Enum):
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:
# 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):
"""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
"""
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:
@ -98,13 +100,16 @@ class ScoExcelBook:
def __init__(self):
self.sheets = [] # list of sheets
self.wb = Workbook(write_only=True)
def create_sheet(self, sheet_name="feuille", default_style=None):
"""Crée une nouvelle feuille dans ce classeur
sheet_name -- le nom de la feuille
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)
return sheet
@ -112,12 +117,12 @@ class ScoExcelBook:
"""génération d'un stream binaire représentant la totalité du classeur.
retourne le flux
"""
wb = Workbook(write_only=True)
for sheet in self.sheets:
sheet.generate(self)
# construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream)
sheet.prepare()
# construction d'un flux
# (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream)
with NamedTemporaryFile() as tmp:
wb.save(tmp.name)
self.wb.save(tmp.name)
tmp.seek(0)
return tmp.read()
@ -125,6 +130,7 @@ class ScoExcelBook:
def excel_make_style(
bold=False,
italic=False,
outline=False,
color: COLORS = COLORS.BLACK,
bgcolor: COLORS = None,
halign=None,
@ -145,7 +151,14 @@ def excel_make_style(
size -- taille de police
"""
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
if bgcolor:
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):
"""Création de la feuille.
sheet_name -- le nom de la feuille
default_style -- le style par défaut des cellules
wb -- le WorkBook dans laquelle se trouve la feuille. Si wb est None (cas d'un classeur mono-feuille),
un workbook est crée et associé à cette feuille.
"""Création de la feuille. sheet_name
-- le nom de la feuille default_style
-- le style par défaut des cellules ws
-- None si la feuille est autonome (dans ce cas ell crée son propre wb), sinon c'est la worksheet
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.
# si la taille du nom de feuille est > 31 on tronque (on pourrait remplacer par 'feuille' ?)
self.sheet_name = 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 }
self.sheet_name = adjust_sheetname(sheet_name)
if default_style is None:
default_style = excel_make_style()
self.default_style = default_style
self.wb = wb or Workbook(write_only=True) # Création de workbook si nécessaire
self.ws = self.wb.create_sheet(title=self.sheet_name)
if wb is None:
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.row_dimensions = {}
def set_column_dimension_width(self, cle, value):
"""Détermine la largeur d'une colonne.
cle -- identifie la colonne ("A"n "B", ...)
value -- la dimension (unité : 7 pixels comme affiché dans Excel)
def excel_make_composite_style(
self,
alignment=None,
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):
"""Masque ou affiche une colonne.
cle -- identifie la colonne ("A"n "B", ...)
def set_row_dimension_height(self, cle=None, value=21):
"""Détermine la hauteur d'une ligne. cle -- identifie la ligne (1, 2, ...) si None,
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)
"""
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):
"""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é
"""
cell = WriteOnlyCell(self.ws, value or "")
if not (isinstance(value, int) or isinstance(value, float)):
cell.data_type = "s"
# if style is not None and "fill" in style:
# toto()
# adapatation des valeurs si nécessaire
if value is None:
value = ""
elif value is True:
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:
style = self.default_style
if "font" in style:
cell.font = style["font"]
if "alignment" in style:
cell.alignment = style["alignment"]
if "border" in style:
cell.border = style["border"]
if "fill" in style:
cell.fill = style["fill"]
if "number_format" in style:
cell.number_format = style["number_format"]
if "fill" in style:
@ -245,6 +325,16 @@ class ScoExcelSheet:
lines = comment.splitlines()
cell.comment.width = 7 * max([len(line) for line in 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
def make_row(self, values: list, style=None, comments=None):
@ -272,73 +362,31 @@ class ScoExcelSheet:
"""ajoute une ligne déjà construite à la feuille."""
self.rows.append(row)
# def set_style(self, style=None, li=None, co=None):
# 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):
def prepare(self):
"""génére un flux décrivant la feuille.
Ce flux pourra ensuite être repris dans send_excel_file (classeur mono feille)
ou pour la génération d'un classeur multi-feuilles
"""
for col in self.column_dimensions.keys():
self.ws.column_dimensions[col] = self.column_dimensions[col]
for row in self.column_dimensions.keys():
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:
self.ws.append(row)
def generate_standalone(self):
def generate(self):
"""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)
self.prepare()
with NamedTemporaryFile() as tmp:
self.wb.save(tmp.name)
tmp.seek(0)
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(
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
cells.append(ws.make_cell(it, cell_style))
ws.append_row(cells)
return ws.generate_standalone()
return ws.generate()
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),
]
)
return ws.generate_standalone()
return ws.generate()
def excel_bytes_to_list(bytes_content):
filelike = io.BytesIO(bytes_content)
return _excel_to_list(filelike)
try:
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):
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
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)
except:
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(
"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)
ws.append_row([None, cell_2])
return ws.generate_standalone()
return ws.generate()

View File

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

View File

@ -27,7 +27,7 @@
"""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.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}
def scodoc_table_results(
start_date="", end_date="", types_parcours=[], format="html", REQUEST=None
):
def scodoc_table_results(start_date="", end_date="", types_parcours=[], format="html"):
"""Page affichant la table des résultats
Les dates sont en dd/mm/yyyy (datepicker javascript)
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
)
tab.base_url = "%s?start_date=%s&end_date=%s&types_parcours=%s" % (
REQUEST.URL0,
request.base_url,
start_date,
end_date,
"&types_parcours=".join([str(x) for x in types_parcours]),
)
if format != "html":
return tab.make_page(
format=format, with_html_headers=False, REQUEST=REQUEST
)
return tab.make_page(format=format, with_html_headers=False)
tab_html = tab.html()
nb_rows = tab.get_nb_rows()
else:

View File

@ -136,11 +136,11 @@ def search_etud_in_dept(expnom=""):
vals = {}
url_args = {"scodoc_dept": g.scodoc_dept}
if "dest_url" in request.form:
endpoint = request.form["dest_url"]
if "dest_url" in vals:
endpoint = vals["dest_url"]
else:
endpoint = "scolar.ficheEtud"
if "parameters_keys" in request.form:
if "parameters_keys" in vals:
for key in vals["parameters_keys"].split(","):
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
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)
return tab.make_page(
format=format, with_html_headers=False, REQUEST=REQUEST, publish=True
)
return tab.make_page(format=format, with_html_headers=False, publish=True)

View File

@ -31,7 +31,8 @@ from operator import itemgetter
import xml.dom.minidom
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
@ -93,13 +94,20 @@ def formation_has_locked_sems(formation_id):
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
in desired format
"""
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
for ue in ues:
ue_id = ue["ue_id"]
@ -108,14 +116,14 @@ def formation_export(
del ue["formation_id"]
if ue["ects"] is None:
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
for mat in mats:
matiere_id = mat["matiere_id"]
if not export_ids:
del mat["matiere_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
for mod in mods:
if export_tags:
@ -132,7 +140,7 @@ def formation_export(
del mod["ects"]
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)
assert D[0] == "formation"
F = D[1]
F_quoted = F.copy()
log("F=%s" % F)
ndb.quote_dict(F_quoted)
log("F_quoted=%s" % F_quoted)
# F_quoted = F.copy()
# ndb.quote_dict(F_quoted)
F["dept_id"] = g.scodoc_dept_id
# find new version number
cnx = ndb.GetDBConnexion()
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(
"select max(version) from notes_formations where acronyme=%(acronyme)s and titre=%(titre)s",
F_quoted,
"""SELECT max(version)
FROM notes_formations
WHERE acronyme=%(acronyme)s and titre=%(titre)s and dept_id=%(dept_id)s
""",
F,
)
res = cursor.fetchall()
try:
@ -196,7 +202,7 @@ def formation_import_xml(doc: str, import_tags=True):
assert ue_info[0] == "ue"
ue_info[1]["formation_id"] = formation_id
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"]
else:
xml_ue_id = None
@ -212,7 +218,7 @@ def formation_import_xml(doc: str, import_tags=True):
for mod_info in mat_info[2]:
assert mod_info[0] == "module"
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"]
else:
xml_module_id = None
@ -230,7 +236,7 @@ def formation_import_xml(doc: str, import_tags=True):
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
and listing associated semestres
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"
)
editable = REQUEST.AUTHENTICATED_USER.has_permission(Permission.ScoChangeFormation)
editable = current_user.has_permission(Permission.ScoChangeFormation)
# Traduit/ajoute des champs à afficher:
for f in formations:
@ -257,7 +263,11 @@ def formation_list_table(formation_id=None, args={}, REQUEST=None):
).NAME
except:
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_id"] = "titre-%s" % f["acronyme"].lower().replace(" ", "-")
# 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_with_td_classes=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,
pdf_title=title,
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"
xml = formation_export(formation_id, export_ids=True, format="xml")
new_id, modules_old2new, ues_old2new = formation_import_xml(xml)
resp = formation_export(formation_id, export_ids=True, format="xml")
xml_data = resp.get_data(as_text=True)
new_id, modules_old2new, ues_old2new = formation_import_xml(xml_data)
# news
F = formation_list(args={"formation_id": new_id})[0]
sco_news.add(
@ -368,7 +379,7 @@ def formation_create_new_version(formation_id, redirect=True, REQUEST=None):
if redirect:
return flask.redirect(
url_for(
"notes.ue_list",
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=new_id,
msg="Nouvelle version !",

View File

@ -31,7 +31,7 @@ from app.scodoc.sco_exceptions import ScoValueError
import time
from operator import itemgetter
from flask import g
from flask import g, request
import app
from app.models import Departement
@ -61,6 +61,7 @@ _formsemestreEditor = ndb.EditableTable(
"gestion_semestrielle",
"etat",
"bul_hide_xml",
"block_moyennes",
"bul_bgcolor",
"modalite",
"resp_can_edit",
@ -68,7 +69,6 @@ _formsemestreEditor = ndb.EditableTable(
"ens_can_edit_eval",
"elt_sem_apo",
"elt_annee_apo",
"scodoc7_id",
),
filter_dept=True,
sortkey="date_debut",
@ -82,6 +82,7 @@ _formsemestreEditor = ndb.EditableTable(
"etat": bool,
"gestion_compensation": bool,
"bul_hide_xml": bool,
"block_moyennes": bool,
"gestion_semestrielle": bool,
"gestion_compensation": 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"
if formsemestre_id in g.stored_get_formsemestre:
return g.stored_get_formsemestre[formsemestre_id]
if not isinstance(formsemestre_id, int):
raise ScoValueError(
"""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"""
)
try:
sem = do_formsemestre_list(args={"formsemestre_id": formsemestre_id})[0]
return sem
except:
raise ValueError("formsemestre_id must be an integer !")
sems = do_formsemestre_list(args={"formsemestre_id": formsemestre_id})
if not sems:
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):
@ -242,7 +246,7 @@ def do_formsemestre_create(args, silent=False):
default=True,
redirect=0,
)
_group_id = sco_groups.createGroup(partition_id, default=True)
_group_id = sco_groups.create_group(partition_id, default=True)
# news
if "titre" not in args:
@ -565,7 +569,7 @@ def list_formsemestre_by_etape(etape_apo=False, annee_scolaire=False):
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"""
if etape_apo:
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>
</form>""",
)
tab.base_url = "%s?etape_apo=%s" % (REQUEST.URL0, etape_apo or "")
return tab.make_page(format=format, REQUEST=REQUEST)
tab.base_url = "%s?etape_apo=%s" % (request.base_url, etape_apo or "")
return tab.make_page(format=format)
def sem_has_etape(sem, code_etape):

View File

@ -28,7 +28,7 @@
"""Menu "custom" (défini par l'utilisateur) dans les semestres
"""
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.notesdb as ndb
@ -77,16 +77,14 @@ def formsemestre_custommenu_html(formsemestre_id):
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"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
dest_url = (
scu.NotesURL() + "/formsemestre_status?formsemestre_id=%s" % formsemestre_id
)
H = [
html_sco_header.html_sem_header(
REQUEST, "Modification du menu du semestre ", sem
),
html_sco_header.html_sem_header("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">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["url_" + str(item["custommenu_id"])] = item["url"]
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
descr,
initvalues=initvalues,
cancelbutton="Annuler",

View File

@ -28,7 +28,7 @@
"""Form choix modules / responsables et creation formsemestre
"""
import flask
from flask import url_for, g
from flask import url_for, g, request
from flask_login import current_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_formations
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups_copy
from app.scodoc import sco_modalites
from app.scodoc import sco_moduleimpl
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_preferences
from app.scodoc import sco_users
import six
def _default_sem_title(F):
@ -66,7 +66,7 @@ def _default_sem_title(F):
return F["titre"]
def formsemestre_createwithmodules(REQUEST=None):
def formsemestre_createwithmodules():
"""Page création d'un semestre"""
H = [
html_sco_header.sco_header(
@ -77,7 +77,7 @@ def formsemestre_createwithmodules(REQUEST=None):
),
"""<h2>Mise en place d'un semestre de formation</h2>""",
]
r = do_formsemestre_createwithmodules(REQUEST=REQUEST)
r = do_formsemestre_createwithmodules()
if isinstance(r, str):
H.append(r)
else:
@ -85,13 +85,12 @@ def formsemestre_createwithmodules(REQUEST=None):
return "\n".join(H) + html_sco_header.sco_footer()
def formsemestre_editwithmodules(REQUEST, formsemestre_id):
def formsemestre_editwithmodules(formsemestre_id):
"""Page modification semestre"""
# portage from dtml
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [
html_sco_header.html_sem_header(
REQUEST,
"Modification du semestre",
sem,
javascripts=["libjs/AutoSuggest.js"],
@ -105,12 +104,13 @@ def formsemestre_editwithmodules(REQUEST, formsemestre_id):
% scu.icontag("lock_img", border="0", title="Semestre verrouillé")
)
else:
r = do_formsemestre_createwithmodules(REQUEST=REQUEST, edit=1)
r = do_formsemestre_createwithmodules(edit=1)
if isinstance(r, str):
H.append(r)
else:
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(
"""<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>
@ -121,7 +121,7 @@ def formsemestre_editwithmodules(REQUEST, formsemestre_id):
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"""
sem = sem or sco_formsemestre.get_formsemestre(formsemestre_id)
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
def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
def do_formsemestre_createwithmodules(edit=False):
"Form choix modules / responsables et creation formsemestre"
# Fonction accessible à tous, controle acces à la main:
vals = scu.get_request_args()
if edit:
formsemestre_id = int(REQUEST.form["formsemestre_id"])
formsemestre_id = int(vals["formsemestre_id"])
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if not current_user.has_permission(Permission.ScoImplement):
if not edit:
@ -156,21 +157,21 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
uid2display[u.id] = u.get_nomplogin()
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})
if not F:
raise ScoValueError("Formation inexistante !")
F = F[0]
if not edit:
initvalues = {"titre": _default_sem_title(F)}
semestre_id = int(REQUEST.form["semestre_id"])
semestre_id = int(vals["semestre_id"])
sem_module_ids = set()
else:
# setup form init values
initvalues = sem
semestre_id = initvalues["semestre_id"]
# 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])
initvalues["tf-checked"] = ["MI" + str(x["module_id"]) 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( )
# mais si on veut l'ordre du PPN (groupe par UE et matieres) il faut:
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:
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:
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
for m in modsmat:
if m["ue_id"] != ue["ue_id"]:
@ -309,7 +310,9 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
{
"size": 40,
"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),
},
),
@ -501,6 +504,14 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
"labels": [""],
},
),
(
"block_moyennes",
{
"input_type": "boolcheckbox",
"title": "Bloquer moyennes",
"explanation": "empêcher le calcul des moyennes d'UE et générale.",
},
),
(
"sep",
{
@ -534,7 +545,7 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
select_name = "%s!group_id" % mod["module_id"]
def opt_selected(gid):
if gid == REQUEST.form.get(select_name):
if gid == vals.get(select_name):
return "selected"
else:
return ""
@ -623,38 +634,29 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
initvalues["gestion_compensation_lst"] = ["X"]
else:
initvalues["gestion_compensation_lst"] = []
if (
REQUEST.form.get("tf_submitted", False)
and "gestion_compensation_lst" not in REQUEST.form
):
REQUEST.form["gestion_compensation_lst"] = []
if vals.get("tf_submitted", False) and "gestion_compensation_lst" not in vals:
vals["gestion_compensation_lst"] = []
initvalues["gestion_semestrielle"] = initvalues.get("gestion_semestrielle", False)
if initvalues["gestion_semestrielle"]:
initvalues["gestion_semestrielle_lst"] = ["X"]
else:
initvalues["gestion_semestrielle_lst"] = []
if (
REQUEST.form.get("tf_submitted", False)
and "gestion_semestrielle_lst" not in REQUEST.form
):
REQUEST.form["gestion_semestrielle_lst"] = []
if vals.get("tf_submitted", False) and "gestion_semestrielle_lst" not in vals:
vals["gestion_semestrielle_lst"] = []
initvalues["bul_hide_xml"] = initvalues.get("bul_hide_xml", False)
if not initvalues["bul_hide_xml"]:
initvalues["bul_publish_xml_lst"] = ["X"]
else:
initvalues["bul_publish_xml_lst"] = []
if (
REQUEST.form.get("tf_submitted", False)
and "bul_publish_xml_lst" not in REQUEST.form
):
REQUEST.form["bul_publish_xml_lst"] = []
if vals.get("tf_submitted", False) and "bul_publish_xml_lst" not in vals:
vals["bul_publish_xml_lst"] = []
#
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
vals,
modform,
submitlabel=submitlabel,
cancelbutton="Annuler",
@ -673,7 +675,7 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
if tf[0] == 0 or msg:
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
+ msg
+ str(tf[1])
@ -693,7 +695,6 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
tf[2]["bul_hide_xml"] = False
else:
tf[2]["bul_hide_xml"] = True
# remap les identifiants de responsables:
tf[2]["responsable_id"] = User.get_user_id_from_nomplogin(
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)
checkedmods = [int(x[2:]) for x in tf[2]["tf-checked"]]
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]
mods_tocreate = [x for x in checkedmods if not x in existingmods]
# 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)],
}
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"])]
# INSCRIPTIONS DES ETUDIANTS
log(
@ -786,7 +787,6 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
moduleimpl_id,
formsemestre_id,
etudids,
REQUEST=REQUEST,
)
msg += [
"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)
msg += diag
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
)[0]["moduleimpl_id"]
modargs = {
@ -813,7 +813,7 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
sco_moduleimpl.do_moduleimpl_edit(
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:
msg_html = (
@ -846,10 +846,10 @@ def formsemestre_delete_moduleimpls(formsemestre_id, module_ids_to_del):
msg = []
for module_id in module_ids_to_del:
# get id
moduleimpl_id = sco_moduleimpl.do_moduleimpl_list(
moduleimpl_id = sco_moduleimpl.moduleimpl_list(
formsemestre_id=formsemestre_id, module_id=module_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 ?
evals = sco_evaluations.do_evaluation_list({"moduleimpl_id": moduleimpl_id})
if evals:
@ -867,7 +867,7 @@ def formsemestre_delete_moduleimpls(formsemestre_id, module_ids_to_del):
return ok, msg
def formsemestre_clone(formsemestre_id, REQUEST=None):
def formsemestre_clone(formsemestre_id):
"""
Formulaire clonage d'un semestre
"""
@ -888,7 +888,6 @@ def formsemestre_clone(formsemestre_id, REQUEST=None):
H = [
html_sco_header.html_sem_header(
REQUEST,
"Copie du semestre",
sem,
javascripts=["libjs/AutoSuggest.js"],
@ -959,8 +958,8 @@ def formsemestre_clone(formsemestre_id, REQUEST=None):
),
]
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
descr,
submitlabel="Dupliquer ce semestre",
cancelbutton="Annuler",
@ -985,7 +984,6 @@ def formsemestre_clone(formsemestre_id, REQUEST=None):
tf[2]["date_fin"],
clone_evaluations=tf[2]["clone_evaluations"],
clone_partitions=tf[2]["clone_partitions"],
REQUEST=REQUEST,
)
return flask.redirect(
"formsemestre_status?formsemestre_id=%s&head_message=Nouveau%%20semestre%%20créé"
@ -1000,7 +998,6 @@ def do_formsemestre_clone(
date_fin, # 'dd/mm/yyyy'
clone_evaluations=False,
clone_partitions=False,
REQUEST=None,
):
"""Clone a semestre: make copy, same modules, same options, same resps, same partitions.
New dates, responsable_id
@ -1018,7 +1015,7 @@ def do_formsemestre_clone(
formsemestre_id = sco_formsemestre.do_formsemestre_create(args)
log("created formsemestre %s" % formsemestre_id)
# 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:
args = mod_orig.copy()
args["formsemestre_id"] = formsemestre_id
@ -1040,7 +1037,7 @@ def do_formsemestre_clone(
args = e.copy()
del args["jour"] # erase date
args["moduleimpl_id"] = mid
_ = sco_evaluations.do_evaluation_create(REQUEST=REQUEST, **args)
_ = sco_evaluations.do_evaluation_create(**args)
# 3- copy uecoefs
objs = sco_formsemestre.formsemestre_uecoef_list(
@ -1077,32 +1074,11 @@ def do_formsemestre_clone(
args["formsemestre_id"] = formsemestre_id
_ = sco_compute_moy.formsemestre_ue_computation_expr_create(cnx, args)
# 5- Copy partitions
# 5- Copy partitions and groups
if clone_partitions:
listgroups = []
listnamegroups = []
# 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)
sco_groups_copy.clone_partitions_and_groups(
orig_formsemestre_id, formsemestre_id
)
return formsemestre_id
@ -1113,10 +1089,11 @@ def do_formsemestre_clone(
def formsemestre_associate_new_version(
formsemestre_id,
other_formsemestre_ids=[],
REQUEST=None,
dialog_confirmed=False,
):
"""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:
# dresse le liste des semestres de la meme formation et version
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
@ -1161,15 +1138,19 @@ def formsemestre_associate_new_version(
)
else:
do_formsemestres_associate_new_version(
[formsemestre_id] + other_formsemestre_ids, REQUEST=REQUEST
[formsemestre_id] + other_formsemestre_ids
)
return flask.redirect(
"formsemestre_status?formsemestre_id=%s&head_message=Formation%%20dupliquée"
% formsemestre_id
url_for(
"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.
Tous les moduleimpl sont -associés à la nouvelle formation, ainsi que les decisions de jury
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:
return
# Check: tous de la même formation
assert isinstance(formsemestre_ids[0], int)
sem = sco_formsemestre.get_formsemestre(formsemestre_ids[0])
formation_id = sem["formation_id"]
for formsemestre_id in formsemestre_ids[1:]:
assert isinstance(formsemestre_id, int)
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if formation_id != sem["formation_id"]:
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,
modules_old2new,
ues_old2new,
) = sco_formations.formation_create_new_version(
formation_id, redirect=False, REQUEST=REQUEST
)
) = sco_formations.formation_create_new_version(formation_id, redirect=False)
for formsemestre_id in formsemestre_ids:
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).
"""
# 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:
mod["module_id"] = modules_old2new[mod["module_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)
def formsemestre_delete(formsemestre_id, REQUEST=None):
def formsemestre_delete(formsemestre_id):
"""Delete a formsemestre (affiche avertissements)"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
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>
<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>
@ -1261,8 +1242,8 @@ def formsemestre_delete(formsemestre_id, REQUEST=None):
else:
submit_label = "Confirmer la suppression du semestre"
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
(("formsemestre_id", {"input_type": "hidden"}),),
initvalues=F,
submitlabel=submit_label,
@ -1327,7 +1308,7 @@ def do_formsemestre_delete(formsemestre_id):
sco_cache.EvaluationCache.invalidate_sem(formsemestre_id)
# --- 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:
# evaluations
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
(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)
if not ok:
return err
return sco_preferences.SemPreferences(formsemestre_id).edit(
REQUEST=REQUEST, categories=["bul"]
)
return sco_preferences.SemPreferences(formsemestre_id).edit(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é)
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)
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}
sco_formsemestre.do_formsemestre_edit(args)
if REQUEST:
return flask.redirect(
"formsemestre_status?formsemestre_id=%s" % formsemestre_id
)
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"""
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)
etat = not sem["bul_hide_xml"]
if REQUEST and not dialog_confirmed:
if not dialog_confirmed:
if etat:
msg = "non"
else:
@ -1499,14 +1456,14 @@ def formsemestre_change_publication_bul(
args = {"formsemestre_id": formsemestre_id, "bul_hide_xml": etat}
sco_formsemestre.do_formsemestre_edit(args)
if REQUEST:
if redirect:
return flask.redirect(
"formsemestre_status?formsemestre_id=%s" % formsemestre_id
)
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."""
from app.scodoc import notes_table
@ -1538,9 +1495,7 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None, REQUEST=None):
</p>
"""
H = [
html_sco_header.html_sem_header(
REQUEST, "Coefficients des UE du semestre", sem
),
html_sco_header.html_sem_header("Coefficients des UE du semestre", sem),
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))
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
form,
submitlabel="Changer les coefficients",
cancelbutton="Annuler",
@ -1652,9 +1607,7 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None, REQUEST=None):
formsemestre_id=formsemestre_id
) # > modif coef UE cap (modifs notes de _certains_ etudiants)
header = html_sco_header.html_sem_header(
REQUEST, "Coefficients des UE du semestre", sem
)
header = html_sco_header.html_sem_header("Coefficients des UE du semestre", sem)
return (
header
+ "\n".join(z)

View File

@ -34,7 +34,7 @@ Ces semestres n'auront qu'un seul inscrit !
import time
import flask
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
@ -52,7 +52,7 @@ from app.scodoc import sco_parcours_dut
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.
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
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"""
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
H = [
@ -181,8 +181,8 @@ def formsemestre_ext_create_form(etudid, formsemestre_id, REQUEST=None):
]
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
descr,
cancelbutton="Annuler",
method="post",
@ -204,13 +204,13 @@ def formsemestre_ext_create_form(etudid, formsemestre_id, REQUEST=None):
)
else:
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(
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)
pour un semestre extérieur.
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)
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
ue_list = _list_ue_with_coef_and_validations(sem, etudid)
descr = _ue_form_description(ue_list, REQUEST.form)
if REQUEST and REQUEST.method == "GET":
ues = _list_ue_with_coef_and_validations(sem, etudid)
descr = _ue_form_description(ues, scu.get_request_args())
if request.method == "GET":
initvalues = {
"note_" + str(ue["ue_id"]): ue["validation"].get("moy_ue", "")
for ue in ue_list
"note_" + str(ue["ue_id"]): ue["validation"].get("moy_ue", "") for ue in ues
}
else:
initvalues = {}
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
descr,
cssclass="tf_ext_edit_ue_validations",
submitlabel="Enregistrer ces validations",
@ -242,27 +241,25 @@ def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid, REQUEST=None):
if tf[0] == -1:
return "<h4>annulation</h4>"
else:
H = _make_page(etud, sem, tf, REQUEST=REQUEST)
H = _make_page(etud, sem, tf)
if tf[0] == 0: # premier affichage
return "\n".join(H)
else: # soumission
# simule erreur
ok, message = _check_values(ue_list, tf[2])
ok, message = _check_values(ues, tf[2])
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)
else:
# Submit
_record_ue_validations_and_coefs(
formsemestre_id, etudid, ue_list, tf[2], REQUEST=REQUEST
)
_record_ue_validations_and_coefs(formsemestre_id, etudid, ues, tf[2])
return flask.redirect(
"formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s"
% (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"])
moy_gen = nt.get_etud_moy_gen(etud["etudid"])
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
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"}),
("etudid", {"input_type": "hidden"}),
]
for ue in ue_list:
for ue in ues:
# Menu pour code validation UE:
# Ne propose que ADM, CMP et "Non inscrit"
select_name = "valid_" + str(ue["ue_id"])
@ -439,8 +436,8 @@ def _list_ue_with_coef_and_validations(sem, etudid):
"""
cnx = ndb.GetDBConnexion()
formsemestre_id = sem["formsemestre_id"]
ue_list = sco_edit_ue.do_ue_list({"formation_id": sem["formation_id"]})
for ue in ue_list:
ues = sco_edit_ue.ue_list({"formation_id": sem["formation_id"]})
for ue in ues:
# add coefficient
uecoef = sco_formsemestre.formsemestre_uecoef_list(
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]
else:
ue["validation"] = {}
return ue_list
return ues
def _record_ue_validations_and_coefs(
formsemestre_id, etudid, ue_list, values, REQUEST=None
):
for ue in ue_list:
def _record_ue_validations_and_coefs(formsemestre_id, etudid, ues, values):
for ue in ues:
code = values.get("valid_" + str(ue["ue_id"]), False)
if code == "None":
code = None
@ -492,5 +487,4 @@ def _record_ue_validations_and_coefs(
now_dmy,
code=code,
ue_coefficient=coef,
REQUEST=REQUEST,
)

View File

@ -30,12 +30,12 @@
import time
import flask
from flask import url_for, g
from flask import url_for, g, request
import app.scodoc.sco_utils as scu
from app import log
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_codes_parcours import UE_STANDARD, UE_SPORT, UE_TYPE_NAME
import app.scodoc.notesdb as ndb
@ -55,6 +55,7 @@ _formsemestre_inscriptionEditor = ndb.EditableTable(
"formsemestre_inscription_id",
("formsemestre_inscription_id", "etudid", "formsemestre_id", "etat", "etape"),
sortkey="formsemestre_id",
insert_ignore_conflicts=True,
)
@ -126,6 +127,42 @@ def do_formsemestre_inscription_delete(oid, formsemestre_id=None):
) # > 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):
"edit a formsemestre_inscription"
cnx = ndb.GetDBConnexion()
@ -236,7 +273,7 @@ def do_formsemestre_inscription_with_modules(
gdone[group_id] = 1
# 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
)
for mod in modimpls:
@ -248,7 +285,7 @@ def do_formsemestre_inscription_with_modules(
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.
Si etudid n'est pas specifié, form. choix etudiant.
@ -263,7 +300,7 @@ def formsemestre_inscription_with_modules_etud(
)
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(
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.
@ -334,7 +371,6 @@ def formsemestre_inscription_with_modules(
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
H = [
html_sco_header.html_sem_header(
REQUEST,
"Inscription de %s dans ce semestre" % etud["nomprenom"],
sem,
)
@ -415,7 +451,7 @@ def formsemestre_inscription_with_modules(
<input type="hidden" name="etudid" 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))
@ -431,7 +467,7 @@ def formsemestre_inscription_with_modules(
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."""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if not sem["etat"]:
@ -448,7 +484,7 @@ def formsemestre_inscription_option(etudid, formsemestre_id, REQUEST=None):
]
# 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)
# Formulaire
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(
"%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 ?
for ins in inscr:
if ins["moduleimpl_id"] == mod["moduleimpl_id"]:
@ -533,8 +570,8 @@ function chkbx_select(field_id, state) {
"""
)
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
descr,
initvalues,
cancelbutton="Annuler",
@ -659,7 +696,7 @@ function chkbx_select(field_id, state) {
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
@ -679,7 +716,7 @@ def do_moduleimpl_incription_options(
# inscriptions
for moduleimpl_id in a_inscrire:
# 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:
raise ScoValueError(
"inscription: invalid moduleimpl_id: %s" % moduleimpl_id
@ -692,7 +729,7 @@ def do_moduleimpl_incription_options(
# desinscriptions
for moduleimpl_id in a_desinscrire:
# 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:
raise ScoValueError(
"desinscription: invalid moduleimpl_id: %s" % moduleimpl_id
@ -711,17 +748,16 @@ def do_moduleimpl_incription_options(
oid, formsemestre_id=mod["formsemestre_id"]
)
if REQUEST:
H = [
html_sco_header.sco_header(),
"""<h3>Modifications effectuées</h3>
<p><a class="stdlink" href="%s">
Retour à la fiche étudiant</a></p>
"""
% url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
html_sco_header.sco_footer(),
]
return "\n".join(H)
H = [
html_sco_header.sco_header(),
"""<h3>Modifications effectuées</h3>
<p><a class="stdlink" href="%s">
Retour à la fiche étudiant</a></p>
"""
% url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
html_sco_header.sco_footer(),
]
return "\n".join(H)
def est_inscrit_ailleurs(etudid, formsemestre_id):
@ -756,14 +792,13 @@ def list_inscrits_ailleurs(formsemestre_id):
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
dont les dates recouvrent le semestre indiqué.
"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [
html_sco_header.html_sem_header(
REQUEST,
"Inscriptions multiples parmi les étudiants du semestre ",
sem,
)

View File

@ -105,7 +105,7 @@ def _build_menu_stats(formsemestre_id):
"title": "Documents Avis Poursuite Etudes",
"endpoint": "notes.pe_view_sem_recap",
"args": {"formsemestre_id": formsemestre_id},
"enabled": True,
"enabled": current_app.config["TESTING"] or current_app.config["DEBUG"],
},
{
"title": 'Table "débouchés"',
@ -141,7 +141,7 @@ def formsemestre_status_menubar(sem):
},
{
"title": "Voir la formation %(acronyme)s (v%(version)s)" % F,
"endpoint": "notes.ue_list",
"endpoint": "notes.ue_table",
"args": {"formation_id": sem["formation_id"]},
"enabled": True,
"helpmsg": "Tableau de bord du semestre",
@ -337,7 +337,7 @@ def formsemestre_status_menubar(sem):
submenu.append(
{
"title": "%s" % partition["partition_name"],
"endpoint": "scolar.affectGroups",
"endpoint": "scolar.affect_groups",
"args": {"partition_id": partition["partition_id"]},
"enabled": enabled,
}
@ -436,7 +436,7 @@ def formsemestre_status_menubar(sem):
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
arguments de la requête:
formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id
@ -447,12 +447,13 @@ def retreive_formsemestre_from_request():
args = request.form
else:
return None
formsemestre_id = None
# Search formsemestre
group_ids = args.get("group_ids", [])
if "formsemestre_id" in args:
formsemestre_id = args["formsemestre_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:
return None # suppressed ?
modimpl = modimpl[0]
@ -462,7 +463,7 @@ def retreive_formsemestre_from_request():
if not E:
return None # evaluation suppressed ?
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"]
elif "group_id" in args:
group = sco_groups.get_group(args["group_id"])
@ -479,16 +480,17 @@ def retreive_formsemestre_from_request():
elif "partition_id" in args:
partition = sco_groups.get_partition(args["partition_id"])
formsemestre_id = partition["formsemestre_id"]
else:
if not formsemestre_id:
return None # no current formsemestre
return formsemestre_id
return int(formsemestre_id)
# Element HTML decrivant un semestre (barre de menu et infos)
def formsemestre_page_title():
"""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()
#
@ -503,15 +505,29 @@ def formsemestre_page_title():
fill_formsemestre(sem)
H = [
"""<div class="formsemestre_page_title">""",
"""<div class="infos">
<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>"""
% sem,
formsemestre_status_menubar(sem),
"""</div>""",
]
return "\n".join(H)
h = f"""<div class="formsemestre_page_title">
<div class="infos">
<span class="semtitle"><a class="stdlink" title="{sem['session_id']}"
href="{url_for('notes.formsemestre_status',
scodoc_dept=g.scodoc_dept, formsemestre_id=sem['formsemestre_id'])}"
>{sem['titre']}</a><a
title="{sem['etape_apo_str']}">{sem['num_sem']}</a>{sem['modalitestr']}</span><span
class="dates"><a
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):
@ -568,7 +584,7 @@ def fill_formsemestre(sem):
# 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
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)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id
)
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
R = []
sum_coef = 0
@ -610,7 +624,7 @@ def formsemestre_description_table(formsemestre_id, REQUEST=None, with_evals=Fal
moduleimpl_id=M["moduleimpl_id"]
)
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 = {
"UE": M["ue"]["acronyme"],
@ -698,41 +712,37 @@ def formsemestre_description_table(formsemestre_id, REQUEST=None, with_evals=Fal
html_caption=title,
html_class="table_leftalign formsemestre_description",
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,
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,
preferences=sco_preferences.SemPreferences(formsemestre_id),
)
def formsemestre_description(
formsemestre_id, format="html", with_evals=False, REQUEST=None
):
def formsemestre_description(formsemestre_id, format="html", with_evals=False):
"""Description du semestre sous forme de table exportable
Liste des modules et de leurs coefficients
"""
with_evals = int(with_evals)
tab = formsemestre_description_table(
formsemestre_id, REQUEST, with_evals=with_evals
)
tab = formsemestre_description_table(formsemestre_id, with_evals=with_evals)
tab.html_before_table = """<form name="f" method="get" action="%s">
<input type="hidden" name="formsemestre_id" value="%s"></input>
<input type="checkbox" name="with_evals" value="1" onchange="document.f.submit()" """ % (
REQUEST.URL0,
request.base_url,
formsemestre_id,
)
if with_evals:
tab.html_before_table += "checked"
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
def _make_listes_sem(sem, REQUEST=None, with_absences=True):
def _make_listes_sem(sem, with_absences=True):
# construit l'URL "destination"
# (a laquelle on revient apres saisie absences)
destination = url_for(
@ -845,7 +855,7 @@ def _make_listes_sem(sem, REQUEST=None, with_absences=True):
H.append('<p class="help indent">Aucun groupe dans cette partition')
if sco_groups.sco_permissions_check.can_change_groups(formsemestre_id):
H.append(
f""" (<a href="{url_for("scolar.affectGroups",
f""" (<a href="{url_for("scolar.affect_groups",
scodoc_dept=g.scodoc_dept,
partition_id=partition["partition_id"])
}" class="stdlink">créer</a>)"""
@ -873,7 +883,7 @@ def html_expr_diagnostic(diagnostics):
last_id, last_msg = None, None
for diag in diagnostics:
if "moduleimpl_id" in diag:
mod = sco_moduleimpl.do_moduleimpl_withmodule_list(
mod = sco_moduleimpl.moduleimpl_withmodule_list(
moduleimpl_id=diag["moduleimpl_id"]
)[0]
H.append(
@ -886,7 +896,7 @@ def html_expr_diagnostic(diagnostics):
)
else:
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(
'<li>UE "%s": %s</li>'
% (ue["acronyme"] or ue["titre"] or "?", diag["msg"])
@ -897,7 +907,7 @@ def html_expr_diagnostic(diagnostics):
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" """
semlist = sco_formsemestre.do_formsemestre_list(
args={"formsemestre_id": formsemestre_id}
@ -912,12 +922,12 @@ def formsemestre_status_head(formsemestre_id=None, REQUEST=None, page_title=None
H = [
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>
<a href="Notes/ue_list?formation_id=%(formation_id)s" class="discretelink" title="Formation %(acronyme)s, v%(version)s">%(titre)s</a>"""
% F,
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=F['formation_id'])}"
class="discretelink" title="Formation {F['acronyme']}, v{F['version']}">{F['titre']}</a>""",
]
if sem["semestre_id"] >= 0:
H.append(", %s %s" % (parcours.SESSION_NAME, sem["semestre_id"]))
@ -948,10 +958,13 @@ Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur ind
</td></tr>"""
)
H.append("</table>")
sem_warning = ""
if sem["bul_hide_xml"]:
H.append(
'<p class="fontorange"><em>Bulletins non publiés sur le portail</em></p>'
)
sem_warning += "Bulletins non publiés sur le portail. "
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):
H.append(
'<p class="fontorange"><em>Attention: ce semestre couvre plusieurs années scolaires !</em></p>'
@ -962,20 +975,18 @@ Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur ind
return "".join(H)
def formsemestre_status(formsemestre_id=None, REQUEST=None):
def formsemestre_status(formsemestre_id=None):
"""Tableau de bord semestre HTML"""
# porté du DTML
cnx = ndb.GetDBConnexion()
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id
)
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
# inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
# args={"formsemestre_id": formsemestre_id}
# )
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 = [
html_sco_header.sco_header(page_title="Semestre %s" % sem["titreannee"]),
@ -1018,11 +1029,9 @@ def formsemestre_status(formsemestre_id=None, REQUEST=None):
ModInscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
moduleimpl_id=M["moduleimpl_id"]
)
mails_enseignants.add(
sco_users.user_info(M["responsable_id"], REQUEST)["email"]
)
mails_enseignants.add(sco_users.user_info(M["responsable_id"])["email"])
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"]
if prev_ue_id != ue["ue_id"]:
@ -1147,7 +1156,7 @@ def formsemestre_status(formsemestre_id=None, REQUEST=None):
# --- LISTE DES ETUDIANTS
H += [
'<div id="groupes">',
_make_listes_sem(sem, REQUEST),
_make_listes_sem(sem),
"</div>",
]
# --- Lien mail enseignants:

View File

@ -27,10 +27,10 @@
"""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
from flask import url_for, g
from flask import url_for, g, request
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
@ -64,7 +64,6 @@ def formsemestre_validation_etud_form(
desturl=None,
sortcol=None,
readonly=True,
REQUEST=None,
):
nt = sco_cache.NotesTableCache.get(
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>'
% (
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
sco_photos.etud_photo_html(
etud, title="fiche de %s" % etud["nom"], REQUEST=REQUEST
),
sco_photos.etud_photo_html(etud, title="fiche de %s" % etud["nom"]),
)
)
@ -163,10 +160,11 @@ def formsemestre_validation_etud_form(
if etud_etat != "I":
H.append(
tf_error_message(
"""Impossible de statuer sur cet étudiant:
il est démissionnaire ou défaillant (voir <a href="%s">sa fiche</a>)
f"""Impossible de statuer sur cet étudiant:
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)
@ -178,16 +176,19 @@ def formsemestre_validation_etud_form(
)
if check:
if not desturl:
desturl = (
"formsemestre_recapcomplet?modejury=1&hidemodules=1&hidebac=1&pref_override=0&formsemestre_id="
+ str(formsemestre_id)
desturl = url_for(
"notes.formsemestre_recapcomplet",
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:
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)
H.append(f'<ul><li><a href="{desturl}">Continuer</a></li></ul>')
return "\n".join(H + Footer)
@ -197,8 +198,12 @@ def formsemestre_validation_etud_form(
if nt.etud_has_notes_attente(etudid):
H.append(
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>)"""
% formsemestre_id
f"""Impossible de statuer sur cet étudiant: il a des notes en
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)
@ -213,14 +218,24 @@ def formsemestre_validation_etud_form(
if not Se.prev_decision:
H.append(
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>)"""
% (Se.prev["formsemestre_id"], etudid)
f"""Le jury n'a pas statué sur le semestre précédent ! (<a href="{
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:
H.append(
'<a href="formsemestre_validation_suppress_etud?etudid=%s&formsemestre_id=%s" class="stdlink">Supprimer décision existante</a>'
% (etudid, formsemestre_id)
f"""<a href="{
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())
return "\n".join(H)
@ -338,7 +353,6 @@ def formsemestre_validation_etud(
codechoice=None, # required
desturl="",
sortcol=None,
REQUEST=None,
):
"""Enregistre validation"""
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
@ -354,9 +368,9 @@ def formsemestre_validation_etud(
if not selected_choice:
raise ValueError("code choix invalide ! (%s)" % codechoice)
#
Se.valide_decision(selected_choice, REQUEST) # enregistre
Se.valide_decision(selected_choice) # enregistre
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,
desturl="",
sortcol=None,
REQUEST=None,
redirect=True,
):
"""Enregistre validation"""
@ -399,22 +412,20 @@ def formsemestre_validation_etud_manu(
formsemestre_id_utilise_pour_compenser=formsemestre_id_utilise_pour_compenser,
)
#
Se.valide_decision(choice, REQUEST) # enregistre
Se.valide_decision(choice) # enregistre
if redirect:
return _redirect_valid_choice(
formsemestre_id, etudid, Se, choice, desturl, sortcol, REQUEST
formsemestre_id, etudid, Se, choice, desturl, sortcol
)
def _redirect_valid_choice(
formsemestre_id, etudid, Se, choice, desturl, sortcol, REQUEST
):
def _redirect_valid_choice(formsemestre_id, etudid, Se, choice, desturl, sortcol):
adr = "formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s&check=1" % (
formsemestre_id,
etudid,
)
if sortcol:
adr += "&sortcol=" + sortcol
adr += "&sortcol=" + str(sortcol)
# if desturl:
# desturl += "&desturl=" + desturl
return flask.redirect(adr)
@ -483,7 +494,7 @@ def formsemestre_recap_parcours_table(
with_links=False,
with_all_columns=True,
a_url="",
sem_info={},
sem_info=None,
show_details=False,
):
"""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
with_all_columns: si faux, pas de colonne "assiduité".
"""
sem_info = sem_info or {}
H = []
linktmpl = '<span onclick="toggle_vis(this);" class="toggle_sem sem_%%s">%s</span>'
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"
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [
html_sco_header.html_sem_header(
REQUEST, "Saisie automatique des décisions du semestre", sem
"Saisie automatique des décisions du semestre", sem
),
"""
<ul>
@ -851,7 +863,7 @@ def formsemestre_validation_auto(formsemestre_id, REQUEST):
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"
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
next_semestre_id = sem["semestre_id"] + 1
@ -907,7 +919,6 @@ def do_formsemestre_validation_auto(formsemestre_id, REQUEST):
code_etat=ADM,
devenir="NEXT",
assidu=True,
REQUEST=REQUEST,
redirect=False,
)
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)
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
(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>'
% (
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
sco_photos.etud_photo_html(
etud, title="fiche de %s" % etud["nom"], REQUEST=REQUEST
),
sco_photos.etud_photo_html(etud, title="fiche de %s" % etud["nom"]),
)
),
"""<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)
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_ids = [""] + [ue["ue_id"] for ue in ues]
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
(
("etudid", {"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]["date"],
semestre_id=semestre_id,
REQUEST=REQUEST,
)
return flask.redirect(
scu.ScoURL()
@ -1109,7 +1117,6 @@ def do_formsemestre_validate_previous_ue(
code=ADM,
semestre_id=None,
ue_coefficient=None,
REQUEST=None,
):
"""Enregistre (ou modifie) validation d'UE (obtenue hors ScoDoc).
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)
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"""
valids = ndb.SimpleDictFetch(
"""SELECT SFV.*
@ -1201,7 +1208,7 @@ def get_etud_ue_cap_html(etudid, formsemestre_id, ue_id, REQUEST=None):
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"""
log("etud_ue_suppress_validation( %s, %s, %s)" % (etudid, formsemestre_id, ue_id))
cnx = ndb.GetDBConnexion()
@ -1227,7 +1234,7 @@ def check_formation_ues(formation_id):
définition du programme: cette fonction retourne un bout de HTML
à 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 ] }
for ue in ues:
# formsemestres utilisant cette ue ?

View File

@ -42,12 +42,12 @@ from xml.etree import ElementTree
from xml.etree.ElementTree import Element
import flask
from flask import g
from flask import url_for
from flask import g, request
from flask import url_for, make_response
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log
from app import log, cache
from app.scodoc.scolog import logdb
from app.scodoc import html_sco_header
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.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(
"partition",
@ -103,8 +89,8 @@ def get_group(group_id):
"""Returns group object, with partition"""
r = ndb.SimpleDictFetch(
"""SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.*
FROM group_descr gd, partition p
WHERE gd.id=%(group_id)s
FROM group_descr gd, partition p
WHERE gd.id=%(group_id)s
AND p.id = gd.partition_id
""",
{"group_id": group_id},
@ -126,8 +112,8 @@ def group_delete(group, force=False):
def get_partition(partition_id):
r = ndb.SimpleDictFetch(
"""SELECT p.id AS partition_id, p.*
FROM partition p
"""SELECT p.id AS partition_id, p.*
FROM partition p
WHERE p.id = %(partition_id)s
""",
{"partition_id": partition_id},
@ -140,7 +126,7 @@ def get_partition(partition_id):
def get_partitions_list(formsemestre_id, with_default=True):
"""Liste des partitions pour ce semestre (list of dicts)"""
partitions = ndb.SimpleDictFetch(
"""SELECT p.id AS partition_id, p.*
"""SELECT p.id AS partition_id, p.*
FROM partition p
WHERE formsemestre_id=%(formsemestre_id)s
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)"""
r = ndb.SimpleDictFetch(
"""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
""",
{"formsemestre_id": formsemestre_id},
@ -184,10 +170,10 @@ def get_partition_groups(partition):
"""List of groups in this partition (list of dicts).
Some groups may be empty."""
return ndb.SimpleDictFetch(
"""SELECT gd.id AS group_id, p.id AS partition_id, gd.*, p.*
FROM group_descr gd, partition p
WHERE gd.partition_id=%(partition_id)s
AND gd.partition_id=p.id
"""SELECT gd.id AS group_id, p.id AS partition_id, gd.*, p.*
FROM group_descr gd, partition p
WHERE gd.partition_id=%(partition_id)s
AND gd.partition_id=p.id
ORDER BY group_name
""",
partition,
@ -198,9 +184,9 @@ def get_default_group(formsemestre_id, fix_if_missing=False):
"""Returns group_id for default ('tous') group"""
r = ndb.SimpleDictFetch(
"""SELECT gd.id AS group_id
FROM group_descr gd, partition p
WHERE p.formsemestre_id=%(formsemestre_id)s
AND p.partition_name is NULL
FROM group_descr gd, partition p
WHERE p.formsemestre_id=%(formsemestre_id)s
AND p.partition_name is NULL
AND p.id = gd.partition_id
""",
{"formsemestre_id": formsemestre_id},
@ -219,7 +205,7 @@ def get_default_group(formsemestre_id, fix_if_missing=False):
partition_id = partition_create(
formsemestre_id, default=True, redirect=False
)
group_id = createGroup(partition_id, default=True)
group_id = create_group(partition_id, default=True)
return group_id
# debug check
if len(r) != 1:
@ -232,8 +218,8 @@ def get_sem_groups(formsemestre_id):
"""Returns groups for this sem (in all partitions)."""
return ndb.SimpleDictFetch(
"""SELECT gd.id AS group_id, p.id AS partition_id, gd.*, p.*
FROM group_descr gd, partition p
WHERE p.formsemestre_id=%(formsemestre_id)s
FROM group_descr gd, partition p
WHERE p.formsemestre_id=%(formsemestre_id)s
AND p.id = gd.partition_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
[ 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
WHERE gm.etudid=%(etudid)s
and gm.group_id = g.id
@ -391,10 +377,10 @@ def formsemestre_get_etud_groupnames(formsemestre_id, attr="group_name"):
{ etudid : { partition_id : group_name }} (attr=group_name or group_id)
"""
infos = ndb.SimpleDictFetch(
"""SELECT i.id AS etudid, p.id AS partition_id,
gd.group_name, gd.id AS group_id
FROM notes_formsemestre_inscription i, partition p,
group_descr gd, group_membership gm
"""SELECT i.id AS etudid, p.id AS partition_id,
gd.group_name, 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 p.id = gd.partition_id
@ -427,7 +413,7 @@ def etud_add_group_infos(etud, sem, sep=" "):
FROM group_descr g, partition p, group_membership gm WHERE gm.etudid=%(etudid)s
and gm.group_id = g.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
""",
{"etudid": etud["etudid"], "formsemestre_id": sem["formsemestre_id"]},
@ -452,6 +438,7 @@ def etud_add_group_infos(etud, sem, sep=" "):
return etud
@cache.memoize(timeout=50) # seconds
def get_etud_groups_in_partition(partition_id):
"""Returns { etudid : group }, with all students in this partition"""
infos = ndb.SimpleDictFetch(
@ -468,7 +455,7 @@ def get_etud_groups_in_partition(partition_id):
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
Supported formats: xml, json
"""
@ -476,11 +463,11 @@ def formsemestre_partition_list(formsemestre_id, format="xml", REQUEST=None):
# Ajoute les groupes
for p in partitions:
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
def XMLgetGroupsInPartition(partition_id, REQUEST=None): # was XMLgetGroupesTD
def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
"""
Deprecated: use group_list
Liste des étudiants dans chaque groupe de cette partition.
@ -492,6 +479,8 @@ def XMLgetGroupsInPartition(partition_id, REQUEST=None): # was XMLgetGroupesTD
"""
from app.scodoc import sco_formsemestre
cnx = ndb.GetDBConnexion()
t0 = time.time()
partition = get_partition(partition_id)
formsemestre_id = partition["formsemestre_id"]
@ -499,8 +488,8 @@ def XMLgetGroupsInPartition(partition_id, REQUEST=None): # was XMLgetGroupesTD
groups = get_partition_groups(partition)
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > inscrdict
etuds_set = set(nt.inscrdict)
# XML response:
REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE)
# Build XML:
t1 = time.time()
doc = Element("ajax-response")
x_response = Element("response", type="object", id="MyUpdater")
doc.append(x_response)
@ -514,7 +503,8 @@ def XMLgetGroupsInPartition(partition_id, REQUEST=None): # was XMLgetGroupesTD
)
x_response.append(x_group)
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(
Element(
"etud",
@ -541,6 +531,7 @@ def XMLgetGroupsInPartition(partition_id, REQUEST=None): # was XMLgetGroupesTD
doc.append(x_group)
for etudid in etuds_set:
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(
Element(
"etud",
@ -551,8 +542,13 @@ def XMLgetGroupsInPartition(partition_id, REQUEST=None): # was XMLgetGroupesTD
origin=comp_origin(etud, sem),
)
)
log("XMLgetGroupsInPartition: %s seconds" % (time.time() - t0))
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING)
t2 = time.time()
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):
@ -652,7 +648,6 @@ def setGroups(
groupsLists="", # members of each existing group
groupsToCreate="", # name and members of new groups
groupsToDelete="", # groups to delete
REQUEST=None,
):
"""Affect groups (Ajax request)
groupsLists: lignes de la forme "group_id;etudid;...\n"
@ -716,7 +711,7 @@ def setGroups(
# Supprime les groupes indiqués comme supprimés:
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
for line in groupsToCreate.split("\n"): # for each group_name (one per line)
@ -724,22 +719,20 @@ def setGroups(
group_name = fs[0].strip()
if not group_name:
continue
# ajax arguments are encoded in utf-8:
# group_name = six.text_type(group_name, "utf-8").encode(
# scu.SCO_ENCODING
# ) # #py3 #sco8
group_id = createGroup(partition_id, group_name)
group_id = create_group(partition_id, group_name)
# Place dans ce groupe les etudiants indiqués:
for etudid in fs[1:-1]:
change_etud_group_in_partition(etudid, group_id, partition)
REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE)
return (
data = (
'<?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"""
partition = get_partition(partition_id)
formsemestre_id = partition["formsemestre_id"]
@ -759,12 +752,12 @@ def createGroup(partition_id, group_name="", default=False):
group_id = groupEditor.create(
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
def suppressGroup(group_id, partition_id=None, REQUEST=None):
def delete_group(group_id, partition_id=None):
"""form suppression d'un groupe.
(ne desinscrit pas les etudiants, change juste leur
affectation aux groupes)
@ -781,7 +774,7 @@ def suppressGroup(group_id, partition_id=None, REQUEST=None):
if not sco_permissions_check.can_change_groups(partition["formsemestre_id"]):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
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_delete(group)
@ -798,7 +791,7 @@ def partition_create(
if not sco_permissions_check.can_change_groups(formsemestre_id):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
if partition_name:
partition_name = partition_name.strip()
partition_name = str(partition_name).strip()
if default:
partition_name = None
if not partition_name and not default:
@ -813,8 +806,21 @@ def partition_create(
)
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(
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)
#
@ -830,7 +836,7 @@ def partition_create(
return partition_id
def getArrowIconsTags():
def get_arrow_icons_tags():
"""returns html tags for arrows"""
#
arrow_up = scu.icontag("arrow_up", title="remonter")
@ -840,13 +846,13 @@ def getArrowIconsTags():
return arrow_up, arrow_down, arrow_none
def editPartitionForm(formsemestre_id=None, REQUEST=None):
def editPartitionForm(formsemestre_id=None):
"""Form to create/suppress partitions"""
# ad-hoc form
if not sco_permissions_check.can_change_groups(formsemestre_id):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
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(
"delete_small_img", border="0", alt="supprimer", title="Supprimer"
)
@ -907,7 +913,7 @@ def editPartitionForm(formsemestre_id=None, REQUEST=None):
H.append(", ".join(lg))
H.append(
f"""</td><td><a class="stdlink" href="{
url_for("scolar.affectGroups",
url_for("scolar.affect_groups",
scodoc_dept=g.scodoc_dept,
partition_id=p["partition_id"])
}">répartir</a></td>
@ -968,7 +974,7 @@ def editPartitionForm(formsemestre_id=None, REQUEST=None):
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"""
if attr not in {"bul_show_rank", "show_in_lists"}:
raise ValueError("invalid partition attribute: %s" % attr)
@ -991,9 +997,7 @@ def partition_set_attr(partition_id, attr, value, REQUEST=None):
return "enregistré"
def partition_delete(
partition_id, REQUEST=None, force=False, redirect=1, dialog_confirmed=False
):
def partition_delete(partition_id, force=False, redirect=1, dialog_confirmed=False):
"""Suppress a partition (and all groups within).
default partition cannot be suppressed (unless force)"""
partition = get_partition(partition_id)
@ -1036,7 +1040,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)"""
partition = get_partition(partition_id)
formsemestre_id = partition["formsemestre_id"]
@ -1050,7 +1054,7 @@ def partition_move(partition_id, after=0, REQUEST=None, redirect=1):
others = get_partitions_list(formsemestre_id)
if len(others) > 1:
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
if after == 0 and pidx > 0:
neigh = others[pidx - 1]
@ -1058,8 +1062,20 @@ def partition_move(partition_id, after=0, REQUEST=None, redirect=1):
neigh = others[pidx + 1]
if neigh: #
# swap numero between partition and its neighbor
log("moving partition %s" % partition_id)
# log("moving partition %s" % partition_id)
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"]
partitionEditor.edit(cnx, partition)
partitionEditor.edit(cnx, neigh)
@ -1071,7 +1087,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"""
partition = get_partition(partition_id)
formsemestre_id = partition["formsemestre_id"]
@ -1079,8 +1095,8 @@ def partition_rename(partition_id, REQUEST=None):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
H = ["<h2>Renommer une partition</h2>"]
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
(
("partition_id", {"default": partition_id, "input_type": "hidden"}),
(
@ -1110,14 +1126,12 @@ def partition_rename(partition_id, REQUEST=None):
)
else:
# form submission
return partition_set_name(
partition_id, tf[2]["partition_name"], REQUEST=REQUEST, redirect=1
)
return partition_set_name(partition_id, tf[2]["partition_name"])
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"""
partition_name = partition_name.strip()
partition_name = str(partition_name).strip()
if not partition_name:
raise ValueError("partition name must be non empty")
partition = get_partition(partition_id)
@ -1127,13 +1141,13 @@ def partition_set_name(partition_id, partition_name, REQUEST=None, redirect=1):
# check unicity
r = ndb.SimpleDictFetch(
"""SELECT p.* FROM partition p
WHERE p.partition_name = %(partition_name)s
"""SELECT p.* FROM partition p
WHERE p.partition_name = %(partition_name)s
AND formsemestre_id = %(formsemestre_id)s
""",
{"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(
"Partition %s déjà existante dans ce semestre !" % partition_name
)
@ -1153,7 +1167,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"""
if group_name:
group_name = group_name.strip()
@ -1173,14 +1187,14 @@ def group_set_name(group_id, group_name, REQUEST=None, redirect=1):
if redirect:
return flask.redirect(
url_for(
"scolar.affectGroups",
"scolar.affect_groups",
scodoc_dept=g.scodoc_dept,
partition_id=group["partition_id"],
)
)
def group_rename(group_id, REQUEST=None):
def group_rename(group_id):
"""Form to rename a group"""
group = get_group(group_id)
formsemestre_id = group["formsemestre_id"]
@ -1188,8 +1202,8 @@ def group_rename(group_id, REQUEST=None):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
H = ["<h2>Renommer un groupe de %s</h2>" % group["partition_name"]]
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
(
("group_id", {"default": group_id, "input_type": "hidden"}),
(
@ -1216,19 +1230,17 @@ def group_rename(group_id, REQUEST=None):
elif tf[0] == -1:
return flask.redirect(
url_for(
"scolar.affectGroups",
"scolar.affect_groups",
scodoc_dept=g.scodoc_dept,
partition_id=group["partition_id"],
)
)
else:
# form submission
return group_set_name(
group_id, tf[2]["group_name"], REQUEST=REQUEST, redirect=1
)
return group_set_name(group_id, tf[2]["group_name"])
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
et la mixité.
"""
@ -1238,7 +1250,7 @@ def groups_auto_repartition(partition_id=None, REQUEST=None):
formsemestre_id = partition["formsemestre_id"]
# renvoie sur page édition groupes
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):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
@ -1268,8 +1280,8 @@ def groups_auto_repartition(partition_id=None, REQUEST=None):
]
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
descr,
{},
cancelbutton="Annuler",
@ -1299,8 +1311,8 @@ def groups_auto_repartition(partition_id=None, REQUEST=None):
# checkGroupName(group_name)
# except:
# 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)
group_ids.append(createGroup(partition_id, group_name))
# return '\n'.join(H) + tf[1] + html_sco_header.sco_footer()
group_ids.append(create_group(partition_id, group_name))
#
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > identdict
identdict = nt.identdict
@ -1360,6 +1372,7 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"):
"""
from app.scodoc import sco_formsemestre_inscriptions
partition_name = str(partition_name)
log("create_etapes_partition(%s)" % formsemestre_id)
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
args={"formsemestre_id": formsemestre_id}
@ -1384,7 +1397,7 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"):
groups_by_names = {g["group_name"]: g for g in groups}
for etape in etapes:
if not (etape in groups_by_names):
gid = createGroup(pid, etape)
gid = create_group(pid, etape)
g = get_group(gid)
groups_by_names[etape] = g
# Place les etudiants dans les groupes
@ -1405,6 +1418,7 @@ def do_evaluation_listeetuds_groups(
Si include_dems, compte aussi les etudiants démissionnaires
(sinon, par défaut, seulement les 'I')
"""
# nb: pour notes_table / do_evaluation_etat, getallstudents est vrai et include_dems faux
fromtables = [
"notes_moduleimpl_inscription Im",
"notes_formsemestre_inscription Isem",
@ -1416,7 +1430,7 @@ def do_evaluation_listeetuds_groups(
if not groups:
return [] # no groups, so no students
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 p.formsemestre_id = Isem.formsemestre_id
"""
@ -1429,7 +1443,7 @@ def do_evaluation_listeetuds_groups(
req = (
"SELECT distinct Im.etudid FROM "
+ ", ".join(fromtables)
+ """ WHERE Isem.etudid = Im.etudid
+ """ WHERE Isem.etudid = Im.etudid
and Im.moduleimpl_id = M.id
and Isem.formsemestre_id = M.formsemestre_id
and E.moduleimpl_id = M.id
@ -1440,10 +1454,9 @@ def do_evaluation_listeetuds_groups(
req += " and Isem.etat='I'"
req += r
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor = cnx.cursor()
cursor.execute(req, {"evaluation_id": evaluation_id})
res = cursor.fetchall()
return [x[0] for x in res]
return [x[0] for x in cursor]
def do_evaluation_listegroupes(evaluation_id, include_default=False):
@ -1457,7 +1470,7 @@ def do_evaluation_listegroupes(evaluation_id, include_default=False):
else:
c = " AND p.partition_name is not NULL"
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor = cnx.cursor()
cursor.execute(
"""SELECT DISTINCT gd.id AS group_id
FROM group_descr gd, group_membership gm, partition p,
@ -1471,8 +1484,7 @@ def do_evaluation_listegroupes(evaluation_id, include_default=False):
+ c,
{"evaluation_id": evaluation_id},
)
res = cursor.fetchall()
group_ids = [x[0] for x in res]
group_ids = [x[0] for x in cursor]
return listgroups(group_ids)
@ -1482,9 +1494,9 @@ def listgroups(group_ids):
groups = []
for group_id in group_ids:
cursor.execute(
"""SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.*
FROM group_descr gd, partition p
WHERE p.id = gd.partition_id
"""SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.*
FROM group_descr gd, partition p
WHERE p.id = gd.partition_id
AND gd.id = %(group_id)s
""",
{"group_id": group_id},
@ -1499,7 +1511,7 @@ def _sortgroups(groups):
# 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]
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

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
"""
from flask import render_template
from app.scodoc import html_sco_header
from app.scodoc import sco_groups
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.
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)
formsemestre_id = partition["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")
H = [
html_sco_header.sco_header(
raise AccessDenied("vous n'avez pas la permission de modifier les groupes")
return render_template(
"scolar/affect_groups.html",
sco_header=html_sco_header.sco_header(
page_title="Affectation aux groupes",
javascripts=["js/groupmgr.js"],
cssstyles=["css/groups.css"],
),
"""<h2 class="formsemestre">Affectation aux groupes de %s</h2><form id="sp">"""
% partition["partition_name"],
]
H += [
"""</select></form>""",
"""<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)
sco_footer=html_sco_header.sco_footer(),
partition=partition,
partitions_list=sco_groups.get_partitions_list(
formsemestre_id, with_default=False
),
formsemestre_id=formsemestre_id,
)

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 io
import os
import re
import time
from datetime import date
import flask
from flask import g, url_for
import app.scodoc.sco_utils as scu
@ -219,21 +219,20 @@ def sco_import_generate_excel_sample(
def students_import_excel(
csvfile,
REQUEST=None,
formsemestre_id=None,
check_homonyms=True,
require_ine=False,
return_html=True,
):
"import students from Excel file"
diag = scolars_import_excel_file(
csvfile,
REQUEST,
formsemestre_id=formsemestre_id,
check_homonyms=check_homonyms,
require_ine=require_ine,
exclude_cols=["photo_filename"],
)
if REQUEST:
if return_html:
if formsemestre_id:
dest = url_for(
"notes.formsemestre_status",
@ -253,8 +252,7 @@ def students_import_excel(
def scolars_import_excel_file(
datafile,
REQUEST,
datafile: io.BytesIO,
formsemestre_id=None,
check_homonyms=True,
require_ine=False,
@ -416,17 +414,14 @@ def scolars_import_excel_file(
if NbHomonyms:
NbImportedHomonyms += 1
# Insert in DB tables
formsemestre_to_invalidate.add(
_import_one_student(
cnx,
REQUEST,
formsemestre_id,
values,
GroupIdInferers,
annee_courante,
created_etudids,
linenum,
)
formsemestre_id_etud = _import_one_student(
cnx,
formsemestre_id,
values,
GroupIdInferers,
annee_courante,
created_etudids,
linenum,
)
# Verification proportion d'homonymes: si > 10%, abandonne
@ -492,16 +487,15 @@ def scolars_import_excel_file(
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)"
diag = scolars_import_admission(
csvfile,
REQUEST,
formsemestre_id=formsemestre_id,
type_admission=type_admission,
)
if REQUEST:
if return_html:
H = [html_sco_header.sco_header(page_title="Import données admissions")]
H.append("<p>Import terminé !</p>")
H.append(
@ -520,14 +514,13 @@ def students_import_admission(
def _import_one_student(
cnx,
REQUEST,
formsemestre_id,
values,
GroupIdInferers,
annee_courante,
created_etudids,
linenum,
):
) -> int:
"""
Import d'un étudiant et inscription dans le semestre.
Return: id du semestre dans lequel il a été inscrit.
@ -538,7 +531,7 @@ def _import_one_student(
)
# Identite
args = values.copy()
etudid = sco_etud.identite_create(cnx, args, REQUEST=REQUEST)
etudid = sco_etud.identite_create(cnx, args)
created_etudids.append(etudid)
# Admissions
args["etudid"] = etudid
@ -555,6 +548,12 @@ def _import_one_student(
else:
args["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:
if formsemestre_id not in GroupIdInferers:
GroupIdInferers[formsemestre_id] = sco_groups.GroupIdInferer(formsemestre_id)
@ -571,7 +570,7 @@ def _import_one_student(
)
do_formsemestre_inscription_with_modules(
args["formsemestre_id"],
int(args["formsemestre_id"]),
etudid,
group_ids,
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)
def scolars_import_admission(
datafile, REQUEST, formsemestre_id=None, type_admission=None
):
def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None):
"""Importe données admission depuis un fichier Excel quelconque
par exemple ceux utilisés avec APB

View File

@ -27,27 +27,23 @@
"""Import d'utilisateurs via fichier Excel
"""
import random, time
import re
import random
import time
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.header import Header
from flask import g, url_for
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
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_preferences
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")
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.
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)
* call :func:`import_users` to actually do the job
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
"""
# Check current user privilege
auth_dept = current_user.dept
auth_name = str(current_user)
if not current_user.is_administrator():
raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name)
@ -109,8 +104,11 @@ def import_excel_file(datafile):
if not exceldata:
raise ScoValueError("Ficher excel vide ou invalide")
_, data = sco_excel.excel_bytes_to_list(exceldata)
if not data: # probably a bug
raise ScoException("import_excel_file: empty file !")
if not data:
raise ScoValueError(
"""Le fichier xlsx attendu semble vide !
"""
)
# 1- --- check title line
fs = [scu.stripquotes(s).lower() for s in data[0]]
log("excel: fs='%s'\ndata=%s" % (str(fs), str(data)))
@ -124,7 +122,8 @@ def import_excel_file(datafile):
del cols[tit]
if cols or unknown:
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)
)
# ok, same titles... : build the list of dictionaries
@ -135,10 +134,10 @@ def import_excel_file(datafile):
d[fs[i]] = line[i]
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.
@ -179,60 +178,35 @@ def import_users(users):
line = line + 1
user_ok, msg = sco_users.check_modif_user(
0,
enforce_optionals=not force,
user_name=u["user_name"],
nom=u["nom"],
prenom=u["prenom"],
email=u["email"],
roles=u["roles"].split(","),
roles=[r for r in u["roles"].split(",") if r],
dept=u["dept"],
)
if not user_ok:
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()
#
# 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():
user_ok = False
append_msg(
"l'utilisateur '%s' a déjà été décrit ligne %s"
% (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
# roles_string (expected by User) appears as column 'roles' in excel file
roles_list = []
for role in u["roles"].split(","):
try:
_, _ = UserRole.role_dept_from_string(role.strip())
roles_list.append(role.strip())
role = role.strip()
if role:
_, _ = UserRole.role_dept_from_string(role)
roles_list.append(role)
except ScoValueError as value_error:
user_ok = False
append_msg("role %s : %s" % (role, value_error))
@ -244,7 +218,7 @@ def import_users(users):
import_ok = False
except ScoValueError as value_error:
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:
for u in created.values():
# Création de l'utilisateur (via SQLAlchemy)
@ -264,7 +238,7 @@ def import_users(users):
ALPHABET = r"""ABCDEFGHIJKLMNPQRSTUVWXYZ123456789123456789AEIOU"""
PASSLEN = 6
PASSLEN = 8
RNG = random.Random(time.time())
@ -279,23 +253,18 @@ def generate_password():
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"
if not u["email"]:
if not user["email"]:
return
u[
"url"
] = (
scu.ScoURL()
) # TODO set auth page URL ? (shared by all departments) ../auth/login
user["url"] = url_for("scodoc.index", _external=True)
txt = (
"""
Bonjour %(prenom)s %(nom)s,
"""
% u
% user
)
if reset:
txt += (
@ -305,10 +274,10 @@ votre mot de passe ScoDoc a été ré-initialisé.
Le nouveau mot de passe est: %(passwd)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
"""
% u
% user
)
else:
txt += (
@ -320,16 +289,15 @@ Votre mot de passe est: %(passwd)s
Le logiciel est accessible sur: %(url)s
Vous êtes invité à changer ce mot de passe au plus vite (cliquez sur
votre nom en haut à gauche de la page d'accueil).
Vous êtes invité à changer ce mot de passe au plus vite (cliquez sur votre nom en haut à gauche de la page d'accueil).
"""
% u
% user
)
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
"""
@ -341,4 +309,4 @@ Pour plus d'informations sur ce logiciel, voir %s
else:
subject = "Votre accès ScoDoc"
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
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.sco_utils as scu
@ -81,6 +81,7 @@ def list_authorized_etuds_by_sem(sem, delai=274):
"title": src["titreannee"],
"title_target": "formsemestre_status?formsemestre_id=%s"
% src["formsemestre_id"],
"filename": "etud_autorises",
},
}
# 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"],
"comment": " actuellement inscrits dans ce semestre",
"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)
sem["date_debut_iso"] = ndb.DateDMYtoISO(sem["date_debut"])
cursor.execute(
"""SELECT I.etudid
FROM
notes_formsemestre_inscription ins,
notes_formsemestre S,
identite i
"""SELECT ins.etudid
FROM
notes_formsemestre_inscription ins,
notes_formsemestre S
WHERE ins.formsemestre_id = S.id
AND S.id != %(formsemestre_id)s
AND S.date_debut <= %(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,
)
@ -264,7 +265,6 @@ def formsemestre_inscr_passage(
inscrit_groupes=False,
submitted=False,
dialog_confirmed=False,
REQUEST=None,
):
"""Form. pour inscription des etudiants d'un semestre dans un autre
(donné par formsemestre_id).
@ -287,9 +287,11 @@ def formsemestre_inscr_passage(
header = html_sco_header.sco_header(page_title="Passage des étudiants")
footer = html_sco_header.sco_footer()
H = [header]
if type(etuds) == type(""):
if isinstance(etuds, str):
etuds = etuds.split(",") # vient du form de confirmation
elif isinstance(etuds, int):
etuds = [etuds]
etuds = [int(x) for x in etuds]
auth_etuds_by_sem, inscrits, candidats = list_authorized_etuds_by_sem(sem)
etuds_set = set(etuds)
candidats_set = set(candidats)
@ -312,7 +314,6 @@ def formsemestre_inscr_passage(
if not submitted:
H += build_page(
REQUEST,
sem,
auth_etuds_by_sem,
inscrits,
@ -342,18 +343,22 @@ def formsemestre_inscr_passage(
% inscrits[etudid]
)
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(
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,
cancel_url="formsemestre_inscr_passage?formsemestre_id="
+ str(formsemestre_id),
OK="Effectuer l'opération",
OK="Effectuer l'opération" if todo else "",
parameters={
"formsemestre_id": formsemestre_id,
"etuds": ",".join(etuds),
"etuds": ",".join([str(x) for x in etuds]),
"inscrit_groupes": inscrit_groupes,
"submitted": 1,
},
@ -385,7 +390,7 @@ def formsemestre_inscr_passage(
): # il y a au moins une vraie partition
H.append(
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"])
}">Répartir les groupes de {partition["partition_name"]}</a></li>
"""
@ -397,7 +402,6 @@ def formsemestre_inscr_passage(
def build_page(
REQUEST,
sem,
auth_etuds_by_sem,
inscrits,
@ -413,9 +417,9 @@ def build_page(
H = [
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="submit" name="submitted" value="Appliquer les modifications"/>
&nbsp;<a href="#help">aide</a>
@ -507,7 +511,12 @@ def etuds_select_boxes(
</script>
<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():
infos = auth_etuds_by_cat[src_cat]["infos"]
infos["comment"] = infos.get("comment", "") # commentaire dans sous-titre boite
@ -550,10 +559,8 @@ def etuds_select_boxes(
if with_checkbox or sel_inscrits:
H.append(")")
if base_url and etuds:
H.append(
'<a href="%s&export_cat_xls=%s">%s</a>&nbsp;'
% (base_url, src_cat, scu.ICON_XLS)
)
url = scu.build_url_query(base_url, export_cat_xls=src_cat)
H.append(f'<a href="{url}">{scu.ICON_XLS}</a>&nbsp;')
H.append("</div>")
for etud in etuds:
if etud.get("inscrit", False):
@ -633,4 +640,4 @@ def etuds_select_box_xls(src_cat):
caption="%(title)s. %(help)s" % src_cat["infos"],
preferences=sco_preferences.SemPreferences(),
)
return tab.excel()
return tab.excel() # tab.make_page(filename=src_cat["infos"]["filename"])

View File

@ -27,11 +27,11 @@
"""Liste des notes d'une évaluation
"""
import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error
from operator import itemgetter
import urllib
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.notesdb as ndb
@ -56,20 +56,21 @@ from app.scodoc.gen_tables import GenTable
from app.scodoc.htmlutils import histogram_notes
def do_evaluation_listenotes(REQUEST):
def do_evaluation_listenotes():
"""
Affichage des notes d'une évaluation
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
if "evaluation_id" in REQUEST.form:
evaluation_id = int(REQUEST.form["evaluation_id"])
vals = scu.get_request_args()
if "evaluation_id" in vals:
evaluation_id = int(vals["evaluation_id"])
mode = "eval"
evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})
if "moduleimpl_id" in REQUEST.form:
moduleimpl_id = int(REQUEST.form["moduleimpl_id"])
if "moduleimpl_id" in vals and vals["moduleimpl_id"]:
moduleimpl_id = int(vals["moduleimpl_id"])
mode = "module"
evals = sco_evaluations.do_evaluation_list({"moduleimpl_id": moduleimpl_id})
if not mode:
@ -77,7 +78,7 @@ def do_evaluation_listenotes(REQUEST):
if not evals:
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
# description de l'evaluation
if mode == "eval":
@ -177,8 +178,8 @@ def do_evaluation_listenotes(REQUEST):
),
]
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
descr,
cancelbutton=None,
submitbutton=None,
@ -201,7 +202,6 @@ def do_evaluation_listenotes(REQUEST):
hide_groups = tf[2]["hide_groups"]
with_emails = tf[2]["with_emails"]
return _make_table_notes(
REQUEST,
tf[1],
evals,
format=format,
@ -214,7 +214,6 @@ def do_evaluation_listenotes(REQUEST):
def _make_table_notes(
REQUEST,
html_form,
evals,
format="",
@ -229,8 +228,8 @@ def _make_table_notes(
return "<p>Aucune évaluation !</p>"
E = evals[0]
moduleimpl_id = E["moduleimpl_id"]
M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
Mod = sco_edit_module.do_module_list(args={"module_id": M["module_id"]})[0]
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
# (debug) check that all evals are in same module:
for e in evals:
@ -316,7 +315,7 @@ def _make_table_notes(
rows.append(
{
"code": code,
"code": str(code), # INE, NIP ou etudid
"_code_td_attrs": 'style="padding-left: 1em; padding-right: 2em;"',
"etudid": etudid,
"nom": etud["nom"].upper(),
@ -375,9 +374,11 @@ def _make_table_notes(
columns_ids.append(e["evaluation_id"])
#
if anonymous_listing:
rows.sort(key=lambda x: x["code"])
rows.sort(key=lambda x: x["code"] or "")
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:
if len(evals) > 1:
@ -482,7 +483,7 @@ def _make_table_notes(
# html_generate_cells=False # la derniere ligne (moyennes) est incomplete
)
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":
return t
@ -760,9 +761,7 @@ def evaluation_check_absences(evaluation_id):
return ValButAbs, AbsNonSignalee, ExcNonSignalee, ExcNonJust, AbsButExc
def evaluation_check_absences_html(
evaluation_id, with_header=True, show_ok=True, REQUEST=None
):
def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True):
"""Affiche etat verification absences d'une evaluation"""
E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0]
@ -778,9 +777,7 @@ def evaluation_check_absences_html(
if with_header:
H = [
html_sco_header.html_sem_header(
REQUEST, "Vérification absences à l'évaluation"
),
html_sco_header.html_sem_header("Vérification absences à l'évaluation"),
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
"""<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.</p>""",
]
@ -817,8 +814,8 @@ def evaluation_check_absences_html(
'<a class="stdlink" href="Absences/doSignaleAbsence?etudid=%s&datedebut=%s&datefin=%s&demijournee=%s&moduleimpl_id=%s">signaler cette absence</a>'
% (
etud["etudid"],
six.moves.urllib.parse.quote(E["jour"]),
six.moves.urllib.parse.quote(E["jour"]),
urllib.parse.quote(E["jour"]),
urllib.parse.quote(E["jour"]),
demijournee,
E["moduleimpl_id"],
)
@ -861,12 +858,11 @@ def evaluation_check_absences_html(
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 !"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [
html_sco_header.html_sem_header(
REQUEST,
"Vérification absences aux évaluations de ce semestre",
sem,
),
@ -876,9 +872,7 @@ def formsemestre_check_absences_html(formsemestre_id, REQUEST=None):
</p>""",
]
# Modules, dans l'ordre
Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id
)
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
for M in Mlist:
evals = sco_evaluations.do_evaluation_list(
{"moduleimpl_id": M["moduleimpl_id"]}
@ -894,7 +888,6 @@ def formsemestre_check_absences_html(formsemestre_id, REQUEST=None):
E["evaluation_id"],
with_header=False,
show_ok=False,
REQUEST=REQUEST,
)
)
if evals:

View File

@ -34,30 +34,170 @@ SCODOC_LOGOS_DIR /opt/scodoc-data/config/logos
"""
import imghdr
import os
import re
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
import PIL
from PIL import Image as PILImage
GLOBAL = "_SERVER" # category for server level logos
LOGOS_DIR_PREFIX = "logos_"
LOGO_FILE_PREFIX = "logo_"
ALLOWED_EXT = "|".join(scu.LOGOS_IMAGES_ALLOWED_TYPES)
FILENAME_PARSER = re.compile(r"logo_([^.]*).(%s)" % ALLOWED_EXT)
def get_logo_filename(logo_type: str, scodoc_dept: str) -> str:
"""return full filename for this logo, or "" if not found
an existing file with extension.
logo_type: "header" or "footer"
scodoc-dept: acronym
def get_logo(logoname, dept_id=None):
"""Recherche le logo 'name' d'abord dans le département puis si non trouvé au niveau global"""
logo = Logo(logoname, dept_id)
try:
logo.read()
except ScoValueError: # logo non trouvé au niveau du département recherche au niveau global
logo = Logo(logoname=logoname, dept_id=None).read()
return logo
def get_logo_filename(name, dept_id=None):
breakpoint()
return get_logo(name, dept_id).read().filename
def get_logo_url(name, dept_id):
return get_logo(name, dept_id).read().get_url()
def write_logo(stream, name, dept_id=None):
Logo(logoname=name, dept_id=dept_id).create(stream)
def list_logos():
inventory = {GLOBAL: _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.acronym] = _list_dept_logos(dept.id)
return inventory
def _list_dept_logos(dept_id=None):
logos = {}
path_dir = Path(scu.SCODOC_LOGOS_DIR)
if dept_id:
path_dir = Path(
os.path.sep.join([scu.SCODOC_LOGOS_DIR, 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).read()
return logos if len(logos.keys()) > 0 else None
class Logo:
"""Responsable des opérations (read, create)
du calcul des chemins
et de la récupération des informations sur un logp
"""
# Search logos in dept specific dir (/opt/scodoc-data/config/logos/logos_<dept>),
# then in config dir /opt/scodoc-data/config/logos/
for image_dir in (
scu.SCODOC_LOGOS_DIR + "/logos_" + scodoc_dept,
scu.SCODOC_LOGOS_DIR, # global logos
):
for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES:
filename = os.path.join(image_dir, f"logo_{logo_type}.{suffix}")
if os.path.isfile(filename) and os.access(filename, os.R_OK):
return filename
return ""
def __init__(self, logoname, dept_id=None):
"""Initialisation des noms et département des logos.
Le format est renseigné au moment de la lecture ou de la création du logo
"""
self.logoname = logoname
self.scodoc_dept = dept_id
self.suffix = None
self.dimensions = None
if self.scodoc_dept:
self.dirname = os.path.sep.join(
[scu.SCODOC_LOGOS_DIR, LOGOS_DIR_PREFIX + secure_filename(str(dept_id))]
)
else:
self.dirname = scu.SCODOC_LOGOS_DIR
self.basename = os.path.sep.join(
[self.dirname, LOGO_FILE_PREFIX + secure_filename(self.logoname)]
)
self.filename = None
def set_format(self, fmt):
self.suffix = fmt
self.filename = self.basename + "." + fmt
def _ensure_directory_exists(self):
"create enclosing directory if necessary"
if not Path(self.dirname).exists():
current_app.logger.info(f"sco_logos creating directory %s", self.dirname)
os.mkdir(self.dirname)
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.basename + "." + 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.basename + "." + suffix)
except IOError:
pass
def read(self):
"""
Récupération des données pour un logo existant (sinon -> Exception)
il doit exister un et un seul fichier image parmi les types autorisés
(sinon on considère le premier trouvé)
permet d'affiner le format d'un logo de format inconnu
"""
for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES:
path = Path(self.basename + "." + suffix)
if path.exists():
self.set_format(suffix)
with open(self.filename, "rb") as f:
img = PILImage.open(f)
self.dimensions = img.size
return self
# if no file found, raise exception
raise ScoValueError(
"Logo %s not found for dept %s" % (self.logoname, self.scodoc_dept)
)
def get_url(self):
if self.scodoc_dept
return url_for(
"scodoc.logo_custom", scodoc_dept=self.scodoc_dept, name=self.logoname
)
# def get_logo_filename(logo_type: str, scodoc_dept: str) -> str:
# """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>),
# # then in config dir /opt/scodoc-data/config/logos/
# for image_dir in (
# scu.SCODOC_LOGOS_DIR + "/logos_" + scodoc_dept,
# scu.SCODOC_LOGOS_DIR, # global logos
# ):
# for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES:
# filename = os.path.join(image_dir, f"logo_{logo_type}.{suffix}")
# if os.path.isfile(filename) and os.access(filename, os.R_OK):
# return filename
#
# return ""
def guess_image_type(stream) -> str:
@ -68,28 +208,3 @@ def guess_image_type(stream) -> str:
if not fmt:
return None
return fmt if fmt != "jpeg" else "jpg"
def _ensure_directory_exists(filename):
"create enclosing directory if necessary"
directory = os.path.split(filename)[0]
if not os.path.exists(directory):
current_app.logger.info(f"sco_logos creating directory %s", directory)
os.mkdir(directory)
def store_image(stream, basename):
img_type = guess_image_type(stream)
if img_type not in scu.LOGOS_IMAGES_ALLOWED_TYPES:
abort(400, "type d'image invalide")
filename = basename + "." + img_type
_ensure_directory_exists(filename)
with open(filename, "wb") as f:
f.write(stream.read())
current_app.logger.info(f"sco_logos.store_image %s", filename)
# erase other formats if they exists
for extension in set(scu.LOGOS_IMAGES_ALLOWED_TYPES) - set([img_type]):
try:
os.unlink(basename + "." + extension)
except IOError:
pass

View File

@ -31,7 +31,7 @@
"""
from operator import itemgetter
from flask import url_for, g
from flask import url_for, g, request
import app
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
de _tous_ les départements.
"""
@ -84,8 +84,8 @@ def scodoc_table_etuds_lycees(format="html", REQUEST=None):
sco_preferences.SemPreferences(),
no_links=True,
)
tab.base_url = REQUEST.URL0
t = tab.make_page(format=format, with_html_headers=False, REQUEST=REQUEST)
tab.base_url = request.base_url
t = tab.make_page(format=format, with_html_headers=False)
if format != "html":
return t
H = [
@ -181,23 +181,22 @@ def formsemestre_etuds_lycees(
format="html",
only_primo=False,
no_grouping=False,
REQUEST=None,
):
"""Table des lycées d'origine"""
tab, etuds_by_lycee = formsemestre_table_etuds_lycees(
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:
tab.base_url += "&only_primo=1"
if no_grouping:
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":
return t
F = [
sco_report.tsp_form_primo_group(
REQUEST, only_primo, no_grouping, formsemestre_id, format
only_primo, no_grouping, formsemestre_id, format
)
]
H = [

View File

@ -88,14 +88,14 @@ def do_modalite_list(*args, **kw):
return _modaliteEditor.list(cnx, *args, **kw)
def do_modalite_create(args, REQUEST=None):
def do_modalite_create(args):
"create a modalite"
cnx = ndb.GetDBConnexion()
r = _modaliteEditor.create(cnx, args)
return r
def do_modalite_delete(oid, REQUEST=None):
def do_modalite_delete(oid):
"delete a modalite"
cnx = ndb.GetDBConnexion()
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
def do_moduleimpl_list(
moduleimpl_id=None, formsemestre_id=None, module_id=None, REQUEST=None
):
def moduleimpl_list(moduleimpl_id=None, formsemestre_id=None, module_id=None):
"list moduleimpls"
args = locals()
cnx = ndb.GetDBConnexion()
@ -110,7 +108,7 @@ def do_moduleimpl_list(
# Ajoute la liste des enseignants
for mo in modimpls:
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):
@ -124,10 +122,11 @@ def do_moduleimpl_edit(args, formsemestre_id=None, cnx=None):
) # > modif moduleimpl
def do_moduleimpl_withmodule_list(
moduleimpl_id=None, formsemestre_id=None, module_id=None, REQUEST=None
def moduleimpl_withmodule_list(
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.
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_module
args = locals()
del args["REQUEST"]
modimpls = do_moduleimpl_list(
modimpls = moduleimpl_list(
**{
"moduleimpl_id": moduleimpl_id,
"formsemestre_id": formsemestre_id,
"module_id": module_id,
}
)
for mo in modimpls:
mo["module"] = sco_edit_module.do_module_list(
args={"module_id": mo["module_id"]}
)[0]
mo["ue"] = sco_edit_ue.do_ue_list(args={"ue_id": mo["module"]["ue_id"]})[0]
mo["matiere"] = sco_edit_matiere.do_matiere_list(
args={"matiere_id": mo["module"]["matiere_id"]}
)[0]
ues = {}
matieres = {}
modules = {}
for mi in modimpls:
module_id = mi["module_id"]
if not mi["module_id"] in modules:
modules[module_id] = sco_edit_module.module_list(
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
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"
args = locals()
cnx = ndb.GetDBConnexion()
return scu.return_text_if_published(
_moduleimpl_inscriptionEditor.list(cnx, args), REQUEST
)
return _moduleimpl_inscriptionEditor.list(cnx, args)
def do_moduleimpl_listeetuds(moduleimpl_id):
def moduleimpl_listeetuds(moduleimpl_id):
"retourne liste des etudids inscrits a ce module"
req = """SELECT DISTINCT Im.etudid
FROM notes_moduleimpl_inscription Im,
@ -244,9 +264,7 @@ def do_moduleimpl_inscription_delete(oid, formsemestre_id=None):
) # > moduleimpl_inscription
def do_moduleimpl_inscrit_etuds(
moduleimpl_id, formsemestre_id, etudids, reset=False, REQUEST=None
):
def do_moduleimpl_inscrit_etuds(moduleimpl_id, formsemestre_id, etudids, reset=False):
"""Inscrit les etudiants (liste d'etudids) a ce module.
Si reset, desinscrit tous les autres.
"""
@ -309,11 +327,11 @@ def do_ens_create(args):
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).
= 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
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
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):
"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
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
if not sem["etat"]:

View File

@ -30,7 +30,8 @@
from operator import itemgetter
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.sco_utils as scu
@ -50,9 +51,7 @@ from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission
def moduleimpl_inscriptions_edit(
moduleimpl_id, etuds=[], submitted=False, REQUEST=None
):
def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
"""Formulaire inscription des etudiants a ce module
* Gestion des inscriptions
Nom TD TA TP (triable)
@ -64,9 +63,9 @@ def moduleimpl_inscriptions_edit(
* 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"]
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)
# -- check lock
if not sem["etat"]:
@ -137,7 +136,7 @@ def moduleimpl_inscriptions_edit(
</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(
"""
<input type="hidden" name="moduleimpl_id" value="%(moduleimpl_id)s"/>
@ -199,7 +198,7 @@ def moduleimpl_inscriptions_edit(
else: # SUBMISSION
# inscrit a ce module tous les etuds selectionnes
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))
#
@ -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
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>
"""
authuser = REQUEST.AUTHENTICATED_USER
authuser = current_user
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
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"]
# Liste des modules
Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id
)
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
# Decrit les inscriptions aux modules:
commons = [] # modules communs a tous les etuds du semestre
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
options.append(mod)
# Page HTML:
H = [
html_sco_header.html_sem_header(REQUEST, "Inscriptions aux modules du semestre")
]
H = [html_sco_header.html_sem_header("Inscriptions aux modules du semestre")]
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)
if UECaps:
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"])
for ue in ues:
H.append(
@ -524,40 +519,39 @@ def is_inscrit_ue(etudid, formsemestre_id, ue_id):
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."""
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"""DELETE FROM notes_moduleimpl_inscription
WHERE moduleimpl_inscription_id IN (
SELECT i.moduleimpl_inscription_id FROM
WHERE id IN (
SELECT i.id FROM
notes_moduleimpl mi, notes_modules mod,
notes_formsemestre sem, notes_moduleimpl_inscription i
WHERE sem.formsemestre_id = %(formsemestre_id)s
AND mi.formsemestre_id = sem.formsemestre_id
AND mod.module_id = mi.module_id
WHERE sem.id = %(formsemestre_id)s
AND mi.formsemestre_id = sem.id
AND mod.id = mi.module_id
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
)
""",
{"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id},
)
if REQUEST:
logdb(
cnx,
method="etud_desinscrit_ue",
etudid=etudid,
msg="desinscription UE %s" % ue_id,
commit=False,
)
logdb(
cnx,
method="etud_desinscrit_ue",
etudid=etudid,
msg="desinscription UE %s" % ue_id,
commit=False,
)
sco_cache.invalidate_formsemestre(
formsemestre_id=formsemestre_id
) # > 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."""
# Verifie qu'il est bien inscrit au semestre
insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(

View File

@ -28,7 +28,7 @@
"""Tableau de bord module
"""
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_login import current_user
@ -55,16 +55,16 @@ from app.scodoc import sco_users
# ported from old DTML code in oct 2009
# 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"
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"])
if (
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
):
@ -80,7 +80,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0, REQUEST=None):
"evaluation_id": evaluation_id,
},
"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,
},
"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
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,
},
"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": {
"evaluation_id": evaluation_id,
},
"enabled": nbnotes == 0
and sco_permissions_check.can_edit_notes(
REQUEST.AUTHENTICATED_USER, E["moduleimpl_id"]
"enabled": sco_permissions_check.can_edit_notes(
current_user, E["moduleimpl_id"]
),
},
{
"title": "Absences ce jour",
"endpoint": "absences.EtatAbsencesDate",
"args": {
"date": six.moves.urllib.parse.quote(E["jour"], safe=""),
"date": urllib.parse.quote(E["jour"], safe=""),
"group_ids": group_id,
},
"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)
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)"""
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"]
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)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
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"]
)
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"])
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>""",
]
try:
sco_moduleimpl.can_change_module_resp(REQUEST, moduleimpl_id)
sco_moduleimpl.can_change_module_resp(moduleimpl_id)
H.append(
"""<a class="stdlink" href="edit_moduleimpl_resp?moduleimpl_id=%s">modifier</a>"""
% moduleimpl_id
@ -515,7 +514,6 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None, REQUEST=None):
moduleimpl_evaluation_menu(
eval["evaluation_id"],
nbnotes=etat["nb_notes"],
REQUEST=REQUEST,
)
)
H.append("</td>")

View File

@ -174,7 +174,7 @@ def _get_formsemestre_infos_from_news(n):
elif n["type"] == NEWS_NOTE:
moduleimpl_id = 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:
return {} # module does not exists anymore
return {} # pas d'indication du module

View File

@ -30,7 +30,8 @@
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.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_permissions_check
from app.scodoc import sco_photos
from app.scodoc import sco_users
from app.scodoc import sco_report
from app.scodoc import sco_etud
from app.scodoc.sco_bulletins import etud_descr_situation_semestre
@ -142,18 +144,18 @@ def _menuScolarite(authuser, sem, etudid):
)
def ficheEtud(etudid=None, REQUEST=None):
def ficheEtud(etudid=None):
"fiche d'informations sur un etudiant"
authuser = REQUEST.AUTHENTICATED_USER
authuser = current_user
cnx = ndb.GetDBConnexion()
if etudid and REQUEST:
if etudid:
# la sidebar est differente s'il y a ou pas un etudid
# voir html_sidebar.sidebar()
REQUEST.form["etudid"] = etudid
g.etudid = etudid
args = sco_etud.make_etud_args(etudid=etudid)
etuds = sco_etud.etudident_list(cnx, args)
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 !")
etud = etuds[0]
etudid = etud["etudid"]
@ -167,7 +169,7 @@ def ficheEtud(etudid=None, REQUEST=None):
info["info_naissance"] += " à " + info["lieu_naissance"]
if 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 (
(not info["domicile"])
and (not info["codepostaldomicile"])
@ -254,10 +256,19 @@ def ficheEtud(etudid=None, REQUEST=None):
with_all_columns=False,
a_url="Notes/",
)
info["link_bul_pdf"] = (
'<span class="link_bul_pdf"><a class="stdlink" href="Notes/etud_bulletins_pdf?etudid=%(etudid)s">tous les bulletins</a></span>'
% etud
)
info[
"link_bul_pdf"
] = 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:
# non inscrit
l = ["<p><b>Etudiant%s non inscrit%s" % (info["ne"], info["ne"])]
@ -269,6 +280,7 @@ def ficheEtud(etudid=None, REQUEST=None):
l.append("</b></b>")
info["liste_inscriptions"] = "\n".join(l)
info["link_bul_pdf"] = ""
info["link_inscrire_ailleurs"] = ""
# Liste des annotations
alist = []
@ -289,9 +301,11 @@ def ficheEtud(etudid=None, REQUEST=None):
title="Supprimer cette annotation",
),
)
author = sco_users.user_info(a["author"])
alist.append(
'<tr><td><span class="annodate">Le %(date)s par %(author)s : </span><span class="annoc">%(comment)s</span></td>%(dellink)s</tr>'
% a
f"""<tr><td><span class="annodate">Le {a['date']} par {author['prenomnom']} :
</span><span class="annoc">{a['comment']}</span></td>{a['dellink']}</tr>
"""
)
info["liste_annotations"] = "\n".join(alist)
# fiche admission
@ -345,7 +359,7 @@ def ficheEtud(etudid=None, REQUEST=None):
# Fichiers archivés:
info["fichiers_archive_htm"] = (
'<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:
@ -392,10 +406,11 @@ def ficheEtud(etudid=None, REQUEST=None):
"inscriptions_mkup"
] = """<div class="ficheinscriptions" id="ficheinscriptions">
<div class="fichetitre">Parcours</div>%s
%s
%s %s
</div>""" % (
info["liste_inscriptions"],
info["link_bul_pdf"],
info["link_inscrire_ailleurs"],
)
#
@ -405,7 +420,7 @@ def ficheEtud(etudid=None, REQUEST=None):
)
else:
info["groupes_row"] = ""
info["menus_etud"] = menus_etud(REQUEST)
info["menus_etud"] = menus_etud(etudid)
tmpl = """<div class="menus_etud">%(menus_etud)s</div>
<div class="ficheEtud" id="ficheEtud"><table>
<tr><td>
@ -487,13 +502,11 @@ def ficheEtud(etudid=None, REQUEST=None):
return header + tmpl % info + html_sco_header.sco_footer()
def menus_etud(REQUEST=None):
def menus_etud(etudid):
"""Menu etudiant (operations sur l'etudiant)"""
if "etudid" not in REQUEST.form:
return ""
authuser = REQUEST.AUTHENTICATED_USER
authuser = current_user
etud = sco_etud.get_etud_info(filled=True)[0]
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
menuEtud = [
{
@ -532,16 +545,14 @@ def menus_etud(REQUEST=None):
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.
Used for popups information windows.
"""
formsemestre_id = sco_formsemestre_status.retreive_formsemestre_from_request()
with_photo = int(with_photo)
etud = sco_etud.get_etud_info(filled=True)[0]
photo_html = sco_photos.etud_photo_html(
etud, title="fiche de " + etud["nom"], REQUEST=REQUEST
)
photo_html = sco_photos.etud_photo_html(etud, title="fiche de " + etud["nom"])
# experimental: may be too slow to be here
etud["codeparcours"], etud["decisions_jury"] = sco_report.get_codeparcoursetud(
etud, prefix="S", separator=", "

View File

@ -535,7 +535,7 @@ class SituationEtudParcoursGeneric(object):
validated = True
return s
def valide_decision(self, decision, REQUEST):
def valide_decision(self, decision):
"""Enregistre la decision (instance de DecisionSem)
Enregistre codes semestre et UE, et autorisations inscription.
"""
@ -588,7 +588,6 @@ class SituationEtudParcoursGeneric(object):
self.etudid,
decision.code_etat,
decision.assiduite,
REQUEST=REQUEST,
)
# -- modification du code du semestre precedent
if self.prev and decision.new_code_prev:
@ -619,7 +618,6 @@ class SituationEtudParcoursGeneric(object):
self.etudid,
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...
REQUEST=REQUEST,
)
sco_cache.invalidate_formsemestre(
@ -897,9 +895,7 @@ def formsemestre_update_validation_sem(
return to_invalidate
def formsemestre_validate_ues(
formsemestre_id, etudid, code_etat_sem, assiduite, REQUEST=None
):
def formsemestre_validate_ues(formsemestre_id, etudid, code_etat_sem, assiduite):
"""Enregistre codes UE, selon état semestre.
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.
@ -920,7 +916,7 @@ def formsemestre_validate_ues(
and ue_status["moy"] >= nt.parcours.NOTES_BARRE_VALID_UE
):
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
code_ue = None
elif valid_semestre:
@ -933,14 +929,13 @@ def formsemestre_validate_ues(
cnx, nt, formsemestre_id, etudid, ue_id, code_ue
)
if REQUEST:
logdb(
cnx,
method="validate_ue",
etudid=etudid,
msg="ue_id=%s code=%s" % (ue_id, code_ue),
commit=False,
)
logdb(
cnx,
method="validate_ue",
etudid=etudid,
msg="ue_id=%s code=%s" % (ue_id, code_ue),
commit=False,
)
cnx.commit()

View File

@ -66,7 +66,7 @@ from app.scodoc.sco_utils import (
LOGOS_IMAGES_ALLOWED_TYPES,
)
from app import log
from app.scodoc.sco_exceptions import ScoGenError
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
import sco_version
PAGE_HEIGHT = defaultPageSize[1]
@ -121,6 +121,7 @@ def makeParas(txt, style, suppress_empty=False):
"""Returns a list of Paragraph instances from a text
with one or more <para> ... </para>
"""
result = []
try:
paras = _splitPara(txt)
if suppress_empty:
@ -133,21 +134,30 @@ def makeParas(txt, style, suppress_empty=False):
if m.group(1): # non empty paragraph
r.append(para)
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:
detail = " " + str(e)
log(traceback.format_exc())
log("Invalid pdf para format: %s" % txt)
return [
result = [
Paragraph(
SU(
'<font color="red"><i>Erreur: format invalide{}</i></font>'.format(
detail
)
),
SU('<font color="red"><i>Erreur: format invalide</i></font>'),
style,
)
]
return result
def bold_paras(L, tag="b", close=None):
@ -340,7 +350,6 @@ def pdf_basic_page(
# Gestion du lock pdf
import threading, time, six.moves.queue, six.moves._thread
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_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"])
if not sem["etat"]:
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)
if moduleimpl_id is None:
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"])
if (
@ -200,4 +200,4 @@ def can_handle_passwd(user, allow_admindepts=False):
if (current_user.dept == user.dept) or allow_admindepts:
return True
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 glob
import io
@ -52,6 +54,7 @@ import requests
import time
import traceback
import PIL
from PIL import Image as PILImage
from flask import request, g
@ -118,7 +121,7 @@ def etud_photo_url(etud, size="small", fast=False):
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)
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)
if not filename:
filename = UNKNOWN_IMAGE_PATH
return _http_jpeg_file(filename, REQUEST=REQUEST)
return _http_jpeg_file(filename)
def _http_jpeg_file(filename, REQUEST=None):
"""returns an image.
This function will be modified when we kill #zope
"""
def _http_jpeg_file(filename):
"""returns an image as a Flask response"""
st = os.stat(filename)
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
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")
if header is not None:
header = header.split(";")[0]
@ -159,20 +152,27 @@ def _http_jpeg_file(filename, REQUEST=None):
try:
dt = datetime.datetime.strptime(header, "%a, %d %b %Y %H:%M:%S GMT")
mod_since = dt.timestamp()
except:
except ValueError:
mod_since = None
if (mod_since is not None) and last_modified <= mod_since:
RESPONSE.setStatus(304) # not modified
return ""
return open(filename, mode="rb").read()
return "", 304 # not modified
#
last_modified_str = time.strftime(
"%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"):
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)
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.
Full-size images are always stored locally in the filesystem.
They are the original uploaded images, converted in jpeg.
"""
return etud_photo_html(
etud=etud, etudid=etudid, title=title, size="orig", REQUEST=REQUEST
)
return etud_photo_html(etud=etud, etudid=etudid, title=title, size="orig")
def photo_pathname(etud, size="orig"):
@ -246,7 +244,10 @@ def store_photo(etud, data):
filesize = len(data)
if filesize < 10 or filesize > MAX_FILE_SIZE:
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:
etud["photo_filename"] = filename
etud["foto"] = None
@ -260,7 +261,7 @@ def store_photo(etud, data):
return 1, "ok"
def suppress_photo(etud, REQUEST=None):
def suppress_photo(etud):
"""Suppress a photo"""
log("suppress_photo etudid=%s" % etud["etudid"])
rel_path = photo_pathname(etud)
@ -278,8 +279,7 @@ def suppress_photo(etud, REQUEST=None):
log("removing file %s" % filename)
os.remove(filename)
# 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)
path = os.path.join(PHOTO_DIR, filename)
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)
# resize:
img = scale_height(img)
@ -341,7 +342,7 @@ def find_new_dir():
def copy_portal_photo_to_fs(etud):
"""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)
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)
r = requests.get(url, timeout=portal_timeout)
except:
log("download failed: exception:\n%s" % traceback.format_exc())
log("called from:\n" + "".join(traceback.format_stack()))
# log("download failed: exception:\n%s" % traceback.format_exc())
# 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)
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)
data = r.content # image bytes
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.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_preferences
import six
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)
if infos:
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:
log("invalid XML response from getEtapes Web Service\n%s" % etapes_url)
# 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
from flask import url_for, g
from flask import url_for, g, request
import app.scodoc.sco_utils as scu
from app.scodoc import sco_abs
@ -164,7 +164,7 @@ def _getEtudInfoGroupes(group_ids, etat=None):
return etuds
def formsemestre_poursuite_report(formsemestre_id, format="html", REQUEST=None):
def formsemestre_poursuite_report(formsemestre_id, format="html"):
"""Table avec informations "poursuite" """
sem = sco_formsemestre.get_formsemestre(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.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(
title="""<h2 class="formsemestre">Poursuite d'études</h2>""",
init_qtip=True,
javascripts=["js/etud_info.js"],
format=format,
REQUEST=REQUEST,
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:
sco_preferences.SemPreferences(formsemestre_id)
- 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:
SemPreferences(formsemestre_id).edit()
@ -111,7 +111,8 @@ get_base_preferences(formsemestre_id)
"""
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.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):
"""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
# search in scu.CONFIG
@ -1408,7 +1409,7 @@ class BasePreferences(object):
{
"initvalue": 1,
"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",
"labels": ["non", "oui"],
"category": "bul",
@ -1891,23 +1892,11 @@ class BasePreferences(object):
def get(self, formsemestre_id, name):
"""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 = {
"dept_id": self.dept_id,
"name": 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"]
if formsemestre_id in self.prefs:
return self.prefs[formsemestre_id].get(name, self.prefs[None][name])
return self.prefs[None][name]
def __contains__(self, item):
return item in self.prefs[None]
@ -1948,6 +1937,15 @@ class BasePreferences(object):
"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:
# crée préférence
log("create pref sem=%s %s=%s" % (formsemestre_id, name, value))
@ -1961,10 +1959,8 @@ class BasePreferences(object):
},
)
modif = True
log("create pref sem=%s %s=%s" % (formsemestre_id, name, value))
else:
# edit existing value
existing_value = pdb[0]["value"] # old stored value
if (
(existing_value != value)
@ -2013,7 +2009,7 @@ class BasePreferences(object):
self._editor.delete(cnx, pdb[0]["pref_id"])
sco_cache.invalidate_formsemestre() # > modif preferences
def edit(self, REQUEST):
def edit(self):
"""HTML dialog: edit global preferences"""
from app.scodoc import html_sco_header
@ -2022,15 +2018,17 @@ class BasePreferences(object):
html_sco_header.sco_header(page_title="Préférences"),
"<h2>Préférences globales pour %s</h2>" % scu.ScoURL(),
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="msg">Attention: cliquez sur "Enregistrer les modifications" en bas de page pour appliquer vos changements !</p>
""",
]
form = self.build_tf_form()
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
form,
initvalues=self.prefs[None],
submitlabel="Enregistrer les modifications",
@ -2140,7 +2138,7 @@ class SemPreferences(object):
return self.base_prefs.is_global(self.formsemestre_id, name)
# The dialog
def edit(self, categories=[], REQUEST=None):
def edit(self, categories=[]):
"""Dialog to edit semestre preferences in given categories"""
from app.scodoc import html_sco_header
from app.scodoc import sco_formsemestre
@ -2151,7 +2149,7 @@ class SemPreferences(object):
) # a bug !
sem = sco_formsemestre.get_formsemestre(self.formsemestre_id)
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="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(("formsemestre_id", {"input_type": "hidden"}))
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
form,
initvalues=self,
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")
elif destination == "again":
return flask.redirect(
REQUEST.URL0 + "?formsemestre_id=" + str(self.formsemestre_id)
request.base_url + "?formsemestre_id=" + str(self.formsemestre_id)
)
elif destination == "global":
return flask.redirect(scu.ScoURL() + "/edit_preferences")
@ -2253,7 +2251,7 @@ function set_global_pref(el, pref_name) {
#
def doc_preferences():
""" Liste les preferences en MarkDown, pour la documentation"""
"""Liste les preferences en MarkDown, pour la documentation"""
L = []
for cat, cat_descr in PREF_CATEGORIES:
L.append([""])

View File

@ -31,6 +31,9 @@ import time
from openpyxl.styles.numbers import FORMAT_NUMBER_00
from flask import request
from flask_login import current_user
import app.scodoc.sco_utils as scu
from app.scodoc import sco_abs
from app.scodoc import sco_groups
@ -45,7 +48,7 @@ from app.scodoc import sco_preferences
from app.scodoc.sco_excel import ScoExcelSheet
def feuille_preparation_jury(formsemestre_id, REQUEST):
def feuille_preparation_jury(formsemestre_id):
"Feuille excel pour preparation des jurys"
nt = sco_cache.NotesTableCache.get(
formsemestre_id
@ -318,9 +321,14 @@ def feuille_preparation_jury(formsemestre_id, REQUEST):
% (
sco_version.SCONAME,
time.strftime("%d/%m/%Y"),
REQUEST.BASE0,
REQUEST.AUTHENTICATED_USER,
request.url_root,
current_user,
)
)
xls = ws.generate_standalone()
return sco_excel.send_excel_file(REQUEST, xls, f"PrepaJury{sn}{scu.XLSX_SUFFIX}")
xls = ws.generate()
return scu.send_file(
xls,
f"PrepaJury{sn}",
scu.XLSX_SUFFIX,
mime=scu.XLSX_MIMETYPE,
)

View File

@ -52,7 +52,7 @@ from reportlab.platypus import Paragraph
from reportlab.lib import styles
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.notesdb as ndb
@ -92,7 +92,7 @@ def _descr_decisions_ues(nt, etudid, decisions_ue, decision_sem):
and sco_codes_parcours.code_semestre_validant(decision_sem["code"])
)
):
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]
uelist.append(ue)
except:
log("descr_decisions_ues: ue_id=%s decisions_ue=%s" % (ue_id, decisions_ue))
@ -495,7 +495,7 @@ def pvjury_table(
return lines, titles, columns_ids
def formsemestre_pvjury(formsemestre_id, format="html", publish=True, REQUEST=None):
def formsemestre_pvjury(formsemestre_id, format="html", publish=True):
"""Page récapitulant les décisions de jury
dpv: result of dict_pvjury
"""
@ -535,13 +535,11 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True, REQUEST=No
return tab.make_page(
format=format,
with_html_headers=False,
REQUEST=REQUEST,
publish=publish,
)
tab.base_url = "%s?formsemestre_id=%s" % (REQUEST.URL0, formsemestre_id)
tab.base_url = "%s?formsemestre_id=%s" % (request.base_url, formsemestre_id)
H = [
html_sco_header.html_sem_header(
REQUEST,
"Décisions du jury pour le semestre",
sem,
init_qtip=True,
@ -599,7 +597,7 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True, REQUEST=No
# ---------------------------------------------------------------------------
def formsemestre_pvjury_pdf(formsemestre_id, group_ids=[], etudid=None, REQUEST=None):
def formsemestre_pvjury_pdf(formsemestre_id, group_ids=[], etudid=None):
"""Generation PV jury en PDF: saisie des paramètres
Si etudid, PV pour un seul etudiant. Sinon, tout les inscrits au groupe indiqué.
"""
@ -628,7 +626,6 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids=[], etudid=None, REQUEST=
H = [
html_sco_header.html_sem_header(
REQUEST,
"Edition du PV de jury %s" % etuddescr,
sem=sem,
javascripts=sco_groups_view.JAVASCRIPTS,
@ -658,8 +655,8 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids=[], etudid=None, REQUEST=
else:
menu_choix_groupe = "" # un seul etudiant à editer
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
descr,
cancelbutton="Annuler",
method="get",
@ -707,7 +704,7 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids=[], etudid=None, REQUEST=
else:
groups_filename = ""
filename = "PV-%s%s-%s.pdf" % (sem["titre_num"], groups_filename, dt)
return scu.sendPDFFile(REQUEST, pdfdoc, filename)
return scu.sendPDFFile(pdfdoc, filename)
def descrform_pvjury(sem):
@ -793,7 +790,7 @@ def descrform_pvjury(sem):
]
def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[], REQUEST=None):
def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
"Lettres avis jury en PDF"
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if not group_ids:
@ -806,8 +803,7 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[], REQUEST=No
H = [
html_sco_header.html_sem_header(
REQUEST,
"Edition des lettres individuelles",
"Édition des lettres individuelles",
sem=sem,
javascripts=sco_groups_view.JAVASCRIPTS,
cssstyles=sco_groups_view.CSSSTYLES,
@ -827,8 +823,8 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[], REQUEST=No
)
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
request.base_url,
scu.get_request_args(),
descr,
cancelbutton="Annuler",
method="POST",
@ -868,7 +864,7 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[], REQUEST=No
dt = time.strftime("%Y-%m-%d")
groups_filename = "-" + groups_infos.groups_filename
filename = "lettres-%s%s-%s.pdf" % (sem["titre_num"], groups_filename, dt)
return scu.sendPDFFile(REQUEST, pdfdoc, filename)
return scu.sendPDFFile(pdfdoc, filename)
def descrform_lettres_individuelles():

View File

@ -27,10 +27,14 @@
"""Tableau recapitulatif des notes d'un semestre
"""
import time
import datetime
import json
import time
from xml.etree import ElementTree
from flask import request
from flask import make_response
import app.scodoc.sco_utils as scu
from app import log
from app.scodoc import html_sco_header
@ -65,7 +69,6 @@ def formsemestre_recapcomplet(
rank_partition_id=None, # si None, calcul rang global
pref_override=True, # si vrai, les prefs ont la priorite sur le param hidebac
force_publishing=True, # publie les XML/JSON meme si bulletins non publiés
REQUEST=None,
):
"""Page récapitulant les notes d'un semestre.
Grand tableau récapitulatif avec toutes les notes de modules
@ -98,9 +101,9 @@ def formsemestre_recapcomplet(
javascripts=["libjs/sorttable.js", "js/etud_info.js"],
),
sco_formsemestre_status.formsemestre_status_head(
formsemestre_id=formsemestre_id, REQUEST=REQUEST
formsemestre_id=formsemestre_id
),
'<form name="f" method="get" action="%s">' % REQUEST.URL0,
'<form name="f" method="get" action="%s">' % request.base_url,
'<input type="hidden" name="formsemestre_id" value="%s"></input>'
% formsemestre_id,
'<input type="hidden" name="pref_override" value="0"></input>',
@ -144,22 +147,22 @@ def formsemestre_recapcomplet(
if hidebac:
H.append("checked")
H.append(""" >cacher bac</input>""")
if tabformat == "xml":
REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE)
H.append(
do_formsemestre_recapcomplet(
REQUEST,
formsemestre_id,
format=tabformat,
hidemodules=hidemodules,
hidebac=hidebac,
modejury=modejury,
sortcol=sortcol,
xml_with_decisions=xml_with_decisions,
rank_partition_id=rank_partition_id,
force_publishing=force_publishing,
)
data = do_formsemestre_recapcomplet(
formsemestre_id,
format=tabformat,
hidemodules=hidemodules,
hidebac=hidebac,
modejury=modejury,
sortcol=sortcol,
xml_with_decisions=xml_with_decisions,
rank_partition_id=rank_partition_id,
force_publishing=force_publishing,
)
if tabformat == "xml":
response = make_response(data)
response.headers["Content-Type"] = scu.XML_MIMETYPE
return response
H.append(data)
if not isFile:
H.append("</form>")
@ -197,7 +200,6 @@ def formsemestre_recapcomplet(
def do_formsemestre_recapcomplet(
REQUEST=None,
formsemestre_id=None,
format="html", # html, xml, xls, xlsall, json
hidemodules=False, # ne pas montrer les modules (ignoré en XML)
@ -227,11 +229,14 @@ def do_formsemestre_recapcomplet(
if format == "xml" or format == "html":
return data
elif format == "csv":
return scu.sendCSVFile(REQUEST, data, filename)
elif format[:3] == "xls":
return sco_excel.send_excel_file(REQUEST, data, filename)
return scu.send_file(data, filename=filename, mime=scu.CSV_MIMETYPE)
elif format.startswith("xls") or format.startswith("xlsx"):
return scu.send_file(data, filename=filename, mime=scu.XLSX_MIMETYPE)
elif format == "json":
return scu.sendJSON(REQUEST, data)
js = json.dumps(data, indent=1, cls=scu.ScoDocJSONEncoder)
return scu.send_file(
js, filename=filename, suffix=scu.JSON_SUFFIX, mime=scu.JSON_MIMETYPE
)
else:
raise ValueError("unknown format %s" % format)
@ -343,7 +348,6 @@ def make_formsemestre_recapcomplet(
if not hidemodules:
h.append("")
pass
if not hidemodules and not ue["is_external"]:
for modimpl in modimpls:
if modimpl["module"]["ue_id"] == ue["ue_id"]:
@ -664,11 +668,11 @@ def make_formsemestre_recapcomplet(
else:
cells = '<tr class="recap_row_odd" id="etudid%s">' % etudid
ir += 1
# XXX nsn = [ x.replace('NA0', '-') for x in l[:-2] ]
# notes sans le NA0:
# XXX nsn = [ x.replace('NA', '-') for x in l[:-2] ]
# notes sans le NA:
nsn = l[:-2] # copy
for i in range(len(nsn)):
if nsn[i] == "NA0":
if nsn[i] == "NA":
nsn[i] = "-"
cells += '<td class="recap_col">%s</td>' % nsn[0] # rang
cells += '<td class="recap_col">%s</td>' % el # nom etud (lien)
@ -953,7 +957,7 @@ def _formsemestre_recapcomplet_json(
return J, "", "json"
def formsemestres_bulletins(annee_scolaire, REQUEST=None):
def formsemestres_bulletins(annee_scolaire):
"""Tous les bulletins des semestres publiés des semestres de l'année indiquée.
:param annee_scolaire(int): année de début de l'année scoalaire
:returns: JSON
@ -967,4 +971,4 @@ def formsemestres_bulletins(annee_scolaire, REQUEST=None):
)
jslist.append(J)
return scu.sendJSON(REQUEST, jslist)
return scu.sendJSON(jslist)

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