forked from ScoDoc/ScoDoc
Compare commits
597 Commits
master
...
reset_pass
Author | SHA1 | Date |
---|---|---|
Jean-Marie Place | 1cf3f283dc | |
Emmanuel Viennet | f7db75e1a2 | |
Emmanuel Viennet | 831d14cf7d | |
Emmanuel Viennet | b8b3185901 | |
Emmanuel Viennet | da5445baa8 | |
Emmanuel Viennet | 69d494c02c | |
Emmanuel Viennet | fa99cbf3d0 | |
Emmanuel Viennet | b6cedbd6b6 | |
Emmanuel Viennet | 55bd15a67b | |
Emmanuel Viennet | ec108a4454 | |
Emmanuel Viennet | a5c0619102 | |
Emmanuel Viennet | 6f0b03242d | |
Emmanuel Viennet | af12191cc4 | |
Emmanuel Viennet | 126f719f7a | |
Emmanuel Viennet | b2893a3371 | |
Emmanuel Viennet | 00b6d19c0c | |
Emmanuel Viennet | 58a6d16d12 | |
Emmanuel Viennet | 0a1264051c | |
Emmanuel Viennet | f7dbff782f | |
Sébastien Lehmann | acdd037483 | |
Emmanuel Viennet | 68dec8e1f8 | |
Emmanuel Viennet | 9172282451 | |
Emmanuel Viennet | 24bfb8a13d | |
Emmanuel Viennet | fbae5d268f | |
Emmanuel Viennet | f2e9fbb8cd | |
Emmanuel Viennet | 0c57aa83ca | |
Emmanuel Viennet | 8b2edca257 | |
Emmanuel Viennet | 0ceb1c8046 | |
Emmanuel Viennet | b1ab7a4df9 | |
Emmanuel Viennet | d68f81b4ea | |
Emmanuel Viennet | 4140f11b7b | |
Emmanuel Viennet | d1ff47727b | |
Emmanuel Viennet | 20c8f22c7b | |
Emmanuel Viennet | d6c6a08828 | |
Emmanuel Viennet | d622b313b0 | |
Emmanuel Viennet | db9acb67dd | |
Emmanuel Viennet | 8a415984c6 | |
Emmanuel Viennet | 6157e54a5f | |
Jean-Marie PLACE | efe997fe55 | |
Emmanuel Viennet | ff948cb98d | |
Emmanuel Viennet | 4b63fe81e4 | |
Emmanuel Viennet | 5895e5c33c | |
Emmanuel Viennet | e3535aa4da | |
Emmanuel Viennet | e1adf93bf0 | |
Emmanuel Viennet | be2227f8a3 | |
Emmanuel Viennet | 9b9b2f270b | |
Emmanuel Viennet | 6fe77988a0 | |
Emmanuel Viennet | a1bb957eaf | |
Emmanuel Viennet | f2e21e0cc2 | |
Emmanuel Viennet | e838321e0c | |
Emmanuel Viennet | 46c64ba78b | |
Emmanuel Viennet | dc004de8ef | |
Emmanuel Viennet | 8bd9cf8956 | |
Emmanuel Viennet | 8ff524bf5f | |
Jean-Marie PLACE | 9f3f2b33e3 | |
Emmanuel Viennet | 9b5b4777e2 | |
Emmanuel Viennet | 9cd31e66f0 | |
Emmanuel Viennet | 2d2b2b2f39 | |
Emmanuel Viennet | 429820b786 | |
Emmanuel Viennet | e7d2094f0b | |
Emmanuel Viennet | 253e42d9f3 | |
Emmanuel Viennet | d12db96389 | |
Emmanuel Viennet | 9eb2c2462b | |
Emmanuel Viennet | 799245b265 | |
Emmanuel Viennet | d86bb3e9b7 | |
Emmanuel Viennet | 5422124d68 | |
Emmanuel Viennet | 8db9a027cb | |
Emmanuel Viennet | 69fc831ef3 | |
Emmanuel Viennet | 3631719f54 | |
Sébastien Lehmann | 6baec61e0e | |
Jean-Marie PLACE | 12f646547e | |
Sébastien Lehmann | be3a9078e2 | |
Emmanuel Viennet | 6dbba98097 | |
Sébastien Lehmann | e0edde3f46 | |
Emmanuel Viennet | 2ab7cef447 | |
Emmanuel Viennet | ce2ae9340c | |
Emmanuel Viennet | 5e31d3372a | |
Emmanuel Viennet | 271d7fba05 | |
Sébastien Lehmann | fdee02d726 | |
Emmanuel Viennet | 06ca136384 | |
Emmanuel Viennet | 58cacb67b8 | |
Emmanuel Viennet | f1c43a5bb8 | |
Emmanuel Viennet | 8532ab5134 | |
Emmanuel Viennet | 8059e1622f | |
Sébastien Lehmann | f263354f88 | |
Sébastien Lehmann | 19c736c894 | |
Emmanuel Viennet | ac5c433f5a | |
Emmanuel Viennet | c882a96556 | |
Emmanuel Viennet | 3d8fe461b9 | |
Emmanuel Viennet | 6d7645a599 | |
Emmanuel Viennet | 43a47f3416 | |
Emmanuel Viennet | 1aa822c906 | |
Emmanuel Viennet | 562888eff6 | |
Sébastien Lehmann | 3114189e4c | |
Sébastien Lehmann | ec61fced73 | |
Sébastien Lehmann | 3346eded05 | |
Sébastien Lehmann | 0b03b0c188 | |
Emmanuel Viennet | 6596e70eec | |
Emmanuel Viennet | 5e65e75a3b | |
Emmanuel Viennet | 4511951255 | |
Emmanuel Viennet | 593317a50a | |
Emmanuel Viennet | 71615613f1 | |
SebL68 | 339af76a33 | |
Emmanuel Viennet | ff1e503d30 | |
Emmanuel Viennet | b709b9c2f1 | |
Emmanuel Viennet | 87bcfc801a | |
Emmanuel Viennet | 8c95edf01c | |
Emmanuel Viennet | 1a7310950b | |
Emmanuel Viennet | eb5a1c727c | |
Emmanuel Viennet | b153c9ad9e | |
Emmanuel Viennet | 78c7c1a763 | |
Emmanuel Viennet | 40b31602d2 | |
Emmanuel Viennet | 5f409f0267 | |
Emmanuel Viennet | 3ec5cef3c0 | |
Emmanuel Viennet | d40d82aeb7 | |
Emmanuel Viennet | bf14f8ed34 | |
Emmanuel Viennet | d89d1be041 | |
Emmanuel Viennet | ef8e9b4ef0 | |
Emmanuel Viennet | e06cf82db8 | |
Emmanuel Viennet | 1b2573d130 | |
Jean-Marie Place | 051e0d24e2 | |
Emmanuel Viennet | 79e2c9476b | |
Emmanuel Viennet | 25a441f7f2 | |
Emmanuel Viennet | 4d6d7ad168 | |
Emmanuel Viennet | 0d75d64e85 | |
Emmanuel Viennet | af5c79d95d | |
Emmanuel Viennet | 683b760341 | |
Jean-Marie Place | 56d9681c87 | |
Emmanuel Viennet | 8bf5a0ab3e | |
Emmanuel Viennet | 83f8f2ddbc | |
Emmanuel Viennet | ae42db06da | |
Emmanuel Viennet | 69431b4de0 | |
Emmanuel Viennet | 5e492dc145 | |
Emmanuel Viennet | 862ffb89a1 | |
Emmanuel Viennet | 66f39b8f16 | |
Emmanuel Viennet | 1e7a509879 | |
Emmanuel Viennet | 44237c648b | |
Emmanuel Viennet | 79c4b33bee | |
Emmanuel Viennet | 2244f29c33 | |
Emmanuel Viennet | 28f266fac6 | |
Emmanuel Viennet | b319d9c0d3 | |
Emmanuel Viennet | 13d0e462cc | |
Emmanuel Viennet | 4aa4ab316c | |
Emmanuel Viennet | 5c90448656 | |
Emmanuel Viennet | cf939587b1 | |
Emmanuel Viennet | 4a5509694f | |
pascal.bouron | 861a7da8a1 | |
Emmanuel Viennet | 10941b6ef4 | |
Emmanuel Viennet | 3eda56e89c | |
Emmanuel Viennet | 848a168f02 | |
Emmanuel Viennet | 567d95b61d | |
Emmanuel Viennet | d6ebb55b95 | |
Emmanuel Viennet | cf996e46d0 | |
Emmanuel Viennet | abc7fb3378 | |
Emmanuel Viennet | 44d2ffb240 | |
Emmanuel Viennet | 711ca9c220 | |
Emmanuel Viennet | 80b8956af5 | |
Emmanuel Viennet | 77be33d046 | |
Emmanuel Viennet | 547e5f989d | |
Emmanuel Viennet | f94d40d316 | |
Emmanuel Viennet | fea2078201 | |
Emmanuel Viennet | 1e96d72ab1 | |
Jean-Marie Place | 235ca69a82 | |
Jean-Marie Place | 4f8f327cb6 | |
Jean-Marie Place | 8a5c4b5ced | |
Emmanuel Viennet | aa78346a06 | |
Jean-Marie Place | 23f1dc4ed2 | |
Jean-Marie Place | 2920c6f131 | |
Jean-Marie Place | 483c22678a | |
Jean-Marie Place | d8091b4efb | |
Jean-Marie Place | a6c95b013b | |
Jean-Marie Place | 51506c6d6f | |
Jean-Marie Place | df3439351d | |
Jean-Marie Place | b336a1c1a2 | |
Jean-Marie Place | 29c9982afc | |
Jean-Marie Place | b3e1659049 | |
Jean-Marie Place | f219b8d003 | |
Jean-Marie Place | ecd637fb39 | |
Jean-Marie Place | d7e34b8ce2 | |
Jean-Marie Place | 5e461f7dd6 | |
Emmanuel Viennet | 915d4059a7 | |
Emmanuel Viennet | 5c2c97cfb3 | |
Emmanuel Viennet | 46d9316984 | |
Emmanuel Viennet | 49db8c3c71 | |
Emmanuel Viennet | c2c1c5b5f2 | |
Emmanuel Viennet | 2cca1e9bbd | |
Emmanuel Viennet | 272de740e1 | |
Emmanuel Viennet | 2f9b2a5a2d | |
Emmanuel Viennet | e5324e214c | |
Emmanuel Viennet | 517ee8bd2d | |
Emmanuel Viennet | c332f1cee7 | |
Emmanuel Viennet | 0c49cfaf78 | |
Emmanuel Viennet | acebc8ab08 | |
Emmanuel Viennet | 8ce6c907f3 | |
Emmanuel Viennet | 23d908b932 | |
Emmanuel Viennet | 478688fe49 | |
Emmanuel Viennet | aeb0d67f38 | |
Emmanuel Viennet | ab1898b185 | |
Emmanuel Viennet | 857c3007a5 | |
Emmanuel Viennet | 11b3f64319 | |
Emmanuel Viennet | 270d03057f | |
Emmanuel Viennet | 45f4fe3e12 | |
Emmanuel Viennet | 36b432839a | |
Emmanuel Viennet | 9927368680 | |
Jean-Marie Place | f40817dbf5 | |
Emmanuel Viennet | 5b7adf16ec | |
Emmanuel Viennet | 7d5160bb83 | |
Emmanuel Viennet | 902df8f886 | |
Emmanuel Viennet | f2db115fdf | |
Emmanuel Viennet | 6cc76037a6 | |
Emmanuel Viennet | fa4dcb3169 | |
Emmanuel Viennet | 3b30a67b17 | |
BOURON PASCAL | 065fc460cd | |
Emmanuel Viennet | 177f23b48c | |
Emmanuel Viennet | 20470484d1 | |
Emmanuel Viennet | e4b9a48ebf | |
Emmanuel Viennet | fd80ee2452 | |
Emmanuel Viennet | 3ba30f6250 | |
Emmanuel Viennet | 1a673862aa | |
Emmanuel Viennet | 507ff12642 | |
Emmanuel Viennet | 24440f457b | |
Emmanuel Viennet | f555122989 | |
Emmanuel Viennet | d79f376b24 | |
Emmanuel Viennet | f105e1f331 | |
Emmanuel Viennet | 3aa115e7a2 | |
Emmanuel Viennet | 5fc0363265 | |
Emmanuel Viennet | cd694a956d | |
Emmanuel Viennet | 2e1ec1c5ea | |
Emmanuel Viennet | b1bc8b3f41 | |
BOURON PASCAL | 540a956fae | |
Emmanuel Viennet | c455f6261f | |
Emmanuel Viennet | 958539977a | |
Emmanuel Viennet | 6627a9c6b2 | |
Emmanuel Viennet | ec93a8cdbc | |
Jean-Marie Place | 0a000afba4 | |
Emmanuel Viennet | 4d857a1567 | |
Emmanuel Viennet | e88e280994 | |
Emmanuel Viennet | 9ee0acd60b | |
Emmanuel Viennet | d435f3b835 | |
Emmanuel Viennet | d2b69c2f73 | |
Emmanuel Viennet | 440e9157b4 | |
Emmanuel Viennet | 5d8dad3711 | |
Emmanuel Viennet | 83f522f08c | |
Emmanuel Viennet | 47e752c95c | |
Emmanuel Viennet | 8647203f43 | |
Emmanuel Viennet | fa896b77ab | |
Emmanuel Viennet | 09eb73be4a | |
Emmanuel Viennet | be8925e163 | |
Emmanuel Viennet | 7819d382e4 | |
Emmanuel Viennet | 47c1a75bb0 | |
Emmanuel Viennet | baa5286dae | |
Emmanuel Viennet | 8fc16dfeda | |
Emmanuel Viennet | 21fa7112c8 | |
Emmanuel Viennet | 480af81e0d | |
Emmanuel Viennet | 5f868fd27c | |
Emmanuel Viennet | 5a3c25e67f | |
Emmanuel Viennet | d05ce6a93b | |
pascal.bouron | f47cd46abc | |
Emmanuel Viennet | 83ba9cf186 | |
Emmanuel Viennet | daa06651d5 | |
Emmanuel Viennet | 1b7a28ac8d | |
Emmanuel Viennet | 502f6a9277 | |
Emmanuel Viennet | 4f90404c3a | |
Emmanuel Viennet | a83ab8f684 | |
Emmanuel Viennet | 5e2e36cfab | |
Emmanuel Viennet | 6c747b1f0e | |
Emmanuel Viennet | adbbd51cf1 | |
Emmanuel Viennet | ea8598d411 | |
Emmanuel Viennet | 3a0a2382c8 | |
Emmanuel Viennet | 504b12cadb | |
Emmanuel Viennet | 780a117fbd | |
Emmanuel Viennet | 042d5080b2 | |
Emmanuel Viennet | 08d4258a49 | |
Emmanuel Viennet | bfd60ffce4 | |
Emmanuel Viennet | 84f25817d1 | |
Emmanuel Viennet | e706407bcb | |
Emmanuel Viennet | 01ea6286ee | |
Emmanuel Viennet | b28ffdd7a8 | |
Emmanuel Viennet | b2c98af293 | |
Emmanuel Viennet | e3219a6b0c | |
Emmanuel Viennet | 8f4299e880 | |
Emmanuel Viennet | 58a7508043 | |
Emmanuel Viennet | fc6da6c976 | |
Emmanuel Viennet | e26dfd7042 | |
pascal.bouron | 673ac2fdc9 | |
pascal.bouron | 51430f82bd | |
pascal.bouron | 8dc98c64f1 | |
Emmanuel Viennet | 7ebd6f31dc | |
pascal.bouron | 5d9513315b | |
pascal.bouron | 9d8b2d5071 | |
jeromemartin | 9e8da925a5 | |
Emmanuel Viennet | 52fa49d7f6 | |
pascal.bouron | 4d9d5293c1 | |
Emmanuel Viennet | 14ab816bee | |
Emmanuel Viennet | 4182fd494c | |
Emmanuel Viennet | 5b534abf5f | |
Emmanuel Viennet | f6b2297bd3 | |
Emmanuel Viennet | 1488689bfb | |
Emmanuel Viennet | 459e75db89 | |
Emmanuel Viennet | 477c2efac9 | |
Emmanuel Viennet | c0dd83fadb | |
Emmanuel Viennet | 89e7250f4a | |
Emmanuel Viennet | d71b399c3d | |
Emmanuel Viennet | 47d728376c | |
Emmanuel Viennet | ce4115eeef | |
Emmanuel Viennet | 0e6513e339 | |
Emmanuel Viennet | 12c0af8bb6 | |
Jean-Marie Place | c23a5abb6c | |
Emmanuel Viennet | 090391300a | |
Emmanuel Viennet | 139eb8171a | |
Emmanuel Viennet | ddf3d73c92 | |
Emmanuel Viennet | 94325cef2c | |
Emmanuel Viennet | 03cfcf3298 | |
Emmanuel Viennet | 0573b259d9 | |
Emmanuel Viennet | a4e4c39797 | |
Emmanuel Viennet | ade1b4445c | |
Emmanuel Viennet | 7f2e87e9d0 | |
Emmanuel Viennet | 3bb9a5cb76 | |
Emmanuel Viennet | 981f2c0e46 | |
Emmanuel Viennet | ae525fd267 | |
Emmanuel Viennet | a25ebe9a46 | |
Emmanuel Viennet | ac09c104e9 | |
Emmanuel Viennet | 50115337b1 | |
Emmanuel Viennet | afb94cb011 | |
Emmanuel Viennet | f6dfa912d7 | |
Emmanuel Viennet | 5f19931d63 | |
Emmanuel Viennet | bd5c4d8243 | |
Emmanuel Viennet | 2f72401ba1 | |
Emmanuel Viennet | f534e9757f | |
Emmanuel Viennet | 4bf983dbe4 | |
Emmanuel Viennet | 23a59357e9 | |
Emmanuel Viennet | 81720facff | |
Emmanuel Viennet | 518b9c049c | |
Emmanuel Viennet | 1c719b5c7c | |
Emmanuel Viennet | ddcc518807 | |
Emmanuel Viennet | 5002afade1 | |
Emmanuel Viennet | 39a9f353d2 | |
Emmanuel Viennet | 7589d4cc34 | |
Emmanuel Viennet | 01a84f3b12 | |
Emmanuel Viennet | 9f9cb6cca2 | |
Emmanuel Viennet | d8e1c428b0 | |
Emmanuel Viennet | 4fc31d8b47 | |
Emmanuel Viennet | e46c6a410f | |
Jean-Marie Place | 68ac7c293a | |
Jean-Marie Place | 8f1e465280 | |
Jean-Marie Place | c248def7f2 | |
Jean-Marie Place | 2b91fd78df | |
Emmanuel Viennet | b1aa36b136 | |
Emmanuel Viennet | d2f41b6a21 | |
Emmanuel Viennet | db937ca7c5 | |
Jean-Marie Place | 461f14631b | |
Emmanuel Viennet | 668210aaef | |
Emmanuel Viennet | 0da60384a1 | |
Emmanuel Viennet | c29199eff4 | |
Emmanuel Viennet | 5268ea4f13 | |
IDK | 1be2ba1498 | |
Emmanuel Viennet | 66d443944a | |
Emmanuel Viennet | ad0cd6236c | |
Emmanuel Viennet | e8ce1e303e | |
Emmanuel Viennet | 2fe9e5ec39 | |
Emmanuel Viennet | c49aecaa2f | |
Emmanuel Viennet | 0f67ee33ae | |
Emmanuel Viennet | 280f6cf1c1 | |
Emmanuel Viennet | 92de66f734 | |
Emmanuel Viennet | f73e720de1 | |
Emmanuel Viennet | 0c913dacdc | |
Emmanuel Viennet | 66dbec86bf | |
Emmanuel Viennet | e56a97eaf6 | |
IDK | 3878d68b38 | |
IDK | e249f45ce9 | |
Emmanuel Viennet | 54ed09ed08 | |
Emmanuel Viennet | 565055b4e5 | |
Emmanuel Viennet | 63d73c9ecd | |
Jean-Marie Place | d69a6c283f | |
Jean-Marie Place | 264c0d7d9e | |
Emmanuel Viennet | 29ec51c001 | |
Emmanuel Viennet | a909a307c0 | |
Emmanuel Viennet | c96b114b08 | |
Jean-Marie Place | 390118226d | |
Emmanuel Viennet | bb7ed682c0 | |
Emmanuel Viennet | 256e89605b | |
Emmanuel Viennet | 2ca91fc4e9 | |
Emmanuel Viennet | c56a4257bd | |
Jean-Marie Place | 3c38ef4cc0 | |
Emmanuel Viennet | c658c7675e | |
Jean-Marie Place | 7cc9f6d1f4 | |
Emmanuel Viennet | c05e763900 | |
Jean-Marie Place | 4ce50927b0 | |
Jean-Marie Place | 177a891236 | |
Emmanuel Viennet | 9c50b58d5f | |
Emmanuel Viennet | c2de33f7f5 | |
Jean-Marie Place | c68633bf5b | |
Jean-Marie Place | 45d20789dd | |
Emmanuel Viennet | 93a23ff112 | |
Emmanuel Viennet | e8e3423193 | |
Emmanuel Viennet | e243fe6bb0 | |
Emmanuel Viennet | 46269fcebe | |
Emmanuel Viennet | a539061c1f | |
Emmanuel Viennet | 9694ba61c4 | |
Jean-Marie Place | 9c528bec7f | |
Emmanuel Viennet | 1b8186e69b | |
Emmanuel Viennet | f0d641a31e | |
Jean-Marie Place | feb57c2ac6 | |
Jean-Marie Place | 071c15af79 | |
Emmanuel Viennet | dc26d1edea | |
Emmanuel Viennet | 6e1bc9665d | |
Emmanuel Viennet | c1d13d6089 | |
IDK | aed2d6ce10 | |
Emmanuel Viennet | 3c5b721a3a | |
Emmanuel Viennet | 165220e2f1 | |
Emmanuel Viennet | 17cfd7ad79 | |
Emmanuel Viennet | 179442aa69 | |
Emmanuel Viennet | e6e1835cca | |
Emmanuel Viennet | fdd7af6a8a | |
Jean-Marie Place | 7d5eff4f82 | |
Jean-Marie Place | 76bc957373 | |
Emmanuel Viennet | d980c6794a | |
Emmanuel Viennet | cd6fd10abf | |
Emmanuel Viennet | 3e45762382 | |
Emmanuel Viennet | 085ef05e01 | |
Emmanuel Viennet | 7dda35d37e | |
Emmanuel Viennet | b38ee4ea25 | |
Emmanuel Viennet | 47f1497e5e | |
Emmanuel Viennet | 8667bd58ba | |
Emmanuel Viennet | 1f688e2cd5 | |
Emmanuel Viennet | 52e837dc81 | |
Emmanuel Viennet | 190304043d | |
Jean-Marie Place | 9015780eb7 | |
Jean-Marie Place | 19586559ba | |
Emmanuel Viennet | 5ac5f5eb19 | |
Emmanuel Viennet | ef6a6d6ec2 | |
Emmanuel Viennet | 1fe814a674 | |
Jean-Marie Place | 8ab9a67fa6 | |
Jean-Marie Place | bf83d8475a | |
Emmanuel Viennet | 79b8520034 | |
Emmanuel Viennet | 54f0b87d39 | |
Emmanuel Viennet | 51bd6ba141 | |
Emmanuel Viennet | 3e1136a077 | |
Emmanuel Viennet | 0ab9a281a9 | |
Emmanuel Viennet | e32d7b1b4e | |
Emmanuel Viennet | f59308b863 | |
Emmanuel Viennet | dd8a07ef64 | |
Emmanuel Viennet | bc112efd76 | |
Emmanuel Viennet | 1781548b66 | |
Emmanuel Viennet | 1c927cb541 | |
Emmanuel Viennet | 814a8dbc24 | |
Jean-Marie Place | 4a3e37d371 | |
Emmanuel Viennet | a447c6e5f9 | |
Emmanuel Viennet | 8463d368a1 | |
Emmanuel Viennet | 1f125d3a1d | |
Emmanuel Viennet | 51fec2d301 | |
Emmanuel Viennet | 8bfa936361 | |
Emmanuel Viennet | 1c27ec7dc2 | |
Emmanuel Viennet | dffb369bb0 | |
Emmanuel Viennet | 656c8a9f22 | |
Emmanuel Viennet | 5dfdf4265e | |
Emmanuel Viennet | 36c22a7ca7 | |
Emmanuel Viennet | 4728e77a7b | |
Emmanuel Viennet | f79003186a | |
Emmanuel Viennet | 11ef8857e2 | |
Emmanuel Viennet | f5529ec4a6 | |
Emmanuel Viennet | 550a7888bf | |
Emmanuel Viennet | a8198f889a | |
Emmanuel Viennet | 651f111839 | |
Emmanuel Viennet | 76af0eb166 | |
Emmanuel Viennet | bf57f2bfa5 | |
Emmanuel Viennet | c7aba95015 | |
Emmanuel Viennet | f012fe6fcf | |
Emmanuel Viennet | d577066911 | |
Emmanuel Viennet | b1fa9b8ef8 | |
Emmanuel Viennet | 2a1c541fbd | |
Emmanuel Viennet | 1b89010b45 | |
Emmanuel Viennet | 59e1fdc15e | |
Emmanuel Viennet | 4429ffd3c8 | |
Emmanuel Viennet | 057832c309 | |
Jean-Marie Place | bb47e89e97 | |
Jean-Marie Place | ccd1a0daba | |
Jean-Marie Place | 230c7d488e | |
Jean-Marie Place | 9ee7dec202 | |
Emmanuel Viennet | 18d6324488 | |
Jean-Marie Place | 16b3701815 | |
Jean-Marie Place | 5ea4e74117 | |
Emmanuel Viennet | ce31d3148d | |
Emmanuel Viennet | fa5539fd75 | |
Emmanuel Viennet | ddf4bf788f | |
Emmanuel Viennet | 14d533b38a | |
Emmanuel Viennet | 671ef6a7fa | |
Emmanuel Viennet | edc6da3005 | |
Emmanuel Viennet | b015cf3f88 | |
Emmanuel Viennet | a2c16207cb | |
Jean-Marie Place | 00dbd25b42 | |
Emmanuel Viennet | 4e59b9597b | |
Emmanuel Viennet | f1660e12e1 | |
Emmanuel Viennet | cb03cc962c | |
Jean-Marie Place | 81df68b491 | |
Emmanuel Viennet | 1741e75f72 | |
Emmanuel Viennet | c41726c4a8 | |
Emmanuel Viennet | 7879c176dd | |
Emmanuel Viennet | 75f43bbdde | |
Emmanuel Viennet | 0a50edc9f0 | |
Emmanuel Viennet | 373feece76 | |
Emmanuel Viennet | 6d1ffb122b | |
Emmanuel Viennet | 92c401f17c | |
Emmanuel Viennet | 36c7358eed | |
Emmanuel Viennet | 9c5408f503 | |
Emmanuel Viennet | 2add3e12cc | |
Emmanuel Viennet | d0ab9dc66a | |
Emmanuel Viennet | beeca54a94 | |
Emmanuel Viennet | 73cf9a6f4d | |
Emmanuel Viennet | fede1ae7af | |
Jean-Marie Place | 845152afdd | |
Jean-Marie Place | a4d091fa2d | |
Jean-Marie Place | ffa7e07cd3 | |
Emmanuel Viennet | 865192bc0d | |
Jean-Marie Place | b56f205e89 | |
Emmanuel Viennet | eded2fffe9 | |
Emmanuel Viennet | 13f1539282 | |
Emmanuel Viennet | ae51e4c17a | |
Emmanuel Viennet | 7214627994 | |
Emmanuel Viennet | 6cc1b60da4 | |
Jean-Marie Place | 4297d36dad | |
Emmanuel Viennet | 2999199b19 | |
Emmanuel Viennet | f516ccdfe7 | |
Emmanuel Viennet | 2c97349acf | |
Emmanuel Viennet | 5dfc64a62d | |
Jean-Marie Place | 9dd8198c7b | |
Emmanuel Viennet | f18a9c7559 | |
Emmanuel Viennet | 985c6df3b6 | |
Emmanuel Viennet | 286e9cdc2f | |
Emmanuel Viennet | 0381576750 | |
Emmanuel Viennet | 7a0a04bdb3 | |
Emmanuel Viennet | 35f23995aa | |
Emmanuel Viennet | 29221666a4 | |
Emmanuel Viennet | d7e6a7d714 | |
Jean-Marie Place | 179be1baa0 | |
Jean-Marie Place | a5ed9b815f | |
Emmanuel Viennet | 13c027fc19 | |
Emmanuel Viennet | 31505e1330 | |
Emmanuel Viennet | 9a9dc4a483 | |
Emmanuel Viennet | 11ba73d264 | |
Emmanuel Viennet | 7daa49f2aa | |
Jean-Marie Place | f7961a135a | |
Jean-Marie Place | c955870e1e | |
Jean-Marie Place | 80f5536de5 | |
Jean-Marie Place | 2519d08e40 | |
Emmanuel Viennet | 987800c30e | |
Jean-Marie Place | 2a72fb881b | |
Jean-Marie Place | 87ecd09f0e | |
Jean-Marie Place | 6e7a104fb0 | |
Jean-Marie Place | b03eee12a1 | |
Jean-Marie Place | 44117fb0e2 | |
Jean-Marie Place | 42ef9f795f | |
Emmanuel Viennet | bd2e0ccde5 | |
Jean-Marie Place | 5f0f437f2e | |
Jean-Marie Place | b6cc251c94 | |
Jean-Marie Place | 5f6c434497 | |
Emmanuel Viennet | 45352d9248 | |
Jean-Marie Place | b3225e07f7 | |
Jean-Marie Place | 0ef822cfd8 | |
Jean-Marie Place | a23ae38014 | |
Emmanuel Viennet | 7d59b52018 | |
Emmanuel Viennet | 80238545f3 | |
Emmanuel Viennet | 72e075530c | |
Emmanuel Viennet | 91cc421ef8 | |
Emmanuel Viennet | 8b6a569a31 | |
Emmanuel Viennet | c8949e870f | |
Emmanuel Viennet | 30481e4729 | |
Emmanuel Viennet | 085aff657a | |
Emmanuel Viennet | 3666f8b1ec | |
Emmanuel Viennet | bec7deb581 | |
Emmanuel Viennet | 6dbbcde454 | |
Emmanuel Viennet | 9578c789dc | |
Emmanuel Viennet | 0fedb7771c | |
Emmanuel Viennet | 6dba8933c4 | |
Emmanuel Viennet | 5efc493542 | |
Emmanuel Viennet | a34dd656be | |
Emmanuel Viennet | 2ec2be4234 | |
Emmanuel Viennet | 49609fa657 | |
Emmanuel Viennet | 8a16216d4b | |
Emmanuel Viennet | 96f457260f | |
Emmanuel Viennet | 0f9b52bc9b | |
Emmanuel Viennet | 83174f2f5e | |
Emmanuel Viennet | 3fbda90a2f | |
Emmanuel Viennet | de206674d9 | |
Emmanuel Viennet | b06f37b18e | |
Emmanuel Viennet | 3496cc7beb | |
Jean-Marie Place | 01c264c3c7 | |
Jean-Marie Place | c44aa808df | |
Jean-Marie Place | c8872bd220 | |
Jean-Marie Place | 7f63ab222b | |
Jean-Marie Place | ed07e42222 | |
Jean-Marie Place | 35768e9241 | |
Jean-Marie Place | 050e54de3e | |
Jean-Marie Place | 37484b7fc9 | |
Jean-Marie Place | f828134ea2 | |
Jean-Marie Place | a4d0205cc7 | |
Jean-Marie Place | 770ccb4d6e |
|
@ -170,3 +170,4 @@ Thumbs.db
|
|||
*.code-workspace
|
||||
|
||||
|
||||
copy
|
||||
|
|
68
README.md
68
README.md
|
@ -1,9 +1,7 @@
|
|||
|
||||
# ScoDoc - Gestion de la scolarité - Version ScoDoc 9
|
||||
|
||||
(c) Emmanuel Viennet 1999 - 2021 (voir LICENCE.txt)
|
||||
|
||||
VERSION EXPERIMENTALE - NE PAS DEPLOYER - TESTS EN COURS
|
||||
(c) Emmanuel Viennet 1999 - 2022 (voir LICENCE.txt)
|
||||
|
||||
Installation: voir instructions à jour sur <https://scodoc.org/GuideInstallDebian11>
|
||||
|
||||
|
@ -11,8 +9,6 @@ Documentation utilisateur: <https://scodoc.org>
|
|||
|
||||
## Version ScoDoc 9
|
||||
|
||||
N'utiliser que pour les développements et tests.
|
||||
|
||||
La version ScoDoc 9 est basée sur Flask (au lieu de Zope) et sur
|
||||
**python 3.9+**.
|
||||
|
||||
|
@ -22,15 +18,12 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
|
|||
|
||||
|
||||
|
||||
### État actuel (27 août 21)
|
||||
### État actuel (4 dec 21)
|
||||
|
||||
- Tests en cours, notamment système d'installation et de migration.
|
||||
- 9.0 (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf:
|
||||
- ancien module "Entreprises" (obsolète)
|
||||
|
||||
**Fonctionnalités non intégrées:**
|
||||
|
||||
- feuille "placement" (en cours)
|
||||
|
||||
- ancien module "Entreprises" (obsolete)
|
||||
- 9.1 (branche "PNBUT") est la version de développement.
|
||||
|
||||
|
||||
### Lignes de commandes
|
||||
|
@ -46,7 +39,7 @@ les fichiers locaux (archives, photos, configurations, logs) sous
|
|||
postgresql et la configuration du système Linux.
|
||||
|
||||
### Fichiers locaux
|
||||
Sous `/opt/scodoc-data`, fichiers et répertoires appartienant à l'utilisateur `scodoc`.
|
||||
Sous `/opt/scodoc-data`, fichiers et répertoires appartenant à l'utilisateur `scodoc`.
|
||||
Ils ne doivent pas être modifiés à la main, sauf certains fichiers de configuration sous
|
||||
`/opt/scodoc-data/config`.
|
||||
|
||||
|
@ -93,11 +86,22 @@ exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
|
|||
|
||||
### Tests unitaires
|
||||
|
||||
Les tests unitaires utilisent normalement la base postgresql `SCODOC_TEST`.
|
||||
Avant le premier lancement, créer cette base ainsi:
|
||||
|
||||
./tools/create_database.sh SCODOC_TEST
|
||||
export FLASK_ENV=test
|
||||
flask db upgrade
|
||||
|
||||
Cette commande n'est nécessaire que la première fois (le contenu de la base
|
||||
est effacé au début de chaque test, mais son schéma reste) et aussi si des
|
||||
migrations (changements de schéma) ont eu lieu dans le code.
|
||||
|
||||
Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les
|
||||
scripts de tests:
|
||||
Lancer au préalable:
|
||||
|
||||
flask sco-delete-dept TEST00 && flask sco-create-dept TEST00
|
||||
flask delete-dept TEST00 && flask create-dept TEST00
|
||||
|
||||
Puis dérouler les tests unitaires:
|
||||
|
||||
|
@ -110,13 +114,16 @@ Ou avec couverture (`pip install pytest-cov`)
|
|||
|
||||
#### Utilisation des tests unitaires pour initialiser la base de dev
|
||||
On peut aussi utiliser les tests unitaires pour mettre la base
|
||||
de données de développement dans un état connu, par exemple pour éviter de recréer à la main étudianst et semestres quand on développe.
|
||||
de données de développement dans un état connu, par exemple pour éviter de
|
||||
recréer à la main étudiants et semestres quand on développe.
|
||||
|
||||
Il suffit de positionner une variable d'environnement indiquant la BD utilisée par les tests:
|
||||
Il suffit de positionner une variable d'environnement indiquant la BD
|
||||
utilisée par les tests:
|
||||
|
||||
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
|
||||
|
||||
puis de les lancer normalement, par exemple:
|
||||
(si elle n'existe pas, voir plus loin pour la créer) puis de les lancer
|
||||
normalement, par exemple:
|
||||
|
||||
pytest tests/unit/test_sco_basic.py
|
||||
|
||||
|
@ -132,12 +139,13 @@ base de données (tous les départements, et les utilisateurs) avant de commence
|
|||
|
||||
On utilise SQLAlchemy avec Alembic et Flask-Migrate.
|
||||
|
||||
flask db migrate -m "ScoDoc 9.0.x: ..." # ajuster le message !
|
||||
flask db migrate -m "message explicatif....."
|
||||
flask db upgrade
|
||||
|
||||
Ne pas oublier de commiter les migrations (`git add migrations` ...).
|
||||
Ne pas oublier de d'ajouter le script de migration à git (`git add migrations/...`).
|
||||
|
||||
Mémo pour développeurs: séquence re-création d'une base:
|
||||
**Mémo**: séquence re-création d'une base (vérifiez votre `.env`
|
||||
ou variables d'environnement pour interroger la bonne base !).
|
||||
|
||||
dropdb SCODOC_DEV
|
||||
tools/create_database.sh SCODOC_DEV # créé base SQL
|
||||
|
@ -152,7 +160,25 @@ Si la base utilisée pour les dev n'est plus en phase avec les scripts de
|
|||
migration, utiliser les commandes `flask db history`et `flask db stamp`pour se
|
||||
positionner à la bonne étape.
|
||||
|
||||
# Paquet debian 11
|
||||
### Profiling
|
||||
|
||||
Sur une machine de DEV, lancer
|
||||
|
||||
flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data
|
||||
|
||||
le fichier `.prof` sera alors écrit dans `/opt/scodoc-data` (on peut aussi utiliser `/tmp`).
|
||||
|
||||
Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bien:
|
||||
|
||||
pip install snakeviz
|
||||
|
||||
puis
|
||||
|
||||
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
|
||||
|
||||
|
||||
|
||||
# Paquet Debian 11
|
||||
|
||||
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
|
||||
important est `postinst`qui se charge de configurer le système (install ou
|
||||
|
|
121
app/__init__.py
121
app/__init__.py
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: UTF-8 -*
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
|
@ -17,14 +18,19 @@ from flask import render_template
|
|||
from flask.logging import default_handler
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
from flask_login import LoginManager
|
||||
from flask_login import LoginManager, current_user
|
||||
from flask_mail import Mail
|
||||
from flask_bootstrap import Bootstrap
|
||||
from flask_moment import Moment
|
||||
from flask_caching import Cache
|
||||
import sqlalchemy
|
||||
|
||||
from app.scodoc.sco_exceptions import ScoValueError, APIInvalidParams
|
||||
from app.scodoc.sco_exceptions import (
|
||||
AccessDenied,
|
||||
ScoGenError,
|
||||
ScoValueError,
|
||||
APIInvalidParams,
|
||||
)
|
||||
from config import DevConfig
|
||||
import sco_version
|
||||
|
||||
|
@ -50,10 +56,21 @@ def handle_sco_value_error(exc):
|
|||
return render_template("sco_value_error.html", exc=exc), 404
|
||||
|
||||
|
||||
def handle_access_denied(exc):
|
||||
return render_template("error_access_denied.html", exc=exc), 403
|
||||
|
||||
|
||||
def internal_server_error(e):
|
||||
"""Bugs scodoc, erreurs 500"""
|
||||
# note that we set the 500 status explicitly
|
||||
return render_template("error_500.html", SCOVERSION=sco_version.SCOVERSION), 500
|
||||
return (
|
||||
render_template(
|
||||
"error_500.html",
|
||||
SCOVERSION=sco_version.SCOVERSION,
|
||||
date=datetime.datetime.now().isoformat(),
|
||||
),
|
||||
500,
|
||||
)
|
||||
|
||||
|
||||
def handle_invalid_usage(error):
|
||||
|
@ -82,7 +99,7 @@ def postgresql_server_error(e):
|
|||
return render_raw_html("error_503.html", SCOVERSION=sco_version.SCOVERSION), 503
|
||||
|
||||
|
||||
class RequestFormatter(logging.Formatter):
|
||||
class LogRequestFormatter(logging.Formatter):
|
||||
"""Ajoute URL et remote_addr for logging"""
|
||||
|
||||
def format(self, record):
|
||||
|
@ -92,10 +109,46 @@ class RequestFormatter(logging.Formatter):
|
|||
else:
|
||||
record.url = None
|
||||
record.remote_addr = None
|
||||
record.sco_user = current_user
|
||||
if has_request_context():
|
||||
record.sco_admin_mail = current_app.config["SCODOC_ADMIN_MAIL"]
|
||||
else:
|
||||
record.sco_admin_mail = "(pas de requête)"
|
||||
|
||||
return super().format(record)
|
||||
|
||||
|
||||
class LogExceptionFormatter(logging.Formatter):
|
||||
"""Formatteur pour les exceptions: ajoute détails"""
|
||||
|
||||
def format(self, record):
|
||||
if has_request_context():
|
||||
record.url = request.url
|
||||
record.remote_addr = request.environ.get(
|
||||
"HTTP_X_FORWARDED_FOR", request.remote_addr
|
||||
)
|
||||
record.http_referrer = request.referrer
|
||||
record.http_method = request.method
|
||||
if request.method == "GET":
|
||||
record.http_params = str(request.args)
|
||||
else:
|
||||
# rep = reprlib.Repr() # abbrège
|
||||
record.http_params = str(request.form)[:2048]
|
||||
else:
|
||||
record.url = None
|
||||
record.remote_addr = None
|
||||
record.http_referrer = None
|
||||
record.http_method = None
|
||||
record.http_params = None
|
||||
record.sco_user = current_user
|
||||
|
||||
if has_request_context():
|
||||
record.sco_admin_mail = current_app.config["SCODOC_ADMIN_MAIL"]
|
||||
else:
|
||||
record.sco_admin_mail = "(pas de requête)"
|
||||
return super().format(record)
|
||||
|
||||
|
||||
class ScoSMTPHandler(SMTPHandler):
|
||||
def getSubject(self, record: logging.LogRecord) -> str:
|
||||
stack_summary = traceback.extract_tb(record.exc_info[2])
|
||||
|
@ -105,8 +158,24 @@ class ScoSMTPHandler(SMTPHandler):
|
|||
return subject
|
||||
|
||||
|
||||
class ReverseProxied(object):
|
||||
"""Adaptateur wsgi qui nous permet d'avoir toutes les URL calculées en https
|
||||
sauf quand on est en dev.
|
||||
La variable HTTP_X_FORWARDED_PROTO est positionnée par notre config nginx"""
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
scheme = environ.get("HTTP_X_FORWARDED_PROTO")
|
||||
if scheme:
|
||||
environ["wsgi.url_scheme"] = scheme # ou forcer à https ici ?
|
||||
return self.app(environ, start_response)
|
||||
|
||||
|
||||
def create_app(config_class=DevConfig):
|
||||
app = Flask(__name__, static_url_path="/ScoDoc/static", static_folder="static")
|
||||
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
||||
app.logger.setLevel(logging.DEBUG)
|
||||
app.config.from_object(config_class)
|
||||
|
||||
|
@ -119,7 +188,10 @@ def create_app(config_class=DevConfig):
|
|||
cache.init_app(app)
|
||||
sco_cache.CACHE = cache
|
||||
|
||||
app.register_error_handler(ScoGenError, handle_sco_value_error)
|
||||
app.register_error_handler(ScoValueError, handle_sco_value_error)
|
||||
|
||||
app.register_error_handler(AccessDenied, handle_access_denied)
|
||||
app.register_error_handler(500, internal_server_error)
|
||||
app.register_error_handler(503, postgresql_server_error)
|
||||
app.register_error_handler(APIInvalidParams, handle_invalid_usage)
|
||||
|
@ -148,9 +220,18 @@ def create_app(config_class=DevConfig):
|
|||
absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences"
|
||||
)
|
||||
app.register_blueprint(api_bp, url_prefix="/ScoDoc/api")
|
||||
scodoc_exc_formatter = RequestFormatter(
|
||||
"[%(asctime)s] %(remote_addr)s requested %(url)s\n"
|
||||
"%(levelname)s in %(module)s: %(message)s"
|
||||
scodoc_log_formatter = LogRequestFormatter(
|
||||
"[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n"
|
||||
"%(levelname)s: %(message)s"
|
||||
)
|
||||
# les champs additionnels sont définis dans LogRequestFormatter
|
||||
scodoc_exc_formatter = LogExceptionFormatter(
|
||||
"[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n"
|
||||
"%(levelname)s: %(message)s\n"
|
||||
"Referrer: %(http_referrer)s\n"
|
||||
"Method: %(http_method)s\n"
|
||||
"Params: %(http_params)s\n"
|
||||
"Admin mail: %(sco_admin_mail)s\n"
|
||||
)
|
||||
if not app.testing:
|
||||
if not app.debug:
|
||||
|
@ -179,7 +260,7 @@ def create_app(config_class=DevConfig):
|
|||
app.logger.addHandler(mail_handler)
|
||||
else:
|
||||
# Pour logs en DEV uniquement:
|
||||
default_handler.setFormatter(scodoc_exc_formatter)
|
||||
default_handler.setFormatter(scodoc_log_formatter)
|
||||
|
||||
# Config logs pour DEV et PRODUCTION
|
||||
# Configuration des logs (actifs aussi en mode development)
|
||||
|
@ -188,9 +269,17 @@ def create_app(config_class=DevConfig):
|
|||
file_handler = WatchedFileHandler(
|
||||
app.config["SCODOC_LOG_FILE"], encoding="utf-8"
|
||||
)
|
||||
file_handler.setFormatter(scodoc_exc_formatter)
|
||||
file_handler.setFormatter(scodoc_log_formatter)
|
||||
file_handler.setLevel(logging.INFO)
|
||||
app.logger.addHandler(file_handler)
|
||||
# Log pour les erreurs (exceptions) uniquement:
|
||||
# usually /opt/scodoc-data/log/scodoc_exc.log
|
||||
file_handler = WatchedFileHandler(
|
||||
app.config["SCODOC_ERR_FILE"], encoding="utf-8"
|
||||
)
|
||||
file_handler.setFormatter(scodoc_exc_formatter)
|
||||
file_handler.setLevel(logging.ERROR)
|
||||
app.logger.addHandler(file_handler)
|
||||
|
||||
# app.logger.setLevel(logging.INFO)
|
||||
app.logger.info(f"{sco_version.SCONAME} {sco_version.SCOVERSION} startup")
|
||||
|
@ -199,15 +288,19 @@ def create_app(config_class=DevConfig):
|
|||
)
|
||||
# ---- INITIALISATION SPECIFIQUES A SCODOC
|
||||
from app.scodoc import sco_bulletins_generator
|
||||
from app.scodoc.sco_bulletins_example import BulletinGeneratorExample
|
||||
|
||||
from app.scodoc.sco_bulletins_legacy import BulletinGeneratorLegacy
|
||||
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
|
||||
from app.scodoc.sco_bulletins_ucac import BulletinGeneratorUCAC
|
||||
|
||||
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorExample)
|
||||
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorLegacy)
|
||||
# l'ordre est important, le premeir sera le "défaut" pour les nouveaux départements.
|
||||
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandard)
|
||||
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorLegacy)
|
||||
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorUCAC)
|
||||
if app.testing or app.debug:
|
||||
from app.scodoc.sco_bulletins_example import BulletinGeneratorExample
|
||||
|
||||
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorExample)
|
||||
|
||||
return app
|
||||
|
||||
|
@ -225,6 +318,8 @@ def set_sco_dept(scodoc_dept: str):
|
|||
g.scodoc_dept_id = dept.id # l'id
|
||||
if not hasattr(g, "db_conn"):
|
||||
ndb.open_db_connection()
|
||||
if not hasattr(g, "stored_get_formsemestre"):
|
||||
g.stored_get_formsemestre = {}
|
||||
|
||||
|
||||
def user_db_init():
|
||||
|
@ -263,7 +358,7 @@ def sco_db_insert_constants():
|
|||
|
||||
current_app.logger.info("Init Sco db")
|
||||
# Modalités:
|
||||
models.NotesFormModalite.insert_modalites()
|
||||
models.FormationModalite.insert_modalites()
|
||||
|
||||
|
||||
def initialize_scodoc_database(erase=False, create_all=False):
|
||||
|
|
|
@ -2,7 +2,25 @@
|
|||
"""
|
||||
|
||||
from flask import Blueprint
|
||||
from flask import request
|
||||
|
||||
bp = Blueprint("api", __name__)
|
||||
|
||||
|
||||
def requested_format(default_format="json", allowed_formats=None):
|
||||
"""Extract required format from query string.
|
||||
* default value is json. A list of allowed formats may be provided
|
||||
(['json'] considered if not provided).
|
||||
* if the required format is not in allowed list, returns None.
|
||||
|
||||
NB: if json in not in allowed_formats, format specification is mandatory.
|
||||
"""
|
||||
format_type = request.args.get("format", default_format)
|
||||
if format_type in (allowed_formats or ["json"]):
|
||||
return format_type
|
||||
return None
|
||||
|
||||
|
||||
from app.api import tokens
|
||||
from app.api import sco_api
|
||||
from app.api import logos
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: UTF-8 -*
|
||||
# Authentication code borrowed from Miguel Grinberg's Mega Tutorial
|
||||
# (see https://github.com/miguelgrinberg/microblog)
|
||||
# and modified for ScoDoc
|
||||
|
||||
# Under The MIT License (MIT)
|
||||
|
||||
|
@ -23,6 +24,7 @@
|
|||
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from flask import g
|
||||
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
|
||||
from app.auth.models import User
|
||||
from app.api.errors import error_response
|
||||
|
@ -33,8 +35,9 @@ token_auth = HTTPTokenAuth()
|
|||
|
||||
@basic_auth.verify_password
|
||||
def verify_password(username, password):
|
||||
user = User.query.filter_by(username=username).first()
|
||||
user = User.query.filter_by(user_name=username).first()
|
||||
if user and user.check_password(password):
|
||||
g.current_user = user
|
||||
return user
|
||||
|
||||
|
||||
|
@ -45,9 +48,30 @@ def basic_auth_error(status):
|
|||
|
||||
@token_auth.verify_token
|
||||
def verify_token(token):
|
||||
return User.check_token(token) if token else None
|
||||
user = User.check_token(token) if token else None
|
||||
g.current_user = user
|
||||
return user
|
||||
|
||||
|
||||
@token_auth.error_handler
|
||||
def token_auth_error(status):
|
||||
return error_response(status)
|
||||
|
||||
|
||||
@token_auth.get_user_roles
|
||||
def get_user_roles(user):
|
||||
return user.roles
|
||||
|
||||
|
||||
# def token_permission_required(permission):
|
||||
# def decorator(f):
|
||||
# @wraps(f)
|
||||
# def decorated_function(*args, **kwargs):
|
||||
# scodoc_dept = getattr(g, "scodoc_dept", None)
|
||||
# if not current_user.has_permission(permission, scodoc_dept):
|
||||
# abort(403)
|
||||
# return f(*args, **kwargs)
|
||||
|
||||
# return login_required(decorated_function)
|
||||
|
||||
# return decorator
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""API: gestion des logos
|
||||
Contrib @jmp
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from flask import jsonify, g, send_file
|
||||
|
||||
from app.api import bp
|
||||
from app.api import requested_format
|
||||
from app.api.auth import token_auth
|
||||
from app.api.errors import error_response
|
||||
from app.models import Departement
|
||||
from app.scodoc.sco_logos import list_logos, find_logo
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
@bp.route("/logos", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def api_get_glob_logos():
|
||||
if not g.current_user.has_permission(Permission.ScoSuperAdmin, None):
|
||||
return error_response(401, message="accès interdit")
|
||||
required_format = requested_format() # json only
|
||||
if required_format is None:
|
||||
return error_response(400, "Illegal format")
|
||||
logos = list_logos()[None]
|
||||
return jsonify(list(logos.keys()))
|
||||
|
||||
|
||||
@bp.route("/logos/<string:logoname>", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def api_get_glob_logo(logoname):
|
||||
if not g.current_user.has_permission(Permission.ScoSuperAdmin, None):
|
||||
return error_response(401, message="accès interdit")
|
||||
logo = find_logo(logoname=logoname)
|
||||
if logo is None:
|
||||
return error_response(404, message="logo not found")
|
||||
logo.select()
|
||||
return send_file(
|
||||
logo.filepath,
|
||||
mimetype=f"image/{logo.suffix}",
|
||||
last_modified=datetime.now(),
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/departements/<string:departement>/logos", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def api_get_local_logos(departement):
|
||||
dept_id = Departement.from_acronym(departement).id
|
||||
if not g.current_user.has_permission(Permission.ScoChangePreferences, departement):
|
||||
return error_response(401, message="accès interdit")
|
||||
logos = list_logos().get(dept_id, dict())
|
||||
return jsonify(list(logos.keys()))
|
||||
|
||||
|
||||
@bp.route("/departements/<string:departement>/logos/<string:logoname>", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def api_get_local_logo(departement, logoname):
|
||||
# format = requested_format("jpg", ['png', 'jpg']) XXX ?
|
||||
dept_id = Departement.from_acronym(departement).id
|
||||
if not g.current_user.has_permission(Permission.ScoChangePreferences, departement):
|
||||
return error_response(401, message="accès interdit")
|
||||
logo = find_logo(logoname=logoname, dept_id=dept_id)
|
||||
if logo is None:
|
||||
return error_response(404, message="logo not found")
|
||||
logo.select()
|
||||
return send_file(
|
||||
logo.filepath,
|
||||
mimetype=f"image/{logo.suffix}",
|
||||
last_modified=datetime.now(),
|
||||
)
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -29,7 +29,7 @@
|
|||
"""
|
||||
# PAS ENCORE IMPLEMENTEE, juste un essai
|
||||
# Pour P. Bouron, il faudrait en priorité l'équivalent de
|
||||
# Scolarite/Notes/do_moduleimpl_withmodule_list
|
||||
# Scolarite/Notes/moduleimpl_withmodule_list (alias scodoc7 do_moduleimpl_withmodule_list)
|
||||
# Scolarite/Notes/evaluation_create
|
||||
# Scolarite/Notes/evaluation_delete
|
||||
# Scolarite/Notes/formation_list
|
||||
|
@ -38,19 +38,43 @@
|
|||
# Scolarite/Notes/groups_view
|
||||
# Scolarite/Notes/moduleimpl_status
|
||||
# Scolarite/setGroups
|
||||
from datetime import datetime
|
||||
|
||||
from flask import jsonify, request, url_for, abort
|
||||
from app import db
|
||||
from app.api import bp
|
||||
from flask import jsonify, request, g, send_file
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app import db, log
|
||||
from app.api import bp, requested_format
|
||||
from app.api.auth import token_auth
|
||||
from app.api.errors import bad_request
|
||||
|
||||
from app.api.errors import error_response
|
||||
from app import models
|
||||
from app.models import FormSemestre, FormSemestreInscription, Identite
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
@bp.route("/ScoDoc/api/list_depts", methods=["GET"])
|
||||
@bp.route("list_depts", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def list_depts():
|
||||
depts = models.Departement.query.filter_by(visible=True).all()
|
||||
data = {"items": [d.to_dict() for d in depts]}
|
||||
data = [d.to_dict() for d in depts]
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@bp.route("/etudiants/courant", methods=["GET"])
|
||||
@token_auth.login_required
|
||||
def etudiants():
|
||||
"""Liste de tous les étudiants actuellement inscrits à un semestre
|
||||
en cours.
|
||||
"""
|
||||
# Vérification de l'accès: permission Observateir sur tous les départements
|
||||
# (c'est un exemple à compléter)
|
||||
if not g.current_user.has_permission(Permission.ScoObservateur, None):
|
||||
return error_response(401, message="accès interdit")
|
||||
|
||||
query = db.session.query(Identite).filter(
|
||||
FormSemestreInscription.formsemestre_id == FormSemestre.id,
|
||||
FormSemestreInscription.etudid == Identite.id,
|
||||
FormSemestre.date_debut <= func.now(),
|
||||
FormSemestre.date_fin >= func.now(),
|
||||
)
|
||||
return jsonify([e.to_dict_bul(include_urls=False) for e in query])
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -10,7 +10,8 @@ import re
|
|||
from time import time
|
||||
from typing import Optional
|
||||
|
||||
from flask import current_app, url_for, g
|
||||
import cracklib # pylint: disable=import-error
|
||||
from flask import current_app, g
|
||||
from flask_login import UserMixin, AnonymousUserMixin
|
||||
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
@ -18,14 +19,32 @@ from werkzeug.security import generate_password_hash, check_password_hash
|
|||
import jwt
|
||||
|
||||
from app import db, login
|
||||
|
||||
from app.models import Departement
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_etud # a deplacer dans scu
|
||||
|
||||
VALID_LOGIN_EXP = re.compile(r"^[a-zA-Z0-9@\\\-_\\\.]+$")
|
||||
VALID_LOGIN_EXP = re.compile(r"^[a-zA-Z0-9@\\\-_\.]+$")
|
||||
|
||||
|
||||
def is_valid_password(cleartxt):
|
||||
"""Check password.
|
||||
returns True if OK.
|
||||
"""
|
||||
if (
|
||||
hasattr(scu.CONFIG, "MIN_PASSWORD_LENGTH")
|
||||
and scu.CONFIG.MIN_PASSWORD_LENGTH > 0
|
||||
and len(cleartxt) < scu.CONFIG.MIN_PASSWORD_LENGTH
|
||||
):
|
||||
return False # invalid: too short
|
||||
try:
|
||||
_ = cracklib.FascistCheck(cleartxt)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
|
@ -37,7 +56,7 @@ class User(UserMixin, db.Model):
|
|||
|
||||
nom = db.Column(db.String(64))
|
||||
prenom = db.Column(db.String(64))
|
||||
dept = db.Column(db.String(32), index=True)
|
||||
dept = db.Column(db.String(SHORT_STR_LEN), index=True)
|
||||
active = db.Column(db.Boolean, default=True, index=True)
|
||||
|
||||
password_hash = db.Column(db.String(128))
|
||||
|
@ -47,12 +66,19 @@ class User(UserMixin, db.Model):
|
|||
date_created = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
date_expiration = db.Column(db.DateTime, default=None)
|
||||
passwd_temp = db.Column(db.Boolean, default=False)
|
||||
token = db.Column(db.String(32), index=True, unique=True)
|
||||
token = db.Column(db.Text(), index=True, unique=True)
|
||||
token_expiration = db.Column(db.DateTime)
|
||||
|
||||
roles = db.relationship("Role", secondary="user_role", viewonly=True)
|
||||
Permission = Permission
|
||||
|
||||
_departement = db.relationship(
|
||||
"Departement",
|
||||
foreign_keys=[Departement.acronym],
|
||||
primaryjoin=(dept == Departement.acronym),
|
||||
lazy="dynamic",
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.roles = []
|
||||
self.user_roles = []
|
||||
|
@ -86,6 +112,7 @@ class User(UserMixin, db.Model):
|
|||
self.password_hash = generate_password_hash(password)
|
||||
else:
|
||||
self.password_hash = None
|
||||
self.passwd_temp = False
|
||||
|
||||
def check_password(self, password):
|
||||
"""Check given password vs current one.
|
||||
|
@ -110,6 +137,7 @@ class User(UserMixin, db.Model):
|
|||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
def get_reset_password_token(self, expires_in=600):
|
||||
"Un token pour réinitialiser son mot de passe"
|
||||
return jwt.encode(
|
||||
{"reset_password": self.id, "exp": time() + expires_in},
|
||||
current_app.config["SECRET_KEY"],
|
||||
|
@ -118,15 +146,17 @@ class User(UserMixin, db.Model):
|
|||
|
||||
@staticmethod
|
||||
def verify_reset_password_token(token):
|
||||
"Vérification du token de reéinitialisation du mot de passe"
|
||||
try:
|
||||
id = jwt.decode(
|
||||
user_id = jwt.decode(
|
||||
token, current_app.config["SECRET_KEY"], algorithms=["HS256"]
|
||||
)["reset_password"]
|
||||
except:
|
||||
return
|
||||
return User.query.get(id)
|
||||
return User.query.get(user_id)
|
||||
|
||||
def to_dict(self, include_email=True):
|
||||
"""l'utilisateur comme un dict, avec des champs supplémentaires"""
|
||||
data = {
|
||||
"date_expiration": self.date_expiration.isoformat() + "Z"
|
||||
if self.date_expiration
|
||||
|
@ -177,8 +207,9 @@ class User(UserMixin, db.Model):
|
|||
if "roles_string" in data:
|
||||
self.user_roles = []
|
||||
for r_d in data["roles_string"].split(","):
|
||||
role, dept = UserRole.role_dept_from_string(r_d)
|
||||
self.add_role(role, dept)
|
||||
if r_d:
|
||||
role, dept = UserRole.role_dept_from_string(r_d)
|
||||
self.add_role(role, dept)
|
||||
|
||||
def get_token(self, expires_in=3600):
|
||||
now = datetime.utcnow()
|
||||
|
@ -194,11 +225,20 @@ class User(UserMixin, db.Model):
|
|||
|
||||
@staticmethod
|
||||
def check_token(token):
|
||||
"""Retreive user for given token, chek token's validity
|
||||
and returns the user object.
|
||||
"""
|
||||
user = User.query.filter_by(token=token).first()
|
||||
if user is None or user.token_expiration < datetime.utcnow():
|
||||
return None
|
||||
return user
|
||||
|
||||
def get_dept_id(self) -> int:
|
||||
"returns user's department id, or None"
|
||||
if self.dept:
|
||||
return self._departement.first().id
|
||||
return None
|
||||
|
||||
# Permissions management:
|
||||
def has_permission(self, perm: int, dept=False):
|
||||
"""Check if user has permission `perm` in given `dept`.
|
||||
|
@ -250,7 +290,7 @@ class User(UserMixin, db.Model):
|
|||
"""string repr. of user's roles (with depts)
|
||||
e.g. "Ens_RT, Ens_Info, Secr_CJ"
|
||||
"""
|
||||
return ",".join(f"{r.role.name}_{r.dept or ''}" for r in self.user_roles)
|
||||
return ",".join(f"{r.role.name or ''}_{r.dept or ''}" for r in self.user_roles)
|
||||
|
||||
def is_administrator(self):
|
||||
"True if i'm an active SuperAdmin"
|
||||
|
@ -329,7 +369,7 @@ class Role(db.Model):
|
|||
"""Roles for ScoDoc"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(64), unique=True)
|
||||
name = db.Column(db.String(64), unique=True) # TODO: , nullable=False))
|
||||
default = db.Column(db.Boolean, default=False, index=True)
|
||||
permissions = db.Column(db.BigInteger) # 64 bits
|
||||
users = db.relationship("User", secondary="user_role", viewonly=True)
|
||||
|
@ -388,7 +428,7 @@ class UserRole(db.Model):
|
|||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||
role_id = db.Column(db.Integer, db.ForeignKey("role.id"))
|
||||
dept = db.Column(db.String(64)) # dept acronym
|
||||
dept = db.Column(db.String(64)) # dept acronym ou NULL
|
||||
user = db.relationship(
|
||||
User, backref=db.backref("user_roles", cascade="all, delete-orphan")
|
||||
)
|
||||
|
@ -407,6 +447,9 @@ class UserRole(db.Model):
|
|||
"""
|
||||
fields = role_dept.split("_", 1) # maxsplit=1, le dept peut contenir un "_"
|
||||
if len(fields) != 2:
|
||||
current_app.logger.warning(
|
||||
f"role_dept_from_string: Invalid role_dept '{role_dept}'"
|
||||
)
|
||||
raise ScoValueError("Invalid role_dept")
|
||||
role_name, dept = fields
|
||||
if dept == "":
|
||||
|
@ -418,7 +461,7 @@ class UserRole(db.Model):
|
|||
|
||||
|
||||
def get_super_admin():
|
||||
"""L'utilisateur admin (où le premier, s'il y en a plusieurs).
|
||||
"""L'utilisateur admin (ou le premier, s'il y en a plusieurs).
|
||||
Utilisé par les tests unitaires et le script de migration.
|
||||
"""
|
||||
admin_role = Role.query.filter_by(name="SuperAdmin").first()
|
||||
|
@ -433,5 +476,5 @@ def get_super_admin():
|
|||
|
||||
|
||||
@login.user_loader
|
||||
def load_user(id):
|
||||
return User.query.get(int(id))
|
||||
def load_user(uid):
|
||||
return User.query.get(int(uid))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,424 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
from collections import defaultdict
|
||||
import datetime
|
||||
from flask import url_for, g
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from app import db
|
||||
|
||||
from app.comp import moy_ue, moy_sem, inscr_mod
|
||||
from app.models import ModuleImpl
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_cache import ResultatsSemestreBUTCache
|
||||
from app.scodoc import sco_bulletins_json
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_utils import jsnan, fmt_note
|
||||
|
||||
|
||||
class ResultatsSemestreBUT:
|
||||
"""Structure légère pour stocker les résultats du semestre et
|
||||
générer les bulletins.
|
||||
__init__ : charge depuis le cache ou calcule
|
||||
"""
|
||||
|
||||
_cached_attrs = (
|
||||
"sem_cube",
|
||||
"modimpl_inscr_df",
|
||||
"modimpl_coefs_df",
|
||||
"etud_moy_ue",
|
||||
"modimpls_evals_poids",
|
||||
"modimpls_evals_notes",
|
||||
"etud_moy_gen",
|
||||
"etud_moy_gen_ranks",
|
||||
"modimpls_evaluations_complete",
|
||||
)
|
||||
|
||||
def __init__(self, formsemestre):
|
||||
self.formsemestre = formsemestre
|
||||
self.ues = formsemestre.query_ues().all()
|
||||
self.modimpls = formsemestre.modimpls.all()
|
||||
self.etuds = self.formsemestre.get_inscrits(include_dem=False)
|
||||
self.etud_index = {e.id: idx for idx, e in enumerate(self.etuds)}
|
||||
self.saes = [
|
||||
m for m in self.modimpls if m.module.module_type == scu.ModuleType.SAE
|
||||
]
|
||||
self.ressources = [
|
||||
m for m in self.modimpls if m.module.module_type == scu.ModuleType.RESSOURCE
|
||||
]
|
||||
if not self.load_cached():
|
||||
self.compute()
|
||||
self.store()
|
||||
|
||||
def load_cached(self) -> bool:
|
||||
"Load cached dataframes, returns False si pas en cache"
|
||||
data = ResultatsSemestreBUTCache.get(self.formsemestre.id)
|
||||
if not data:
|
||||
return False
|
||||
for attr in self._cached_attrs:
|
||||
setattr(self, attr, data[attr])
|
||||
return True
|
||||
|
||||
def store(self):
|
||||
"Cache our dataframes"
|
||||
ResultatsSemestreBUTCache.set(
|
||||
self.formsemestre.id,
|
||||
{attr: getattr(self, attr) for attr in self._cached_attrs},
|
||||
)
|
||||
|
||||
def compute(self):
|
||||
"Charge les notes et inscriptions et calcule toutes les moyennes"
|
||||
(
|
||||
self.sem_cube,
|
||||
self.modimpls_evals_poids,
|
||||
self.modimpls_evals_notes,
|
||||
modimpls_evaluations,
|
||||
self.modimpls_evaluations_complete,
|
||||
) = moy_ue.notes_sem_load_cube(self.formsemestre)
|
||||
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
|
||||
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
||||
self.formsemestre, ues=self.ues, modimpls=self.modimpls
|
||||
)
|
||||
# l'idx de la colonne du mod modimpl.id est
|
||||
# modimpl_coefs_df.columns.get_loc(modimpl.id)
|
||||
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
|
||||
self.etud_moy_ue = moy_ue.compute_ue_moys(
|
||||
self.sem_cube,
|
||||
self.etuds,
|
||||
self.modimpls,
|
||||
self.ues,
|
||||
self.modimpl_inscr_df,
|
||||
self.modimpl_coefs_df,
|
||||
)
|
||||
self.etud_moy_gen = moy_sem.compute_sem_moys(
|
||||
self.etud_moy_ue, self.modimpl_coefs_df
|
||||
)
|
||||
self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen)
|
||||
|
||||
def etud_ue_mod_results(self, etud, ue, modimpls) -> dict:
|
||||
"dict synthèse résultats dans l'UE pour les modules indiqués"
|
||||
d = {}
|
||||
etud_idx = self.etud_index[etud.id]
|
||||
ue_idx = self.modimpl_coefs_df.index.get_loc(ue.id)
|
||||
etud_moy_module = self.sem_cube[etud_idx] # module x UE
|
||||
for mi in modimpls:
|
||||
if self.modimpl_inscr_df[str(mi.id)][etud.id]: # si inscrit
|
||||
coef = self.modimpl_coefs_df[mi.id][ue.id]
|
||||
if coef > 0:
|
||||
d[mi.module.code] = {
|
||||
"id": mi.id,
|
||||
"coef": coef,
|
||||
"moyenne": fmt_note(
|
||||
etud_moy_module[
|
||||
self.modimpl_coefs_df.columns.get_loc(mi.id)
|
||||
][ue_idx]
|
||||
),
|
||||
}
|
||||
return d
|
||||
|
||||
def etud_ue_results(self, etud, ue):
|
||||
"dict synthèse résultats UE"
|
||||
d = {
|
||||
"id": ue.id,
|
||||
"numero": ue.numero,
|
||||
"ECTS": {
|
||||
"acquis": 0, # XXX TODO voir jury
|
||||
"total": ue.ects,
|
||||
},
|
||||
"competence": None, # XXX TODO lien avec référentiel
|
||||
"moyenne": {
|
||||
"value": fmt_note(self.etud_moy_ue[ue.id][etud.id]),
|
||||
"min": fmt_note(self.etud_moy_ue[ue.id].min()),
|
||||
"max": fmt_note(self.etud_moy_ue[ue.id].max()),
|
||||
"moy": fmt_note(self.etud_moy_ue[ue.id].mean()),
|
||||
},
|
||||
"bonus": None, # XXX TODO
|
||||
"malus": None, # XXX TODO voir ce qui est ici
|
||||
"capitalise": None, # "AAAA-MM-JJ" TODO
|
||||
"ressources": self.etud_ue_mod_results(etud, ue, self.ressources),
|
||||
"saes": self.etud_ue_mod_results(etud, ue, self.saes),
|
||||
}
|
||||
return d
|
||||
|
||||
def etud_mods_results(self, etud, modimpls) -> dict:
|
||||
"""dict synthèse résultats des modules indiqués,
|
||||
avec évaluations de chacun."""
|
||||
d = {}
|
||||
# etud_idx = self.etud_index[etud.id]
|
||||
for mi in modimpls:
|
||||
# mod_idx = self.modimpl_coefs_df.columns.get_loc(mi.id)
|
||||
# # moyennes indicatives (moyennes de moyennes d'UE)
|
||||
# try:
|
||||
# moyennes_etuds = np.nan_to_num(
|
||||
# np.nanmean(self.sem_cube[:, mod_idx, :], axis=1),
|
||||
# copy=False,
|
||||
# )
|
||||
# except RuntimeWarning: # all nans in np.nanmean (sur certains etuds sans notes valides)
|
||||
# pass
|
||||
# try:
|
||||
# moy_indicative_mod = np.nanmean(self.sem_cube[etud_idx, mod_idx])
|
||||
# except RuntimeWarning: # all nans in np.nanmean
|
||||
# pass
|
||||
if self.modimpl_inscr_df[str(mi.id)][etud.id]: # si inscrit
|
||||
d[mi.module.code] = {
|
||||
"id": mi.id,
|
||||
"titre": mi.module.titre,
|
||||
"code_apogee": mi.module.code_apogee,
|
||||
"url": url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=mi.id,
|
||||
),
|
||||
"moyenne": {
|
||||
# # moyenne indicative de module: moyenne des UE, ignorant celles sans notes (nan)
|
||||
# "value": fmt_note(moy_indicative_mod),
|
||||
# "min": fmt_note(moyennes_etuds.min()),
|
||||
# "max": fmt_note(moyennes_etuds.max()),
|
||||
# "moy": fmt_note(moyennes_etuds.mean()),
|
||||
},
|
||||
"evaluations": [
|
||||
self.etud_eval_results(etud, e)
|
||||
for eidx, e in enumerate(mi.evaluations)
|
||||
if e.visibulletin
|
||||
and self.modimpls_evaluations_complete[mi.id][eidx]
|
||||
],
|
||||
}
|
||||
return d
|
||||
|
||||
def etud_eval_results(self, etud, e) -> dict:
|
||||
"dict resultats d'un étudiant à une évaluation"
|
||||
eval_notes = self.modimpls_evals_notes[e.moduleimpl_id][e.id] # pd.Series
|
||||
notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
|
||||
d = {
|
||||
"id": e.id,
|
||||
"description": e.description,
|
||||
"date": e.jour.isoformat() if e.jour else None,
|
||||
"heure_debut": e.heure_debut.strftime("%H:%M") if e.heure_debut else None,
|
||||
"heure_fin": e.heure_fin.strftime("%H:%M") if e.heure_debut else None,
|
||||
"coef": e.coefficient,
|
||||
"poids": {p.ue.acronyme: p.poids for p in e.ue_poids},
|
||||
"note": {
|
||||
"value": fmt_note(
|
||||
self.modimpls_evals_notes[e.moduleimpl_id][e.id][etud.id],
|
||||
note_max=e.note_max,
|
||||
),
|
||||
"min": fmt_note(notes_ok.min()),
|
||||
"max": fmt_note(notes_ok.max()),
|
||||
"moy": fmt_note(notes_ok.mean()),
|
||||
},
|
||||
"url": url_for(
|
||||
"notes.evaluation_listenotes",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
evaluation_id=e.id,
|
||||
),
|
||||
}
|
||||
return d
|
||||
|
||||
def bulletin_etud(self, etud, formsemestre) -> dict:
|
||||
"""Le bulletin de l'étudiant dans ce semestre"""
|
||||
etat_inscription = etud.etat_inscription(formsemestre.id)
|
||||
d = {
|
||||
"version": "0",
|
||||
"type": "BUT",
|
||||
"date": datetime.datetime.utcnow().isoformat() + "Z",
|
||||
"etudiant": etud.to_dict_bul(),
|
||||
"formation": {
|
||||
"id": formsemestre.formation.id,
|
||||
"acronyme": formsemestre.formation.acronyme,
|
||||
"titre_officiel": formsemestre.formation.titre_officiel,
|
||||
"titre": formsemestre.formation.titre,
|
||||
},
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"etat_inscription": etat_inscription,
|
||||
"options": bulletin_option_affichage(formsemestre),
|
||||
}
|
||||
semestre_infos = {
|
||||
"etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo],
|
||||
"date_debut": formsemestre.date_debut.isoformat(),
|
||||
"date_fin": formsemestre.date_fin.isoformat(),
|
||||
"annee_universitaire": self.formsemestre.annee_scolaire_str(),
|
||||
"inscription": "TODO-MM-JJ", # XXX TODO
|
||||
"numero": formsemestre.semestre_id,
|
||||
"groupes": [], # XXX TODO
|
||||
"absences": { # XXX TODO
|
||||
"injustifie": 1,
|
||||
"total": 33,
|
||||
},
|
||||
}
|
||||
semestre_infos.update(
|
||||
sco_bulletins_json.dict_decision_jury(etud.id, formsemestre.id)
|
||||
)
|
||||
if etat_inscription == scu.INSCRIT:
|
||||
semestre_infos.update(
|
||||
{
|
||||
"notes": { # moyenne des moyennes générales du semestre
|
||||
"value": fmt_note(self.etud_moy_gen[etud.id]),
|
||||
"min": fmt_note(self.etud_moy_gen.min()),
|
||||
"moy": fmt_note(self.etud_moy_gen.mean()),
|
||||
"max": fmt_note(self.etud_moy_gen.max()),
|
||||
},
|
||||
"rang": { # classement wrt moyenne général, indicatif
|
||||
"value": self.etud_moy_gen_ranks[etud.id],
|
||||
"total": len(self.etuds),
|
||||
},
|
||||
},
|
||||
)
|
||||
d.update(
|
||||
{
|
||||
"ressources": self.etud_mods_results(etud, self.ressources),
|
||||
"saes": self.etud_mods_results(etud, self.saes),
|
||||
"ues": {
|
||||
ue.acronyme: self.etud_ue_results(etud, ue) for ue in self.ues
|
||||
},
|
||||
"semestre": semestre_infos,
|
||||
},
|
||||
)
|
||||
else:
|
||||
semestre_infos.update(
|
||||
{
|
||||
"notes": {
|
||||
"value": "DEM",
|
||||
"min": "",
|
||||
"moy": "",
|
||||
"max": "",
|
||||
},
|
||||
"rang": {"value": "DEM", "total": len(self.etuds)},
|
||||
}
|
||||
)
|
||||
d.update(
|
||||
{
|
||||
"semestre": semestre_infos,
|
||||
"ressources": {},
|
||||
"saes": {},
|
||||
"ues": {},
|
||||
}
|
||||
)
|
||||
|
||||
return d
|
||||
|
||||
|
||||
def bulletin_option_affichage(formsemestre):
|
||||
"dict avec les options d'affichages (préférences) pour ce semestre"
|
||||
prefs = sco_preferences.SemPreferences(formsemestre.id)
|
||||
fields = (
|
||||
"bul_show_abs",
|
||||
"bul_show_abs_modules",
|
||||
"bul_show_ects",
|
||||
"bul_show_codemodules",
|
||||
"bul_show_matieres",
|
||||
"bul_show_rangs",
|
||||
"bul_show_ue_rangs",
|
||||
"bul_show_mod_rangs",
|
||||
"bul_show_moypromo",
|
||||
"bul_show_minmax",
|
||||
"bul_show_minmax_mod",
|
||||
"bul_show_minmax_eval",
|
||||
"bul_show_coef",
|
||||
"bul_show_ue_cap_details",
|
||||
"bul_show_ue_cap_current",
|
||||
"bul_show_temporary",
|
||||
"bul_temporary_txt",
|
||||
"bul_show_uevalid",
|
||||
"bul_show_date_inscr",
|
||||
)
|
||||
# on enlève le "bul_" de la clé:
|
||||
return {field[4:]: prefs[field] for field in fields}
|
||||
|
||||
|
||||
# Pour raccorder le code des anciens bulletins qui attendent une NoteTable
|
||||
class APCNotesTableCompat:
|
||||
"""Implementation partielle de NotesTable pour les formations APC
|
||||
Accès aux notes et rangs.
|
||||
"""
|
||||
|
||||
def __init__(self, formsemestre):
|
||||
self.results = ResultatsSemestreBUT(formsemestre)
|
||||
nb_etuds = len(self.results.etuds)
|
||||
self.rangs = self.results.etud_moy_gen_ranks
|
||||
self.moy_min = self.results.etud_moy_gen.min()
|
||||
self.moy_max = self.results.etud_moy_gen.max()
|
||||
self.moy_moy = self.results.etud_moy_gen.mean()
|
||||
self.bonus = defaultdict(lambda: 0.0) # XXX
|
||||
self.ue_rangs = {
|
||||
u.id: (defaultdict(lambda: 0.0), nb_etuds) for u in self.results.ues
|
||||
}
|
||||
self.mod_rangs = {
|
||||
m.id: (defaultdict(lambda: 0), nb_etuds) for m in self.results.modimpls
|
||||
}
|
||||
|
||||
def get_ues(self):
|
||||
ues = []
|
||||
for ue in self.results.ues:
|
||||
d = ue.to_dict()
|
||||
d.update(
|
||||
{
|
||||
"max": self.results.etud_moy_ue[ue.id].max(),
|
||||
"min": self.results.etud_moy_ue[ue.id].min(),
|
||||
"moy": self.results.etud_moy_ue[ue.id].mean(),
|
||||
"nb_moy": len(self.results.etud_moy_ue),
|
||||
}
|
||||
)
|
||||
ues.append(d)
|
||||
return ues
|
||||
|
||||
def get_modimpls(self):
|
||||
return [m.to_dict() for m in self.results.modimpls]
|
||||
|
||||
def get_etud_moy_gen(self, etudid):
|
||||
return self.results.etud_moy_gen[etudid]
|
||||
|
||||
def get_moduleimpls_attente(self):
|
||||
return [] # XXX TODO
|
||||
|
||||
def get_etud_rang(self, etudid):
|
||||
return self.rangs[etudid]
|
||||
|
||||
def get_etud_rang_group(self, etudid, group_id):
|
||||
return (None, 0) # XXX unimplemented TODO
|
||||
|
||||
def get_etud_ue_status(self, etudid, ue_id):
|
||||
return {
|
||||
"cur_moy_ue": self.results.etud_moy_ue[ue_id][etudid],
|
||||
"is_capitalized": False, # XXX TODO
|
||||
}
|
||||
|
||||
def get_etud_mod_moy(self, moduleimpl_id, etudid):
|
||||
mod_idx = self.results.modimpl_coefs_df.columns.get_loc(moduleimpl_id)
|
||||
etud_idx = self.results.etud_index[etudid]
|
||||
# moyenne sur les UE:
|
||||
self.results.sem_cube[etud_idx, mod_idx].mean()
|
||||
|
||||
def get_mod_stats(self, moduleimpl_id):
|
||||
return {
|
||||
"moy": "-",
|
||||
"max": "-",
|
||||
"min": "-",
|
||||
"nb_notes": "-",
|
||||
"nb_missing": "-",
|
||||
"nb_valid_evals": "-",
|
||||
}
|
||||
|
||||
def get_evals_in_mod(self, moduleimpl_id):
|
||||
mi = ModuleImpl.query.get(moduleimpl_id)
|
||||
evals_results = []
|
||||
for e in mi.evaluations:
|
||||
d = e.to_dict()
|
||||
d["heure_debut"] = e.heure_debut # datetime.time
|
||||
d["heure_fin"] = e.heure_fin
|
||||
d["jour"] = e.jour # datetime
|
||||
d["notes"] = {
|
||||
etud.id: {
|
||||
"etudid": etud.id,
|
||||
"value": self.results.modimpls_evals_notes[e.moduleimpl_id][e.id][
|
||||
etud.id
|
||||
],
|
||||
}
|
||||
for etud in self.results.etuds
|
||||
}
|
||||
evals_results.append(d)
|
||||
return evals_results
|
|
@ -0,0 +1,334 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Génération du bulletin en format XML / compatibilité ScoDoc 7
|
||||
|
||||
=> exporte quelques résultats BUT dans le format des anciens bulletins XML ScoDoc 7
|
||||
afin d'avoir un affichage acceptable sur les ENT anciens.
|
||||
|
||||
Les plate-formes modernes utilisent uniquement la version JSON (but/bulletin_but.py)
|
||||
"""
|
||||
|
||||
|
||||
import datetime
|
||||
from xml.etree import ElementTree
|
||||
from xml.etree.ElementTree import Element
|
||||
|
||||
from app import log
|
||||
from app.but import bulletin_but
|
||||
from app.models import FormSemestre, Identite
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_photos
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_xml
|
||||
|
||||
|
||||
def bulletin_but_xml_compat(
|
||||
formsemestre_id,
|
||||
etudid,
|
||||
doc=None, # XML document
|
||||
force_publishing=False,
|
||||
xml_nodate=False,
|
||||
xml_with_decisions=False, # inclue les decisions même si non publiées
|
||||
version="long",
|
||||
) -> str:
|
||||
"""Bulletin XML au format ScoDoc 7, avec informations "BUT" """
|
||||
from app.scodoc import sco_bulletins
|
||||
|
||||
log(
|
||||
"bulletin_but_xml_compat( formsemestre_id=%s, etudid=%s )"
|
||||
% (formsemestre_id, etudid)
|
||||
)
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
etud = Identite.query.get_or_404(etudid)
|
||||
results = bulletin_but.ResultatsSemestreBUT(formsemestre)
|
||||
nb_inscrits = len(results.etuds)
|
||||
etat_inscription = etud.etat_inscription(formsemestre.id)
|
||||
if (not formsemestre.bul_hide_xml) or force_publishing:
|
||||
published = 1
|
||||
else:
|
||||
published = 0
|
||||
if xml_nodate:
|
||||
docdate = ""
|
||||
else:
|
||||
docdate = datetime.datetime.now().isoformat()
|
||||
el = {
|
||||
"etudid": str(etudid),
|
||||
"formsemestre_id": str(formsemestre_id),
|
||||
"date": docdate,
|
||||
"publie": str(published),
|
||||
}
|
||||
if formsemestre.etapes:
|
||||
el["etape_apo"] = formsemestre.etapes[0].etape_apo or ""
|
||||
n = 2
|
||||
for et in formsemestre.etapes[1:]:
|
||||
el["etape_apo" + str(n)] = et.etape_apo or ""
|
||||
n += 1
|
||||
x = Element("bulletinetud", **el)
|
||||
if doc:
|
||||
is_appending = True
|
||||
doc.append(x)
|
||||
else:
|
||||
is_appending = False
|
||||
doc = x
|
||||
# Infos sur l'etudiant
|
||||
doc.append(
|
||||
Element(
|
||||
"etudiant",
|
||||
etudid=str(etudid),
|
||||
code_nip=etud.code_nip or "",
|
||||
code_ine=etud.code_ine or "",
|
||||
nom=scu.quote_xml_attr(etud.nom),
|
||||
prenom=scu.quote_xml_attr(etud.prenom),
|
||||
civilite=scu.quote_xml_attr(etud.civilite_str()),
|
||||
sexe=scu.quote_xml_attr(etud.civilite_str()), # compat
|
||||
photo_url=scu.quote_xml_attr(sco_photos.get_etud_photo_url(etud.id)),
|
||||
email=scu.quote_xml_attr(etud.get_first_email() or ""),
|
||||
emailperso=scu.quote_xml_attr(etud.get_first_email("emailperso") or ""),
|
||||
)
|
||||
)
|
||||
# Disponible pour publication ?
|
||||
if not published:
|
||||
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(
|
||||
scu.SCO_ENCODING
|
||||
) # stop !
|
||||
|
||||
if etat_inscription == scu.INSCRIT:
|
||||
# Moyenne générale:
|
||||
doc.append(
|
||||
Element(
|
||||
"note",
|
||||
value=scu.fmt_note(results.etud_moy_gen[etud.id]),
|
||||
min=scu.fmt_note(results.etud_moy_gen.min()),
|
||||
max=scu.fmt_note(results.etud_moy_gen.max()),
|
||||
moy=scu.fmt_note(results.etud_moy_gen.mean()), # moyenne des moy. gen.
|
||||
)
|
||||
)
|
||||
rang = 0 # XXX TODO rang de l'étduiant selon la moy gen indicative
|
||||
bonus = 0 # XXX TODO valeur du bonus sport
|
||||
doc.append(Element("rang", value=str(rang), ninscrits=str(nb_inscrits)))
|
||||
# XXX TODO: ajouter "rang_group" : rangs dans les partitions
|
||||
doc.append(Element("note_max", value="20")) # notes toujours sur 20
|
||||
doc.append(Element("bonus_sport_culture", value=str(bonus)))
|
||||
# Liste les UE / modules /evals
|
||||
for ue in results.ues:
|
||||
rang_ue = 0 # XXX TODO rang de l'étudiant dans cette UE
|
||||
nb_inscrits_ue = (
|
||||
nb_inscrits # approx: compliqué de définir le "nb d'inscrit à une UE"
|
||||
)
|
||||
x_ue = Element(
|
||||
"ue",
|
||||
id=str(ue.id),
|
||||
numero=scu.quote_xml_attr(ue.numero),
|
||||
acronyme=scu.quote_xml_attr(ue.acronyme or ""),
|
||||
titre=scu.quote_xml_attr(ue.titre or ""),
|
||||
code_apogee=scu.quote_xml_attr(ue.code_apogee or ""),
|
||||
)
|
||||
doc.append(x_ue)
|
||||
if ue.type != sco_codes_parcours.UE_SPORT:
|
||||
v = results.etud_moy_ue[ue.id][etud.id]
|
||||
else:
|
||||
v = 0 # XXX TODO valeur bonus sport pour cet étudiant
|
||||
x_ue.append(
|
||||
Element(
|
||||
"note",
|
||||
value=scu.fmt_note(v),
|
||||
min=scu.fmt_note(results.etud_moy_ue[ue.id].min()),
|
||||
max=scu.fmt_note(results.etud_moy_ue[ue.id].max()),
|
||||
)
|
||||
)
|
||||
x_ue.append(Element("ects", value=str(ue.ects if ue.ects else 0)))
|
||||
x_ue.append(Element("rang", value=str(rang_ue)))
|
||||
x_ue.append(Element("effectif", value=str(nb_inscrits_ue)))
|
||||
# Liste les modules rattachés à cette UE
|
||||
for modimpl in results.modimpls:
|
||||
# Liste ici uniquement les modules rattachés à cette UE
|
||||
if modimpl.module.ue.id == ue.id:
|
||||
mod_moy = scu.fmt_note(results.etud_moy_ue[ue.id][etud.id])
|
||||
coef = results.modimpl_coefs_df[modimpl.id][ue.id]
|
||||
x_mod = Element(
|
||||
"module",
|
||||
id=str(modimpl.id),
|
||||
code=str(modimpl.module.code or ""),
|
||||
coefficient=str(coef),
|
||||
numero=str(modimpl.module.numero or 0),
|
||||
titre=scu.quote_xml_attr(modimpl.module.titre or ""),
|
||||
abbrev=scu.quote_xml_attr(modimpl.module.abbrev or ""),
|
||||
code_apogee=scu.quote_xml_attr(
|
||||
modimpl.module.code_apogee or ""
|
||||
),
|
||||
)
|
||||
x_ue.append(x_mod)
|
||||
x_mod.append(
|
||||
Element(
|
||||
"note",
|
||||
value=mod_moy,
|
||||
min=scu.fmt_note(results.etud_moy_ue[ue.id].min()),
|
||||
max=scu.fmt_note(results.etud_moy_ue[ue.id].max()),
|
||||
moy=scu.fmt_note(results.etud_moy_ue[ue.id].mean()),
|
||||
)
|
||||
)
|
||||
# XXX TODO rangs et effectifs
|
||||
# --- notes de chaque eval:
|
||||
if version != "short":
|
||||
for e in modimpl.evaluations:
|
||||
if e.visibulletin or version == "long":
|
||||
x_eval = Element(
|
||||
"evaluation",
|
||||
jour=e.jour.isoformat() if e.jour else "",
|
||||
heure_debut=e.heure_debut.isoformat()
|
||||
if e.heure_debut
|
||||
else "",
|
||||
heure_fin=e.heure_fin.isoformat()
|
||||
if e.heure_debut
|
||||
else "",
|
||||
coefficient=str(e.coefficient),
|
||||
# pas les poids en XML compat
|
||||
evaluation_type=str(e.evaluation_type),
|
||||
description=scu.quote_xml_attr(e.description),
|
||||
# notes envoyées sur 20, ceci juste pour garder trace:
|
||||
note_max_origin=str(e.note_max),
|
||||
)
|
||||
x_mod.append(x_eval)
|
||||
x_eval.append(
|
||||
Element(
|
||||
"note",
|
||||
value=scu.fmt_note(
|
||||
results.modimpls_evals_notes[
|
||||
e.moduleimpl_id
|
||||
][e.id][etud.id],
|
||||
note_max=e.note_max,
|
||||
),
|
||||
)
|
||||
)
|
||||
# XXX TODO: Evaluations incomplètes ou futures: XXX
|
||||
# XXX TODO UE capitalisee (listee seulement si meilleure que l'UE courante)
|
||||
|
||||
# --- Absences
|
||||
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
|
||||
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
|
||||
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
|
||||
|
||||
# -------- LA SUITE EST COPIEE SANS MODIF DE sco_bulletins_xml.py ---------
|
||||
# TODO : refactoring
|
||||
|
||||
# --- Decision Jury
|
||||
if (
|
||||
sco_preferences.get_preference("bul_show_decision", formsemestre_id)
|
||||
or xml_with_decisions
|
||||
):
|
||||
infos, dpv = sco_bulletins.etud_descr_situation_semestre(
|
||||
etudid,
|
||||
formsemestre_id,
|
||||
format="xml",
|
||||
show_uevalid=sco_preferences.get_preference(
|
||||
"bul_show_uevalid", formsemestre_id
|
||||
),
|
||||
)
|
||||
x_situation = Element("situation")
|
||||
x_situation.text = scu.quote_xml_attr(infos["situation"])
|
||||
doc.append(x_situation)
|
||||
if dpv:
|
||||
decision = dpv["decisions"][0]
|
||||
etat = decision["etat"]
|
||||
if decision["decision_sem"]:
|
||||
code = decision["decision_sem"]["code"] or ""
|
||||
else:
|
||||
code = ""
|
||||
if (
|
||||
decision["decision_sem"]
|
||||
and "compense_formsemestre_id" in decision["decision_sem"]
|
||||
):
|
||||
doc.append(
|
||||
Element(
|
||||
"decision",
|
||||
code=code,
|
||||
etat=str(etat),
|
||||
compense_formsemestre_id=str(
|
||||
decision["decision_sem"]["compense_formsemestre_id"] or ""
|
||||
),
|
||||
)
|
||||
)
|
||||
else:
|
||||
doc.append(Element("decision", code=code, etat=str(etat)))
|
||||
|
||||
if decision[
|
||||
"decisions_ue"
|
||||
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
|
||||
for ue_id in decision["decisions_ue"].keys():
|
||||
ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
|
||||
doc.append(
|
||||
Element(
|
||||
"decision_ue",
|
||||
ue_id=str(ue["ue_id"]),
|
||||
numero=scu.quote_xml_attr(ue["numero"]),
|
||||
acronyme=scu.quote_xml_attr(ue["acronyme"]),
|
||||
titre=scu.quote_xml_attr(ue["titre"]),
|
||||
code=decision["decisions_ue"][ue_id]["code"],
|
||||
)
|
||||
)
|
||||
|
||||
for aut in decision["autorisations"]:
|
||||
doc.append(
|
||||
Element(
|
||||
"autorisation_inscription", semestre_id=str(aut["semestre_id"])
|
||||
)
|
||||
)
|
||||
else:
|
||||
doc.append(Element("decision", code="", etat="DEM"))
|
||||
# --- Appreciations
|
||||
cnx = ndb.GetDBConnexion()
|
||||
apprecs = sco_etud.appreciations_list(
|
||||
cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
|
||||
)
|
||||
for appr in apprecs:
|
||||
x_appr = Element(
|
||||
"appreciation",
|
||||
date=ndb.DateDMYtoISO(appr["date"]),
|
||||
)
|
||||
x_appr.text = scu.quote_xml_attr(appr["comment"])
|
||||
doc.append(x_appr)
|
||||
|
||||
if is_appending:
|
||||
return None
|
||||
else:
|
||||
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING)
|
||||
|
||||
|
||||
"""
|
||||
formsemestre_id=718
|
||||
etudid=12496
|
||||
from app.but.bulletin_but import *
|
||||
mapp.set_sco_dept("RT")
|
||||
sem = FormSemestre.query.get(formsemestre_id)
|
||||
r = ResultatsSemestreBUT(sem)
|
||||
"""
|
|
@ -0,0 +1,35 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""ScoDoc 9 : Formulaires / référentiel de compétence
|
||||
"""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileField, FileAllowed, FileRequired
|
||||
from wtforms import SelectField, SubmitField
|
||||
|
||||
|
||||
class FormationRefCompForm(FlaskForm):
|
||||
referentiel_competence = SelectField("Référentiels déjà chargés")
|
||||
submit = SubmitField("Valider")
|
||||
cancel = SubmitField("Annuler")
|
||||
|
||||
|
||||
class RefCompLoadForm(FlaskForm):
|
||||
upload = FileField(
|
||||
label="Sélectionner un fichier XML Orébut",
|
||||
validators=[
|
||||
FileRequired(),
|
||||
FileAllowed(
|
||||
[
|
||||
"xml",
|
||||
],
|
||||
"Fichier XML Orébut seulement",
|
||||
),
|
||||
],
|
||||
)
|
||||
submit = SubmitField("Valider")
|
||||
cancel = SubmitField("Annuler")
|
|
@ -0,0 +1,126 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
from xml.etree import ElementTree
|
||||
from typing import TextIO
|
||||
|
||||
from app import db
|
||||
|
||||
from app.models.but_refcomp import (
|
||||
ApcReferentielCompetences,
|
||||
ApcCompetence,
|
||||
ApcSituationPro,
|
||||
ApcAppCritique,
|
||||
ApcComposanteEssentielle,
|
||||
ApcNiveau,
|
||||
ApcParcours,
|
||||
ApcAnneeParcours,
|
||||
ApcParcoursNiveauCompetence,
|
||||
)
|
||||
from app.scodoc.sco_exceptions import ScoFormatError
|
||||
|
||||
|
||||
def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
|
||||
"""Importation XML Orébut
|
||||
peut lever TypeError ou ScoFormatError
|
||||
Résultat: instance de ApcReferentielCompetences
|
||||
"""
|
||||
try:
|
||||
root = ElementTree.XML(xml_data)
|
||||
except ElementTree.ParseError:
|
||||
raise ScoFormatError("fichier XML Orébut invalide")
|
||||
if root.tag != "referentiel_competence":
|
||||
raise ScoFormatError("élément racine 'referentiel_competence' manquant")
|
||||
args = ApcReferentielCompetences.attr_from_xml(root.attrib)
|
||||
args["dept_id"] = dept_id
|
||||
args["scodoc_orig_filename"] = orig_filename
|
||||
ref = ApcReferentielCompetences(**args)
|
||||
db.session.add(ref)
|
||||
competences = root.find("competences")
|
||||
if not competences:
|
||||
raise ScoFormatError("élément 'competences' manquant")
|
||||
for competence in competences.findall("competence"):
|
||||
c = ApcCompetence(**ApcCompetence.attr_from_xml(competence.attrib))
|
||||
ref.competences.append(c)
|
||||
# --- SITUATIONS
|
||||
situations = competence.find("situations")
|
||||
for situation in situations:
|
||||
libelle = "".join(situation.itertext()).strip()
|
||||
s = ApcSituationPro(competence_id=c.id, libelle=libelle)
|
||||
c.situations.append(s)
|
||||
# --- COMPOSANTES ESSENTIELLES
|
||||
composantes = competence.find("composantes_essentielles")
|
||||
for composante in composantes:
|
||||
libelle = "".join(composante.itertext()).strip()
|
||||
ce = ApcComposanteEssentielle(libelle=libelle)
|
||||
c.composantes_essentielles.append(ce)
|
||||
# --- NIVEAUX (années)
|
||||
niveaux = competence.find("niveaux")
|
||||
for niveau in niveaux:
|
||||
niv = ApcNiveau(**ApcNiveau.attr_from_xml(niveau.attrib))
|
||||
c.niveaux.append(niv)
|
||||
acs = niveau.find("acs")
|
||||
for ac in acs:
|
||||
libelle = "".join(ac.itertext()).strip()
|
||||
code = ac.attrib["code"]
|
||||
niv.app_critiques.append(ApcAppCritique(code=code, libelle=libelle))
|
||||
# --- PARCOURS
|
||||
parcours = root.find("parcours")
|
||||
if not parcours:
|
||||
raise ScoFormatError("élément 'parcours' manquant")
|
||||
for parcour in parcours.findall("parcour"):
|
||||
parc = ApcParcours(**ApcParcours.attr_from_xml(parcour.attrib))
|
||||
ref.parcours.append(parc)
|
||||
for annee in parcour.findall("annee"):
|
||||
a = ApcAnneeParcours(**ApcAnneeParcours.attr_from_xml(annee.attrib))
|
||||
parc.annees.append(a)
|
||||
for competence in annee.findall("competence"):
|
||||
nom = competence.attrib["nom"]
|
||||
niveau = int(competence.attrib["niveau"])
|
||||
# Retrouve la competence
|
||||
comp = ref.competences.filter_by(titre=nom).all()
|
||||
if len(comp) == 0:
|
||||
raise ScoFormatError(f"competence {nom} référencée mais on définie")
|
||||
elif len(comp) > 1:
|
||||
raise ScoFormatError(f"competence {nom} ambigüe")
|
||||
ass = ApcParcoursNiveauCompetence(
|
||||
niveau=niveau, annee_parcours=a, competence=comp[0]
|
||||
)
|
||||
db.session.add(ass)
|
||||
|
||||
db.session.commit()
|
||||
return ref
|
||||
|
||||
|
||||
"""
|
||||
xmlfile = open("but-RT-refcomp-30112021.xml")
|
||||
tree = ElementTree.parse(xmlfile)
|
||||
# get root element
|
||||
root = tree.getroot()
|
||||
assert root.tag == "referentiel_competence"
|
||||
|
||||
ref = ApcReferentielCompetences(**ApcReferentielCompetences.attr_from_xml(root.attrib))
|
||||
|
||||
competences = root.find("competences")
|
||||
if not competences:
|
||||
raise ScoFormatError("élément 'competences' manquant")
|
||||
|
||||
competence = competences.findall("competence")[0] # XXX
|
||||
|
||||
from app.but.import_refcomp import *
|
||||
dept_id = models.Departement.query.first().id
|
||||
data = open("tests/data/but-RT-refcomp-exemple.xml").read()
|
||||
ref = orebut_import_refcomp(data, dept_id)
|
||||
#------
|
||||
from app.but.import_refcomp import *
|
||||
ref = ApcReferentielCompetences.query.first()
|
||||
p = ApcParcours(code="PARC", libelle="Parcours Test")
|
||||
ref.parcours.append(p)
|
||||
annee = ApcAnneeParcours(numero=1)
|
||||
p.annees.append(annee)
|
||||
annee.competences
|
||||
c = ref.competences.filter_by(titre="Administrer").first()
|
||||
annee.competences.append(c)
|
||||
"""
|
|
@ -0,0 +1,49 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""caches pour tables APC
|
||||
"""
|
||||
|
||||
from app.scodoc import sco_cache
|
||||
|
||||
|
||||
class ModuleCoefsCache(sco_cache.ScoDocCache):
|
||||
"""Cache for module coefs
|
||||
Clé: formation_id.semestre_idx
|
||||
Valeur: DataFrame (df_load_module_coefs)
|
||||
"""
|
||||
|
||||
prefix = "MCO"
|
||||
|
||||
|
||||
class EvaluationsPoidsCache(sco_cache.ScoDocCache):
|
||||
"""Cache for poids evals
|
||||
Clé: moduleimpl_id
|
||||
Valeur: DataFrame (df_load_evaluations_poids)
|
||||
"""
|
||||
|
||||
prefix = "EPC"
|
|
@ -0,0 +1,70 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Matrices d'inscription aux modules d'un semestre
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from app import db
|
||||
from app import models
|
||||
|
||||
#
|
||||
# Le chargement des inscriptions est long: matrice nb_module x nb_etuds
|
||||
# sur test debug 116 etuds, 18 modules, on est autour de 250ms.
|
||||
# On a testé trois approches, ci-dessous (et retenu la 1ere)
|
||||
#
|
||||
def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame:
|
||||
"""Charge la matrice des inscriptions aux modules du semestre
|
||||
rows: etudid
|
||||
columns: moduleimpl_id (en chaîne)
|
||||
value: bool (0/1 inscrit ou pas)
|
||||
"""
|
||||
# méthode la moins lente: une requete par module, merge les dataframes
|
||||
moduleimpl_ids = [m.id for m in formsemestre.modimpls]
|
||||
etudids = [i.etudid for i in formsemestre.get_inscrits(include_dem=False)]
|
||||
df = pd.DataFrame(index=etudids, dtype=int)
|
||||
for moduleimpl_id in moduleimpl_ids:
|
||||
ins_df = pd.read_sql_query(
|
||||
"""SELECT etudid, 1 AS "%(moduleimpl_id)s"
|
||||
FROM notes_moduleimpl_inscription
|
||||
WHERE moduleimpl_id=%(moduleimpl_id)s""",
|
||||
db.engine,
|
||||
params={"moduleimpl_id": moduleimpl_id},
|
||||
index_col="etudid",
|
||||
dtype=int,
|
||||
)
|
||||
df = df.merge(ins_df, how="left", left_index=True, right_index=True)
|
||||
# les colonnes de df sont en float (Nan) quand il n'y a
|
||||
# aucun inscrit au module.
|
||||
df.fillna(0, inplace=True) # les non-inscrits
|
||||
return df.astype(bool) # x100 25.5s 15s 17s
|
||||
|
||||
|
||||
# chrono avec timeit:
|
||||
# timeit.timeit('x = df_load_module_inscr_v0(696)', number=100, globals=globals())
|
||||
|
||||
|
||||
def df_load_modimpl_inscr_v0(formsemestre):
|
||||
# methode 0, pur SQL Alchemy, 1.5 à 2 fois plus lente
|
||||
moduleimpl_ids = [m.id for m in formsemestre.modimpls]
|
||||
etudids = [i.etudid for i in formsemestre.inscriptions]
|
||||
df = pd.DataFrame(False, columns=moduleimpl_ids, index=etudids, dtype=bool)
|
||||
for modimpl in formsemestre.modimpls:
|
||||
ins_mod = df[modimpl.id]
|
||||
for inscr in modimpl.inscriptions:
|
||||
ins_mod[inscr.etudid] = True
|
||||
return df # x100 30.7s 46s 32s
|
||||
|
||||
|
||||
def df_load_modimpl_inscr_v2(formsemestre):
|
||||
moduleimpl_ids = [m.id for m in formsemestre.modimpls]
|
||||
etudids = [i.etudid for i in formsemestre.inscriptions]
|
||||
df = pd.DataFrame(False, columns=moduleimpl_ids, index=etudids, dtype=bool)
|
||||
cursor = db.engine.execute(
|
||||
"select moduleimpl_id, etudid from notes_moduleimpl_inscription i, notes_moduleimpl m where i.moduleimpl_id = m.id and m.formsemestre_id = %(formsemestre_id)s",
|
||||
{"formsemestre_id": formsemestre.id},
|
||||
)
|
||||
for moduleimpl_id, etudid in cursor:
|
||||
df[moduleimpl_id][etudid] = True
|
||||
return df # x100 44s, 31s, 29s, 28s
|
|
@ -0,0 +1,243 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Fonctions de calcul des moyennes de modules (modules, ressources ou SAÉ)
|
||||
|
||||
Rappel: pour éviter les confusions, on appelera *poids* les coefficients d'une
|
||||
évaluation dans un module, et *coefficients* ceux utilisés pour le calcul de la
|
||||
moyenne générale d'une UE.
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pandas.core.frame import DataFrame
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app import models
|
||||
from app.models import ModuleImpl, Evaluation, EvaluationUEPoids
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
def df_load_evaluations_poids(
|
||||
moduleimpl_id: int, default_poids=1.0
|
||||
) -> tuple[pd.DataFrame, list]:
|
||||
"""Charge poids des évaluations d'un module et retourne un dataframe
|
||||
rows = evaluations, columns = UE, value = poids (float).
|
||||
Les valeurs manquantes (évaluations sans coef vers des UE) sont
|
||||
remplies par default_poids.
|
||||
Résultat: (evals_poids, liste de UE du semestre)
|
||||
"""
|
||||
modimpl = ModuleImpl.query.get(moduleimpl_id)
|
||||
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
|
||||
ues = modimpl.formsemestre.query_ues(with_sport=False).all()
|
||||
ue_ids = [ue.id for ue in ues]
|
||||
evaluation_ids = [evaluation.id for evaluation in evaluations]
|
||||
df = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
|
||||
for eval_poids in EvaluationUEPoids.query.join(
|
||||
EvaluationUEPoids.evaluation
|
||||
).filter_by(moduleimpl_id=moduleimpl_id):
|
||||
df[eval_poids.ue_id][eval_poids.evaluation_id] = eval_poids.poids
|
||||
if default_poids is not None:
|
||||
df.fillna(value=default_poids, inplace=True)
|
||||
return df, ues
|
||||
|
||||
|
||||
def check_moduleimpl_conformity(
|
||||
moduleimpl, evals_poids: pd.DataFrame, modules_coefficients: pd.DataFrame
|
||||
) -> bool:
|
||||
"""Vérifie que les évaluations de ce moduleimpl sont bien conformes
|
||||
au PN.
|
||||
Un module est dit *conforme* si et seulement si la somme des poids de ses
|
||||
évaluations vers une UE de coefficient non nul est non nulle.
|
||||
"""
|
||||
nb_evals, nb_ues = evals_poids.shape
|
||||
if nb_evals == 0:
|
||||
return True # modules vides conformes
|
||||
if nb_ues == 0:
|
||||
return False # situation absurde (pas d'UE)
|
||||
if len(modules_coefficients) != nb_ues:
|
||||
# bug ?
|
||||
log(
|
||||
"check_moduleimpl_conformity: nb ue incoherent (moduleimpl.id={moduleimpl.id})"
|
||||
)
|
||||
return False
|
||||
module_evals_poids = evals_poids.transpose().sum(axis=1).to_numpy() != 0
|
||||
check = all(
|
||||
(modules_coefficients[moduleimpl.module.id].to_numpy() != 0)
|
||||
== module_evals_poids
|
||||
)
|
||||
return check
|
||||
|
||||
|
||||
def df_load_modimpl_notes(moduleimpl_id: int) -> tuple:
|
||||
"""Construit un dataframe avec toutes les notes de toutes les évaluations du module.
|
||||
colonnes: le nom de la colonne est l'evaluation_id (int)
|
||||
index (lignes): etudid (int)
|
||||
|
||||
Résultat: (evals_notes, liste de évaluations du moduleimpl,
|
||||
liste de booleens indiquant si l'évaluation est "complete")
|
||||
|
||||
L'ensemble des étudiants est celui des inscrits au SEMESTRE.
|
||||
|
||||
Les notes renvoyées sont "brutes" (séries de floats) et peuvent prendre les valeurs:
|
||||
note : float (valeur enregistrée brute, non normalisée sur 20)
|
||||
pas de note: NaN (rien en bd, ou étudiant non inscrit au module)
|
||||
absent: NOTES_ABSENCE (NULL en bd)
|
||||
excusé: NOTES_NEUTRALISE (voir sco_utils)
|
||||
attente: NOTES_ATTENTE
|
||||
|
||||
L'évaluation "complete" (prise en compte dans les calculs) si:
|
||||
- soit tous les étudiants inscrits au module ont des notes
|
||||
- soit elle a été déclarée "à prise ne compte immédiate" (publish_incomplete)
|
||||
|
||||
N'utilise pas de cache ScoDoc.
|
||||
"""
|
||||
# L'index du dataframe est la liste des étudiants inscrits au semestre,
|
||||
# sans les démissionnaires
|
||||
etudids = [
|
||||
e.etudid
|
||||
for e in ModuleImpl.query.get(moduleimpl_id).formsemestre.get_inscrits(
|
||||
include_dem=False
|
||||
)
|
||||
]
|
||||
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
|
||||
# --- Calcul nombre d'inscrits pour détermnier si évaluation "complete":
|
||||
if evaluations:
|
||||
# on prend les inscrits au module ET au semestre (donc sans démissionnaires)
|
||||
inscrits_module = {
|
||||
ins.etud.id for ins in evaluations[0].moduleimpl.inscriptions
|
||||
}.intersection(etudids)
|
||||
nb_inscrits_module = len(inscrits_module)
|
||||
else:
|
||||
nb_inscrits_module = 0
|
||||
# empty df with all students:
|
||||
evals_notes = pd.DataFrame(index=etudids, dtype=float)
|
||||
evaluations_completes = []
|
||||
for evaluation in evaluations:
|
||||
eval_df = pd.read_sql_query(
|
||||
"""SELECT n.etudid, n.value AS "%(evaluation_id)s"
|
||||
FROM notes_notes n, notes_moduleimpl_inscription i
|
||||
WHERE evaluation_id=%(evaluation_id)s
|
||||
AND n.etudid = i.etudid
|
||||
AND i.moduleimpl_id = %(moduleimpl_id)s
|
||||
ORDER BY n.etudid
|
||||
""",
|
||||
db.engine,
|
||||
params={
|
||||
"evaluation_id": evaluation.id,
|
||||
"moduleimpl_id": evaluation.moduleimpl.id,
|
||||
},
|
||||
index_col="etudid",
|
||||
)
|
||||
eval_df[str(evaluation.id)] = pd.to_numeric(eval_df[str(evaluation.id)])
|
||||
# is_complete ssi tous les inscrits (non dem) au semestre ont une note
|
||||
is_complete = (
|
||||
len(set(eval_df.index).intersection(etudids)) == nb_inscrits_module
|
||||
) or evaluation.publish_incomplete
|
||||
evaluations_completes.append(is_complete)
|
||||
# NULL en base => ABS (= -999)
|
||||
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
|
||||
# Ce merge met à NULL les élements non présents
|
||||
# (notes non saisies ou etuds non inscrits au module):
|
||||
evals_notes = evals_notes.merge(
|
||||
eval_df, how="left", left_index=True, right_index=True
|
||||
)
|
||||
# Force columns names to integers (evaluation ids)
|
||||
evals_notes.columns = pd.Int64Index(
|
||||
[int(x) for x in evals_notes.columns], dtype="int64"
|
||||
)
|
||||
return evals_notes, evaluations, evaluations_completes
|
||||
|
||||
|
||||
def compute_module_moy(
|
||||
evals_notes_df: pd.DataFrame,
|
||||
evals_poids_df: pd.DataFrame,
|
||||
evaluations: list,
|
||||
evaluations_completes: list,
|
||||
) -> pd.DataFrame:
|
||||
"""Calcule les moyennes des étudiants dans ce module
|
||||
|
||||
- evals_notes : DataFrame, colonnes: EVALS, Lignes: etudid
|
||||
valeur: notes brutes, float ou NOTES_ATTENTE, NOTES_NEUTRALISE,
|
||||
NOTES_ABSENCE.
|
||||
Les NaN désignent les notes manquantes (non saisies).
|
||||
|
||||
- evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
|
||||
|
||||
- evaluations: séquence d'évaluations (utilisées pour le coef et
|
||||
le barème)
|
||||
|
||||
- evaluations_completes: séquence de booléens indiquant les
|
||||
évals à prendre en compte.
|
||||
|
||||
Résultat: DataFrame, colonnes UE, lignes etud
|
||||
= la note de l'étudiant dans chaque UE pour ce module.
|
||||
ou NaN si les évaluations (dans lesquelles l'étudiant à des notes)
|
||||
ne donnent pas de coef vers cette UE.
|
||||
"""
|
||||
nb_etuds, nb_evals = evals_notes_df.shape
|
||||
nb_ues = evals_poids_df.shape[1]
|
||||
assert evals_poids_df.shape[0] == nb_evals # compat notes/poids
|
||||
if nb_etuds == 0:
|
||||
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
|
||||
# Coefficients des évaluations, met à zéro ceux des évals incomplètes:
|
||||
evals_coefs = (
|
||||
np.array(
|
||||
[e.coefficient for e in evaluations],
|
||||
dtype=float,
|
||||
)
|
||||
* evaluations_completes
|
||||
).reshape(-1, 1)
|
||||
evals_poids = evals_poids_df.values * evals_coefs
|
||||
# -> evals_poids shape : (nb_evals, nb_ues)
|
||||
assert evals_poids.shape == (nb_evals, nb_ues)
|
||||
# Remplace les notes ATT, EXC, ABS, NaN par zéro et mets les notes sur 20:
|
||||
evals_notes = np.where(
|
||||
evals_notes_df.values > scu.NOTES_ABSENCE, evals_notes_df.values, 0.0
|
||||
) / [e.note_max / 20.0 for e in evaluations]
|
||||
# Les poids des évals pour les étudiant: là où il a des notes non neutralisées
|
||||
# (ABS n'est pas neutralisée, mais ATTENTE et NEUTRALISE oui)
|
||||
# Note: les NaN sont remplacés par des 0 dans evals_notes
|
||||
# et dans dans evals_poids_etuds
|
||||
# (rappel: la comparaison est toujours false face à un NaN)
|
||||
# shape: (nb_etuds, nb_evals, nb_ues)
|
||||
poids_stacked = np.stack([evals_poids] * nb_etuds)
|
||||
evals_poids_etuds = np.where(
|
||||
np.stack([evals_notes_df.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
|
||||
poids_stacked,
|
||||
0,
|
||||
)
|
||||
# Calcule la moyenne pondérée sur les notes disponibles:
|
||||
evals_notes_stacked = np.stack([evals_notes] * nb_ues, axis=2)
|
||||
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||
etuds_moy_module = np.sum(
|
||||
evals_poids_etuds * evals_notes_stacked, axis=1
|
||||
) / np.sum(evals_poids_etuds, axis=1)
|
||||
etuds_moy_module_df = pd.DataFrame(
|
||||
etuds_moy_module, index=evals_notes_df.index, columns=evals_poids_df.columns
|
||||
)
|
||||
return etuds_moy_module_df
|
|
@ -0,0 +1,79 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Fonctions de calcul des moyennes de semestre (indicatives dans le BUT)
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def compute_sem_moys(etud_moy_ue_df, modimpl_coefs_df):
|
||||
"""Calcule la moyenne générale indicative
|
||||
= moyenne des moyennes d'UE, pondérée par la somme de leurs coefs
|
||||
|
||||
etud_moy_ue_df: DataFrame, colonnes ue_id, lignes etudid
|
||||
modimpl_coefs_df: DataFrame, colonnes moduleimpl_id, lignes UE
|
||||
|
||||
Result: panda Series, index etudid, valeur float (moyenne générale)
|
||||
"""
|
||||
moy_gen = (etud_moy_ue_df * modimpl_coefs_df.values.sum(axis=1)).sum(
|
||||
axis=1
|
||||
) / modimpl_coefs_df.values.sum()
|
||||
return moy_gen
|
||||
|
||||
|
||||
def comp_ranks_series(notes: pd.Series):
|
||||
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur numérique)
|
||||
en tenant compte des ex-aequos
|
||||
Le resultat est: { etudid : rang } où rang est une chaine decrivant le rang
|
||||
"""
|
||||
notes = notes.sort_values(ascending=False) # Serie, tri par ordre décroissant
|
||||
rangs = pd.Series(index=notes.index, dtype=str) # le rang est une chaîne
|
||||
N = len(notes)
|
||||
nb_ex = 0 # nb d'ex-aequo consécutifs en cours
|
||||
notes_i = notes.iat
|
||||
for i, etudid in enumerate(notes.index):
|
||||
# test ex-aequo
|
||||
if i < (N - 1):
|
||||
next = notes_i[i + 1]
|
||||
else:
|
||||
next = None
|
||||
val = notes_i[i]
|
||||
if nb_ex:
|
||||
srang = "%d ex" % (i + 1 - nb_ex)
|
||||
if val == next:
|
||||
nb_ex += 1
|
||||
else:
|
||||
nb_ex = 0
|
||||
else:
|
||||
if val == next:
|
||||
srang = "%d ex" % (i + 1 - nb_ex)
|
||||
nb_ex = 1
|
||||
else:
|
||||
srang = "%d" % (i + 1)
|
||||
rangs[etudid] = srang
|
||||
return rangs
|
|
@ -0,0 +1,236 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Fonctions de calcul des moyennes d'UE
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from app import db
|
||||
from app import models
|
||||
from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef
|
||||
from app.comp import moy_mod
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc import sco_codes_parcours
|
||||
|
||||
|
||||
def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.DataFrame:
|
||||
"""Charge les coefs des modules de la formation pour le semestre indiqué.
|
||||
|
||||
Ces coefs lient les modules à chaque UE.
|
||||
|
||||
Résultat: (module_coefs_df, ues, modules)
|
||||
DataFrame rows = UEs, columns = modules, value = coef.
|
||||
|
||||
Considère toutes les UE (sauf sport) et modules du semestre.
|
||||
Les coefs non définis (pas en base) sont mis à zéro.
|
||||
|
||||
Si semestre_idx None, prend toutes les UE de la formation.
|
||||
"""
|
||||
ues = (
|
||||
UniteEns.query.filter_by(formation_id=formation_id)
|
||||
.filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
|
||||
.order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme)
|
||||
)
|
||||
modules = Module.query.filter_by(formation_id=formation_id).order_by(
|
||||
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
|
||||
)
|
||||
if semestre_idx is not None:
|
||||
ues = ues.filter_by(semestre_idx=semestre_idx)
|
||||
modules = modules.filter_by(semestre_id=semestre_idx)
|
||||
ues = ues.all()
|
||||
modules = modules.all()
|
||||
ue_ids = [ue.id for ue in ues]
|
||||
module_ids = [module.id for module in modules]
|
||||
module_coefs_df = pd.DataFrame(columns=module_ids, index=ue_ids, dtype=float)
|
||||
query = (
|
||||
db.session.query(ModuleUECoef)
|
||||
.filter(UniteEns.formation_id == formation_id)
|
||||
.filter(ModuleUECoef.ue_id == UniteEns.id)
|
||||
)
|
||||
if semestre_idx is not None:
|
||||
query = query.filter(UniteEns.semestre_idx == semestre_idx)
|
||||
|
||||
for mod_coef in query:
|
||||
module_coefs_df[mod_coef.module_id][mod_coef.ue_id] = mod_coef.coef
|
||||
|
||||
module_coefs_df.fillna(value=0, inplace=True)
|
||||
|
||||
return module_coefs_df, ues, modules
|
||||
|
||||
|
||||
def df_load_modimpl_coefs(
|
||||
formsemestre: models.FormSemestre, ues=None, modimpls=None
|
||||
) -> pd.DataFrame:
|
||||
"""Charge les coefs des modules du formsemestre indiqué.
|
||||
|
||||
Comme df_load_module_coefs mais prend seulement les UE
|
||||
et modules du formsemestre.
|
||||
Si ues et modimpls sont None, prend tous ceux du formsemestre.
|
||||
Résultat: (module_coefs_df, ues, modules)
|
||||
DataFrame rows = UEs, columns = modimpl, value = coef.
|
||||
"""
|
||||
if ues is None:
|
||||
ues = formsemestre.query_ues().all()
|
||||
ue_ids = [x.id for x in ues]
|
||||
if modimpls is None:
|
||||
modimpls = formsemestre.modimpls.all()
|
||||
modimpl_ids = [x.id for x in modimpls]
|
||||
mod2impl = {m.module.id: m.id for m in modimpls}
|
||||
modimpl_coefs_df = pd.DataFrame(columns=modimpl_ids, index=ue_ids, dtype=float)
|
||||
mod_coefs = (
|
||||
db.session.query(ModuleUECoef)
|
||||
.filter(ModuleUECoef.module_id == ModuleImpl.module_id)
|
||||
.filter(ModuleImpl.formsemestre_id == formsemestre.id)
|
||||
)
|
||||
|
||||
for mod_coef in mod_coefs:
|
||||
modimpl_coefs_df[mod2impl[mod_coef.module_id]][mod_coef.ue_id] = mod_coef.coef
|
||||
modimpl_coefs_df.fillna(value=0, inplace=True)
|
||||
return modimpl_coefs_df, ues, modimpls
|
||||
|
||||
|
||||
def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
|
||||
"""Réuni les notes moyennes des modules du semestre en un "cube"
|
||||
|
||||
modimpls_notes : liste des moyennes de module
|
||||
(DataFrames rendus par compute_module_moy, (etud x UE))
|
||||
Resultat: ndarray (etud x module x UE)
|
||||
"""
|
||||
assert len(modimpls_notes)
|
||||
modimpls_notes_arr = [df.values for df in modimpls_notes]
|
||||
modimpls_notes = np.stack(modimpls_notes_arr)
|
||||
# passe de (mod x etud x ue) à (etud x mod x UE)
|
||||
return modimpls_notes.swapaxes(0, 1)
|
||||
|
||||
|
||||
def notes_sem_load_cube(formsemestre):
|
||||
"""Calcule le cube des notes du semestre
|
||||
(charge toutes les notes, calcule les moyenne des modules
|
||||
et assemble le cube)
|
||||
Resultat:
|
||||
sem_cube : ndarray (etuds x modimpls x UEs)
|
||||
modimpls_evals_poids dict { modimpl.id : evals_poids }
|
||||
modimpls_evals_notes dict { modimpl.id : evals_notes }
|
||||
modimpls_evaluations dict { modimpl.id : liste des évaluations }
|
||||
modimpls_evaluations_complete: {modimpl_id : liste de booleens (complete/non)}
|
||||
"""
|
||||
modimpls_evals_poids = {}
|
||||
modimpls_evals_notes = {}
|
||||
modimpls_evaluations = {}
|
||||
modimpls_evaluations_complete = {}
|
||||
modimpls_notes = []
|
||||
for modimpl in formsemestre.modimpls:
|
||||
evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes(
|
||||
modimpl.id
|
||||
)
|
||||
evals_poids, ues = moy_mod.df_load_evaluations_poids(modimpl.id)
|
||||
etuds_moy_module = moy_mod.compute_module_moy(
|
||||
evals_notes, evals_poids, evaluations, evaluations_completes
|
||||
)
|
||||
modimpls_evals_poids[modimpl.id] = evals_poids
|
||||
modimpls_evals_notes[modimpl.id] = evals_notes
|
||||
modimpls_evaluations[modimpl.id] = evaluations
|
||||
modimpls_evaluations_complete[modimpl.id] = evaluations_completes
|
||||
modimpls_notes.append(etuds_moy_module)
|
||||
if len(modimpls_notes):
|
||||
cube = notes_sem_assemble_cube(modimpls_notes)
|
||||
else:
|
||||
nb_etuds = formsemestre.etuds.count()
|
||||
cube = np.zeros((nb_etuds, 0, 0), dtype=float)
|
||||
return (
|
||||
cube,
|
||||
modimpls_evals_poids,
|
||||
modimpls_evals_notes,
|
||||
modimpls_evaluations,
|
||||
modimpls_evaluations_complete,
|
||||
)
|
||||
|
||||
|
||||
def compute_ue_moys(
|
||||
sem_cube: np.array,
|
||||
etuds: list,
|
||||
modimpls: list,
|
||||
ues: list,
|
||||
modimpl_inscr_df: pd.DataFrame,
|
||||
modimpl_coefs_df: pd.DataFrame,
|
||||
) -> pd.DataFrame:
|
||||
"""Calcul de la moyenne d'UE
|
||||
La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR
|
||||
NI non inscrit à (au moins un) module de cette UE
|
||||
NA pas de notes disponibles
|
||||
ERR erreur dans une formule utilisateur. [XXX pas encore gérées ici]
|
||||
|
||||
sem_cube: notes moyennes aux modules
|
||||
ndarray (etuds x modimpls x UEs)
|
||||
(floats avec des NaN)
|
||||
etuds : lites des étudiants (dim. 0 du cube)
|
||||
modimpls : liste des modules à considérer (dim. 1 du cube)
|
||||
ues : liste des UE (dim. 2 du cube)
|
||||
module_inscr_df: matrice d'inscription du semestre (etud x modimpl)
|
||||
module_coefs_df: matrice coefficients (UE x modimpl)
|
||||
|
||||
Resultat: DataFrame columns UE, rows etudid
|
||||
"""
|
||||
nb_etuds, nb_modules, nb_ues = sem_cube.shape
|
||||
assert len(modimpls) == nb_modules
|
||||
if nb_modules == 0 or nb_etuds == 0:
|
||||
return pd.DataFrame(
|
||||
index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
|
||||
)
|
||||
assert len(etuds) == nb_etuds
|
||||
assert len(ues) == nb_ues
|
||||
assert modimpl_inscr_df.shape[0] == nb_etuds
|
||||
assert modimpl_inscr_df.shape[1] == nb_modules
|
||||
assert modimpl_coefs_df.shape[0] == nb_ues
|
||||
assert modimpl_coefs_df.shape[1] == nb_modules
|
||||
modimpl_inscr = modimpl_inscr_df.values
|
||||
modimpl_coefs = modimpl_coefs_df.values
|
||||
# Duplique les inscriptions sur les UEs:
|
||||
modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues, axis=2)
|
||||
# Enlève les NaN du numérateur:
|
||||
# si on veut prendre en compte les modules avec notes neutralisées ?
|
||||
sem_cube_no_nan = np.nan_to_num(sem_cube, nan=0.0)
|
||||
|
||||
# Ne prend pas en compte les notes des étudiants non inscrits au module:
|
||||
# Annule les notes:
|
||||
sem_cube_inscrits = np.where(modimpl_inscr_stacked, sem_cube_no_nan, 0.0)
|
||||
# Annule les coefs des modules où l'étudiant n'est pas inscrit:
|
||||
modimpl_coefs_etuds = np.where(
|
||||
modimpl_inscr_stacked, np.stack([modimpl_coefs.T] * nb_etuds), 0.0
|
||||
)
|
||||
# Annule les coefs des modules NaN
|
||||
modimpl_coefs_etuds_no_nan = np.where(np.isnan(sem_cube), 0.0, modimpl_coefs_etuds)
|
||||
#
|
||||
# Version vectorisée
|
||||
#
|
||||
etud_moy_ue = np.sum(
|
||||
modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1
|
||||
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
|
||||
return pd.DataFrame(
|
||||
etud_moy_ue, index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
|
||||
)
|
|
@ -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]
|
||||
|
|
|
@ -0,0 +1,403 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Formulaires configuration logos
|
||||
|
||||
Contrib @jmp, dec 21
|
||||
"""
|
||||
import re
|
||||
|
||||
from flask import flash, url_for, redirect, render_template
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileField, FileAllowed
|
||||
from wtforms import SelectField, SubmitField, FormField, validators, FieldList
|
||||
from wtforms.fields.simple import StringField, HiddenField
|
||||
|
||||
from app import AccessDenied
|
||||
from app.models import Departement
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.scodoc import sco_logos, html_sco_header
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_config_actions import (
|
||||
LogoDelete,
|
||||
LogoUpdate,
|
||||
LogoInsert,
|
||||
BonusSportUpdate,
|
||||
)
|
||||
|
||||
from flask_login import current_user
|
||||
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
|
||||
JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + []
|
||||
|
||||
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
|
||||
|
||||
# class ItemForm(FlaskForm):
|
||||
# """Unused Generic class to document common behavior for classes
|
||||
# * ScoConfigurationForm
|
||||
# * DeptForm
|
||||
# * LogoForm
|
||||
# Some or all of these implements:
|
||||
# * Composite design pattern (ScoConfigurationForm and DeptForm)
|
||||
# - a FieldList(FormField(ItemForm))
|
||||
# - FieldListItem are created by browsing the model
|
||||
# - index dictionnary to provide direct access to a SubItemForm
|
||||
# - the direct access method (get_form)
|
||||
# * have some information added to be displayed
|
||||
# - information are collected from a model object
|
||||
# Common methods:
|
||||
# * build(model) (not for LogoForm who has no child)
|
||||
# for each child:
|
||||
# * create en entry in the FieldList for each subitem found
|
||||
# * update self.index
|
||||
# * fill_in additional information into the form
|
||||
# * recursively calls build for each chid
|
||||
# some spécific information may be added after standard processing
|
||||
# (typically header/footer description)
|
||||
# * preview(data)
|
||||
# check the data from a post and build a list of operations that has to be done.
|
||||
# for a two phase process:
|
||||
# * phase 1 (list all opérations)
|
||||
# * phase 2 (may be confirmation and execure)
|
||||
# - if no op found: return to the form with a message 'Aucune modification trouvée'
|
||||
# - only one operation found: execute and go to main page
|
||||
# - more than 1 operation found. asked form confirmation (and execution if confirmed)
|
||||
#
|
||||
# Someday we'll have time to refactor as abstract classes but Abstract FieldList makes this
|
||||
# a bit complicated
|
||||
# """
|
||||
|
||||
# Terminology:
|
||||
# dept_id : identifies a dept in modele (= list_logos()). None designates globals logos
|
||||
# dept_key : identifies a dept in this form only (..index[dept_key], and fields 'dept_key').
|
||||
# 'GLOBAL' designates globals logos (we need a string value to set up HiddenField
|
||||
GLOBAL = "_"
|
||||
|
||||
|
||||
def dept_id_to_key(dept_id):
|
||||
if dept_id is None:
|
||||
return GLOBAL
|
||||
return dept_id
|
||||
|
||||
|
||||
def dept_key_to_id(dept_key):
|
||||
if dept_key == GLOBAL:
|
||||
return None
|
||||
return dept_key
|
||||
|
||||
|
||||
class AddLogoForm(FlaskForm):
|
||||
"""Formulaire permettant l'ajout d'un logo (dans un département)"""
|
||||
|
||||
dept_key = HiddenField()
|
||||
name = StringField(
|
||||
label="Nom",
|
||||
validators=[
|
||||
validators.regexp(
|
||||
r"^[a-zA-Z0-9-]*$",
|
||||
re.IGNORECASE,
|
||||
"Ne doit comporter que lettres, chiffres ou -",
|
||||
),
|
||||
validators.Length(
|
||||
max=20, message="Un nom ne doit pas dépasser 20 caractères"
|
||||
),
|
||||
validators.DataRequired("Nom de logo requis (alphanumériques ou '-')"),
|
||||
],
|
||||
)
|
||||
upload = FileField(
|
||||
label="Sélectionner l'image",
|
||||
validators=[
|
||||
FileAllowed(
|
||||
scu.LOGOS_IMAGES_ALLOWED_TYPES,
|
||||
f"n'accepte que les fichiers image {', '.join(scu.LOGOS_IMAGES_ALLOWED_TYPES)}",
|
||||
),
|
||||
validators.DataRequired("Fichier image manquant"),
|
||||
],
|
||||
)
|
||||
do_insert = SubmitField("ajouter une image")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs["meta"] = {"csrf": False}
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def validate_name(self, name):
|
||||
dept_id = dept_key_to_id(self.dept_key.data)
|
||||
if dept_id == GLOBAL:
|
||||
dept_id = None
|
||||
if find_logo(logoname=name.data, dept_id=dept_id) is not None:
|
||||
raise validators.ValidationError("Un logo de même nom existe déjà")
|
||||
|
||||
def select_action(self):
|
||||
if self.data["do_insert"]:
|
||||
if self.validate():
|
||||
return LogoInsert.build_action(self.data)
|
||||
return None
|
||||
|
||||
|
||||
class LogoForm(FlaskForm):
|
||||
"""Embed both presentation of a logo (cf. template file configuration.html)
|
||||
and all its data and UI action (change, delete)"""
|
||||
|
||||
dept_key = HiddenField()
|
||||
logo_id = HiddenField()
|
||||
upload = FileField(
|
||||
label="Remplacer l'image",
|
||||
validators=[
|
||||
FileAllowed(
|
||||
scu.LOGOS_IMAGES_ALLOWED_TYPES,
|
||||
f"n'accepte que les fichiers image {', '.join(scu.LOGOS_IMAGES_ALLOWED_TYPES)}",
|
||||
)
|
||||
],
|
||||
)
|
||||
do_delete = SubmitField("Supprimer l'image")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs["meta"] = {"csrf": False}
|
||||
super().__init__(*args, **kwargs)
|
||||
self.logo = find_logo(
|
||||
logoname=self.logo_id.data, dept_id=dept_key_to_id(self.dept_key.data)
|
||||
).select()
|
||||
self.description = None
|
||||
self.titre = None
|
||||
self.can_delete = True
|
||||
if self.dept_key.data == GLOBAL:
|
||||
if self.logo_id.data == "header":
|
||||
self.can_delete = False
|
||||
self.description = ""
|
||||
self.titre = "Logo en-tête"
|
||||
if self.logo_id.data == "footer":
|
||||
self.can_delete = False
|
||||
self.titre = "Logo pied de page"
|
||||
self.description = ""
|
||||
else:
|
||||
if self.logo_id.data == "header":
|
||||
self.description = "Se substitue au header défini au niveau global"
|
||||
self.titre = "Logo en-tête"
|
||||
if self.logo_id.data == "footer":
|
||||
self.description = "Se substitue au footer défini au niveau global"
|
||||
self.titre = "Logo pied de page"
|
||||
|
||||
def select_action(self):
|
||||
if self.do_delete.data and self.can_delete:
|
||||
return LogoDelete.build_action(self.data)
|
||||
if self.upload.data and self.validate():
|
||||
return LogoUpdate.build_action(self.data)
|
||||
return None
|
||||
|
||||
|
||||
class DeptForm(FlaskForm):
|
||||
dept_key = HiddenField()
|
||||
dept_name = HiddenField()
|
||||
add_logo = FormField(AddLogoForm)
|
||||
logos = FieldList(FormField(LogoForm))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs["meta"] = {"csrf": False}
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def is_local(self):
|
||||
if self.dept_key.data == GLOBAL:
|
||||
return None
|
||||
return True
|
||||
|
||||
def select_action(self):
|
||||
action = self.add_logo.form.select_action()
|
||||
if action:
|
||||
return action
|
||||
for logo_entry in self.logos.entries:
|
||||
logo_form = logo_entry.form
|
||||
action = logo_form.select_action()
|
||||
if action:
|
||||
return action
|
||||
return None
|
||||
|
||||
def get_form(self, logoname=None):
|
||||
"""Retourne le formulaire associé à un logo. None si pas trouvé"""
|
||||
if logoname is None: # recherche de département
|
||||
return self
|
||||
return self.index.get(logoname, None)
|
||||
|
||||
|
||||
def _make_dept_id_name():
|
||||
"""Cette section assure que tous les départements sont traités (y compris ceux qu'ont pas de logo au départ)
|
||||
et détermine l'ordre d'affichage des DeptForm (GLOBAL d'abord, puis par ordre alpha de nom de département)
|
||||
-> [ (None, None), (dept_id, dept_name)... ]"""
|
||||
depts = [(None, GLOBAL)]
|
||||
for dept in (
|
||||
Departement.query.filter_by(visible=True).order_by(Departement.acronym).all()
|
||||
):
|
||||
depts.append((dept.id, dept.acronym))
|
||||
return depts
|
||||
|
||||
|
||||
def _ordered_logos(modele):
|
||||
"""sort logoname alphabetically but header and footer moved at start. (since there is no space in logoname)"""
|
||||
|
||||
def sort(name):
|
||||
if name == "header":
|
||||
return " 0"
|
||||
if name == "footer":
|
||||
return " 1"
|
||||
return name
|
||||
|
||||
order = sorted(modele.keys(), key=sort)
|
||||
return order
|
||||
|
||||
|
||||
def _make_dept_data(dept_id, dept_name, modele):
|
||||
dept_key = dept_id_to_key(dept_id)
|
||||
data = {
|
||||
"dept_key": dept_key,
|
||||
"dept_name": dept_name,
|
||||
"add_logo": {"dept_key": dept_key},
|
||||
}
|
||||
logos = []
|
||||
if modele is not None:
|
||||
for name in _ordered_logos(modele):
|
||||
logos.append({"dept_key": dept_key, "logo_id": name})
|
||||
data["logos"] = logos
|
||||
return data
|
||||
|
||||
|
||||
def _make_depts_data(modele):
|
||||
data = []
|
||||
for dept_id, dept_name in _make_dept_id_name():
|
||||
data.append(
|
||||
_make_dept_data(
|
||||
dept_id=dept_id, dept_name=dept_name, modele=modele.get(dept_id, None)
|
||||
)
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
def _make_data(bonus_sport, modele):
|
||||
data = {
|
||||
"bonus_sport_func_name": bonus_sport,
|
||||
"depts": _make_depts_data(modele=modele),
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
class ScoDocConfigurationForm(FlaskForm):
|
||||
"Panneau de configuration général"
|
||||
bonus_sport_func_name = SelectField(
|
||||
label="Fonction de calcul des bonus sport&culture",
|
||||
choices=[
|
||||
(x, x if x else "Aucune")
|
||||
for x in ScoDocSiteConfig.get_bonus_sport_func_names()
|
||||
],
|
||||
)
|
||||
depts = FieldList(FormField(DeptForm))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# def _set_global_logos_infos(self):
|
||||
# "specific processing for globals items"
|
||||
# global_header = self.get_form(logoname="header")
|
||||
# global_header.description = (
|
||||
# "image placée en haut de certains documents documents PDF."
|
||||
# )
|
||||
# global_header.titre = "Logo en-tête"
|
||||
# global_header.can_delete = False
|
||||
# global_footer = self.get_form(logoname="footer")
|
||||
# global_footer.description = (
|
||||
# "image placée en pied de page de certains documents documents PDF."
|
||||
# )
|
||||
# global_footer.titre = "Logo pied de page"
|
||||
# global_footer.can_delete = False
|
||||
|
||||
# def _build_dept(self, dept_id, dept_name, modele):
|
||||
# dept_key = dept_id or GLOBAL
|
||||
# data = {"dept_key": dept_key}
|
||||
# entry = self.depts.append_entry(data)
|
||||
# entry.form.build(dept_name, modele.get(dept_id, {}))
|
||||
# self.index[str(dept_key)] = entry.form
|
||||
|
||||
# def build(self, modele):
|
||||
# "Build the Form hierachy (DeptForm, LogoForm) and add extra data (from modele)"
|
||||
# # if entries already initialized (POST). keep subforms
|
||||
# self.index = {}
|
||||
# # create entries in FieldList (one entry per dept
|
||||
# for dept_id, dept_name in self.dept_id_name:
|
||||
# self._build_dept(dept_id=dept_id, dept_name=dept_name, modele=modele)
|
||||
# self._set_global_logos_infos()
|
||||
|
||||
def get_form(self, dept_key=GLOBAL, logoname=None):
|
||||
"""Retourne un formulaire:
|
||||
* pour un département (get_form(dept_id)) ou à un logo (get_form(dept_id, logname))
|
||||
* propre à un département (get_form(dept_id, logoname) ou global (get_form(logoname))
|
||||
retourne None si le formulaire cherché ne peut être trouvé
|
||||
"""
|
||||
dept_form = self.index.get(dept_key, None)
|
||||
if dept_form is None: # département non trouvé
|
||||
return None
|
||||
return dept_form.get_form(logoname)
|
||||
|
||||
def select_action(self):
|
||||
if (
|
||||
self.data["bonus_sport_func_name"]
|
||||
!= ScoDocSiteConfig.get_bonus_sport_func_name()
|
||||
):
|
||||
return BonusSportUpdate(self.data)
|
||||
for dept_entry in self.depts:
|
||||
dept_form = dept_entry.form
|
||||
action = dept_form.select_action()
|
||||
if action:
|
||||
return action
|
||||
return None
|
||||
|
||||
|
||||
def configuration():
|
||||
"""Panneau de configuration général"""
|
||||
auth_name = str(current_user)
|
||||
if not current_user.is_administrator():
|
||||
raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name)
|
||||
form = ScoDocConfigurationForm(
|
||||
data=_make_data(
|
||||
bonus_sport=ScoDocSiteConfig.get_bonus_sport_func_name(),
|
||||
modele=sco_logos.list_logos(),
|
||||
)
|
||||
)
|
||||
if form.is_submitted():
|
||||
action = form.select_action()
|
||||
if action:
|
||||
action.execute()
|
||||
flash(action.message)
|
||||
return redirect(
|
||||
url_for(
|
||||
"scodoc.configuration",
|
||||
)
|
||||
)
|
||||
return render_template(
|
||||
"configuration.html",
|
||||
scodoc_dept=None,
|
||||
title="Configuration ScoDoc",
|
||||
form=form,
|
||||
)
|
|
@ -0,0 +1,63 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Formulaires création département
|
||||
"""
|
||||
|
||||
from flask import flash, url_for, redirect, render_template
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SubmitField, validators
|
||||
from wtforms.fields.simple import StringField, BooleanField
|
||||
|
||||
from app.models import SHORT_STR_LEN
|
||||
|
||||
|
||||
class CreateDeptForm(FlaskForm):
|
||||
"""Formulaire permettant l'ajout d'un département"""
|
||||
|
||||
acronym = StringField(
|
||||
label="Acronyme",
|
||||
validators=[
|
||||
validators.regexp(
|
||||
r"^[a-zA-Z0-9_\-]*$",
|
||||
message="Ne doit comporter que lettres, chiffres ou -",
|
||||
),
|
||||
validators.Length(
|
||||
max=SHORT_STR_LEN,
|
||||
message=f"L'acronyme ne doit pas dépasser {SHORT_STR_LEN} caractères",
|
||||
),
|
||||
validators.DataRequired("acronyme du département requis"),
|
||||
],
|
||||
)
|
||||
# description = StringField(label="Description")
|
||||
visible = BooleanField(
|
||||
"Visible sur page d'accueil",
|
||||
default=True,
|
||||
)
|
||||
submit = SubmitField("Valider")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
|
@ -30,37 +30,44 @@ from app.models.etudiants import (
|
|||
EtudAnnotation,
|
||||
)
|
||||
from app.models.events import Scolog, ScolarNews
|
||||
from app.models.formations import (
|
||||
NotesFormation,
|
||||
NotesUE,
|
||||
NotesMatiere,
|
||||
NotesModule,
|
||||
NotesTag,
|
||||
notes_modules_tags,
|
||||
)
|
||||
from app.models.formations import Formation, Matiere
|
||||
from app.models.modules import Module, ModuleUECoef, NotesTag, notes_modules_tags
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.formsemestre import (
|
||||
FormSemestre,
|
||||
NotesFormsemestreEtape,
|
||||
NotesFormModalite,
|
||||
NotesFormsemestreUECoef,
|
||||
NotesFormsemestreUEComputationExpr,
|
||||
NotesFormsemestreCustomMenu,
|
||||
NotesFormsemestreInscription,
|
||||
FormSemestreEtape,
|
||||
FormationModalite,
|
||||
FormSemestreUECoef,
|
||||
FormSemestreUEComputationExpr,
|
||||
FormSemestreCustomMenu,
|
||||
FormSemestreInscription,
|
||||
notes_formsemestre_responsables,
|
||||
NotesModuleImpl,
|
||||
notes_modules_enseignants,
|
||||
NotesModuleImplInscription,
|
||||
NotesEvaluation,
|
||||
NotesSemSet,
|
||||
notes_semset_formsemestre,
|
||||
)
|
||||
from app.models.moduleimpls import (
|
||||
ModuleImpl,
|
||||
notes_modules_enseignants,
|
||||
ModuleImplInscription,
|
||||
)
|
||||
from app.models.evaluations import (
|
||||
Evaluation,
|
||||
EvaluationUEPoids,
|
||||
)
|
||||
from app.models.groups import Partition, GroupDescr, group_membership
|
||||
from app.models.notes import (
|
||||
ScolarEvent,
|
||||
ScolarFormsemestreValidation,
|
||||
ScolarFormSemestreValidation,
|
||||
ScolarAutorisationInscription,
|
||||
NotesAppreciations,
|
||||
BulAppreciations,
|
||||
NotesNotes,
|
||||
NotesNotesLog,
|
||||
)
|
||||
from app.models.preferences import ScoPreference, ScoDocSiteConfig
|
||||
|
||||
from app.models.but_refcomp import (
|
||||
ApcReferentielCompetences,
|
||||
ApcCompetence,
|
||||
ApcSituationPro,
|
||||
ApcAppCritique,
|
||||
)
|
||||
|
|
|
@ -4,9 +4,6 @@
|
|||
"""
|
||||
|
||||
from app import db
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import CODE_STR_LEN
|
||||
|
||||
|
||||
class Absence(db.Model):
|
||||
|
@ -49,7 +46,7 @@ class AbsenceNotification(db.Model):
|
|||
nbabsjust = db.Column(db.Integer)
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
db.ForeignKey("notes_formsemestre.id", ondelete="CASCADE"),
|
||||
)
|
||||
|
||||
|
||||
|
@ -73,3 +70,17 @@ class BilletAbsence(db.Model):
|
|||
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
# true si l'absence _pourrait_ etre justifiée
|
||||
justified = db.Column(db.Boolean(), default=False, server_default="false")
|
||||
|
||||
def to_dict(self):
|
||||
data = {
|
||||
"id": self.id,
|
||||
"billet_id": self.id,
|
||||
"etudid": self.etudid,
|
||||
"abs_begin": self.abs_begin,
|
||||
"abs_end": self.abs_begin,
|
||||
"description": self.description,
|
||||
"etat": self.etat,
|
||||
"entry_date": self.entry_date,
|
||||
"justified": self.justified,
|
||||
}
|
||||
return data
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
"""ScoDoc 9 models : Formation BUT 2021
|
||||
"""
|
||||
from enum import unique
|
||||
from typing import Any
|
||||
|
||||
from app import db
|
||||
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
class APCFormation(db.Model):
|
||||
"""Formation par compétence"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||
specialite = db.Column(db.Text(), nullable=False) # "RT"
|
||||
specialite_long = db.Column(
|
||||
db.Text(), nullable=False
|
||||
) # "Réseaux et télécommunications"
|
|
@ -0,0 +1,287 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 models : Référentiel Compétence BUT 2021
|
||||
"""
|
||||
from datetime import datetime
|
||||
from enum import unique
|
||||
from typing import Any
|
||||
|
||||
from app import db
|
||||
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
class XMLModel:
|
||||
_xml_attribs = {} # to be overloaded
|
||||
id = "_"
|
||||
|
||||
@classmethod
|
||||
def attr_from_xml(cls, args: dict) -> dict:
|
||||
"""dict with attributes imported from Orébut XML
|
||||
and renamed for our models.
|
||||
The mapping is specified by the _xml_attribs
|
||||
attribute in each model class.
|
||||
"""
|
||||
return {cls._xml_attribs.get(k, k): v for (k, v) in args.items()}
|
||||
|
||||
def __repr__(self):
|
||||
return f'<{self.__class__.__name__} {self.id} "{self.titre if hasattr(self, "titre") else ""}">'
|
||||
|
||||
|
||||
class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||
specialite = db.Column(db.Text())
|
||||
specialite_long = db.Column(db.Text())
|
||||
type_titre = db.Column(db.Text())
|
||||
_xml_attribs = { # Orébut xml attrib : attribute
|
||||
"type": "type_titre",
|
||||
}
|
||||
# ScoDoc specific fields:
|
||||
scodoc_date_loaded = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
scodoc_orig_filename = db.Column(db.Text())
|
||||
# Relations:
|
||||
competences = db.relationship(
|
||||
"ApcCompetence",
|
||||
backref="referentiel",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
parcours = db.relationship(
|
||||
"ApcParcours",
|
||||
backref="referentiel",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
formations = db.relationship("Formation", backref="referentiel_competence")
|
||||
|
||||
def to_dict(self):
|
||||
"""Représentation complète du ref. de comp.
|
||||
comme un dict.
|
||||
"""
|
||||
return {
|
||||
"dept_id": self.dept_id,
|
||||
"specialite": self.specialite,
|
||||
"specialite_long": self.specialite_long,
|
||||
"type_titre": self.type_titre,
|
||||
"scodoc_date_loaded": self.scodoc_date_loaded.isoformat() + "Z"
|
||||
if self.scodoc_date_loaded
|
||||
else "",
|
||||
"scodoc_orig_filename": self.scodoc_orig_filename,
|
||||
"competences": {x.titre: x.to_dict() for x in self.competences},
|
||||
"parcours": {x.code: x.to_dict() for x in self.parcours},
|
||||
}
|
||||
|
||||
|
||||
class ApcCompetence(db.Model, XMLModel):
|
||||
__table_args__ = (
|
||||
# les compétences dans Orébut sont identifiées par leur "titre"
|
||||
# unique au sein d'un référentiel:
|
||||
db.UniqueConstraint(
|
||||
"referentiel_id", "titre", name="apc_competence_referentiel_id_titre_key"
|
||||
),
|
||||
)
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
referentiel_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
|
||||
)
|
||||
titre = db.Column(db.Text(), nullable=False, index=True)
|
||||
titre_long = db.Column(db.Text())
|
||||
couleur = db.Column(db.Text())
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
_xml_attribs = { # xml_attrib : attribute
|
||||
"name": "titre",
|
||||
"libelle_long": "titre_long",
|
||||
}
|
||||
situations = db.relationship(
|
||||
"ApcSituationPro",
|
||||
backref="competence",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
composantes_essentielles = db.relationship(
|
||||
"ApcComposanteEssentielle",
|
||||
backref="competence",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
niveaux = db.relationship(
|
||||
"ApcNiveau",
|
||||
backref="competence",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"titre": self.titre,
|
||||
"titre_long": self.titre_long,
|
||||
"couleur": self.couleur,
|
||||
"numero": self.numero,
|
||||
"situations": [x.to_dict() for x in self.situations],
|
||||
"composantes_essentielles": [
|
||||
x.to_dict() for x in self.composantes_essentielles
|
||||
],
|
||||
"niveaux": {x.annee: x.to_dict() for x in self.niveaux},
|
||||
}
|
||||
|
||||
|
||||
class ApcSituationPro(db.Model, XMLModel):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
competence_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
|
||||
)
|
||||
libelle = db.Column(db.Text(), nullable=False)
|
||||
# aucun attribut (le text devient le libellé)
|
||||
def to_dict(self):
|
||||
return {"libelle": self.libelle}
|
||||
|
||||
|
||||
class ApcComposanteEssentielle(db.Model, XMLModel):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
competence_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
|
||||
)
|
||||
libelle = db.Column(db.Text(), nullable=False)
|
||||
|
||||
def to_dict(self):
|
||||
return {"libelle": self.libelle}
|
||||
|
||||
|
||||
class ApcNiveau(db.Model, XMLModel):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
competence_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
|
||||
)
|
||||
libelle = db.Column(db.Text(), nullable=False)
|
||||
annee = db.Column(db.Text(), nullable=False) # "BUT2"
|
||||
# L'ordre est l'année d'apparition de ce niveau
|
||||
ordre = db.Column(db.Integer, nullable=False) # 1, 2, 3
|
||||
app_critiques = db.relationship(
|
||||
"ApcAppCritique",
|
||||
backref="niveau",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"libelle": self.libelle,
|
||||
"annee": self.annee,
|
||||
"ordre": self.ordre,
|
||||
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques},
|
||||
}
|
||||
|
||||
|
||||
class ApcAppCritique(db.Model, XMLModel):
|
||||
"Apprentissage Critique BUT"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
niveau_id = db.Column(db.Integer, db.ForeignKey("apc_niveau.id"), nullable=False)
|
||||
code = db.Column(db.Text(), nullable=False, index=True)
|
||||
libelle = db.Column(db.Text())
|
||||
|
||||
modules = db.relationship(
|
||||
"Module",
|
||||
secondary="apc_modules_acs",
|
||||
lazy="dynamic",
|
||||
backref=db.backref("app_critiques", lazy="dynamic"),
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return {"libelle": self.libelle}
|
||||
|
||||
def get_label(self):
|
||||
return self.code + " - " + self.titre
|
||||
|
||||
def __repr__(self):
|
||||
return "<AppCritique {}>".format(self.code)
|
||||
|
||||
def get_saes(self):
|
||||
"""Liste des SAE associées"""
|
||||
return [m for m in self.modules if m.module_type == ModuleType.SAE]
|
||||
|
||||
|
||||
ApcAppCritiqueModules = db.Table(
|
||||
"apc_modules_acs",
|
||||
db.Column("module_id", db.ForeignKey("notes_modules.id")),
|
||||
db.Column("app_crit_id", db.ForeignKey("apc_app_critique.id")),
|
||||
)
|
||||
|
||||
|
||||
class ApcParcours(db.Model, XMLModel):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
referentiel_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
|
||||
)
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
code = db.Column(db.Text(), nullable=False)
|
||||
libelle = db.Column(db.Text(), nullable=False)
|
||||
annees = db.relationship(
|
||||
"ApcAnneeParcours",
|
||||
backref="parcours",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"code": self.code,
|
||||
"numero": self.numero,
|
||||
"libelle": self.libelle,
|
||||
"annees": {x.ordre: x.to_dict() for x in self.annees},
|
||||
}
|
||||
|
||||
|
||||
class ApcAnneeParcours(db.Model, XMLModel):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
parcours_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_parcours.id"), nullable=False
|
||||
)
|
||||
ordre = db.Column(db.Integer)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"ordre": self.ordre,
|
||||
"competences": {
|
||||
x.competence.titre: {"niveau": x.niveau}
|
||||
for x in self.niveaux_competences
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class ApcParcoursNiveauCompetence(db.Model):
|
||||
"""Association entre année de parcours et compétence.
|
||||
Le "niveau" de la compétence est donné ici
|
||||
(convention Orébut)
|
||||
"""
|
||||
|
||||
competence_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("apc_competence.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
)
|
||||
annee_parcours_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("apc_annee_parcours.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
)
|
||||
niveau = db.Column(db.Integer, nullable=False) # 1, 2, 3
|
||||
competence = db.relationship(
|
||||
ApcCompetence,
|
||||
backref=db.backref(
|
||||
"annee_parcours",
|
||||
passive_deletes=True,
|
||||
cascade="save-update, merge, delete, delete-orphan",
|
||||
),
|
||||
)
|
||||
annee_parcours = db.relationship(
|
||||
ApcAnneeParcours,
|
||||
backref=db.backref(
|
||||
"niveaux_competences",
|
||||
passive_deletes=True,
|
||||
cascade="save-update, merge, delete, delete-orphan",
|
||||
),
|
||||
)
|
|
@ -12,8 +12,10 @@ class Departement(db.Model):
|
|||
"""Un département ScoDoc"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
acronym = db.Column(db.String(SHORT_STR_LEN), nullable=False, index=True)
|
||||
description = db.Column(db.Text())
|
||||
acronym = db.Column(
|
||||
db.String(SHORT_STR_LEN), nullable=False, index=True
|
||||
) # ne change jamais, voir la pref. DeptName
|
||||
description = db.Column(db.Text()) # pas utilisé par ScoDoc : voir DeptFullName
|
||||
date_creation = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
visible = db.Column(
|
||||
db.Boolean(), nullable=False, default=True, server_default="true"
|
||||
|
@ -21,9 +23,7 @@ class Departement(db.Model):
|
|||
|
||||
entreprises = db.relationship("Entreprise", lazy="dynamic", backref="departement")
|
||||
etudiants = db.relationship("Identite", lazy="dynamic", backref="departement")
|
||||
formations = db.relationship(
|
||||
"NotesFormation", lazy="dynamic", backref="departement"
|
||||
)
|
||||
formations = db.relationship("Formation", lazy="dynamic", backref="departement")
|
||||
formsemestres = db.relationship(
|
||||
"FormSemestre", lazy="dynamic", backref="departement"
|
||||
)
|
||||
|
@ -33,7 +33,7 @@ class Departement(db.Model):
|
|||
semsets = db.relationship("NotesSemSet", lazy="dynamic", backref="departement")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Departement {self.acronym}>"
|
||||
return f"<{self.__class__.__name__}(id={self.id}, acronym='{self.acronym}')>"
|
||||
|
||||
def to_dict(self):
|
||||
data = {
|
||||
|
@ -44,3 +44,20 @@ class Departement(db.Model):
|
|||
"date_creation": self.date_creation,
|
||||
}
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def from_acronym(cls, acronym):
|
||||
dept = cls.query.filter_by(acronym=acronym).first_or_404()
|
||||
return dept
|
||||
|
||||
|
||||
def create_dept(acronym: str, visible=True) -> Departement:
|
||||
"Create new departement"
|
||||
from app.models import ScoPreference
|
||||
|
||||
departement = Departement(acronym=acronym, visible=visible)
|
||||
p1 = ScoPreference(name="DeptName", value=acronym, departement=departement)
|
||||
db.session.add(p1)
|
||||
db.session.add(departement)
|
||||
db.session.commit()
|
||||
return departement
|
||||
|
|
|
@ -4,9 +4,6 @@
|
|||
"""
|
||||
|
||||
from app import db
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import CODE_STR_LEN
|
||||
|
||||
|
||||
class Entreprise(db.Model):
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
et données rattachées (adresses, annotations, ...)
|
||||
"""
|
||||
|
||||
from flask import g, url_for
|
||||
|
||||
from app import db
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import CODE_STR_LEN
|
||||
from app import models
|
||||
|
||||
|
||||
class Identite(db.Model):
|
||||
|
@ -38,10 +38,83 @@ class Identite(db.Model):
|
|||
boursier = db.Column(db.Boolean()) # True si boursier ('O' en ScoDoc7)
|
||||
photo_filename = db.Column(db.Text())
|
||||
# Codes INE et NIP pas unique car le meme etud peut etre ds plusieurs dept
|
||||
code_nip = db.Column(db.Text())
|
||||
code_ine = db.Column(db.Text())
|
||||
code_nip = db.Column(db.Text(), index=True)
|
||||
code_ine = db.Column(db.Text(), index=True)
|
||||
# Ancien id ScoDoc7 pour les migrations de bases anciennes
|
||||
# ne pas utiliser après migrate_scodoc7_dept_archives
|
||||
scodoc7_id = db.Column(db.Text(), nullable=True)
|
||||
#
|
||||
adresses = db.relationship("Adresse", lazy="dynamic", backref="etud")
|
||||
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Etud {self.id} {self.nom} {self.prenom}>"
|
||||
|
||||
def civilite_str(self):
|
||||
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
|
||||
personnes ne souhaitant pas d'affichage).
|
||||
"""
|
||||
return {"M": "M.", "F": "Mme", "X": ""}[self.civilite]
|
||||
|
||||
def nom_disp(self):
|
||||
"nom à afficher"
|
||||
if self.nom_usuel:
|
||||
return (
|
||||
(self.nom_usuel + " (" + self.nom + ")") if self.nom else self.nom_usuel
|
||||
)
|
||||
else:
|
||||
return self.nom
|
||||
|
||||
def get_first_email(self, field="email") -> str:
|
||||
"le mail associé à la première adrese de l'étudiant, ou None"
|
||||
return self.adresses[0].email or None if self.adresses.count() > 0 else None
|
||||
|
||||
def to_dict_bul(self, include_urls=True):
|
||||
"""Infos exportées dans les bulletins"""
|
||||
from app.scodoc import sco_photos
|
||||
|
||||
d = {
|
||||
"civilite": self.civilite,
|
||||
"code_ine": self.code_ine,
|
||||
"code_nip": self.code_nip,
|
||||
"date_naissance": self.date_naissance.isoformat()
|
||||
if self.date_naissance
|
||||
else None,
|
||||
"email": self.get_first_email(),
|
||||
"emailperso": self.get_first_email("emailperso"),
|
||||
"etudid": self.id,
|
||||
"nom": self.nom_disp(),
|
||||
"prenom": self.prenom,
|
||||
}
|
||||
if include_urls:
|
||||
d["fiche_url"] = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=self.id
|
||||
)
|
||||
d["photo_url"] = (sco_photos.get_etud_photo_url(self.id),)
|
||||
return d
|
||||
|
||||
def inscription_courante(self):
|
||||
"""La première inscription à un formsemestre _actuellement_ en cours.
|
||||
None s'il n'y en a pas (ou plus, ou pas encore).
|
||||
"""
|
||||
r = [
|
||||
ins
|
||||
for ins in self.formsemestre_inscriptions
|
||||
if ins.formsemestre.est_courant()
|
||||
]
|
||||
return r[0] if r else None
|
||||
|
||||
def etat_inscription(self, formsemestre_id):
|
||||
"""etat de l'inscription de cet étudiant au semestre:
|
||||
False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF
|
||||
"""
|
||||
# voir si ce n'est pas trop lent:
|
||||
ins = models.FormSemestreInscription.query.filter_by(
|
||||
etudid=self.id, formsemestre_id=formsemestre_id
|
||||
).first()
|
||||
if ins:
|
||||
return ins.etat
|
||||
return False
|
||||
|
||||
|
||||
class Adresse(db.Model):
|
||||
|
@ -146,10 +219,13 @@ class ItemSuiviTag(db.Model):
|
|||
# Association tag <-> module
|
||||
itemsuivi_tags_assoc = db.Table(
|
||||
"itemsuivi_tags_assoc",
|
||||
db.Column("tag_id", db.Integer, db.ForeignKey("itemsuivi_tags.id")),
|
||||
db.Column("itemsuivi_id", db.Integer, db.ForeignKey("itemsuivi.id")),
|
||||
db.Column(
|
||||
"tag_id", db.Integer, db.ForeignKey("itemsuivi_tags.id", ondelete="CASCADE")
|
||||
),
|
||||
db.Column(
|
||||
"itemsuivi_id", db.Integer, db.ForeignKey("itemsuivi.id", ondelete="CASCADE")
|
||||
),
|
||||
)
|
||||
# ON DELETE CASCADE ?
|
||||
|
||||
|
||||
class EtudAnnotation(db.Model):
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
# -*- coding: UTF-8 -*
|
||||
|
||||
"""ScoDoc models: evaluations
|
||||
"""
|
||||
|
||||
from app import db
|
||||
from app.models import UniteEns
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import sco_evaluation_db
|
||||
|
||||
|
||||
class Evaluation(db.Model):
|
||||
"""Evaluation (contrôle, examen, ...)"""
|
||||
|
||||
__tablename__ = "notes_evaluation"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
evaluation_id = db.synonym("id")
|
||||
moduleimpl_id = db.Column(
|
||||
db.Integer, db.ForeignKey("notes_moduleimpl.id"), index=True
|
||||
)
|
||||
jour = db.Column(db.Date)
|
||||
heure_debut = db.Column(db.Time)
|
||||
heure_fin = db.Column(db.Time)
|
||||
description = db.Column(db.Text)
|
||||
note_max = db.Column(db.Float)
|
||||
coefficient = db.Column(db.Float)
|
||||
visibulletin = db.Column(
|
||||
db.Boolean, nullable=False, default=True, server_default="true"
|
||||
)
|
||||
publish_incomplete = db.Column(
|
||||
db.Boolean, nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# type d'evaluation: 0 normale, 1 rattrapage, 2 "2eme session"
|
||||
evaluation_type = db.Column(
|
||||
db.Integer, nullable=False, default=0, server_default="0"
|
||||
)
|
||||
# ordre de presentation (par défaut, le plus petit numero
|
||||
# est la plus ancienne eval):
|
||||
numero = db.Column(db.Integer)
|
||||
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<Evaluation {self.id} {self.jour.isoformat() if self.jour else ''} "{self.description[:16] if self.description else ''}">"""
|
||||
|
||||
def to_dict(self):
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
# ScoDoc7 output_formators
|
||||
e["evaluation_id"] = self.id
|
||||
e["jour"] = ndb.DateISOtoDMY(e["jour"])
|
||||
e["numero"] = ndb.int_null_is_zero(e["numero"])
|
||||
return sco_evaluation_db.evaluation_enrich_dict(e)
|
||||
|
||||
def from_dict(self, data):
|
||||
"""Set evaluation attributes from given dict values."""
|
||||
sco_evaluation_db._check_evaluation_args(data)
|
||||
for k in self.__dict__.keys():
|
||||
if k != "_sa_instance_state" and k != "id" and k in data:
|
||||
setattr(self, k, data[k])
|
||||
|
||||
def clone(self, not_copying=()):
|
||||
"""Clone, not copying the given attrs
|
||||
Attention: la copie n'a pas d'id avant le prochain commit
|
||||
"""
|
||||
d = dict(self.__dict__)
|
||||
d.pop("id") # get rid of id
|
||||
d.pop("_sa_instance_state") # get rid of SQLAlchemy special attr
|
||||
for k in not_copying:
|
||||
d.pop(k)
|
||||
copy = self.__class__(**d)
|
||||
db.session.add(copy)
|
||||
return copy
|
||||
|
||||
def set_ue_poids(self, ue, poids: float) -> None:
|
||||
"""Set poids évaluation vers cette UE"""
|
||||
self.update_ue_poids_dict({ue.id: poids})
|
||||
|
||||
def set_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
||||
"""set poids vers les UE (remplace existants)
|
||||
ue_poids_dict = { ue_id : poids }
|
||||
"""
|
||||
L = []
|
||||
for ue_id, poids in ue_poids_dict.items():
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
L.append(EvaluationUEPoids(evaluation=self, ue=ue, poids=poids))
|
||||
self.ue_poids = L
|
||||
self.moduleimpl.invalidate_evaluations_poids() # inval cache
|
||||
|
||||
def update_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
||||
"""update poids vers UE (ajoute aux existants)"""
|
||||
current = self.get_ue_poids_dict()
|
||||
current.update(ue_poids_dict)
|
||||
self.set_ue_poids_dict(current)
|
||||
|
||||
def get_ue_poids_dict(self) -> dict:
|
||||
"""returns { ue_id : poids }"""
|
||||
return {p.ue.id: p.poids for p in self.ue_poids}
|
||||
|
||||
def get_ue_poids_str(self) -> str:
|
||||
"""string describing poids, for excel cells and pdfs
|
||||
Note: si les poids ne sont pas initialisés (poids par défaut),
|
||||
ils ne sont pas affichés.
|
||||
"""
|
||||
return ", ".join([f"{p.ue.acronyme}: {p.poids}" for p in self.ue_poids])
|
||||
|
||||
|
||||
class EvaluationUEPoids(db.Model):
|
||||
"""Poids des évaluations (BUT)
|
||||
association many to many
|
||||
"""
|
||||
|
||||
evaluation_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_evaluation.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
)
|
||||
ue_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_ue.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
)
|
||||
poids = db.Column(
|
||||
db.Float,
|
||||
nullable=False,
|
||||
)
|
||||
evaluation = db.relationship(
|
||||
Evaluation,
|
||||
backref=db.backref("ue_poids", cascade="all, delete-orphan"),
|
||||
)
|
||||
ue = db.relationship(
|
||||
UniteEns,
|
||||
backref=db.backref("evaluation_ue_poids", cascade="all, delete-orphan"),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<EvaluationUEPoids {self.evaluation} {self.ue} poids={self.poids}>"
|
|
@ -4,9 +4,7 @@
|
|||
"""
|
||||
|
||||
from app import db
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import CODE_STR_LEN
|
||||
|
||||
|
||||
class Scolog(db.Model):
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
"""ScoDoc8 models : Formations (hors BUT)
|
||||
"""ScoDoc 9 models : Formations
|
||||
"""
|
||||
from typing import Any
|
||||
|
||||
from app import db
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.comp import df_cache
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models.modules import Module
|
||||
from app.models.ues import UniteEns
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
class NotesFormation(db.Model):
|
||||
class Formation(db.Model):
|
||||
"""Programme pédagogique d'une formation"""
|
||||
|
||||
__tablename__ = "notes_formations"
|
||||
__table_args__ = (db.UniqueConstraint("acronyme", "titre", "version"),)
|
||||
__table_args__ = (db.UniqueConstraint("dept_id", "acronyme", "titre", "version"),)
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
formation_id = db.synonym("id")
|
||||
|
@ -30,39 +35,117 @@ class NotesFormation(db.Model):
|
|||
type_parcours = db.Column(db.Integer, default=0, server_default="0")
|
||||
code_specialite = db.Column(db.String(SHORT_STR_LEN))
|
||||
|
||||
formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation")
|
||||
|
||||
|
||||
class NotesUE(db.Model):
|
||||
"""Unité d'Enseignement"""
|
||||
|
||||
__tablename__ = "notes_ue"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
ue_id = db.synonym("id")
|
||||
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
|
||||
acronyme = db.Column(db.Text(), nullable=False)
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
titre = db.Column(db.Text())
|
||||
# Type d'UE: 0 normal ("fondamentale"), 1 "sport", 2 "projet et stage (LP)",
|
||||
# 4 "élective"
|
||||
type = db.Column(db.Integer, default=0, server_default="0")
|
||||
# Les UE sont "compatibles" (pour la capitalisation) ssi elles ont ^m code
|
||||
# note: la fonction SQL notes_newid_ucod doit être créée à part
|
||||
ue_code = db.Column(
|
||||
db.String(SHORT_STR_LEN),
|
||||
server_default=db.text("notes_newid_ucod()"),
|
||||
nullable=False,
|
||||
# Optionnel, pour les formations type BUT
|
||||
referentiel_competence_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_referentiel_competences.id")
|
||||
)
|
||||
ects = db.Column(db.Float) # nombre de credits ECTS
|
||||
is_external = db.Column(db.Boolean(), default=False, server_default="false")
|
||||
# id de l'element pedagogique Apogee correspondant:
|
||||
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
# coef UE, utilise seulement si l'option use_ue_coefs est activée:
|
||||
coefficient = db.Column(db.Float)
|
||||
ues = db.relationship("UniteEns", backref="formation", lazy="dynamic")
|
||||
formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation")
|
||||
ues = db.relationship("UniteEns", lazy="dynamic", backref="formation")
|
||||
modules = db.relationship("Module", lazy="dynamic", backref="formation")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme}')>"
|
||||
|
||||
def to_dict(self):
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
# ScoDoc7 output_formators: (backward compat)
|
||||
e["formation_id"] = self.id
|
||||
return e
|
||||
|
||||
def get_parcours(self):
|
||||
"""get l'instance de TypeParcours de cette formation"""
|
||||
return sco_codes_parcours.get_parcours_from_code(self.type_parcours)
|
||||
|
||||
def is_apc(self):
|
||||
"True si formation APC avec SAE (BUT)"
|
||||
return self.get_parcours().APC_SAE
|
||||
|
||||
def get_module_coefs(self, semestre_idx: int = None):
|
||||
"""Les coefs des modules vers les UE (accès via cache)"""
|
||||
from app.comp import moy_ue
|
||||
|
||||
if semestre_idx is None:
|
||||
key = f"{self.id}"
|
||||
else:
|
||||
key = f"{self.id}.{semestre_idx}"
|
||||
|
||||
modules_coefficients = df_cache.ModuleCoefsCache.get(key)
|
||||
if modules_coefficients is None:
|
||||
modules_coefficients, _, _ = moy_ue.df_load_module_coefs(
|
||||
self.id, semestre_idx
|
||||
)
|
||||
df_cache.ModuleCoefsCache.set(key, modules_coefficients)
|
||||
return modules_coefficients
|
||||
|
||||
def has_locked_sems(self):
|
||||
"True if there is a locked formsemestre in this formation"
|
||||
return len(self.formsemestres.filter_by(etat=False).all()) > 0
|
||||
|
||||
def invalidate_module_coefs(self, semestre_idx: int = None):
|
||||
"""Invalide les coefficients de modules cachés.
|
||||
Si semestre_idx est None, invalide tous les semestres,
|
||||
sinon invalide le semestre indiqué et le cache de la formation.
|
||||
"""
|
||||
if semestre_idx is None:
|
||||
keys = {f"{self.id}.{m.semestre_id}" for m in self.modules}
|
||||
else:
|
||||
keys = f"{self.id}.{semestre_idx}"
|
||||
df_cache.ModuleCoefsCache.delete_many(keys | {f"{self.id}"})
|
||||
sco_cache.invalidate_formsemestre()
|
||||
|
||||
def invalidate_cached_sems(self):
|
||||
for sem in self.formsemestres:
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=sem.id)
|
||||
|
||||
def sanitize_old_formation(self) -> None:
|
||||
"""
|
||||
Corrige si nécessaire certains champs issus d'anciennes versions de ScoDoc:
|
||||
- affecte à chaque module de cette formation le semestre de son UE de rattachement,
|
||||
si elle en a une.
|
||||
- si le module_type n'est pas renseigné, le met à STANDARD.
|
||||
|
||||
Devrait être appelé lorsqu'on change le type de formation vers le BUT, et aussi
|
||||
lorsqu'on change le semestre d'une UE BUT.
|
||||
Utile pour la migration des anciennes formations vers le BUT.
|
||||
|
||||
En cas de changement, invalide les caches coefs/poids.
|
||||
"""
|
||||
if not self.is_apc():
|
||||
return
|
||||
change = False
|
||||
for mod in self.modules:
|
||||
# --- Indices de semestres:
|
||||
if (
|
||||
mod.ue.semestre_idx is not None
|
||||
and mod.ue.semestre_idx > 0
|
||||
and mod.semestre_id != mod.ue.semestre_idx
|
||||
):
|
||||
mod.semestre_id = mod.ue.semestre_idx
|
||||
db.session.add(mod)
|
||||
change = True
|
||||
# --- Types de modules
|
||||
if mod.module_type is None:
|
||||
mod.module_type = scu.ModuleType.STANDARD
|
||||
db.session.add(mod)
|
||||
change = True
|
||||
# --- Numéros de modules
|
||||
if Module.query.filter_by(formation_id=self.id, numero=None).count() > 0:
|
||||
scu.objects_renumber(db, self.modules.all())
|
||||
# --- Types d'UE (avant de rendre le type non nullable)
|
||||
ues_sans_type = UniteEns.query.filter_by(formation_id=self.id, type=None)
|
||||
if ues_sans_type.count() > 0:
|
||||
for ue in ues_sans_type:
|
||||
ue.type = 0
|
||||
db.session.add(ue)
|
||||
|
||||
db.session.commit()
|
||||
if change:
|
||||
self.invalidate_module_coefs()
|
||||
|
||||
|
||||
class NotesMatiere(db.Model):
|
||||
class Matiere(db.Model):
|
||||
"""Matières: regroupe les modules d'une UE
|
||||
La matière a peu d'utilité en dehors de la présentation des modules
|
||||
d'une UE.
|
||||
|
@ -77,50 +160,4 @@ class NotesMatiere(db.Model):
|
|||
titre = db.Column(db.Text())
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
|
||||
|
||||
class NotesModule(db.Model):
|
||||
"""Module"""
|
||||
|
||||
__tablename__ = "notes_modules"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
module_id = db.synonym("id")
|
||||
titre = db.Column(db.Text())
|
||||
abbrev = db.Column(db.Text()) # nom court
|
||||
# certains départements ont des codes infiniment longs: donc Text !
|
||||
code = db.Column(db.Text(), nullable=False)
|
||||
heures_cours = db.Column(db.Float)
|
||||
heures_td = db.Column(db.Float)
|
||||
heures_tp = db.Column(db.Float)
|
||||
coefficient = db.Column(db.Float) # coef PPN
|
||||
ects = db.Column(db.Float) # Crédits ECTS
|
||||
ue_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), index=True)
|
||||
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
|
||||
matiere_id = db.Column(db.Integer, db.ForeignKey("notes_matieres.id"))
|
||||
# pas un id mais le numéro du semestre: 1, 2, ...
|
||||
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
# id de l'element pedagogique Apogee correspondant:
|
||||
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
module_type = db.Column(db.Integer) # NULL ou 0:defaut, 1: malus (NOTES_MALUS)
|
||||
|
||||
|
||||
class NotesTag(db.Model):
|
||||
"""Tag sur un module"""
|
||||
|
||||
__tablename__ = "notes_tags"
|
||||
__table_args__ = (db.UniqueConstraint("title", "dept_id"),)
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
tag_id = db.synonym("id")
|
||||
|
||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||
title = db.Column(db.Text(), nullable=False)
|
||||
|
||||
|
||||
# Association tag <-> module
|
||||
notes_modules_tags = db.Table(
|
||||
"notes_modules_tags",
|
||||
db.Column("tag_id", db.Integer, db.ForeignKey("notes_tags.id")),
|
||||
db.Column("module_id", db.Integer, db.ForeignKey("notes_modules.id")),
|
||||
)
|
||||
modules = db.relationship("Module", lazy="dynamic", backref="matiere")
|
||||
|
|
|
@ -1,19 +1,29 @@
|
|||
# -*- coding: UTF-8 -*
|
||||
|
||||
"""ScoDoc models
|
||||
"""ScoDoc models: formsemestre
|
||||
"""
|
||||
from typing import Any
|
||||
import datetime
|
||||
|
||||
import flask_sqlalchemy
|
||||
|
||||
from app import db
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import CODE_STR_LEN
|
||||
from app.models import UniteEns
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.modules import Module
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
from app.models.etudiants import Identite
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||
|
||||
|
||||
class FormSemestre(db.Model):
|
||||
"""Mise en oeuvre d'un semestre de formation
|
||||
was notes_formsemestre
|
||||
"""
|
||||
"""Mise en oeuvre d'un semestre de formation"""
|
||||
|
||||
__tablename__ = "notes_formsemestre"
|
||||
|
||||
|
@ -32,7 +42,7 @@ class FormSemestre(db.Model):
|
|||
) # False si verrouillé
|
||||
modalite = db.Column(
|
||||
db.String(SHORT_STR_LEN), db.ForeignKey("notes_form_modalites.modalite")
|
||||
)
|
||||
) # "FI", "FAP", "FC", ...
|
||||
# gestion compensation sem DUT:
|
||||
gestion_compensation = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
|
@ -41,6 +51,10 @@ class FormSemestre(db.Model):
|
|||
bul_hide_xml = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# Bloque le calcul des moyennes (générale et d'UE)
|
||||
block_moyennes = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# semestres decales (pour gestion jurys):
|
||||
gestion_semestrielle = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
|
@ -66,16 +80,174 @@ class FormSemestre(db.Model):
|
|||
# code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'
|
||||
elt_annee_apo = db.Column(db.Text())
|
||||
|
||||
# Relations:
|
||||
etapes = db.relationship(
|
||||
"NotesFormsemestreEtape", cascade="all,delete", backref="notes_formsemestre"
|
||||
"FormSemestreEtape", cascade="all,delete", backref="formsemestre"
|
||||
)
|
||||
modimpls = db.relationship("ModuleImpl", backref="formsemestre", lazy="dynamic")
|
||||
etuds = db.relationship(
|
||||
"Identite",
|
||||
secondary="notes_formsemestre_inscription",
|
||||
viewonly=True,
|
||||
lazy="dynamic",
|
||||
)
|
||||
responsables = db.relationship(
|
||||
"User",
|
||||
secondary="notes_formsemestre_responsables",
|
||||
lazy=True,
|
||||
backref=db.backref("formsemestres", lazy=True),
|
||||
)
|
||||
# Ancien id ScoDoc7 pour les migrations de bases anciennes
|
||||
# ne pas utiliser après migrate_scodoc7_dept_archives
|
||||
scodoc7_id = db.Column(db.Text(), nullable=True)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(FormSemestre, self).__init__(**kwargs)
|
||||
if self.modalite is None:
|
||||
self.modalite = NotesFormModalite.DEFAULT_MODALITE
|
||||
self.modalite = FormationModalite.DEFAULT_MODALITE
|
||||
|
||||
def to_dict(self):
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
# ScoDoc7 output_formators: (backward compat)
|
||||
d["formsemestre_id"] = self.id
|
||||
d["date_debut"] = (
|
||||
self.date_debut.strftime("%d/%m/%Y") if self.date_debut else ""
|
||||
)
|
||||
d["date_fin"] = self.date_fin.strftime("%d/%m/%Y") if self.date_fin else ""
|
||||
d["responsables"] = [u.id for u in self.responsables]
|
||||
return d
|
||||
|
||||
def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery:
|
||||
"""UE des modules de ce semestre, triées par numéro.
|
||||
- Formations classiques: les UEs auxquelles appartiennent
|
||||
les modules mis en place dans ce semestre.
|
||||
- Formations APC / BUT: les UEs de la formation qui ont
|
||||
le même numéro de semestre que ce formsemestre.
|
||||
"""
|
||||
if self.formation.get_parcours().APC_SAE:
|
||||
sem_ues = UniteEns.query.filter_by(
|
||||
formation=self.formation, semestre_idx=self.semestre_id
|
||||
)
|
||||
else:
|
||||
sem_ues = db.session.query(UniteEns).filter(
|
||||
ModuleImpl.formsemestre_id == self.id,
|
||||
Module.id == ModuleImpl.module_id,
|
||||
UniteEns.id == Module.ue_id,
|
||||
)
|
||||
if not with_sport:
|
||||
sem_ues = sem_ues.filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
|
||||
return sem_ues.order_by(UniteEns.numero)
|
||||
|
||||
def est_courant(self) -> bool:
|
||||
"""Vrai si la date actuelle (now) est dans le semestre
|
||||
(les dates de début et fin sont incluses)
|
||||
"""
|
||||
today = datetime.date.today()
|
||||
return (self.date_debut <= today) and (today <= self.date_fin)
|
||||
|
||||
def est_decale(self):
|
||||
"""Vrai si semestre "décalé"
|
||||
c'est à dire semestres impairs commençant entre janvier et juin
|
||||
et les pairs entre juillet et decembre
|
||||
"""
|
||||
if self.semestre_id <= 0:
|
||||
return False # formations sans semestres
|
||||
return (self.semestre_id % 2 and self.date_debut.month <= 6) or (
|
||||
not self.semestre_id % 2 and self.date_debut.month > 6
|
||||
)
|
||||
|
||||
def etapes_apo_str(self) -> str:
|
||||
"""Chaine décrivant les étapes de ce semestre
|
||||
ex: "V1RT, V1RT3, V1RT4"
|
||||
"""
|
||||
if not self.etapes:
|
||||
return ""
|
||||
return ", ".join([str(x.etape_apo) for x in self.etapes])
|
||||
|
||||
def responsables_str(self, abbrev_prenom=True) -> str:
|
||||
"""chaîne "J. Dupond, X. Martin"
|
||||
ou "Jacques Dupond, Xavier Martin"
|
||||
"""
|
||||
if not self.responsables:
|
||||
return ""
|
||||
if abbrev_prenom:
|
||||
return ", ".join([u.get_prenomnom() for u in self.responsables])
|
||||
else:
|
||||
return ", ".join([u.get_nomcomplet() for u in self.responsables])
|
||||
|
||||
def annee_scolaire_str(self):
|
||||
"2021 - 2022"
|
||||
return scu.annee_scolaire_repr(self.date_debut.year, self.date_debut.month)
|
||||
|
||||
def session_id(self) -> str:
|
||||
"""identifiant externe de semestre de formation
|
||||
Exemple: RT-DUT-FI-S1-ANNEE
|
||||
|
||||
DEPT-TYPE-MODALITE+-S?|SPECIALITE
|
||||
|
||||
TYPE=DUT|LP*|M*
|
||||
MODALITE=FC|FI|FA (si plusieurs, en inverse alpha)
|
||||
|
||||
SPECIALITE=[A-Z]+ EON,ASSUR, ... (si pas Sn ou SnD)
|
||||
|
||||
ANNEE=annee universitaire de debut (exemple: un S2 de 2013-2014 sera S2-2013)
|
||||
"""
|
||||
imputation_dept = sco_preferences.get_preference("ImputationDept", self.id)
|
||||
if not imputation_dept:
|
||||
imputation_dept = sco_preferences.get_preference("DeptName")
|
||||
imputation_dept = imputation_dept.upper()
|
||||
parcours_name = self.formation.get_parcours().NAME
|
||||
modalite = self.modalite
|
||||
# exception pour code Apprentissage:
|
||||
modalite = (modalite or "").replace("FAP", "FA").replace("APP", "FA")
|
||||
if self.semestre_id > 0:
|
||||
decale = "D" if self.est_decale() else ""
|
||||
semestre_id = f"S{self.semestre_id}{decale}"
|
||||
else:
|
||||
semestre_id = self.formation.code_specialite or ""
|
||||
annee_sco = str(
|
||||
scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month)
|
||||
)
|
||||
return scu.sanitize_string(
|
||||
"-".join((imputation_dept, parcours_name, modalite, semestre_id, annee_sco))
|
||||
)
|
||||
|
||||
def titre_mois(self) -> str:
|
||||
"""Le titre et les dates du semestre, pour affichage dans des listes
|
||||
Ex: "BUT QLIO (PN 2022) semestre 1 FI (Sept 2022 - Jan 2023)"
|
||||
"""
|
||||
return f"""{self.titre_num()} {self.modalite or ''} ({
|
||||
scu.MONTH_NAMES_ABBREV[self.date_debut.month-1]} {
|
||||
self.date_debut.year} - {
|
||||
scu.MONTH_NAMES_ABBREV[self.date_fin.month -1]} {
|
||||
self.date_fin.year})"""
|
||||
|
||||
def titre_num(self) -> str:
|
||||
"""Le titre est le semestre, ex ""DUT Informatique semestre 2"" """
|
||||
if self.semestre_id == sco_codes_parcours.NO_SEMESTRE_ID:
|
||||
return self.titre
|
||||
return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}"
|
||||
|
||||
def get_abs_count(self, etudid):
|
||||
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
||||
tuple (nb abs non justifiées, nb abs justifiées)
|
||||
Utilise un cache.
|
||||
"""
|
||||
from app.scodoc import sco_abs
|
||||
|
||||
return sco_abs.get_abs_count_in_interval(
|
||||
etudid, self.date_debut.isoformat(), self.date_fin.isoformat()
|
||||
)
|
||||
|
||||
def get_inscrits(self, include_dem=False) -> list:
|
||||
"""Liste des étudiants inscrits à ce semestre
|
||||
Si all, tous les étudiants, avec les démissionnaires.
|
||||
"""
|
||||
if include_dem:
|
||||
return [ins.etud for ins in self.inscriptions]
|
||||
else:
|
||||
return [ins.etud for ins in self.inscriptions if ins.etat == scu.INSCRIT]
|
||||
|
||||
|
||||
# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
|
||||
|
@ -90,7 +262,7 @@ notes_formsemestre_responsables = db.Table(
|
|||
)
|
||||
|
||||
|
||||
class NotesFormsemestreEtape(db.Model):
|
||||
class FormSemestreEtape(db.Model):
|
||||
"""Étape Apogée associées au semestre"""
|
||||
|
||||
__tablename__ = "notes_formsemestre_etapes"
|
||||
|
@ -99,10 +271,16 @@ class NotesFormsemestreEtape(db.Model):
|
|||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
)
|
||||
etape_apo = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
etape_apo = db.Column(db.String(APO_CODE_STR_LEN), index=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Etape {self.id} apo={self.etape_apo}>"
|
||||
|
||||
def as_apovdi(self):
|
||||
return ApoEtapeVDI(self.etape_apo)
|
||||
|
||||
|
||||
class NotesFormModalite(db.Model):
|
||||
class FormationModalite(db.Model):
|
||||
"""Modalités de formation, utilisées pour la présentation
|
||||
(grouper les semestres, générer des codes, etc.)
|
||||
"""
|
||||
|
@ -129,7 +307,7 @@ class NotesFormModalite(db.Model):
|
|||
numero = 0
|
||||
try:
|
||||
for (code, titre) in (
|
||||
(NotesFormModalite.DEFAULT_MODALITE, "Formation Initiale"),
|
||||
(FormationModalite.DEFAULT_MODALITE, "Formation Initiale"),
|
||||
("FAP", "Apprentissage"),
|
||||
("FC", "Formation Continue"),
|
||||
("DEC", "Formation Décalées"),
|
||||
|
@ -140,9 +318,9 @@ class NotesFormModalite(db.Model):
|
|||
("EXT", "Extérieur"),
|
||||
("OTHER", "Autres formations"),
|
||||
):
|
||||
modalite = NotesFormModalite.query.filter_by(modalite=code).first()
|
||||
modalite = FormationModalite.query.filter_by(modalite=code).first()
|
||||
if modalite is None:
|
||||
modalite = NotesFormModalite(
|
||||
modalite = FormationModalite(
|
||||
modalite=code, titre=titre, numero=numero
|
||||
)
|
||||
db.session.add(modalite)
|
||||
|
@ -153,7 +331,7 @@ class NotesFormModalite(db.Model):
|
|||
raise
|
||||
|
||||
|
||||
class NotesFormsemestreUECoef(db.Model):
|
||||
class FormSemestreUECoef(db.Model):
|
||||
"""Coef des UE capitalisees arrivant dans ce semestre"""
|
||||
|
||||
__tablename__ = "notes_formsemestre_uecoef"
|
||||
|
@ -172,7 +350,7 @@ class NotesFormsemestreUECoef(db.Model):
|
|||
coefficient = db.Column(db.Float, nullable=False)
|
||||
|
||||
|
||||
class NotesFormsemestreUEComputationExpr(db.Model):
|
||||
class FormSemestreUEComputationExpr(db.Model):
|
||||
"""Formules utilisateurs pour calcul moyenne UE"""
|
||||
|
||||
__tablename__ = "notes_formsemestre_ue_computation_expr"
|
||||
|
@ -192,7 +370,7 @@ class NotesFormsemestreUEComputationExpr(db.Model):
|
|||
computation_expr = db.Column(db.Text())
|
||||
|
||||
|
||||
class NotesFormsemestreCustomMenu(db.Model):
|
||||
class FormSemestreCustomMenu(db.Model):
|
||||
"""Menu custom associe au semestre"""
|
||||
|
||||
__tablename__ = "notes_formsemestre_custommenu"
|
||||
|
@ -208,7 +386,7 @@ class NotesFormsemestreCustomMenu(db.Model):
|
|||
idx = db.Column(db.Integer, default=0, server_default="0") # rang dans le menu
|
||||
|
||||
|
||||
class NotesFormsemestreInscription(db.Model):
|
||||
class FormSemestreInscription(db.Model):
|
||||
"""Inscription à un semestre de formation"""
|
||||
|
||||
__tablename__ = "notes_formsemestre_inscription"
|
||||
|
@ -217,99 +395,30 @@ class NotesFormsemestreInscription(db.Model):
|
|||
id = db.Column(db.Integer, primary_key=True)
|
||||
formsemestre_inscription_id = db.synonym("id")
|
||||
|
||||
etudid = db.Column(db.Integer, db.ForeignKey("identite.id"))
|
||||
etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True)
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
index=True,
|
||||
)
|
||||
etud = db.relationship(
|
||||
Identite,
|
||||
backref=db.backref("formsemestre_inscriptions", cascade="all, delete-orphan"),
|
||||
)
|
||||
formsemestre = db.relationship(
|
||||
FormSemestre,
|
||||
backref=db.backref(
|
||||
"inscriptions",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="FormSemestreInscription.etudid",
|
||||
),
|
||||
)
|
||||
# I inscrit, D demission en cours de semestre, DEF si "defaillant"
|
||||
etat = db.Column(db.String(CODE_STR_LEN))
|
||||
etat = db.Column(db.String(CODE_STR_LEN), index=True)
|
||||
# etape apogee d'inscription (experimental 2020)
|
||||
etape = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
|
||||
|
||||
class NotesModuleImpl(db.Model):
|
||||
"""Mise en oeuvre d'un module pour une annee/semestre"""
|
||||
|
||||
__tablename__ = "notes_moduleimpl"
|
||||
__table_args__ = (db.UniqueConstraint("formsemestre_id", "module_id"),)
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
moduleimpl_id = db.synonym("id")
|
||||
module_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_modules.id"),
|
||||
)
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
index=True,
|
||||
)
|
||||
responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id"))
|
||||
# formule de calcul moyenne:
|
||||
computation_expr = db.Column(db.Text())
|
||||
|
||||
|
||||
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
||||
notes_modules_enseignants = db.Table(
|
||||
"notes_modules_enseignants",
|
||||
db.Column(
|
||||
"moduleimpl_id",
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_moduleimpl.id"),
|
||||
),
|
||||
db.Column("ens_id", db.Integer, db.ForeignKey("user.id")),
|
||||
# ? db.UniqueConstraint("moduleimpl_id", "ens_id"),
|
||||
)
|
||||
# XXX il manque probablement une relation pour gérer cela
|
||||
|
||||
|
||||
class NotesModuleImplInscription(db.Model):
|
||||
"""Inscription à un module (etudiants,moduleimpl)"""
|
||||
|
||||
__tablename__ = "notes_moduleimpl_inscription"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
moduleimpl_inscription_id = db.synonym("id")
|
||||
moduleimpl_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_moduleimpl.id"),
|
||||
index=True,
|
||||
)
|
||||
etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True)
|
||||
|
||||
|
||||
class NotesEvaluation(db.Model):
|
||||
"""Evaluation (contrôle, examen, ...)"""
|
||||
|
||||
__tablename__ = "notes_evaluation"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
evaluation_id = db.synonym("id")
|
||||
moduleimpl_id = db.Column(
|
||||
db.Integer, db.ForeignKey("notes_moduleimpl.id"), index=True
|
||||
)
|
||||
jour = db.Column(db.Date)
|
||||
heure_debut = db.Column(db.Time)
|
||||
heure_fin = db.Column(db.Time)
|
||||
description = db.Column(db.Text)
|
||||
note_max = db.Column(db.Float)
|
||||
coefficient = db.Column(db.Float)
|
||||
visibulletin = db.Column(
|
||||
db.Boolean, nullable=False, default=True, server_default="true"
|
||||
)
|
||||
publish_incomplete = db.Column(
|
||||
db.Boolean, nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# type d'evaluation: 0 normale, 1 rattrapage, 2 "2eme session"
|
||||
evaluation_type = db.Column(
|
||||
db.Integer, nullable=False, default=0, server_default="0"
|
||||
)
|
||||
# ordre de presentation (par défaut, le plus petit numero
|
||||
# est la plus ancienne eval):
|
||||
numero = db.Column(db.Integer)
|
||||
|
||||
|
||||
class NotesSemSet(db.Model):
|
||||
"""semsets: ensemble de formsemestres pour exports Apogée"""
|
||||
|
||||
|
|
|
@ -5,9 +5,7 @@
|
|||
from typing import Any
|
||||
|
||||
from app import db
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import CODE_STR_LEN
|
||||
from app.models import GROUPNAME_STR_LEN
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
# -*- coding: UTF-8 -*
|
||||
"""ScoDoc models: moduleimpls
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
from app import db
|
||||
from app.comp import df_cache
|
||||
from app.models import UniteEns, Identite
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
class ModuleImpl(db.Model):
|
||||
"""Mise en oeuvre d'un module pour une annee/semestre"""
|
||||
|
||||
__tablename__ = "notes_moduleimpl"
|
||||
__table_args__ = (db.UniqueConstraint("formsemestre_id", "module_id"),)
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
moduleimpl_id = db.synonym("id")
|
||||
module_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_modules.id"),
|
||||
)
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
index=True,
|
||||
)
|
||||
responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id"))
|
||||
# formule de calcul moyenne:
|
||||
computation_expr = db.Column(db.Text())
|
||||
|
||||
evaluations = db.relationship("Evaluation", lazy="dynamic", backref="moduleimpl")
|
||||
enseignants = db.relationship(
|
||||
"User",
|
||||
secondary="notes_modules_enseignants",
|
||||
lazy="dynamic",
|
||||
backref="moduleimpl",
|
||||
viewonly=True,
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(ModuleImpl, self).__init__(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
|
||||
|
||||
def get_evaluations_poids(self) -> pd.DataFrame:
|
||||
"""Les poids des évaluations vers les UE (accès via cache)"""
|
||||
evaluations_poids = df_cache.EvaluationsPoidsCache.get(self.id)
|
||||
if evaluations_poids is None:
|
||||
from app.comp import moy_mod
|
||||
|
||||
evaluations_poids, _ = moy_mod.df_load_evaluations_poids(self.id)
|
||||
df_cache.EvaluationsPoidsCache.set(self.id, evaluations_poids)
|
||||
return evaluations_poids
|
||||
|
||||
def invalidate_evaluations_poids(self):
|
||||
"""Invalide poids cachés"""
|
||||
df_cache.EvaluationsPoidsCache.delete(self.id)
|
||||
|
||||
def check_apc_conformity(self) -> bool:
|
||||
"""true si les poids des évaluations du module permettent de satisfaire
|
||||
les coefficients du PN.
|
||||
"""
|
||||
if not self.module.formation.get_parcours().APC_SAE or (
|
||||
self.module.module_type != scu.ModuleType.RESSOURCE
|
||||
and self.module.module_type != scu.ModuleType.SAE
|
||||
):
|
||||
return True
|
||||
from app.comp import moy_mod
|
||||
|
||||
return moy_mod.check_moduleimpl_conformity(
|
||||
self,
|
||||
self.get_evaluations_poids(),
|
||||
self.module.formation.get_module_coefs(self.module.semestre_id),
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
"""as a dict, with the same conversions as in ScoDoc7"""
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
# ScoDoc7 output_formators: (backward compat)
|
||||
e["moduleimpl_id"] = self.id
|
||||
e["ens"] = [
|
||||
{"moduleimpl_id": self.id, "ens_id": e.id} for e in self.enseignants
|
||||
]
|
||||
e["module"] = self.module.to_dict()
|
||||
return e
|
||||
|
||||
|
||||
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
||||
notes_modules_enseignants = db.Table(
|
||||
"notes_modules_enseignants",
|
||||
db.Column(
|
||||
"moduleimpl_id",
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_moduleimpl.id"),
|
||||
),
|
||||
db.Column("ens_id", db.Integer, db.ForeignKey("user.id")),
|
||||
# ? db.UniqueConstraint("moduleimpl_id", "ens_id"),
|
||||
)
|
||||
# XXX il manque probablement une relation pour gérer cela
|
||||
|
||||
|
||||
class ModuleImplInscription(db.Model):
|
||||
"""Inscription à un module (etudiants,moduleimpl)"""
|
||||
|
||||
__tablename__ = "notes_moduleimpl_inscription"
|
||||
__table_args__ = (db.UniqueConstraint("moduleimpl_id", "etudid"),)
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
moduleimpl_inscription_id = db.synonym("id")
|
||||
moduleimpl_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_moduleimpl.id"),
|
||||
index=True,
|
||||
)
|
||||
etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True)
|
||||
etud = db.relationship(
|
||||
Identite,
|
||||
backref=db.backref("moduleimpl_inscriptions", cascade="all, delete-orphan"),
|
||||
)
|
||||
modimpl = db.relationship(
|
||||
ModuleImpl,
|
||||
backref=db.backref("inscriptions", cascade="all, delete-orphan"),
|
||||
)
|
|
@ -0,0 +1,202 @@
|
|||
"""ScoDoc 9 models : Modules
|
||||
"""
|
||||
|
||||
from app import db
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
class Module(db.Model):
|
||||
"""Module"""
|
||||
|
||||
__tablename__ = "notes_modules"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
module_id = db.synonym("id")
|
||||
titre = db.Column(db.Text())
|
||||
abbrev = db.Column(db.Text()) # nom court
|
||||
# certains départements ont des codes infiniment longs: donc Text !
|
||||
code = db.Column(db.Text(), nullable=False)
|
||||
heures_cours = db.Column(db.Float)
|
||||
heures_td = db.Column(db.Float)
|
||||
heures_tp = db.Column(db.Float)
|
||||
coefficient = db.Column(db.Float) # coef PPN (sauf en APC)
|
||||
ects = db.Column(db.Float) # Crédits ECTS
|
||||
ue_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), index=True)
|
||||
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
|
||||
matiere_id = db.Column(db.Integer, db.ForeignKey("notes_matieres.id"))
|
||||
# pas un id mais le numéro du semestre: 1, 2, ...
|
||||
# note: en APC, le semestre qui fait autorité est celui de l'UE
|
||||
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
# id de l'element pedagogique Apogee correspondant:
|
||||
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
# Type: ModuleType: DEFAULT, MALUS, RESSOURCE, MODULE_SAE (enum)
|
||||
module_type = db.Column(db.Integer)
|
||||
# Relations:
|
||||
modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic")
|
||||
ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True)
|
||||
tags = db.relationship(
|
||||
"NotesTag",
|
||||
secondary="notes_modules_tags",
|
||||
lazy=True,
|
||||
backref=db.backref("modules", lazy=True),
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.ue_coefs = []
|
||||
super(Module, self).__init__(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Module{ModuleType(self.module_type or ModuleType.STANDARD).name} id={self.id} code={self.code}>"
|
||||
|
||||
def to_dict(self):
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
# ScoDoc7 output_formators: (backward compat)
|
||||
e["module_id"] = self.id
|
||||
e["heures_cours"] = 0.0 if self.heures_cours is None else self.heures_cours
|
||||
e["heures_td"] = 0.0 if self.heures_td is None else self.heures_td
|
||||
e["heures_tp"] = 0.0 if self.heures_tp is None else self.heures_tp
|
||||
e["numero"] = 0 if self.numero is None else self.numero
|
||||
e["coefficient"] = 0.0 if self.coefficient is None else self.coefficient
|
||||
e["module_type"] = 0 if self.module_type is None else self.module_type
|
||||
return e
|
||||
|
||||
def is_apc(self):
|
||||
"True si module SAÉ ou Ressource"
|
||||
return self.module_type and scu.ModuleType(self.module_type) in {
|
||||
scu.ModuleType.RESSOURCE,
|
||||
scu.ModuleType.SAE,
|
||||
}
|
||||
|
||||
def type_name(self):
|
||||
return scu.MODULE_TYPE_NAMES[self.module_type]
|
||||
|
||||
def set_ue_coef(self, ue, coef: float) -> None:
|
||||
"""Set coef module vers cette UE"""
|
||||
self.update_ue_coef_dict({ue.id: coef})
|
||||
|
||||
def set_ue_coef_dict(self, ue_coef_dict: dict) -> None:
|
||||
"""set coefs vers les UE (remplace existants)
|
||||
ue_coef_dict = { ue_id : coef }
|
||||
Les coefs nuls (zéro) ne sont pas stockés: la relation est supprimée.
|
||||
"""
|
||||
changed = False
|
||||
for ue_id, coef in ue_coef_dict.items():
|
||||
# Existant ?
|
||||
coefs = [c for c in self.ue_coefs if c.ue_id == ue_id]
|
||||
if coefs:
|
||||
ue_coef = coefs[0]
|
||||
if coef == 0.0: # supprime ce coef
|
||||
db.session.delete(ue_coef)
|
||||
changed = True
|
||||
elif coef != ue_coef.coef:
|
||||
ue_coef.coef = coef
|
||||
db.session.add(ue_coef)
|
||||
changed = True
|
||||
else:
|
||||
# crée nouveau coef:
|
||||
if coef != 0.0:
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
ue_coef = ModuleUECoef(module=self, ue=ue, coef=coef)
|
||||
self.ue_coefs.append(ue_coef)
|
||||
changed = True
|
||||
if changed:
|
||||
self.formation.invalidate_module_coefs()
|
||||
|
||||
def update_ue_coef_dict(self, ue_coef_dict: dict):
|
||||
"""update coefs vers UE (ajoute aux existants)"""
|
||||
current = self.get_ue_coef_dict()
|
||||
current.update(ue_coef_dict)
|
||||
self.set_ue_coef_dict(current)
|
||||
|
||||
def get_ue_coef_dict(self):
|
||||
"""returns { ue_id : coef }"""
|
||||
return {p.ue.id: p.coef for p in self.ue_coefs}
|
||||
|
||||
def delete_ue_coef(self, ue):
|
||||
"""delete coef"""
|
||||
ue_coef = ModuleUECoef.query.get((self.id, ue.id))
|
||||
if ue_coef:
|
||||
db.session.delete(ue_coef)
|
||||
self.formation.invalidate_module_coefs()
|
||||
|
||||
def get_ue_coefs_sorted(self):
|
||||
"les coefs d'UE, trié par numéro d'UE"
|
||||
# je n'ai pas su mettre un order_by sur le backref sans avoir
|
||||
# à redéfinir les relationships...
|
||||
return sorted(self.ue_coefs, key=lambda x: x.ue.numero)
|
||||
|
||||
def ue_coefs_descr(self):
|
||||
"""List of tuples [ (ue_acronyme, coef) ]"""
|
||||
return [(c.ue.acronyme, c.coef) for c in self.get_ue_coefs_sorted()]
|
||||
|
||||
|
||||
class ModuleUECoef(db.Model):
|
||||
"""Coefficients des modules vers les UE (APC, BUT)
|
||||
En mode APC, ces coefs remplacent le coefficient "PPN" du module.
|
||||
"""
|
||||
|
||||
__tablename__ = "module_ue_coef"
|
||||
|
||||
module_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_modules.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
)
|
||||
ue_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_ue.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
)
|
||||
coef = db.Column(
|
||||
db.Float,
|
||||
nullable=False,
|
||||
)
|
||||
module = db.relationship(
|
||||
Module,
|
||||
backref=db.backref(
|
||||
"ue_coefs",
|
||||
passive_deletes=True,
|
||||
cascade="save-update, merge, delete, delete-orphan",
|
||||
),
|
||||
)
|
||||
ue = db.relationship(
|
||||
"UniteEns",
|
||||
backref=db.backref(
|
||||
"module_ue_coefs",
|
||||
passive_deletes=True,
|
||||
cascade="save-update, merge, delete, delete-orphan",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class NotesTag(db.Model):
|
||||
"""Tag sur un module"""
|
||||
|
||||
__tablename__ = "notes_tags"
|
||||
__table_args__ = (db.UniqueConstraint("title", "dept_id"),)
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
tag_id = db.synonym("id")
|
||||
|
||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||
title = db.Column(db.Text(), nullable=False)
|
||||
|
||||
|
||||
# Association tag <-> module
|
||||
notes_modules_tags = db.Table(
|
||||
"notes_modules_tags",
|
||||
db.Column(
|
||||
"tag_id",
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_tags.id", ondelete="CASCADE"),
|
||||
),
|
||||
db.Column(
|
||||
"module_id", db.Integer, db.ForeignKey("notes_modules.id", ondelete="CASCADE")
|
||||
),
|
||||
)
|
||||
|
||||
from app.models.ues import UniteEns
|
|
@ -4,7 +4,6 @@
|
|||
"""
|
||||
|
||||
from app import db
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import CODE_STR_LEN
|
||||
|
||||
|
@ -40,7 +39,7 @@ class ScolarEvent(db.Model):
|
|||
)
|
||||
|
||||
|
||||
class ScolarFormsemestreValidation(db.Model):
|
||||
class ScolarFormSemestreValidation(db.Model):
|
||||
"""Décisions de jury"""
|
||||
|
||||
__tablename__ = "scolar_formsemestre_validation"
|
||||
|
@ -52,16 +51,19 @@ class ScolarFormsemestreValidation(db.Model):
|
|||
etudid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("identite.id"),
|
||||
index=True,
|
||||
)
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
index=True,
|
||||
)
|
||||
ue_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_ue.id"),
|
||||
index=True,
|
||||
)
|
||||
code = db.Column(db.String(CODE_STR_LEN), nullable=False)
|
||||
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
|
||||
# NULL pour les UE, True|False pour les semestres:
|
||||
assidu = db.Column(db.Boolean)
|
||||
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
|
@ -100,9 +102,10 @@ class ScolarAutorisationInscription(db.Model):
|
|||
)
|
||||
|
||||
|
||||
class NotesAppreciations(db.Model):
|
||||
class BulAppreciations(db.Model):
|
||||
"""Appréciations sur bulletins"""
|
||||
|
||||
__tablename__ = "notes_appreciations"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
etudid = db.Column(
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"""
|
||||
from app import db, log
|
||||
from app.scodoc import bonus_sport
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
class ScoPreference(db.Model):
|
||||
|
@ -94,7 +95,7 @@ class ScoDocSiteConfig(db.Model):
|
|||
"""returns bonus func with specified name.
|
||||
If name not specified, return the configured function.
|
||||
None if no bonus function configured.
|
||||
Raises NameError if func_name not found in module bonus_sport.
|
||||
Raises ScoValueError if func_name not found in module bonus_sport.
|
||||
"""
|
||||
if func_name is None:
|
||||
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
|
||||
|
@ -103,7 +104,13 @@ class ScoDocSiteConfig(db.Model):
|
|||
func_name = c.value
|
||||
if func_name == "": # pas de bonus défini
|
||||
return None
|
||||
return getattr(bonus_sport, func_name)
|
||||
try:
|
||||
return getattr(bonus_sport, func_name)
|
||||
except AttributeError:
|
||||
raise ScoValueError(
|
||||
f"""Fonction de calcul maison inexistante: {func_name}.
|
||||
(contacter votre administrateur local)."""
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_bonus_sport_func_names(cls):
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
"""ScoDoc 9 models : Unités d'Enseignement (UE)
|
||||
"""
|
||||
|
||||
from app import db
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
class UniteEns(db.Model):
|
||||
"""Unité d'Enseignement (UE)"""
|
||||
|
||||
__tablename__ = "notes_ue"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
ue_id = db.synonym("id")
|
||||
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
|
||||
acronyme = db.Column(db.Text(), nullable=False)
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
titre = db.Column(db.Text())
|
||||
# Le semestre_idx n'est pas un id mais le numéro du semestre: 1, 2, ...
|
||||
# En ScoDoc7 et pour les formations classiques, il est NULL
|
||||
# (le numéro du semestre étant alors déterminé par celui des modules de l'UE)
|
||||
# Pour les formations APC, il est obligatoire (de 1 à 6 pour le BUT):
|
||||
semestre_idx = db.Column(db.Integer, nullable=True, index=True)
|
||||
# Type d'UE: 0 normal ("fondamentale"), 1 "sport", 2 "projet et stage (LP)",
|
||||
# 4 "élective"
|
||||
type = db.Column(db.Integer, default=0, server_default="0")
|
||||
# Les UE sont "compatibles" (pour la capitalisation) ssi elles ont ^m code
|
||||
# note: la fonction SQL notes_newid_ucod doit être créée à part
|
||||
ue_code = db.Column(
|
||||
db.String(SHORT_STR_LEN),
|
||||
server_default=db.text("notes_newid_ucod()"),
|
||||
nullable=False,
|
||||
)
|
||||
ects = db.Column(db.Float) # nombre de credits ECTS
|
||||
is_external = db.Column(db.Boolean(), default=False, server_default="false")
|
||||
# id de l'element pedagogique Apogee correspondant:
|
||||
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
# coef UE, utilise seulement si l'option use_ue_coefs est activée:
|
||||
coefficient = db.Column(db.Float)
|
||||
|
||||
# relations
|
||||
matieres = db.relationship("Matiere", lazy="dynamic", backref="ue")
|
||||
modules = db.relationship("Module", lazy="dynamic", backref="ue")
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<{self.__class__.__name__}(id={self.id}, formation_id={
|
||||
self.formation_id}, acronyme='{self.acronyme}', semestre_idx={
|
||||
self.semestre_idx} {
|
||||
'EXTERNE' if self.is_external else ''})>"""
|
||||
|
||||
def to_dict(self):
|
||||
"""as a dict, with the same conversions as in ScoDoc7"""
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
# ScoDoc7 output_formators
|
||||
e["ue_id"] = self.id
|
||||
e["numero"] = e["numero"] if e["numero"] else 0
|
||||
e["ects"] = e["ects"] if e["ects"] else 0.0
|
||||
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
|
||||
return e
|
||||
|
||||
def is_locked(self):
|
||||
"""True if UE should not be modified
|
||||
(contains modules used in a locked formsemestre)
|
||||
"""
|
||||
# XXX todo : à ré-écrire avec SQLAlchemy
|
||||
from app.scodoc import sco_edit_ue
|
||||
|
||||
return sco_edit_ue.ue_is_locked(self.id)
|
||||
|
||||
def guess_semestre_idx(self) -> None:
|
||||
"""Lorsqu'on prend une ancienne formation non APC,
|
||||
les UE n'ont pas d'indication de semestre.
|
||||
Cette méthode fixe le semestre en prenant celui du premier module,
|
||||
ou à défaut le met à 1.
|
||||
"""
|
||||
if self.semestre_idx is None:
|
||||
module = self.modules.first()
|
||||
if module is None:
|
||||
self.semestre_idx = 1
|
||||
else:
|
||||
self.semestre_idx = module.semestre_id
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
def get_ressources(self):
|
||||
"Liste des modules ressources rattachés à cette UE"
|
||||
return self.modules.filter_by(module_type=scu.ModuleType.RESSOURCE).all()
|
||||
|
||||
def get_saes(self):
|
||||
"Liste des modules SAE rattachés à cette UE"
|
||||
return self.modules.filter_by(module_type=scu.ModuleType.SAE).all()
|
||||
|
||||
def get_modules_not_apc(self):
|
||||
"Listes des modules non SAE et non ressource (standards, mais aussi bonus...)"
|
||||
return self.modules.filter(
|
||||
(Module.module_type != scu.ModuleType.SAE),
|
||||
(Module.module_type != scu.ModuleType.RESSOURCE),
|
||||
).all()
|
|
@ -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.
|
||||
|
||||
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -33,9 +33,9 @@
|
|||
import os
|
||||
import codecs
|
||||
import re
|
||||
from app.scodoc import pe_jurype
|
||||
from app.scodoc import pe_tagtable
|
||||
from app.scodoc import pe_tools
|
||||
from app.pe import pe_tagtable
|
||||
from app.pe import pe_jurype
|
||||
from app.pe import pe_tools
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
@ -48,7 +48,7 @@ from app.scodoc import sco_etud
|
|||
DEBUG = False # Pour debug et repérage des prints à changer en Log
|
||||
|
||||
DONNEE_MANQUANTE = (
|
||||
u"" # Caractère de remplacement des données manquantes dans un avis PE
|
||||
"" # Caractère de remplacement des données manquantes dans un avis PE
|
||||
)
|
||||
|
||||
# ----------------------------------------------------------------------------------------
|
||||
|
@ -102,17 +102,17 @@ def comp_latex_parcourstimeline(etudiant, promo, taille=17):
|
|||
result: chaine unicode (EV:)
|
||||
"""
|
||||
codelatexDebut = (
|
||||
u"""
|
||||
""""
|
||||
\\begin{parcourstimeline}{**debut**}{**fin**}{**nbreSemestres**}{%d}
|
||||
"""
|
||||
% taille
|
||||
)
|
||||
|
||||
modeleEvent = u"""
|
||||
modeleEvent = """
|
||||
\\parcoursevent{**nosem**}{**nomsem**}{**descr**}
|
||||
"""
|
||||
|
||||
codelatexFin = u"""
|
||||
codelatexFin = """
|
||||
\\end{parcourstimeline}
|
||||
"""
|
||||
reslatex = codelatexDebut
|
||||
|
@ -125,13 +125,13 @@ def comp_latex_parcourstimeline(etudiant, promo, taille=17):
|
|||
for no_sem in range(etudiant["nbSemestres"]):
|
||||
descr = modeleEvent
|
||||
nom_semestre_dans_parcours = parcours[no_sem]["nom_semestre_dans_parcours"]
|
||||
descr = descr.replace(u"**nosem**", str(no_sem + 1))
|
||||
descr = descr.replace("**nosem**", str(no_sem + 1))
|
||||
if no_sem % 2 == 0:
|
||||
descr = descr.replace(u"**nomsem**", nom_semestre_dans_parcours)
|
||||
descr = descr.replace(u"**descr**", u"")
|
||||
descr = descr.replace("**nomsem**", nom_semestre_dans_parcours)
|
||||
descr = descr.replace("**descr**", "")
|
||||
else:
|
||||
descr = descr.replace(u"**nomsem**", u"")
|
||||
descr = descr.replace(u"**descr**", nom_semestre_dans_parcours)
|
||||
descr = descr.replace("**nomsem**", "")
|
||||
descr = descr.replace("**descr**", nom_semestre_dans_parcours)
|
||||
reslatex += descr
|
||||
reslatex += codelatexFin
|
||||
return reslatex
|
||||
|
@ -166,7 +166,7 @@ def get_code_latex_avis_etudiant(
|
|||
result: chaine unicode
|
||||
"""
|
||||
if not donnees_etudiant or not un_avis_latex: # Cas d'un template vide
|
||||
return annotationPE if annotationPE else u""
|
||||
return annotationPE if annotationPE else ""
|
||||
|
||||
# Le template latex (corps + footer)
|
||||
code = un_avis_latex + "\n\n" + footer_latex
|
||||
|
@ -189,17 +189,17 @@ def get_code_latex_avis_etudiant(
|
|||
)
|
||||
|
||||
# La macro parcourstimeline
|
||||
elif tag_latex == u"parcourstimeline":
|
||||
elif tag_latex == "parcourstimeline":
|
||||
valeur = comp_latex_parcourstimeline(
|
||||
donnees_etudiant, donnees_etudiant["promo"]
|
||||
)
|
||||
|
||||
# Le tag annotationPE
|
||||
elif tag_latex == u"annotation":
|
||||
elif tag_latex == "annotation":
|
||||
valeur = annotationPE
|
||||
|
||||
# Le tag bilanParTag
|
||||
elif tag_latex == u"bilanParTag":
|
||||
elif tag_latex == "bilanParTag":
|
||||
valeur = get_bilanParTag(donnees_etudiant)
|
||||
|
||||
# Les tags "simples": par ex. nom, prenom, civilite, ...
|
||||
|
@ -249,14 +249,14 @@ def get_annotation_PE(etudid, tag_annotation_pe):
|
|||
]["comment_u"]
|
||||
|
||||
annotationPE = exp.sub(
|
||||
u"", annotationPE
|
||||
"", annotationPE
|
||||
) # Suppression du tag d'annotation PE
|
||||
annotationPE = annotationPE.replace(u"\r", u"") # Suppression des \r
|
||||
annotationPE = annotationPE.replace("\r", "") # Suppression des \r
|
||||
annotationPE = annotationPE.replace(
|
||||
u"<br/>", u"\n\n"
|
||||
"<br/>", "\n\n"
|
||||
) # Interprète les retours chariots html
|
||||
return annotationPE
|
||||
return u"" # pas d'annotations
|
||||
return "" # pas d'annotations
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------------------
|
||||
|
@ -282,7 +282,7 @@ def str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, champ)
|
|||
):
|
||||
donnees_numeriques = donnees_etudiant[aggregat][groupe][tag_scodoc]
|
||||
if champ == "rang":
|
||||
valeur = u"%s/%d" % (
|
||||
valeur = "%s/%d" % (
|
||||
donnees_numeriques[
|
||||
pe_tagtable.TableTag.FORMAT_DONNEES_ETUDIANTS.index("rang")
|
||||
],
|
||||
|
@ -303,9 +303,9 @@ def str_from_syntheseJury(donnees_etudiant, aggregat, groupe, tag_scodoc, champ)
|
|||
if isinstance(
|
||||
donnees_numeriques[indice_champ], float
|
||||
): # valeur numérique avec formattage unicode
|
||||
valeur = u"%2.2f" % donnees_numeriques[indice_champ]
|
||||
valeur = "%2.2f" % donnees_numeriques[indice_champ]
|
||||
else:
|
||||
valeur = u"%s" % donnees_numeriques[indice_champ]
|
||||
valeur = "%s" % donnees_numeriques[indice_champ]
|
||||
|
||||
return valeur
|
||||
|
||||
|
@ -356,29 +356,27 @@ def get_bilanParTag(donnees_etudiant, groupe="groupe"):
|
|||
("\\textit{" + rang + "}") if note else ""
|
||||
) # rang masqué si pas de notes
|
||||
|
||||
code_latex = u"\\begin{tabular}{|c|" + "|c" * (len(entete)) + "|}\n"
|
||||
code_latex += u"\\hline \n"
|
||||
code_latex = "\\begin{tabular}{|c|" + "|c" * (len(entete)) + "|}\n"
|
||||
code_latex += "\\hline \n"
|
||||
code_latex += (
|
||||
u" & "
|
||||
" & "
|
||||
+ " & ".join(["\\textbf{" + intitule + "}" for (agg, intitule, _) in entete])
|
||||
+ " \\\\ \n"
|
||||
)
|
||||
code_latex += u"\\hline"
|
||||
code_latex += u"\\hline \n"
|
||||
code_latex += "\\hline"
|
||||
code_latex += "\\hline \n"
|
||||
for (i, ligne_val) in enumerate(valeurs["note"]):
|
||||
titre = lignes[i] # règle le pb d'encodage
|
||||
code_latex += "\\textbf{" + titre + "} & " + " & ".join(ligne_val) + "\\\\ \n"
|
||||
code_latex += (
|
||||
u"\\textbf{" + titre + u"} & " + " & ".join(ligne_val) + u"\\\\ \n"
|
||||
)
|
||||
code_latex += (
|
||||
u" & "
|
||||
+ u" & ".join(
|
||||
[u"{\\scriptsize " + clsmt + u"}" for clsmt in valeurs["rang"][i]]
|
||||
" & "
|
||||
+ " & ".join(
|
||||
["{\\scriptsize " + clsmt + "}" for clsmt in valeurs["rang"][i]]
|
||||
)
|
||||
+ u"\\\\ \n"
|
||||
+ "\\\\ \n"
|
||||
)
|
||||
code_latex += u"\\hline \n"
|
||||
code_latex += u"\\end{tabular}"
|
||||
code_latex += "\\hline \n"
|
||||
code_latex += "\\end{tabular}"
|
||||
|
||||
return code_latex
|
||||
|
||||
|
@ -397,21 +395,15 @@ def get_avis_poursuite_par_etudiant(
|
|||
nom = jury.syntheseJury[etudid]["nom"].replace(" ", "-")
|
||||
prenom = jury.syntheseJury[etudid]["prenom"].replace(" ", "-")
|
||||
|
||||
nom_fichier = (
|
||||
u"avis_poursuite_"
|
||||
+ pe_tools.remove_accents(nom)
|
||||
+ "_"
|
||||
+ pe_tools.remove_accents(prenom)
|
||||
+ "_"
|
||||
+ str(etudid)
|
||||
nom_fichier = scu.sanitize_filename(
|
||||
"avis_poursuite_%s_%s_%s" % (nom, prenom, etudid)
|
||||
)
|
||||
if pe_tools.PE_DEBUG:
|
||||
pe_tools.pe_print("fichier latex =" + nom_fichier, type(nom_fichier))
|
||||
|
||||
# Entete (commentaire)
|
||||
|
||||
contenu_latex = (
|
||||
u"%% ---- Etudiant: " + civilite_str + " " + nom + " " + prenom + u"\n"
|
||||
"%% ---- Etudiant: " + civilite_str + " " + nom + " " + prenom + "\n"
|
||||
)
|
||||
|
||||
# les annnotations
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -52,10 +52,10 @@ from app.scodoc import sco_cache
|
|||
from app.scodoc import sco_codes_parcours # sco_codes_parcours.NEXT -> sem suivant
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import pe_tagtable
|
||||
from app.scodoc import pe_tools
|
||||
from app.scodoc import pe_semestretag
|
||||
from app.scodoc import pe_settag
|
||||
from app.pe import pe_tagtable
|
||||
from app.pe import pe_tools
|
||||
from app.pe import pe_semestretag
|
||||
from app.pe import pe_settag
|
||||
|
||||
# ----------------------------------------------------------------------------------------
|
||||
def comp_nom_semestre_dans_parcours(sem):
|
||||
|
@ -946,7 +946,7 @@ class JuryPE(object):
|
|||
return list(taglist)
|
||||
|
||||
def get_allTagInSyntheseJury(self):
|
||||
"""Extrait tous les tags du dictionnaire syntheseJury trié par ordre alphabétique. [] si aucun tag """
|
||||
"""Extrait tous les tags du dictionnaire syntheseJury trié par ordre alphabétique. [] si aucun tag"""
|
||||
allTags = set()
|
||||
for nom in JuryPE.PARCOURS.keys():
|
||||
allTags = allTags.union(set(self.get_allTagForAggregat(nom)))
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -40,7 +40,7 @@ from app import log
|
|||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_tag_module
|
||||
from app.scodoc import pe_tagtable
|
||||
from app.pe import pe_tagtable
|
||||
|
||||
|
||||
class SemestreTag(pe_tagtable.TableTag):
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -36,8 +36,8 @@ Created on Fri Sep 9 09:15:05 2016
|
|||
@author: barasc
|
||||
"""
|
||||
|
||||
from app.scodoc.pe_tools import pe_print, PE_DEBUG
|
||||
from app.scodoc import pe_tagtable
|
||||
from app.pe.pe_tools import pe_print, PE_DEBUG
|
||||
from app.pe import pe_tagtable
|
||||
|
||||
|
||||
class SetTag(pe_tagtable.TableTag):
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -44,7 +44,7 @@ import unicodedata
|
|||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
import six
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
|
||||
PE_DEBUG = 0
|
||||
|
||||
|
@ -145,7 +145,7 @@ def escape_for_latex(s):
|
|||
}
|
||||
exp = re.compile(
|
||||
"|".join(
|
||||
re.escape(six.text_type(key))
|
||||
re.escape(key)
|
||||
for key in sorted(list(conv.keys()), key=lambda item: -len(item))
|
||||
)
|
||||
)
|
||||
|
@ -167,8 +167,19 @@ def list_directory_filenames(path):
|
|||
def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip):
|
||||
"""Read pathname server file and add content to zip under path_in_zip"""
|
||||
rooted_path_in_zip = os.path.join(ziproot, path_in_zip)
|
||||
data = open(pathname).read()
|
||||
zipfile.writestr(rooted_path_in_zip, data)
|
||||
zipfile.write(filename=pathname, arcname=rooted_path_in_zip)
|
||||
# data = open(pathname).read()
|
||||
# zipfile.writestr(rooted_path_in_zip, data)
|
||||
|
||||
|
||||
def add_refs_to_register(register, directory):
|
||||
"""Ajoute les fichiers trouvés dans directory au registre (dictionaire) sous la forme
|
||||
filename => pathname
|
||||
"""
|
||||
length = len(directory)
|
||||
for pathname in list_directory_filenames(directory):
|
||||
filename = pathname[length + 1 :]
|
||||
register[filename] = pathname
|
||||
|
||||
|
||||
def add_pe_stuff_to_zip(zipfile, ziproot):
|
||||
|
@ -179,37 +190,23 @@ def add_pe_stuff_to_zip(zipfile, ziproot):
|
|||
|
||||
Also copy logos
|
||||
"""
|
||||
register = {}
|
||||
# first add standard (distrib references)
|
||||
distrib_dir = os.path.join(REP_DEFAULT_AVIS, "distrib")
|
||||
distrib_pathnames = list_directory_filenames(
|
||||
distrib_dir
|
||||
) # eg /opt/scodoc/tools/doc_poursuites_etudes/distrib/modeles/toto.tex
|
||||
l = len(distrib_dir)
|
||||
distrib_filenames = {x[l + 1 :] for x in distrib_pathnames} # eg modeles/toto.tex
|
||||
|
||||
add_refs_to_register(register=register, directory=distrib_dir)
|
||||
# then add local references (some oh them may overwrite distrib refs)
|
||||
local_dir = os.path.join(REP_LOCAL_AVIS, "local")
|
||||
local_pathnames = list_directory_filenames(local_dir)
|
||||
l = len(local_dir)
|
||||
local_filenames = {x[l + 1 :] for x in local_pathnames}
|
||||
|
||||
for filename in distrib_filenames | local_filenames:
|
||||
if filename in local_filenames:
|
||||
add_local_file_to_zip(
|
||||
zipfile, ziproot, os.path.join(local_dir, filename), "avis/" + filename
|
||||
)
|
||||
else:
|
||||
add_local_file_to_zip(
|
||||
zipfile,
|
||||
ziproot,
|
||||
os.path.join(distrib_dir, filename),
|
||||
"avis/" + filename,
|
||||
)
|
||||
add_refs_to_register(register=register, directory=local_dir)
|
||||
# at this point register contains all refs (filename, pathname) to be saved
|
||||
for filename, pathname in register.items():
|
||||
add_local_file_to_zip(zipfile, ziproot, pathname, "avis/" + filename)
|
||||
|
||||
# Logos: (add to logos/ directory in zip)
|
||||
logos_names = ["logo_header.jpg", "logo_footer.jpg"]
|
||||
for f in logos_names:
|
||||
logo = os.path.join(scu.SCODOC_LOGOS_DIR, f)
|
||||
if os.path.isfile(logo):
|
||||
add_local_file_to_zip(zipfile, ziproot, logo, "avis/logos/" + f)
|
||||
logos_names = ["header", "footer"]
|
||||
for name in logos_names:
|
||||
logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id)
|
||||
if logo is not None:
|
||||
add_local_file_to_zip(zipfile, ziproot, logo, "avis/logos/" + logo.filename)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------------------
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -42,10 +42,9 @@ from app.scodoc import sco_formsemestre
|
|||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
from app.scodoc import pe_tools
|
||||
from app.scodoc.pe_tools import PE_LATEX_ENCODING
|
||||
from app.scodoc import pe_jurype
|
||||
from app.scodoc import pe_avislatex
|
||||
from app.pe import pe_tools
|
||||
from app.pe import pe_jurype
|
||||
from app.pe import pe_avislatex
|
||||
|
||||
|
||||
def _pe_view_sem_recap_form(formsemestre_id):
|
||||
|
@ -90,7 +89,6 @@ def pe_view_sem_recap(
|
|||
semBase = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
|
||||
jury = pe_jurype.JuryPE(semBase)
|
||||
|
||||
# Ajout avis LaTeX au même zip:
|
||||
etudids = list(jury.syntheseJury.keys())
|
||||
|
||||
|
@ -150,18 +148,14 @@ def pe_view_sem_recap(
|
|||
footer_latex,
|
||||
prefs,
|
||||
)
|
||||
|
||||
jury.add_file_to_zip(
|
||||
("avis/" + nom_fichier + ".tex").encode(PE_LATEX_ENCODING),
|
||||
contenu_latex.encode(PE_LATEX_ENCODING),
|
||||
)
|
||||
jury.add_file_to_zip("avis/" + nom_fichier + ".tex", contenu_latex)
|
||||
latex_pages[nom_fichier] = contenu_latex # Sauvegarde dans un dico
|
||||
|
||||
# Nouvelle version : 1 fichier par étudiant avec 1 fichier appelant créée ci-dessous
|
||||
doc_latex = "\n% -----\n".join(
|
||||
["\\include{" + nom + "}" for nom in sorted(latex_pages.keys())]
|
||||
)
|
||||
jury.add_file_to_zip("avis/avis_poursuite.tex", doc_latex.encode(PE_LATEX_ENCODING))
|
||||
jury.add_file_to_zip("avis/avis_poursuite.tex", doc_latex)
|
||||
|
||||
# Ajoute image, LaTeX class file(s) and modeles
|
||||
pe_tools.add_pe_stuff_to_zip(jury.zipfile, jury.NOM_EXPORT_ZIP)
|
|
@ -8,6 +8,13 @@
|
|||
|
||||
v 1.3 (python3)
|
||||
"""
|
||||
import html
|
||||
import re
|
||||
|
||||
# re validant dd/mm/yyyy
|
||||
DMY_REGEXP = re.compile(
|
||||
r"^(?:(?:31(\/|-|\.)(?:0?[13578]|1[02]))\1|(?:(?:29|30)(\/|-|\.)(?:0?[13-9]|1[0-2])\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})$|^(?:29(\/|-|\.)0?2\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:0?[1-9]|1\d|2[0-8])(\/|-|\.)(?:(?:0?[1-9])|(?:1[0-2]))\4(?:(?:1[6-9]|[2-9]\d)?\d{2})$"
|
||||
)
|
||||
|
||||
|
||||
def TrivialFormulator(
|
||||
|
@ -50,7 +57,7 @@ def TrivialFormulator(
|
|||
allow_null : if true, field can be left empty (default true)
|
||||
type : 'string', 'int', 'float' (default to string), 'list' (only for hidden)
|
||||
readonly : default False. if True, no form element, display current value.
|
||||
convert_numbers: covert int and float values (from string)
|
||||
convert_numbers: convert int and float values (from string)
|
||||
allowed_values : list of possible values (default: any value)
|
||||
validator : function validating the field (called with (value,field)).
|
||||
min_value : minimum value (for floats and ints)
|
||||
|
@ -65,8 +72,8 @@ def TrivialFormulator(
|
|||
HTML elements:
|
||||
input_type : 'text', 'textarea', 'password',
|
||||
'radio', 'menu', 'checkbox',
|
||||
'hidden', 'separator', 'file', 'date', 'boolcheckbox',
|
||||
'text_suggest'
|
||||
'hidden', 'separator', 'file', 'date', 'datedmy' (avec validation),
|
||||
'boolcheckbox', 'text_suggest'
|
||||
(default text)
|
||||
size : text field width
|
||||
rows, cols: textarea geometry
|
||||
|
@ -134,7 +141,7 @@ class TF(object):
|
|||
is_submitted=False,
|
||||
):
|
||||
self.form_url = form_url
|
||||
self.values = values
|
||||
self.values = values.copy()
|
||||
self.formdescription = list(formdescription)
|
||||
self.initvalues = initvalues
|
||||
self.method = method
|
||||
|
@ -242,6 +249,8 @@ class TF(object):
|
|||
"Le champ '%s' doit être renseigné" % descr.get("title", field)
|
||||
)
|
||||
ok = 0
|
||||
elif val == "" or val == None:
|
||||
continue # allowed empty field, skip
|
||||
# type
|
||||
typ = descr.get("type", "string")
|
||||
if val != "" and val != None:
|
||||
|
@ -299,6 +308,10 @@ class TF(object):
|
|||
if not descr["validator"](val, field):
|
||||
msg.append("valeur invalide (%s) pour le champ '%s'" % (val, field))
|
||||
ok = 0
|
||||
elif descr.get("input_type") == "datedmy":
|
||||
if not DMY_REGEXP.match(val):
|
||||
msg.append("valeur invalide (%s) pour la date '%s'" % (val, field))
|
||||
ok = 0
|
||||
# boolean checkbox
|
||||
if descr.get("input_type", None) == "boolcheckbox":
|
||||
if int(val):
|
||||
|
@ -333,7 +346,7 @@ class TF(object):
|
|||
buttons_markup = ""
|
||||
if self.submitbutton:
|
||||
buttons_markup += (
|
||||
'<input type="submit" name="%s_submit" id="%s_submit" value="%s" %s/>'
|
||||
'<input type="submit" name="%s_submit" id="%s_submit" value="%s" %s>'
|
||||
% (
|
||||
self.formid,
|
||||
self.formid,
|
||||
|
@ -343,7 +356,7 @@ class TF(object):
|
|||
)
|
||||
if self.cancelbutton:
|
||||
buttons_markup += (
|
||||
' <input type="submit" name="%s_cancel" id="%s_cancel" value="%s"/>'
|
||||
' <input type="submit" name="%s_cancel" id="%s_cancel" value="%s">'
|
||||
% (self.formid, self.formid, self.cancelbutton)
|
||||
)
|
||||
|
||||
|
@ -363,7 +376,7 @@ class TF(object):
|
|||
'<form action="%s" method="%s" id="%s" enctype="%s" name="%s" %s>'
|
||||
% (self.form_url, self.method, self.formid, enctype, name, klass)
|
||||
)
|
||||
R.append('<input type="hidden" name="%s_submitted" value="1"/>' % self.formid)
|
||||
R.append('<input type="hidden" name="%s_submitted" value="1">' % self.formid)
|
||||
if self.top_buttons:
|
||||
R.append(buttons_markup + "<p></p>")
|
||||
R.append('<table class="tf">')
|
||||
|
@ -405,7 +418,7 @@ class TF(object):
|
|||
else:
|
||||
checked = ""
|
||||
lab.append(
|
||||
'<input type="checkbox" name="%s:list" value="%s" onclick="tf_enable_elem(this)" %s/>'
|
||||
'<input type="checkbox" name="%s:list" value="%s" onclick="tf_enable_elem(this)" %s>'
|
||||
% ("tf-checked", field, checked)
|
||||
)
|
||||
if title_bubble:
|
||||
|
@ -438,13 +451,13 @@ class TF(object):
|
|||
add_no_enter_js = True
|
||||
# lem.append('onchange="document.%s.%s.focus()"'%(name,nextitemname))
|
||||
# lem.append('onblur="document.%s.%s.focus()"'%(name,nextitemname))
|
||||
lem.append(('value="%(' + field + ')s" />') % values)
|
||||
lem.append(('value="%(' + field + ')s" >') % values)
|
||||
elif input_type == "password":
|
||||
lem.append(
|
||||
'<input type="password" name="%s" id="%s" size="%d" %s'
|
||||
% (field, wid, size, attribs)
|
||||
)
|
||||
lem.append(('value="%(' + field + ')s" />') % values)
|
||||
lem.append(('value="%(' + field + ')s" >') % values)
|
||||
elif input_type == "radio":
|
||||
labels = descr.get("labels", descr["allowed_values"])
|
||||
for i in range(len(labels)):
|
||||
|
@ -548,24 +561,26 @@ class TF(object):
|
|||
if descr.get("type", "") == "list":
|
||||
for v in values[field]:
|
||||
lem.append(
|
||||
'<input type="hidden" name="%s:list" value="%s" %s />'
|
||||
'<input type="hidden" name="%s:list" value="%s" %s >'
|
||||
% (field, v, attribs)
|
||||
)
|
||||
else:
|
||||
lem.append(
|
||||
'<input type="hidden" name="%s" id="%s" value="%s" %s />'
|
||||
'<input type="hidden" name="%s" id="%s" value="%s" %s >'
|
||||
% (field, wid, values[field], attribs)
|
||||
)
|
||||
elif input_type == "separator":
|
||||
pass
|
||||
elif input_type == "file":
|
||||
lem.append(
|
||||
'<input type="file" name="%s" size="%s" value="%s" %s/>'
|
||||
'<input type="file" name="%s" size="%s" value="%s" %s>'
|
||||
% (field, size, values[field], attribs)
|
||||
)
|
||||
elif input_type == "date": # JavaScript widget for date input
|
||||
elif (
|
||||
input_type == "date" or input_type == "datedmy"
|
||||
): # JavaScript widget for date input
|
||||
lem.append(
|
||||
'<input type="text" name="%s" size="10" value="%s" class="datepicker"/>'
|
||||
'<input type="text" name="%s" size="10" value="%s" class="datepicker">'
|
||||
% (field, values[field])
|
||||
)
|
||||
elif input_type == "text_suggest":
|
||||
|
@ -715,14 +730,16 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
|
|||
bool_val = 0
|
||||
R.append(labels[bool_val])
|
||||
if bool_val:
|
||||
R.append('<input type="hidden" name="%s" value="1"/>' % field)
|
||||
R.append('<input type="hidden" name="%s" value="1">' % field)
|
||||
else:
|
||||
labels = descr.get("labels", descr["allowed_values"])
|
||||
for i in range(len(labels)):
|
||||
if str(descr["allowed_values"][i]) == str(self.values[field]):
|
||||
R.append('<span class="tf-ro-value">%s</span>' % labels[i])
|
||||
elif input_type == "textarea":
|
||||
R.append('<div class="tf-ro-textarea">%s</div>' % self.values[field])
|
||||
R.append(
|
||||
'<div class="tf-ro-textarea">%s</div>' % html.escape(self.values[field])
|
||||
)
|
||||
elif input_type == "separator" or input_type == "hidden":
|
||||
pass
|
||||
elif input_type == "file":
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -167,6 +167,23 @@ def bonus_iutlh(notes_sport, coefs, infos=None):
|
|||
return bonus
|
||||
|
||||
|
||||
def bonus_nantes(notes_sport, coefs, infos=None):
|
||||
"""IUT de Nantes (Septembre 2018)
|
||||
Nous avons différents types de bonification
|
||||
bonfication Sport / Culture / engagement citoyen
|
||||
Nous ajoutons sur le bulletin une bonification de 0,2 pour chaque item
|
||||
la bonification totale ne doit pas excéder les 0,5 point.
|
||||
Sur le bulletin nous ne mettons pas une note sur 20 mais directement les bonifications.
|
||||
|
||||
Dans ScoDoc: on a déclaré une UE "sport&culture" dans laquelle on aura des modules
|
||||
pour chaque activité (Sport, Associations, ...)
|
||||
avec à chaque fois une note (ScoDoc l'affichera comme une note sur 20, mais en fait ce sera la
|
||||
valeur de la bonification: entrer 0,1/20 signifiera un bonus de 0,1 point la moyenne générale)
|
||||
"""
|
||||
bonus = min(0.5, sum([x for x in notes_sport])) # plafonnement à 0.5 points
|
||||
return bonus
|
||||
|
||||
|
||||
# Bonus sport IUT Tours
|
||||
def bonus_tours(notes_sport, coefs, infos=None):
|
||||
"""Calcul bonus sport & culture IUT Tours sur moyenne generale
|
||||
|
@ -177,7 +194,8 @@ def bonus_tours(notes_sport, coefs, infos=None):
|
|||
|
||||
|
||||
def bonus_iutr(notes_sport, coefs, infos=None):
|
||||
"""Calcul du bonus , regle de l'IUT de Roanne (contribuée par Raphael C., nov 2012)
|
||||
"""Calcul du bonus , règle de l'IUT de Roanne
|
||||
(contribuée par Raphael C., nov 2012)
|
||||
|
||||
Le bonus est compris entre 0 et 0.35 point.
|
||||
cette procédure modifie la moyenne de chaque UE capitalisable.
|
||||
|
@ -379,6 +397,39 @@ def bonus_iutbethune(notes_sport, coefs, infos=None):
|
|||
return bonus
|
||||
|
||||
|
||||
def bonus_iutbeziers(notes_sport, coefs, infos=None):
|
||||
"""Calcul bonus modules optionels (sport, culture), regle IUT BEZIERS
|
||||
|
||||
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
||||
sport , etc) non rattaches à une unité d'enseignement. Les points
|
||||
au-dessus de 10 sur 20 obtenus dans chacune des matières
|
||||
optionnelles sont cumulés et 3% de ces points cumulés s'ajoutent à
|
||||
la moyenne générale du semestre déjà obtenue par l'étudiant.
|
||||
"""
|
||||
sumc = sum(coefs) # assumes sum. coefs > 0
|
||||
# note_sport = sum(map(mul, notes_sport, coefs)) / sumc # moyenne pondérée
|
||||
bonus = sum([(x - 10) * 0.03 for x in notes_sport if x > 10])
|
||||
# le total du bonus ne doit pas dépasser 0.3 - Fred, 28/01/2020
|
||||
|
||||
if bonus > 0.3:
|
||||
bonus = 0.3
|
||||
return bonus
|
||||
|
||||
|
||||
def bonus_iutlr(notes_sport, coefs, infos=None):
|
||||
"""Calcul bonus modules optionels (sport, culture), règle IUT La Rochelle
|
||||
Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point
|
||||
Si la note de sport est comprise entre 10.1 et 20 : ajout de 1% de cette note sur la moyenne générale du semestre
|
||||
"""
|
||||
# les coefs sont ignorés
|
||||
# une seule note
|
||||
note_sport = notes_sport[0]
|
||||
if note_sport <= 10:
|
||||
return 0
|
||||
bonus = note_sport * 0.01 # 1%
|
||||
return bonus
|
||||
|
||||
|
||||
def bonus_demo(notes_sport, coefs, infos=None):
|
||||
"""Fausse fonction "bonus" pour afficher les informations disponibles
|
||||
et aider les développeurs.
|
||||
|
@ -386,8 +437,8 @@ def bonus_demo(notes_sport, coefs, infos=None):
|
|||
qui est ECRASE à chaque appel.
|
||||
*** Ne pas utiliser en production !!! ***
|
||||
"""
|
||||
f = open("/tmp/scodoc_bonus.log", "w") # mettre 'a' pour ajouter en fin
|
||||
f.write("\n---------------\n" + pprint.pformat(infos) + "\n")
|
||||
with open("/tmp/scodoc_bonus.log", "w") as f: # mettre 'a' pour ajouter en fin
|
||||
f.write("\n---------------\n" + pprint.pformat(infos) + "\n")
|
||||
# Statut de chaque UE
|
||||
# for ue_id in infos['moy_ues']:
|
||||
# ue_status = infos['moy_ues'][ue_id]
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -58,6 +58,7 @@ from app.scodoc import sco_utils as scu
|
|||
from app.scodoc import sco_excel
|
||||
from app.scodoc import sco_pdf
|
||||
from app.scodoc import sco_xml
|
||||
from app.scodoc.sco_exceptions import ScoPDFFormatError
|
||||
from app.scodoc.sco_pdf import SU
|
||||
from app import log
|
||||
|
||||
|
@ -185,6 +186,9 @@ class GenTable(object):
|
|||
else:
|
||||
self.preferences = DEFAULT_TABLE_PREFERENCES()
|
||||
|
||||
def __repr__(self):
|
||||
return f"<gen_table( nrows={self.get_nb_rows()}, ncols={self.get_nb_cols()} )>"
|
||||
|
||||
def get_nb_cols(self):
|
||||
return len(self.columns_ids)
|
||||
|
||||
|
@ -468,7 +472,10 @@ class GenTable(object):
|
|||
|
||||
def excel(self, wb=None):
|
||||
"""Simple Excel representation of the table"""
|
||||
ses = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb)
|
||||
if wb is None:
|
||||
ses = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb)
|
||||
else:
|
||||
ses = wb.create_sheet(sheet_name=self.xls_sheet_name)
|
||||
ses.rows += self.xls_before_table
|
||||
style_bold = sco_excel.excel_make_style(bold=True)
|
||||
style_base = sco_excel.excel_make_style()
|
||||
|
@ -482,9 +489,7 @@ class GenTable(object):
|
|||
ses.append_blank_row() # empty line
|
||||
ses.append_single_cell_row(self.origin, style_base)
|
||||
if wb is None:
|
||||
return ses.generate_standalone()
|
||||
else:
|
||||
ses.generate_embeded()
|
||||
return ses.generate()
|
||||
|
||||
def text(self):
|
||||
"raw text representation of the table"
|
||||
|
@ -494,7 +499,7 @@ class GenTable(object):
|
|||
headline = []
|
||||
return "\n".join(
|
||||
[
|
||||
self.text_fields_separator.join([x for x in line])
|
||||
self.text_fields_separator.join([str(x) for x in line])
|
||||
for line in headline + self.get_data_list()
|
||||
]
|
||||
)
|
||||
|
@ -535,17 +540,18 @@ class GenTable(object):
|
|||
#
|
||||
# titles = ["<para><b>%s</b></para>" % x for x in self.get_titles_list()]
|
||||
pdf_style_list = []
|
||||
Pt = [
|
||||
[Paragraph(SU(str(x)), CellStyle) for x in line]
|
||||
for line in (
|
||||
self.get_data_list(
|
||||
pdf_mode=True,
|
||||
pdf_style_list=pdf_style_list,
|
||||
with_titles=True,
|
||||
omit_hidden_lines=True,
|
||||
)
|
||||
)
|
||||
]
|
||||
data_list = self.get_data_list(
|
||||
pdf_mode=True,
|
||||
pdf_style_list=pdf_style_list,
|
||||
with_titles=True,
|
||||
omit_hidden_lines=True,
|
||||
)
|
||||
try:
|
||||
Pt = [
|
||||
[Paragraph(SU(str(x)), CellStyle) for x in line] for line in data_list
|
||||
]
|
||||
except ValueError as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
pdf_style_list += self.pdf_table_style
|
||||
T = Table(Pt, repeatRows=1, colWidths=self.pdf_col_widths, style=pdf_style_list)
|
||||
|
||||
|
@ -573,7 +579,7 @@ class GenTable(object):
|
|||
"""
|
||||
doc = ElementTree.Element(
|
||||
self.xml_outer_tag,
|
||||
id=self.table_id,
|
||||
id=str(self.table_id),
|
||||
origin=self.origin or "",
|
||||
caption=self.caption or "",
|
||||
)
|
||||
|
@ -587,7 +593,7 @@ class GenTable(object):
|
|||
v = row.get(cid, "")
|
||||
if v is None:
|
||||
v = ""
|
||||
x_cell = ElementTree.Element(cid, value=str(v))
|
||||
x_cell = ElementTree.Element(str(cid), value=str(v))
|
||||
x_row.append(x_cell)
|
||||
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING)
|
||||
|
||||
|
@ -610,7 +616,6 @@ class GenTable(object):
|
|||
format="html",
|
||||
page_title="",
|
||||
filename=None,
|
||||
REQUEST=None,
|
||||
javascripts=[],
|
||||
with_html_headers=True,
|
||||
publish=True,
|
||||
|
@ -643,35 +648,53 @@ class GenTable(object):
|
|||
H.append(html_sco_header.sco_footer())
|
||||
return "\n".join(H)
|
||||
elif format == "pdf":
|
||||
objects = self.pdf()
|
||||
doc = sco_pdf.pdf_basic_page(
|
||||
objects, title=title, preferences=self.preferences
|
||||
pdf_objs = self.pdf()
|
||||
pdf_doc = sco_pdf.pdf_basic_page(
|
||||
pdf_objs, title=title, preferences=self.preferences
|
||||
)
|
||||
if publish:
|
||||
return scu.sendPDFFile(REQUEST, doc, filename + ".pdf")
|
||||
return scu.send_file(
|
||||
pdf_doc,
|
||||
filename,
|
||||
suffix=".pdf",
|
||||
mime=scu.PDF_MIMETYPE,
|
||||
)
|
||||
else:
|
||||
return doc
|
||||
elif format == "xls" or format == "xlsx":
|
||||
return pdf_doc
|
||||
elif format == "xls" or format == "xlsx": # dans les 2 cas retourne du xlsx
|
||||
xls = self.excel()
|
||||
if publish:
|
||||
return sco_excel.send_excel_file(
|
||||
REQUEST, xls, filename + scu.XLSX_SUFFIX
|
||||
return scu.send_file(
|
||||
xls,
|
||||
filename,
|
||||
suffix=scu.XLSX_SUFFIX,
|
||||
mime=scu.XLSX_MIMETYPE,
|
||||
)
|
||||
else:
|
||||
return xls
|
||||
elif format == "text":
|
||||
return self.text()
|
||||
elif format == "csv":
|
||||
return scu.sendCSVFile(REQUEST, self.text(), filename + ".csv")
|
||||
return scu.send_file(
|
||||
self.text(),
|
||||
filename,
|
||||
suffix=".csv",
|
||||
mime=scu.CSV_MIMETYPE,
|
||||
attached=True,
|
||||
)
|
||||
elif format == "xml":
|
||||
xml = self.xml()
|
||||
if REQUEST and publish:
|
||||
REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE)
|
||||
if publish:
|
||||
return scu.send_file(
|
||||
xml, filename, suffix=".xml", mime=scu.XML_MIMETYPE
|
||||
)
|
||||
return xml
|
||||
elif format == "json":
|
||||
js = self.json()
|
||||
if REQUEST and publish:
|
||||
REQUEST.RESPONSE.setHeader("content-type", scu.JSON_MIMETYPE)
|
||||
if publish:
|
||||
return scu.send_file(
|
||||
js, filename, suffix=".json", mime=scu.JSON_MIMETYPE
|
||||
)
|
||||
return js
|
||||
else:
|
||||
log("make_page: format=%s" % format)
|
||||
|
@ -731,6 +754,8 @@ if __name__ == "__main__":
|
|||
)
|
||||
document.build(objects)
|
||||
data = doc.getvalue()
|
||||
open("/tmp/gen_table.pdf", "wb").write(data)
|
||||
p = T.make_page(format="pdf", REQUEST=None)
|
||||
open("toto.pdf", "wb").write(p)
|
||||
with open("/tmp/gen_table.pdf", "wb") as f:
|
||||
f.write(data)
|
||||
p = T.make_page(format="pdf")
|
||||
with open("toto.pdf", "wb") as f:
|
||||
f.write(p)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -87,50 +87,48 @@ Problème de connexion (identifiant, mot de passe): <em>contacter votre responsa
|
|||
)
|
||||
|
||||
|
||||
_TOP_LEVEL_CSS = """
|
||||
<style type="text/css">
|
||||
</style>"""
|
||||
_HTML_BEGIN = """<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
_HTML_BEGIN = """<?xml version="1.0" encoding="%(encoding)s"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>%(page_title)s</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=%(encoding)s" />
|
||||
<meta http-equiv="Content-Style-Type" content="text/css" />
|
||||
<meta name="LANG" content="fr" />
|
||||
<meta name="DESCRIPTION" content="ScoDoc" />
|
||||
<title>%(page_title)s</title>
|
||||
|
||||
<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />
|
||||
|
||||
<link href="/ScoDoc/static/css/scodoc.css" rel="stylesheet" type="text/css" />
|
||||
<link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" />
|
||||
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/menu.js"></script>
|
||||
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/sorttable.js"></script>
|
||||
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/bubble.js"></script>
|
||||
<script type="text/javascript">
|
||||
<script src="/ScoDoc/static/libjs/menu.js"></script>
|
||||
<script src="/ScoDoc/static/libjs/sorttable.js"></script>
|
||||
<script src="/ScoDoc/static/libjs/bubble.js"></script>
|
||||
<script>
|
||||
window.onload=function(){enableTooltips("gtrcontent")};
|
||||
</script>
|
||||
|
||||
<script language="javascript" type="text/javascript" src="/ScoDoc/static/jQuery/jquery.js"></script>
|
||||
<script language="javascript" type="text/javascript" src="/ScoDoc/static/jQuery/jquery-migrate-1.2.0.min.js"></script>
|
||||
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery.field.min.js"></script>
|
||||
<script src="/ScoDoc/static/jQuery/jquery.js"></script>
|
||||
<script src="/ScoDoc/static/jQuery/jquery-migrate-1.2.0.min.js"></script>
|
||||
<script src="/ScoDoc/static/libjs/jquery.field.min.js"></script>
|
||||
|
||||
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
|
||||
<script src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
|
||||
|
||||
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
|
||||
<script src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
|
||||
<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.css" />
|
||||
|
||||
<script language="javascript" type="text/javascript" src="/ScoDoc/static/js/scodoc.js"></script>
|
||||
<script language="javascript" type="text/javascript" src="/ScoDoc/static/js/etud_info.js"></script>
|
||||
<script src="/ScoDoc/static/js/scodoc.js"></script>
|
||||
<script src="/ScoDoc/static/js/etud_info.js"></script>
|
||||
"""
|
||||
|
||||
|
||||
def scodoc_top_html_header(page_title="ScoDoc: bienvenue"):
|
||||
H = [
|
||||
_HTML_BEGIN % {"page_title": page_title, "encoding": scu.SCO_ENCODING},
|
||||
_TOP_LEVEL_CSS,
|
||||
"""</head><body class="gtrcontent" id="gtrcontent">""",
|
||||
"""</head><body id="gtrcontent">""",
|
||||
scu.CUSTOM_HTML_HEADER_CNX,
|
||||
]
|
||||
return "\n".join(H)
|
||||
|
@ -145,8 +143,6 @@ def sco_header(
|
|||
javascripts=[], # additionals JS filenames to load
|
||||
scripts=[], # script to put in page header
|
||||
bodyOnLoad="", # JS
|
||||
init_jquery=True, # load and init jQuery
|
||||
init_jquery_ui=True, # include all stuff for jquery-ui and initialize scripts
|
||||
init_qtip=False, # include qTip
|
||||
init_google_maps=False, # Google maps
|
||||
init_datatables=True,
|
||||
|
@ -181,17 +177,11 @@ def sco_header(
|
|||
else:
|
||||
params["margin_left"] = "140px"
|
||||
|
||||
if init_jquery_ui or init_qtip or init_datatables:
|
||||
init_jquery = True
|
||||
|
||||
H = [
|
||||
"""<?xml version="1.0" encoding="%(encoding)s"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
"""<!DOCTYPE html><html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>%(page_title)s</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=%(encoding)s" />
|
||||
<meta http-equiv="Content-Style-Type" content="text/css" />
|
||||
<meta name="LANG" content="fr" />
|
||||
<meta name="DESCRIPTION" content="ScoDoc" />
|
||||
|
||||
|
@ -199,16 +189,13 @@ def sco_header(
|
|||
% params
|
||||
]
|
||||
# jQuery UI
|
||||
if init_jquery_ui:
|
||||
# can modify loaded theme here
|
||||
H.append(
|
||||
'<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />\n'
|
||||
)
|
||||
# can modify loaded theme here
|
||||
H.append(
|
||||
'<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />\n'
|
||||
)
|
||||
if init_google_maps:
|
||||
# It may be necessary to add an API key:
|
||||
H.append(
|
||||
'<script type="text/javascript" src="https://maps.google.com/maps/api/js"></script>'
|
||||
)
|
||||
H.append('<script src="https://maps.google.com/maps/api/js"></script>')
|
||||
|
||||
# Feuilles de style additionnelles:
|
||||
for cssstyle in cssstyles:
|
||||
|
@ -223,9 +210,9 @@ def sco_header(
|
|||
<link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" />
|
||||
<link href="/ScoDoc/static/css/gt_table.css" rel="stylesheet" type="text/css" />
|
||||
|
||||
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/menu.js"></script>
|
||||
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/bubble.js"></script>
|
||||
<script type="text/javascript">
|
||||
<script src="/ScoDoc/static/libjs/menu.js"></script>
|
||||
<script src="/ScoDoc/static/libjs/bubble.js"></script>
|
||||
<script>
|
||||
window.onload=function(){enableTooltips("gtrcontent")};
|
||||
|
||||
var SCO_URL="%(ScoURL)s";
|
||||
|
@ -234,52 +221,41 @@ def sco_header(
|
|||
)
|
||||
|
||||
# jQuery
|
||||
if init_jquery:
|
||||
H.append(
|
||||
"""<script language="javascript" type="text/javascript" src="/ScoDoc/static/jQuery/jquery.js"></script>
|
||||
"""
|
||||
)
|
||||
H.append(
|
||||
'<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery.field.min.js"></script>'
|
||||
)
|
||||
H.append(
|
||||
"""<script src="/ScoDoc/static/jQuery/jquery.js"></script>
|
||||
"""
|
||||
)
|
||||
H.append('<script src="/ScoDoc/static/libjs/jquery.field.min.js"></script>')
|
||||
# qTip
|
||||
if init_qtip:
|
||||
H.append(
|
||||
'<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>'
|
||||
'<script src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>'
|
||||
)
|
||||
H.append(
|
||||
'<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.css" />'
|
||||
)
|
||||
|
||||
if init_jquery_ui:
|
||||
H.append(
|
||||
'<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>'
|
||||
)
|
||||
# H.append('<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery-ui/js/jquery-ui-i18n.js"></script>')
|
||||
H.append(
|
||||
'<script language="javascript" type="text/javascript" src="/ScoDoc/static/js/scodoc.js"></script>'
|
||||
)
|
||||
H.append(
|
||||
'<script src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>'
|
||||
)
|
||||
|
||||
H.append('<script src="/ScoDoc/static/js/scodoc.js"></script>')
|
||||
if init_google_maps:
|
||||
H.append(
|
||||
'<script type="text/javascript" src="/ScoDoc/static/libjs/jquery.ui.map.full.min.js"></script>'
|
||||
'<script src="/ScoDoc/static/libjs/jquery.ui.map.full.min.js"></script>'
|
||||
)
|
||||
if init_datatables:
|
||||
H.append(
|
||||
'<link rel="stylesheet" type="text/css" href="/ScoDoc/static/DataTables/datatables.min.css"/>'
|
||||
)
|
||||
H.append(
|
||||
'<script type="text/javascript" src="/ScoDoc/static/DataTables/datatables.min.js"></script>'
|
||||
)
|
||||
H.append('<script src="/ScoDoc/static/DataTables/datatables.min.js"></script>')
|
||||
# JS additionels
|
||||
for js in javascripts:
|
||||
H.append(
|
||||
"""<script language="javascript" type="text/javascript" src="/ScoDoc/static/%s"></script>\n"""
|
||||
% js
|
||||
)
|
||||
H.append("""<script src="/ScoDoc/static/%s"></script>\n""" % js)
|
||||
|
||||
H.append(
|
||||
"""<style type="text/css">
|
||||
.gtrcontent {
|
||||
"""<style>
|
||||
#gtrcontent {
|
||||
margin-left: %(margin_left)s;
|
||||
height: 100%%;
|
||||
margin-bottom: 10px;
|
||||
|
@ -290,7 +266,7 @@ def sco_header(
|
|||
)
|
||||
# Scripts de la page:
|
||||
if scripts:
|
||||
H.append("""<script language="javascript" type="text/javascript">""")
|
||||
H.append("""<script>""")
|
||||
for script in scripts:
|
||||
H.append(script)
|
||||
H.append("""</script>""")
|
||||
|
@ -303,7 +279,7 @@ def sco_header(
|
|||
#
|
||||
if not no_side_bar:
|
||||
H.append(html_sidebar.sidebar())
|
||||
H.append("""<div class="gtrcontent" id="gtrcontent">""")
|
||||
H.append("""<div id="gtrcontent">""")
|
||||
#
|
||||
# Barre menu semestre:
|
||||
H.append(formsemestre_page_title())
|
||||
|
@ -337,13 +313,7 @@ def sco_footer():
|
|||
|
||||
|
||||
def html_sem_header(
|
||||
REQUEST,
|
||||
title,
|
||||
sem=None,
|
||||
with_page_header=True,
|
||||
with_h2=True,
|
||||
page_title=None,
|
||||
**args
|
||||
title, sem=None, with_page_header=True, with_h2=True, page_title=None, **args
|
||||
):
|
||||
"Titre d'une page semestre avec lien vers tableau de bord"
|
||||
# sem now unused and thus optional...
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -28,29 +28,25 @@
|
|||
"""
|
||||
Génération de la "sidebar" (marge gauche des pages HTML)
|
||||
"""
|
||||
from flask import url_for
|
||||
from flask import g
|
||||
from flask import request
|
||||
from flask import render_template, url_for
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from sco_version import SCOVERSION
|
||||
|
||||
|
||||
def sidebar_common():
|
||||
"partie commune à toutes les sidebar"
|
||||
params = {
|
||||
"ScoURL": scu.ScoURL(),
|
||||
"UsersURL": scu.UsersURL(),
|
||||
"NotesURL": scu.NotesURL(),
|
||||
"AbsencesURL": scu.AbsencesURL(),
|
||||
"authuser": current_user.user_name,
|
||||
}
|
||||
home_link = url_for("scodoc.index", scodoc_dept=g.scodoc_dept)
|
||||
H = [
|
||||
f"""<a class="scodoc_title" href="about">ScoDoc 9</a>
|
||||
f"""<a class="scodoc_title" href="{home_link}">ScoDoc {SCOVERSION}</a><br>
|
||||
<a href="{home_link}" class="sidebar">Accueil</a> <br>
|
||||
<div id="authuser"><a id="authuserlink" href="{
|
||||
url_for("users.user_info_page", scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
|
||||
url_for("users.user_info_page",
|
||||
scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
|
||||
}">{current_user.user_name}</a>
|
||||
<br/><a id="deconnectlink" href="{url_for("auth.logout")}">déconnexion</a>
|
||||
</div>
|
||||
|
@ -71,7 +67,8 @@ def sidebar_common():
|
|||
|
||||
if current_user.has_permission(Permission.ScoChangePreferences):
|
||||
H.append(
|
||||
f"""<a href="{url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)}" class="sidebar">Paramétrage</a> <br/>"""
|
||||
f"""<a href="{url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)}"
|
||||
class="sidebar">Paramétrage</a> <br/>"""
|
||||
)
|
||||
|
||||
return "".join(H)
|
||||
|
@ -97,11 +94,12 @@ def sidebar():
|
|||
"""
|
||||
]
|
||||
# ---- Il y-a-t-il un etudiant selectionné ?
|
||||
etudid = None
|
||||
if request.method == "GET":
|
||||
etudid = request.args.get("etudid", None)
|
||||
elif request.method == "POST":
|
||||
etudid = request.form.get("etudid", None)
|
||||
etudid = g.get("etudid", None)
|
||||
if not etudid:
|
||||
if request.method == "GET":
|
||||
etudid = request.args.get("etudid", None)
|
||||
elif request.method == "POST":
|
||||
etudid = request.form.get("etudid", None)
|
||||
|
||||
if etudid:
|
||||
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
||||
|
@ -155,8 +153,9 @@ def sidebar():
|
|||
<div class="sidebar-bottom"><a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }" class="sidebar">À propos</a><br/>
|
||||
<a href="{ scu.SCO_USER_MANUAL }" target="_blank" class="sidebar">Aide</a>
|
||||
</div></div>
|
||||
<div class="logo-logo"><a href= { url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }
|
||||
">{ scu.icontag("scologo_img", no_size=True) }</a>
|
||||
<div class="logo-logo">
|
||||
<a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }">
|
||||
{ scu.icontag("scologo_img", no_size=True) }</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end of sidebar -->
|
||||
|
@ -167,19 +166,7 @@ def sidebar():
|
|||
|
||||
def sidebar_dept():
|
||||
"""Partie supérieure de la marge de gauche"""
|
||||
H = [
|
||||
f"""<h2 class="insidebar">Dépt. {sco_preferences.get_preference("DeptName")}</h2>
|
||||
<a href="{url_for("scodoc.index")}" class="sidebar">Accueil</a> <br/> """
|
||||
]
|
||||
dept_intranet_url = sco_preferences.get_preference("DeptIntranetURL")
|
||||
if dept_intranet_url:
|
||||
H.append(
|
||||
f"""<a href="{dept_intranet_url}" class="sidebar">{
|
||||
sco_preferences.get_preference("DeptIntranetTitle")}</a> <br/>"""
|
||||
)
|
||||
# Entreprises pas encore supporté en ScoDoc8
|
||||
# H.append(
|
||||
# """<br/><a href="%(ScoURL)s/Entreprises" class="sidebar">Entreprises</a> <br/>"""
|
||||
# % infos
|
||||
# )
|
||||
return "\n".join(H)
|
||||
return render_template(
|
||||
"sidebar_dept.html",
|
||||
prefs=sco_preferences.SemPreferences(),
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -27,9 +27,12 @@
|
|||
|
||||
"""Various HTML generation functions
|
||||
"""
|
||||
from html.parser import HTMLParser
|
||||
from html.entities import name2codepoint
|
||||
import re
|
||||
|
||||
from flask import g, url_for
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from . import listhistogram
|
||||
|
||||
|
||||
|
@ -104,6 +107,8 @@ def make_menu(title, items, css_class="", alone=False):
|
|||
item["urlq"] = url_for(
|
||||
item["endpoint"], scodoc_dept=g.scodoc_dept, **args
|
||||
)
|
||||
elif "url" in item:
|
||||
item["urlq"] = item["url"]
|
||||
else:
|
||||
item["urlq"] = "#"
|
||||
item["attr"] = item.get("attr", "")
|
||||
|
@ -128,3 +133,63 @@ def make_menu(title, items, css_class="", alone=False):
|
|||
if alone:
|
||||
H.append("</ul>")
|
||||
return "".join(H)
|
||||
|
||||
|
||||
"""
|
||||
HTML <-> text conversions.
|
||||
http://stackoverflow.com/questions/328356/extracting-text-from-html-file-using-python
|
||||
"""
|
||||
|
||||
|
||||
class _HTMLToText(HTMLParser):
|
||||
def __init__(self):
|
||||
HTMLParser.__init__(self)
|
||||
self._buf = []
|
||||
self.hide_output = False
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag in ("p", "br") and not self.hide_output:
|
||||
self._buf.append("\n")
|
||||
elif tag in ("script", "style"):
|
||||
self.hide_output = True
|
||||
|
||||
def handle_startendtag(self, tag, attrs):
|
||||
if tag == "br":
|
||||
self._buf.append("\n")
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == "p":
|
||||
self._buf.append("\n")
|
||||
elif tag in ("script", "style"):
|
||||
self.hide_output = False
|
||||
|
||||
def handle_data(self, text):
|
||||
if text and not self.hide_output:
|
||||
self._buf.append(re.sub(r"\s+", " ", text))
|
||||
|
||||
def handle_entityref(self, name):
|
||||
if name in name2codepoint and not self.hide_output:
|
||||
c = chr(name2codepoint[name])
|
||||
self._buf.append(c)
|
||||
|
||||
def handle_charref(self, name):
|
||||
if not self.hide_output:
|
||||
n = int(name[1:], 16) if name.startswith("x") else int(name)
|
||||
self._buf.append(chr(n))
|
||||
|
||||
def get_text(self):
|
||||
return re.sub(r" +", " ", "".join(self._buf))
|
||||
|
||||
|
||||
def html_to_text(html):
|
||||
"""
|
||||
Given a piece of HTML, return the plain text it contains.
|
||||
This handles entities and char refs, but not javascript and stylesheets.
|
||||
"""
|
||||
parser = _HTMLToText()
|
||||
try:
|
||||
parser.feed(html)
|
||||
parser.close()
|
||||
except: # HTMLParseError: No good replacement?
|
||||
pass
|
||||
return parser.get_text()
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -27,25 +27,21 @@
|
|||
|
||||
"""Calculs sur les notes et cache des resultats
|
||||
"""
|
||||
import inspect
|
||||
import os
|
||||
import pdb
|
||||
import time
|
||||
|
||||
from operator import itemgetter
|
||||
|
||||
from flask import g, url_for
|
||||
|
||||
from app.but import bulletin_but
|
||||
from app.models import FormSemestre, Identite
|
||||
from app.models import ScoDocSiteConfig
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
from app.scodoc.sco_formulas import NoteVector
|
||||
from app.scodoc.sco_exceptions import (
|
||||
AccessDenied,
|
||||
NoteProcessError,
|
||||
ScoException,
|
||||
ScoValueError,
|
||||
)
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
from app.scodoc.sco_formsemestre import (
|
||||
formsemestre_uecoef_list,
|
||||
formsemestre_uecoef_create,
|
||||
|
@ -109,15 +105,13 @@ def get_sem_ues_modimpls(formsemestre_id, modimpls=None):
|
|||
(utilisé quand on ne peut pas construire nt et faire nt.get_ues())
|
||||
"""
|
||||
if modimpls is None:
|
||||
modimpls = sco_moduleimpl.do_moduleimpl_list(formsemestre_id=formsemestre_id)
|
||||
modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
|
||||
uedict = {}
|
||||
for modimpl in modimpls:
|
||||
mod = sco_edit_module.do_module_list(args={"module_id": modimpl["module_id"]})[
|
||||
0
|
||||
]
|
||||
mod = sco_edit_module.module_list(args={"module_id": modimpl["module_id"]})[0]
|
||||
modimpl["module"] = mod
|
||||
if not mod["ue_id"] in uedict:
|
||||
ue = sco_edit_ue.do_ue_list(args={"ue_id": mod["ue_id"]})[0]
|
||||
ue = sco_edit_ue.ue_list(args={"ue_id": mod["ue_id"]})[0]
|
||||
uedict[ue["ue_id"]] = ue
|
||||
ues = list(uedict.values())
|
||||
ues.sort(key=lambda u: u["numero"])
|
||||
|
@ -149,7 +143,7 @@ def comp_etud_sum_coef_modules_ue(formsemestre_id, etudid, ue_id):
|
|||
return s
|
||||
|
||||
|
||||
class NotesTable(object):
|
||||
class NotesTable:
|
||||
"""Une NotesTable représente un tableau de notes pour un semestre de formation.
|
||||
Les colonnes sont des modules.
|
||||
Les lignes des étudiants.
|
||||
|
@ -186,6 +180,8 @@ class NotesTable(object):
|
|||
self.use_ue_coefs = sco_preferences.get_preference(
|
||||
"use_ue_coefs", formsemestre_id
|
||||
)
|
||||
# si vrai, bloque calcul des moy gen. et d'UE.:
|
||||
self.block_moyennes = self.sem["block_moyennes"]
|
||||
# Infos sur les etudiants
|
||||
self.inscrlist = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
|
||||
args={"formsemestre_id": formsemestre_id}
|
||||
|
@ -204,9 +200,7 @@ class NotesTable(object):
|
|||
self.inscrlist.sort(key=itemgetter("nomp"))
|
||||
|
||||
# { etudid : rang dans l'ordre alphabetique }
|
||||
rangalpha = {}
|
||||
for i in range(len(self.inscrlist)):
|
||||
rangalpha[self.inscrlist[i]["etudid"]] = i
|
||||
self.rang_alpha = {e["etudid"]: i for i, e in enumerate(self.inscrlist)}
|
||||
|
||||
self.bonus = scu.DictDefault(defaultvalue=0)
|
||||
# Notes dans les modules { moduleimpl_id : { etudid: note_moyenne_dans_ce_module } }
|
||||
|
@ -217,26 +211,29 @@ class NotesTable(object):
|
|||
valid_evals,
|
||||
mods_att,
|
||||
self.expr_diagnostics,
|
||||
) = sco_compute_moy.do_formsemestre_moyennes(self, formsemestre_id)
|
||||
) = sco_compute_moy.formsemestre_compute_modimpls_moyennes(
|
||||
self, formsemestre_id
|
||||
)
|
||||
self._mods_att = mods_att # liste des modules avec des notes en attente
|
||||
self._matmoys = {} # moyennes par matieres
|
||||
self._valid_evals = {} # { evaluation_id : eval }
|
||||
for e in valid_evals:
|
||||
self._valid_evals[e["evaluation_id"]] = e # Liste des modules et UE
|
||||
uedict = {} # public member: { ue_id : ue }
|
||||
self.uedict = uedict
|
||||
self.uedict = uedict # les ues qui ont un modimpl dans ce semestre
|
||||
for modimpl in self._modimpls:
|
||||
mod = modimpl["module"] # has been added here by do_formsemestre_moyennes
|
||||
# module has been added by formsemestre_compute_modimpls_moyennes
|
||||
mod = modimpl["module"]
|
||||
if not mod["ue_id"] in uedict:
|
||||
ue = sco_edit_ue.do_ue_list(args={"ue_id": mod["ue_id"]})[0]
|
||||
ue = sco_edit_ue.ue_list(args={"ue_id": mod["ue_id"]})[0]
|
||||
uedict[ue["ue_id"]] = ue
|
||||
else:
|
||||
ue = uedict[mod["ue_id"]]
|
||||
modimpl["ue"] = ue # add ue dict to moduleimpl
|
||||
self._matmoys[mod["matiere_id"]] = {}
|
||||
mat = sco_edit_matiere.do_matiere_list(
|
||||
args={"matiere_id": mod["matiere_id"]}
|
||||
)[0]
|
||||
mat = sco_edit_matiere.matiere_list(args={"matiere_id": mod["matiere_id"]})[
|
||||
0
|
||||
]
|
||||
modimpl["mat"] = mat # add matiere dict to moduleimpl
|
||||
# calcul moyennes du module et stocke dans le module
|
||||
# nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif=
|
||||
|
@ -248,6 +245,14 @@ class NotesTable(object):
|
|||
self.formation["type_parcours"]
|
||||
)
|
||||
|
||||
# En APC, il faut avoir toutes les UE du semestre
|
||||
# (elles n'ont pas nécessairement un module rattaché):
|
||||
if self.parcours.APC_SAE:
|
||||
formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||
for ue in formsemestre.query_ues():
|
||||
if ue.id not in self.uedict:
|
||||
self.uedict[ue.id] = ue.to_dict()
|
||||
|
||||
# Decisions jury et UE capitalisées
|
||||
self.comp_decisions_jury()
|
||||
self.comp_ue_capitalisees()
|
||||
|
@ -257,7 +262,7 @@ class NotesTable(object):
|
|||
self._ues.sort(key=lambda u: u["numero"])
|
||||
|
||||
T = []
|
||||
# XXX self.comp_ue_coefs(cnx)
|
||||
|
||||
self.moy_gen = {} # etudid : moy gen (avec UE capitalisées)
|
||||
self.moy_ue = {} # ue_id : { etudid : moy ue } (valeur numerique)
|
||||
self.etud_moy_infos = {} # etudid : resultats de comp_etud_moy_gen()
|
||||
|
@ -297,22 +302,12 @@ class NotesTable(object):
|
|||
t.append(val)
|
||||
#
|
||||
t.append(etudid)
|
||||
T.append(tuple(t))
|
||||
T.append(t)
|
||||
|
||||
self.T = T
|
||||
# tri par moyennes décroissantes,
|
||||
# en laissant les demissionnaires a la fin, par ordre alphabetique
|
||||
def row_key(x):
|
||||
"""clé de tri par moyennes décroissantes,
|
||||
en laissant les demissionnaires a la fin, par ordre alphabetique.
|
||||
(moy_gen, rang_alpha)
|
||||
"""
|
||||
try:
|
||||
moy = -float(x[0])
|
||||
except (ValueError, TypeError):
|
||||
moy = 1000.0
|
||||
return (moy, rangalpha[x[-1]])
|
||||
|
||||
T.sort(key=row_key)
|
||||
self.T = T
|
||||
self.T.sort(key=self._row_key)
|
||||
|
||||
if len(valid_moy):
|
||||
self.moy_min = min(valid_moy)
|
||||
|
@ -342,7 +337,7 @@ class NotesTable(object):
|
|||
ue_eff = len(
|
||||
[x for x in val_ids if isinstance(x[0], float)]
|
||||
) # nombre d'étudiants avec une note dans l'UE
|
||||
val_ids.sort(key=row_key)
|
||||
val_ids.sort(key=self._row_key)
|
||||
ue_rangs[ue_id] = (
|
||||
comp_ranks(val_ids),
|
||||
ue_eff,
|
||||
|
@ -353,13 +348,24 @@ class NotesTable(object):
|
|||
for modimpl in self._modimpls:
|
||||
vals = self._modmoys[modimpl["moduleimpl_id"]]
|
||||
val_ids = [(vals[etudid], etudid) for etudid in vals.keys()]
|
||||
val_ids.sort(key=row_key)
|
||||
val_ids.sort(key=self._row_key)
|
||||
self.mod_rangs[modimpl["moduleimpl_id"]] = (comp_ranks(val_ids), len(vals))
|
||||
#
|
||||
self.compute_moy_moy()
|
||||
#
|
||||
log(f"NotesTable( formsemestre_id={formsemestre_id} ) done.")
|
||||
|
||||
def _row_key(self, x):
|
||||
"""clé de tri par moyennes décroissantes,
|
||||
en laissant les demissionnaires a la fin, par ordre alphabetique.
|
||||
(moy_gen, rang_alpha)
|
||||
"""
|
||||
try:
|
||||
moy = -float(x[0])
|
||||
except (ValueError, TypeError):
|
||||
moy = 1000.0
|
||||
return (moy, self.rang_alpha[x[-1]])
|
||||
|
||||
def get_etudids(self, sorted=False):
|
||||
if sorted:
|
||||
# Tri par moy. generale décroissante
|
||||
|
@ -611,7 +617,7 @@ class NotesTable(object):
|
|||
# si 'NI', etudiant non inscrit a ce module
|
||||
if val != "NI":
|
||||
est_inscrit = True
|
||||
if modimpl["module"]["module_type"] == scu.MODULE_STANDARD:
|
||||
if modimpl["module"]["module_type"] == ModuleType.STANDARD:
|
||||
coef = modimpl["module"]["coefficient"]
|
||||
if modimpl["ue"]["type"] != UE_SPORT:
|
||||
notes.append(val, name=modimpl["module"]["code"])
|
||||
|
@ -634,7 +640,8 @@ class NotesTable(object):
|
|||
matiere_sum_notes += val * coef
|
||||
matiere_sum_coefs += coef
|
||||
matiere_id_last = matiere_id
|
||||
except:
|
||||
except TypeError: # val == "NI" "NA"
|
||||
assert val == "NI" or val == "NA" or val == "ERR"
|
||||
nb_missing = nb_missing + 1
|
||||
coefs.append(0)
|
||||
coefs_mask.append(0)
|
||||
|
@ -647,11 +654,17 @@ class NotesTable(object):
|
|||
except:
|
||||
# log('comp_etud_moy_ue: exception: val=%s coef=%s' % (val,coef))
|
||||
pass
|
||||
elif modimpl["module"]["module_type"] == scu.MODULE_MALUS:
|
||||
elif modimpl["module"]["module_type"] == ModuleType.MALUS:
|
||||
try:
|
||||
ue_malus += val
|
||||
except:
|
||||
pass # si non inscrit ou manquant, ignore
|
||||
elif modimpl["module"]["module_type"] in (
|
||||
ModuleType.RESSOURCE,
|
||||
ModuleType.SAE,
|
||||
):
|
||||
# XXX temporaire pour ne pas bloquer durant le dev
|
||||
pass
|
||||
else:
|
||||
raise ValueError(
|
||||
"invalid module type (%s)" % modimpl["module"]["module_type"]
|
||||
|
@ -675,7 +688,7 @@ class NotesTable(object):
|
|||
|
||||
# Recalcule la moyenne en utilisant une formule utilisateur
|
||||
expr_diag = {}
|
||||
formula = sco_compute_moy.get_ue_expression(self.formsemestre_id, ue_id, cnx)
|
||||
formula = sco_compute_moy.get_ue_expression(self.formsemestre_id, ue_id)
|
||||
if formula:
|
||||
moy = sco_compute_moy.compute_user_formula(
|
||||
self.sem,
|
||||
|
@ -732,12 +745,11 @@ class NotesTable(object):
|
|||
|
||||
Prend toujours en compte les UE capitalisées.
|
||||
"""
|
||||
# log('comp_etud_moy_gen(etudid=%s)' % etudid)
|
||||
|
||||
# Si l'étudiant a Demissionné ou est DEFaillant, on n'enregistre pas ses moyennes
|
||||
# Si l'étudiant a Démissionné ou est DEFaillant, on n'enregistre pas ses moyennes
|
||||
block_computation = (
|
||||
self.inscrdict[etudid]["etat"] == "D"
|
||||
or self.inscrdict[etudid]["etat"] == DEF
|
||||
or self.block_moyennes
|
||||
)
|
||||
|
||||
moy_ues = {}
|
||||
|
@ -968,8 +980,8 @@ class NotesTable(object):
|
|||
def get_table_moyennes_triees(self):
|
||||
return self.T
|
||||
|
||||
def get_etud_rang(self, etudid):
|
||||
return self.rangs[etudid]
|
||||
def get_etud_rang(self, etudid) -> str:
|
||||
return self.rangs.get(etudid, "999")
|
||||
|
||||
def get_etud_rang_group(self, etudid, group_id):
|
||||
"""Returns rank of etud in this group and number of etuds in group.
|
||||
|
@ -1056,7 +1068,7 @@ class NotesTable(object):
|
|||
"Warning: %s capitalized an UE %s which is not part of current sem %s"
|
||||
% (etudid, ue_id, self.formsemestre_id)
|
||||
)
|
||||
ue = sco_edit_ue.do_ue_list(args={"ue_id": ue_id})[0]
|
||||
ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
|
||||
self.uedict[ue_id] = ue # record this UE
|
||||
if ue_id not in self._uecoef:
|
||||
cl = formsemestre_uecoef_list(
|
||||
|
@ -1262,7 +1274,7 @@ class NotesTable(object):
|
|||
),
|
||||
self.get_nom_long(etudid),
|
||||
url_for(
|
||||
"scolar.formsemestre_edit_uecoefs",
|
||||
"notes.formsemestre_edit_uecoefs",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=self.formsemestre_id,
|
||||
err_ue_id=ue["ue_id"],
|
||||
|
@ -1323,3 +1335,27 @@ class NotesTable(object):
|
|||
for e in self.get_evaluations_etats()
|
||||
if e["moduleimpl_id"] == moduleimpl_id
|
||||
]
|
||||
|
||||
def apc_recompute_moyennes(self):
|
||||
"""recalcule les moyennes en APC (BUT)
|
||||
et modifie en place le tableau T.
|
||||
XXX Raccord provisoire avant refonte de cette classe.
|
||||
"""
|
||||
assert self.parcours.APC_SAE
|
||||
formsemestre = FormSemestre.query.get(self.formsemestre_id)
|
||||
results = bulletin_but.ResultatsSemestreBUT(formsemestre)
|
||||
|
||||
# Rappel des épisodes précédents: T est une liste de liste
|
||||
# Colonnes: 0 moy_gen, moy_ue1, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid
|
||||
ues = self.get_ues() # incluant le(s) UE de sport
|
||||
for t in self.T:
|
||||
etudid = t[-1]
|
||||
if etudid in results.etud_moy_gen: # evite les démissionnaires
|
||||
t[0] = results.etud_moy_gen[etudid]
|
||||
for i, ue in enumerate(ues, start=1):
|
||||
if ue["type"] != UE_SPORT:
|
||||
t[i] = results.etud_moy_ue[ue["id"]][etudid]
|
||||
# re-trie selon la nouvelle moyenne générale:
|
||||
self.T.sort(key=self._row_key)
|
||||
# Remplace aussi le rang:
|
||||
self.rangs = results.etud_moy_gen_ranks
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -53,7 +53,7 @@ def close_db_connection():
|
|||
del g.db_conn
|
||||
|
||||
|
||||
def GetDBConnexion(autocommit=True): # on n'utilise plus autocommit
|
||||
def GetDBConnexion():
|
||||
return g.db_conn
|
||||
|
||||
|
||||
|
@ -88,7 +88,15 @@ def SimpleDictFetch(query, args, cursor=None):
|
|||
return cursor.dictfetchall()
|
||||
|
||||
|
||||
def DBInsertDict(cnx, table, vals, commit=0, convert_empty_to_nulls=1, return_id=True):
|
||||
def DBInsertDict(
|
||||
cnx,
|
||||
table,
|
||||
vals,
|
||||
commit=0,
|
||||
convert_empty_to_nulls=1,
|
||||
return_id=True,
|
||||
ignore_conflicts=False,
|
||||
) -> int:
|
||||
"""insert into table values in dict 'vals'
|
||||
Return: id de l'object créé
|
||||
"""
|
||||
|
@ -103,13 +111,18 @@ def DBInsertDict(cnx, table, vals, commit=0, convert_empty_to_nulls=1, return_id
|
|||
fmt = ",".join(["%%(%s)s" % col for col in cols])
|
||||
# print 'insert into %s (%s) values (%s)' % (table,colnames,fmt)
|
||||
oid = None
|
||||
if ignore_conflicts:
|
||||
ignore = " ON CONFLICT DO NOTHING"
|
||||
else:
|
||||
ignore = ""
|
||||
try:
|
||||
if vals:
|
||||
cursor.execute(
|
||||
"insert into %s (%s) values (%s)" % (table, colnames, fmt), vals
|
||||
"insert into %s (%s) values (%s)%s" % (table, colnames, fmt, ignore),
|
||||
vals,
|
||||
)
|
||||
else:
|
||||
cursor.execute("insert into %s default values" % table)
|
||||
cursor.execute("insert into %s default values%s" % (table, ignore))
|
||||
if return_id:
|
||||
cursor.execute(f"SELECT CURRVAL('{table}_id_seq')") # id créé
|
||||
oid = cursor.fetchone()[0]
|
||||
|
@ -252,8 +265,11 @@ def DBUpdateArgs(cnx, table, vals, where=None, commit=False, convert_empty_to_nu
|
|||
cursor.execute(req, vals)
|
||||
# log('req=%s\n'%req)
|
||||
# log('vals=%s\n'%vals)
|
||||
except psycopg2.errors.StringDataRightTruncation:
|
||||
cnx.rollback()
|
||||
raise ScoValueError("champs de texte trop long !")
|
||||
except:
|
||||
cnx.commit() # get rid of this transaction
|
||||
cnx.rollback() # get rid of this transaction
|
||||
log('Exception in DBUpdateArgs:\n\treq="%s"\n\tvals="%s"\n' % (req, vals))
|
||||
raise # and re-raise exception
|
||||
if commit:
|
||||
|
@ -291,6 +307,7 @@ class EditableTable(object):
|
|||
fields_creators={}, # { field : [ sql_command_to_create_it ] }
|
||||
filter_nulls=True, # dont allow to set fields to null
|
||||
filter_dept=False, # ajoute selection sur g.scodoc_dept_id
|
||||
insert_ignore_conflicts=False,
|
||||
):
|
||||
self.table_name = table_name
|
||||
self.id_name = id_name
|
||||
|
@ -311,8 +328,9 @@ class EditableTable(object):
|
|||
self.filter_nulls = filter_nulls
|
||||
self.filter_dept = filter_dept
|
||||
self.sql_default_values = None
|
||||
self.insert_ignore_conflicts = insert_ignore_conflicts
|
||||
|
||||
def create(self, cnx, args):
|
||||
def create(self, cnx, args) -> int:
|
||||
"create object in table"
|
||||
vals = dictfilter(args, self.dbfields, self.filter_nulls)
|
||||
if self.id_name in vals:
|
||||
|
@ -336,6 +354,7 @@ class EditableTable(object):
|
|||
vals,
|
||||
commit=True,
|
||||
return_id=(self.id_name is not None),
|
||||
ignore_conflicts=self.insert_ignore_conflicts,
|
||||
)
|
||||
return new_id
|
||||
|
||||
|
@ -581,6 +600,22 @@ def float_null_is_null(x):
|
|||
return float(x)
|
||||
|
||||
|
||||
BOOL_STR = {
|
||||
"": False,
|
||||
"false": False,
|
||||
"0": False,
|
||||
"1": True,
|
||||
"true": True,
|
||||
}
|
||||
|
||||
|
||||
def bool_or_str(x) -> bool:
|
||||
"""a boolean, may also be encoded as a string "0", "False", "1", "True" """
|
||||
if isinstance(x, str):
|
||||
return BOOL_STR[x.lower()]
|
||||
return bool(x)
|
||||
|
||||
|
||||
# post filtering
|
||||
#
|
||||
def UniqListofDicts(L, key):
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -474,7 +474,7 @@ def _get_abs_description(a, cursor=None):
|
|||
desc = a["description"]
|
||||
if a["moduleimpl_id"] and a["moduleimpl_id"] != "NULL":
|
||||
# Trouver le nom du module
|
||||
Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list(
|
||||
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(
|
||||
moduleimpl_id=a["moduleimpl_id"]
|
||||
)
|
||||
if Mlist:
|
||||
|
@ -546,21 +546,21 @@ def list_abs_non_just(etudid, datedebut):
|
|||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
cursor.execute(
|
||||
"""SELECT ETUDID, JOUR, MATIN FROM ABSENCES A
|
||||
"""SELECT ETUDID, JOUR, MATIN FROM ABSENCES A
|
||||
WHERE A.ETUDID = %(etudid)s
|
||||
AND A.estabs
|
||||
AND A.estabs
|
||||
AND A.jour >= %(datedebut)s
|
||||
EXCEPT SELECT ETUDID, JOUR, MATIN FROM ABSENCES B
|
||||
WHERE B.estjust
|
||||
EXCEPT SELECT ETUDID, JOUR, MATIN FROM ABSENCES B
|
||||
WHERE B.estjust
|
||||
AND B.ETUDID = %(etudid)s
|
||||
ORDER BY JOUR
|
||||
""",
|
||||
vars(),
|
||||
)
|
||||
A = cursor.dictfetchall()
|
||||
for a in A:
|
||||
abs_list = cursor.dictfetchall()
|
||||
for a in abs_list:
|
||||
a["description"] = _get_abs_description(a, cursor=cursor)
|
||||
return A
|
||||
return abs_list
|
||||
|
||||
|
||||
def list_abs_just(etudid, datedebut):
|
||||
|
@ -570,7 +570,7 @@ def list_abs_just(etudid, datedebut):
|
|||
cursor.execute(
|
||||
"""SELECT DISTINCT A.ETUDID, A.JOUR, A.MATIN FROM ABSENCES A, ABSENCES B
|
||||
WHERE A.ETUDID = %(etudid)s
|
||||
AND A.ETUDID = B.ETUDID
|
||||
AND A.ETUDID = B.ETUDID
|
||||
AND A.JOUR = B.JOUR AND A.MATIN = B.MATIN AND A.JOUR >= %(datedebut)s
|
||||
AND A.ESTABS AND (A.ESTJUST OR B.ESTJUST)
|
||||
ORDER BY A.JOUR
|
||||
|
@ -626,7 +626,6 @@ def add_absence(
|
|||
jour,
|
||||
matin,
|
||||
estjust,
|
||||
REQUEST,
|
||||
description=None,
|
||||
moduleimpl_id=None,
|
||||
):
|
||||
|
@ -639,8 +638,12 @@ def add_absence(
|
|||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT into absences (etudid,jour,estabs,estjust,matin,description, moduleimpl_id)
|
||||
VALUES (%(etudid)s, %(jour)s, true, %(estjust)s, %(matin)s, %(description)s, %(moduleimpl_id)s )
|
||||
INSERT into absences
|
||||
(etudid, jour, estabs, estjust, matin, description, moduleimpl_id)
|
||||
VALUES
|
||||
(%(etudid)s, %(jour)s, true, %(estjust)s, %(matin)s,
|
||||
%(description)s, %(moduleimpl_id)s
|
||||
)
|
||||
""",
|
||||
vars(),
|
||||
)
|
||||
|
@ -656,7 +659,7 @@ def add_absence(
|
|||
sco_abs_notification.abs_notify(etudid, jour)
|
||||
|
||||
|
||||
def add_justif(etudid, jour, matin, REQUEST, description=None):
|
||||
def add_justif(etudid, jour, matin, description=None):
|
||||
"Ajoute un justificatif dans la base"
|
||||
# unpublished
|
||||
if _isFarFutur(jour):
|
||||
|
@ -665,7 +668,9 @@ def add_justif(etudid, jour, matin, REQUEST, description=None):
|
|||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
cursor.execute(
|
||||
"insert into absences (etudid,jour,estabs,estjust,matin, description) values (%(etudid)s,%(jour)s, FALSE, TRUE, %(matin)s, %(description)s )",
|
||||
"""INSERT INTO absences (etudid, jour, estabs, estjust, matin, description)
|
||||
VALUES (%(etudid)s, %(jour)s, FALSE, TRUE, %(matin)s, %(description)s)
|
||||
""",
|
||||
vars(),
|
||||
)
|
||||
logdb(
|
||||
|
@ -678,7 +683,7 @@ def add_justif(etudid, jour, matin, REQUEST, description=None):
|
|||
invalidate_abs_etud_date(etudid, jour)
|
||||
|
||||
|
||||
def _add_abslist(abslist, REQUEST, moduleimpl_id=None):
|
||||
def add_abslist(abslist, moduleimpl_id=None):
|
||||
for a in abslist:
|
||||
etudid, jour, ampm = a.split(":")
|
||||
if ampm == "am":
|
||||
|
@ -689,7 +694,7 @@ def _add_abslist(abslist, REQUEST, moduleimpl_id=None):
|
|||
raise ValueError("invalid ampm !")
|
||||
# ajoute abs si pas deja absent
|
||||
if count_abs(etudid, jour, jour, matin, moduleimpl_id) == 0:
|
||||
add_absence(etudid, jour, matin, 0, REQUEST, "", moduleimpl_id)
|
||||
add_absence(etudid, jour, matin, 0, "", moduleimpl_id)
|
||||
|
||||
|
||||
def annule_absence(etudid, jour, matin, moduleimpl_id=None):
|
||||
|
@ -721,7 +726,7 @@ def annule_absence(etudid, jour, matin, moduleimpl_id=None):
|
|||
invalidate_abs_etud_date(etudid, jour)
|
||||
|
||||
|
||||
def annule_justif(etudid, jour, matin, REQUEST=None):
|
||||
def annule_justif(etudid, jour, matin):
|
||||
"Annule un justificatif"
|
||||
# unpublished
|
||||
matin = _toboolean(matin)
|
||||
|
@ -1027,20 +1032,26 @@ def get_abs_count(etudid, sem):
|
|||
tuple (nb abs non justifiées, nb abs justifiées)
|
||||
Utilise un cache.
|
||||
"""
|
||||
date_debut = sem["date_debut_iso"]
|
||||
date_fin = sem["date_fin_iso"]
|
||||
key = str(etudid) + "_" + date_debut + "_" + date_fin
|
||||
return get_abs_count_in_interval(etudid, sem["date_debut_iso"], sem["date_fin_iso"])
|
||||
|
||||
|
||||
def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso):
|
||||
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
|
||||
tuple (nb abs non justifiées, nb abs justifiées)
|
||||
Utilise un cache.
|
||||
"""
|
||||
key = str(etudid) + "_" + date_debut_iso + "_" + date_fin_iso
|
||||
r = sco_cache.AbsSemEtudCache.get(key)
|
||||
if not r:
|
||||
nb_abs = count_abs( # was CountAbs XXX
|
||||
nb_abs = count_abs(
|
||||
etudid=etudid,
|
||||
debut=date_debut,
|
||||
fin=date_fin,
|
||||
debut=date_debut_iso,
|
||||
fin=date_fin_iso,
|
||||
)
|
||||
nb_abs_just = count_abs_just( # XXX was CountAbsJust
|
||||
nb_abs_just = count_abs_just(
|
||||
etudid=etudid,
|
||||
debut=date_debut,
|
||||
fin=date_fin,
|
||||
debut=date_debut_iso,
|
||||
fin=date_fin_iso,
|
||||
)
|
||||
r = (nb_abs, nb_abs_just)
|
||||
ans = sco_cache.AbsSemEtudCache.set(key, r)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -30,7 +30,7 @@
|
|||
"""
|
||||
import datetime
|
||||
|
||||
from flask import url_for, g
|
||||
from flask import url_for, g, request, abort
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import notesdb as ndb
|
||||
|
@ -58,7 +58,6 @@ def doSignaleAbsence(
|
|||
estjust=False,
|
||||
description=None,
|
||||
etudid=False,
|
||||
REQUEST=None,
|
||||
): # etudid implied
|
||||
"""Signalement d'une absence.
|
||||
|
||||
|
@ -69,7 +68,8 @@ def doSignaleAbsence(
|
|||
demijournee: 2 si journée complète, 1 matin, 0 après-midi
|
||||
estjust: absence justifiée
|
||||
description: str
|
||||
etudid: etudiant concerné. Si non spécifié, cherche dans REQUEST.form
|
||||
etudid: etudiant concerné. Si non spécifié, cherche dans
|
||||
les paramètres de la requête courante.
|
||||
"""
|
||||
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
||||
etudid = etud["etudid"]
|
||||
|
@ -86,7 +86,6 @@ def doSignaleAbsence(
|
|||
jour,
|
||||
False,
|
||||
estjust,
|
||||
REQUEST,
|
||||
description_abs,
|
||||
moduleimpl_id,
|
||||
)
|
||||
|
@ -95,7 +94,6 @@ def doSignaleAbsence(
|
|||
jour,
|
||||
True,
|
||||
estjust,
|
||||
REQUEST,
|
||||
description_abs,
|
||||
moduleimpl_id,
|
||||
)
|
||||
|
@ -106,7 +104,6 @@ def doSignaleAbsence(
|
|||
jour,
|
||||
demijournee,
|
||||
estjust,
|
||||
REQUEST,
|
||||
description_abs,
|
||||
moduleimpl_id,
|
||||
)
|
||||
|
@ -118,7 +115,7 @@ def doSignaleAbsence(
|
|||
J = "NON "
|
||||
M = ""
|
||||
if moduleimpl_id and moduleimpl_id != "NULL":
|
||||
mod = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
mod = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
formsemestre_id = mod["formsemestre_id"]
|
||||
nt = sco_cache.NotesTableCache.get(formsemestre_id)
|
||||
ues = nt.get_ues(etudid=etudid)
|
||||
|
@ -156,7 +153,7 @@ def doSignaleAbsence(
|
|||
return "\n".join(H)
|
||||
|
||||
|
||||
def SignaleAbsenceEtud(REQUEST=None): # etudid implied
|
||||
def SignaleAbsenceEtud(): # etudid implied
|
||||
"""Formulaire individuel simple de signalement d'une absence"""
|
||||
# brute-force portage from very old dtml code ...
|
||||
etud = sco_etud.get_etud_info(filled=True)[0]
|
||||
|
@ -228,7 +225,6 @@ def SignaleAbsenceEtud(REQUEST=None): # etudid implied
|
|||
sco_photos.etud_photo_html(
|
||||
etudid=etudid,
|
||||
title="fiche de " + etud["nomprenom"],
|
||||
REQUEST=REQUEST,
|
||||
),
|
||||
"""</a></td></tr></table>""",
|
||||
"""
|
||||
|
@ -281,7 +277,6 @@ def doJustifAbsence(
|
|||
demijournee,
|
||||
description=None,
|
||||
etudid=False,
|
||||
REQUEST=None,
|
||||
): # etudid implied
|
||||
"""Justification d'une absence
|
||||
|
||||
|
@ -291,7 +286,8 @@ def doJustifAbsence(
|
|||
demijournee: 2 si journée complète, 1 matin, 0 après-midi
|
||||
estjust: absence justifiée
|
||||
description: str
|
||||
etudid: etudiant concerné. Si non spécifié, cherche dans REQUEST.form
|
||||
etudid: etudiant concerné. Si non spécifié, cherche dans les
|
||||
paramètres de la requête.
|
||||
"""
|
||||
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
||||
etudid = etud["etudid"]
|
||||
|
@ -305,14 +301,12 @@ def doJustifAbsence(
|
|||
etudid=etudid,
|
||||
jour=jour,
|
||||
matin=False,
|
||||
REQUEST=REQUEST,
|
||||
description=description_abs,
|
||||
)
|
||||
sco_abs.add_justif(
|
||||
etudid=etudid,
|
||||
jour=jour,
|
||||
matin=True,
|
||||
REQUEST=REQUEST,
|
||||
description=description_abs,
|
||||
)
|
||||
nbadded += 2
|
||||
|
@ -321,7 +315,6 @@ def doJustifAbsence(
|
|||
etudid=etudid,
|
||||
jour=jour,
|
||||
matin=demijournee,
|
||||
REQUEST=REQUEST,
|
||||
description=description_abs,
|
||||
)
|
||||
nbadded += 1
|
||||
|
@ -357,7 +350,7 @@ def doJustifAbsence(
|
|||
return "\n".join(H)
|
||||
|
||||
|
||||
def JustifAbsenceEtud(REQUEST=None): # etudid implied
|
||||
def JustifAbsenceEtud(): # etudid implied
|
||||
"""Formulaire individuel simple de justification d'une absence"""
|
||||
# brute-force portage from very old dtml code ...
|
||||
etud = sco_etud.get_etud_info(filled=True)[0]
|
||||
|
@ -376,7 +369,6 @@ def JustifAbsenceEtud(REQUEST=None): # etudid implied
|
|||
sco_photos.etud_photo_html(
|
||||
etudid=etudid,
|
||||
title="fiche de " + etud["nomprenom"],
|
||||
REQUEST=REQUEST,
|
||||
),
|
||||
"""</a></td></tr></table>""",
|
||||
"""
|
||||
|
@ -412,9 +404,7 @@ Raison: <input type="text" name="description" size="42"/> (optionnel)
|
|||
return "\n".join(H)
|
||||
|
||||
|
||||
def doAnnuleAbsence(
|
||||
datedebut, datefin, demijournee, etudid=False, REQUEST=None
|
||||
): # etudid implied
|
||||
def doAnnuleAbsence(datedebut, datefin, demijournee, etudid=False): # etudid implied
|
||||
"""Annulation des absences pour une demi journée"""
|
||||
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
||||
etudid = etud["etudid"]
|
||||
|
@ -462,7 +452,7 @@ autre absence pour <b>%(nomprenom)s</b></a></li>
|
|||
return "\n".join(H)
|
||||
|
||||
|
||||
def AnnuleAbsenceEtud(REQUEST=None): # etudid implied
|
||||
def AnnuleAbsenceEtud(): # etudid implied
|
||||
"""Formulaire individuel simple d'annulation d'une absence"""
|
||||
# brute-force portage from very old dtml code ...
|
||||
etud = sco_etud.get_etud_info(filled=True)[0]
|
||||
|
@ -482,7 +472,6 @@ def AnnuleAbsenceEtud(REQUEST=None): # etudid implied
|
|||
sco_photos.etud_photo_html(
|
||||
etudid=etudid,
|
||||
title="fiche de " + etud["nomprenom"],
|
||||
REQUEST=REQUEST,
|
||||
),
|
||||
"""</a></td></tr></table>""",
|
||||
"""<p>A n'utiliser que suite à une erreur de saisie ou lorsqu'il s'avère que l'étudiant était en fait présent. </p>
|
||||
|
@ -548,7 +537,7 @@ def AnnuleAbsenceEtud(REQUEST=None): # etudid implied
|
|||
return "\n".join(H)
|
||||
|
||||
|
||||
def doAnnuleJustif(datedebut0, datefin0, demijournee, REQUEST=None): # etudid implied
|
||||
def doAnnuleJustif(datedebut0, datefin0, demijournee): # etudid implied
|
||||
"""Annulation d'une justification"""
|
||||
etud = sco_etud.get_etud_info(filled=True)[0]
|
||||
etudid = etud["etudid"]
|
||||
|
@ -558,11 +547,11 @@ def doAnnuleJustif(datedebut0, datefin0, demijournee, REQUEST=None): # etudid i
|
|||
for jour in dates:
|
||||
# Attention: supprime matin et après-midi
|
||||
if demijournee == 2:
|
||||
sco_abs.annule_justif(etudid, jour, False, REQUEST=REQUEST)
|
||||
sco_abs.annule_justif(etudid, jour, True, REQUEST=REQUEST)
|
||||
sco_abs.annule_justif(etudid, jour, False)
|
||||
sco_abs.annule_justif(etudid, jour, True)
|
||||
nbadded += 2
|
||||
else:
|
||||
sco_abs.annule_justif(etudid, jour, demijournee, REQUEST=REQUEST)
|
||||
sco_abs.annule_justif(etudid, jour, demijournee)
|
||||
nbadded += 1
|
||||
#
|
||||
H = [
|
||||
|
@ -716,7 +705,6 @@ def formChoixSemestreGroupe(all=False):
|
|||
def CalAbs(etudid, sco_year=None):
|
||||
"""Calendrier des absences d'un etudiant"""
|
||||
# crude portage from 1999 DTML
|
||||
REQUEST = None # XXX
|
||||
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
||||
etudid = etud["etudid"]
|
||||
anneescolaire = int(scu.AnneeScolaire(sco_year))
|
||||
|
@ -766,7 +754,6 @@ def CalAbs(etudid, sco_year=None):
|
|||
sco_photos.etud_photo_html(
|
||||
etudid=etudid,
|
||||
title="fiche de " + etud["nomprenom"],
|
||||
REQUEST=REQUEST,
|
||||
),
|
||||
),
|
||||
CalHTML,
|
||||
|
@ -786,12 +773,12 @@ def CalAbs(etudid, sco_year=None):
|
|||
|
||||
|
||||
def ListeAbsEtud(
|
||||
etudid,
|
||||
etudid=None,
|
||||
code_nip=None,
|
||||
with_evals=True,
|
||||
format="html",
|
||||
absjust_only=0,
|
||||
sco_year=None,
|
||||
REQUEST=None,
|
||||
):
|
||||
"""Liste des absences d'un étudiant sur l'année en cours
|
||||
En format 'html': page avec deux tableaux (non justifiées et justifiées).
|
||||
|
@ -804,18 +791,23 @@ def ListeAbsEtud(
|
|||
absjust_only: si vrai, renvoie table absences justifiées
|
||||
sco_year: année scolaire à utiliser. Si non spécifier, utilie l'année en cours. e.g. "2005"
|
||||
"""
|
||||
absjust_only = int(absjust_only) # si vrai, table absjust seule (export xls ou pdf)
|
||||
# si absjust_only, table absjust seule (export xls ou pdf)
|
||||
absjust_only = ndb.bool_or_str(absjust_only)
|
||||
datedebut = "%s-08-01" % scu.AnneeScolaire(sco_year=sco_year)
|
||||
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
|
||||
etudid = etudid or False
|
||||
etuds = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=True)
|
||||
if not etuds:
|
||||
log(f"ListeAbsEtud: no etuds with etudid={etudid} or nip={code_nip}")
|
||||
abort(404)
|
||||
etud = etuds[0]
|
||||
etudid = etud["etudid"]
|
||||
# Liste des absences et titres colonnes tables:
|
||||
titles, columns_ids, absnonjust, absjust = _TablesAbsEtud(
|
||||
titles, columns_ids, absnonjust, absjust = _tables_abs_etud(
|
||||
etudid, datedebut, with_evals=with_evals, format=format
|
||||
)
|
||||
if REQUEST:
|
||||
base_url_nj = "%s?etudid=%s&absjust_only=0" % (REQUEST.URL0, etudid)
|
||||
base_url_j = "%s?etudid=%s&absjust_only=1" % (REQUEST.URL0, etudid)
|
||||
if request.base_url:
|
||||
base_url_nj = "%s?etudid=%s&absjust_only=0" % (request.base_url, etudid)
|
||||
base_url_j = "%s?etudid=%s&absjust_only=1" % (request.base_url, etudid)
|
||||
else:
|
||||
base_url_nj = base_url_j = ""
|
||||
tab_absnonjust = GenTable(
|
||||
|
@ -844,9 +836,9 @@ def ListeAbsEtud(
|
|||
# Formats non HTML et demande d'une seule table:
|
||||
if format != "html" and format != "text":
|
||||
if absjust_only == 1:
|
||||
return tab_absjust.make_page(format=format, REQUEST=REQUEST)
|
||||
return tab_absjust.make_page(format=format)
|
||||
else:
|
||||
return tab_absnonjust.make_page(format=format, REQUEST=REQUEST)
|
||||
return tab_absnonjust.make_page(format=format)
|
||||
|
||||
if format == "html":
|
||||
# Mise en forme HTML:
|
||||
|
@ -896,13 +888,12 @@ def ListeAbsEtud(
|
|||
raise ValueError("Invalid format !")
|
||||
|
||||
|
||||
def _TablesAbsEtud(
|
||||
def _tables_abs_etud(
|
||||
etudid,
|
||||
datedebut,
|
||||
with_evals=True,
|
||||
format="html",
|
||||
absjust_only=0,
|
||||
REQUEST=None,
|
||||
):
|
||||
"""Tables des absences justifiees et non justifiees d'un étudiant
|
||||
sur l'année en cours
|
||||
|
@ -928,11 +919,11 @@ def _TablesAbsEtud(
|
|||
cursor.execute(
|
||||
"""SELECT mi.moduleimpl_id
|
||||
FROM absences abs, notes_moduleimpl_inscription mi, notes_moduleimpl m
|
||||
WHERE abs.matin = %(matin)s
|
||||
and abs.jour = %(jour)s
|
||||
and abs.etudid = %(etudid)s
|
||||
and abs.moduleimpl_id = mi.moduleimpl_id
|
||||
and mi.moduleimpl_id = m.id
|
||||
WHERE abs.matin = %(matin)s
|
||||
and abs.jour = %(jour)s
|
||||
and abs.etudid = %(etudid)s
|
||||
and abs.moduleimpl_id = mi.moduleimpl_id
|
||||
and mi.moduleimpl_id = m.id
|
||||
and mi.etudid = %(etudid)s
|
||||
""",
|
||||
{
|
||||
|
@ -954,13 +945,14 @@ def _TablesAbsEtud(
|
|||
return ""
|
||||
ex = []
|
||||
for ev in a["evals"]:
|
||||
mod = sco_moduleimpl.do_moduleimpl_withmodule_list(
|
||||
mod = sco_moduleimpl.moduleimpl_withmodule_list(
|
||||
moduleimpl_id=ev["moduleimpl_id"]
|
||||
)[0]
|
||||
if format == "html":
|
||||
ex.append(
|
||||
'<a href="Notes/moduleimpl_status?moduleimpl_id=%s">%s</a>'
|
||||
% (mod["moduleimpl_id"], mod["module"]["code"])
|
||||
f"""<a href="{url_for('notes.moduleimpl_status',
|
||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"])}
|
||||
">{mod["module"]["code"]}</a>"""
|
||||
)
|
||||
else:
|
||||
ex.append(mod["module"]["code"])
|
||||
|
@ -971,13 +963,14 @@ def _TablesAbsEtud(
|
|||
def descr_abs(a):
|
||||
ex = []
|
||||
for ev in a.get("absent", []):
|
||||
mod = sco_moduleimpl.do_moduleimpl_withmodule_list(
|
||||
mod = sco_moduleimpl.moduleimpl_withmodule_list(
|
||||
moduleimpl_id=ev["moduleimpl_id"]
|
||||
)[0]
|
||||
if format == "html":
|
||||
ex.append(
|
||||
'<a href="Notes/moduleimpl_status?moduleimpl_id=%s">%s</a>'
|
||||
% (mod["moduleimpl_id"], mod["module"]["code"])
|
||||
f"""<a href="{url_for('notes.moduleimpl_status',
|
||||
scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"])}
|
||||
">{mod["module"]["code"]}</a>"""
|
||||
)
|
||||
else:
|
||||
ex.append(mod["module"]["code"])
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -108,13 +108,14 @@ def apo_compare_csv(A_file, B_file, autodetect=True):
|
|||
|
||||
def _load_apo_data(csvfile, autodetect=True):
|
||||
"Read data from request variable and build ApoData"
|
||||
data = csvfile.read()
|
||||
data_b = csvfile.read()
|
||||
if autodetect:
|
||||
data, message = sco_apogee_csv.fix_data_encoding(data)
|
||||
data_b, message = sco_apogee_csv.fix_data_encoding(data_b)
|
||||
if message:
|
||||
log("apo_compare_csv: %s" % message)
|
||||
if not data:
|
||||
if not data_b:
|
||||
raise ScoValueError("apo_compare_csv: no data")
|
||||
data = data_b.decode(sco_apogee_csv.APO_INPUT_ENCODING)
|
||||
apo_data = sco_apogee_csv.ApoData(data, orig_filename=csvfile.filename)
|
||||
return apo_data
|
||||
|
||||
|
@ -256,8 +257,8 @@ def apo_table_compare_etud_results(A, B):
|
|||
"prenom": "Prénom",
|
||||
"elt_code": "Element",
|
||||
"type_res": "Type",
|
||||
"val_A": "A: %s" % A.orig_filename or "",
|
||||
"val_B": "B: %s" % B.orig_filename or "",
|
||||
"val_A": "A: %s" % (A.orig_filename or ""),
|
||||
"val_B": "B: %s" % (B.orig_filename or ""),
|
||||
},
|
||||
columns_ids=("nip", "nom", "prenom", "elt_code", "type_res", "val_A", "val_B"),
|
||||
html_class="table_leftalign",
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -98,7 +98,7 @@ from chardet import detect as chardet_detect
|
|||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
from app.scodoc.sco_exceptions import ScoValueError, FormatError
|
||||
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||
from app.scodoc.sco_codes_parcours import code_semestre_validant
|
||||
|
@ -173,8 +173,10 @@ def guess_data_encoding(text, threshold=0.6):
|
|||
|
||||
|
||||
def fix_data_encoding(
|
||||
text, default_source_encoding=APO_INPUT_ENCODING, dest_encoding=APO_INPUT_ENCODING
|
||||
):
|
||||
text: bytes,
|
||||
default_source_encoding=APO_INPUT_ENCODING,
|
||||
dest_encoding=APO_INPUT_ENCODING,
|
||||
) -> bytes:
|
||||
"""Try to ensure that text is using dest_encoding
|
||||
returns converted text, and a message describing the conversion.
|
||||
"""
|
||||
|
@ -200,7 +202,7 @@ def fix_data_encoding(
|
|||
|
||||
|
||||
class StringIOFileLineWrapper(object):
|
||||
def __init__(self, data):
|
||||
def __init__(self, data: str):
|
||||
self.f = io.StringIO(data)
|
||||
self.lineno = 0
|
||||
|
||||
|
@ -655,7 +657,7 @@ class ApoEtud(dict):
|
|||
class ApoData(object):
|
||||
def __init__(
|
||||
self,
|
||||
data,
|
||||
data: str,
|
||||
periode=None,
|
||||
export_res_etape=True,
|
||||
export_res_sem=True,
|
||||
|
@ -681,7 +683,7 @@ class ApoData(object):
|
|||
self.periode = periode #
|
||||
try:
|
||||
self.read_csv(data)
|
||||
except FormatError as e:
|
||||
except ScoFormatError as e:
|
||||
# essaie de retrouver le nom du fichier pour enrichir le message d'erreur
|
||||
filename = ""
|
||||
if self.orig_filename is None:
|
||||
|
@ -689,11 +691,11 @@ class ApoData(object):
|
|||
filename = self.titles.get("apoC_Fichier_Exp", filename)
|
||||
else:
|
||||
filename = self.orig_filename
|
||||
raise FormatError(
|
||||
raise ScoFormatError(
|
||||
"<h3>Erreur lecture du fichier Apogée <tt>%s</tt></h3><p>" % filename
|
||||
+ e.args[0]
|
||||
+ "</p>"
|
||||
)
|
||||
) from e
|
||||
self.etape_apogee = self.get_etape_apogee() # 'V1RT'
|
||||
self.vdi_apogee = self.get_vdi_apogee() # '111'
|
||||
self.etape = ApoEtapeVDI(etape=self.etape_apogee, vdi=self.vdi_apogee)
|
||||
|
@ -759,16 +761,18 @@ class ApoData(object):
|
|||
|
||||
def read_csv(self, data: str):
|
||||
if not data:
|
||||
raise FormatError("Fichier Apogée vide !")
|
||||
|
||||
raise ScoFormatError("Fichier Apogée vide !")
|
||||
f = StringIOFileLineWrapper(data) # pour traiter comme un fichier
|
||||
# check that we are at the begining of Apogee CSV
|
||||
line = f.readline().strip()
|
||||
if line != "XX-APO_TITRES-XX":
|
||||
raise FormatError("format incorrect: pas de XX-APO_TITRES-XX")
|
||||
raise ScoFormatError("format incorrect: pas de XX-APO_TITRES-XX")
|
||||
|
||||
# 1-- En-tête: du début jusqu'à la balise XX-APO_VALEURS-XX
|
||||
idx = data.index("XX-APO_VALEURS-XX")
|
||||
try:
|
||||
idx = data.index("XX-APO_VALEURS-XX")
|
||||
except ValueError as exc:
|
||||
raise ScoFormatError("format incorrect: pas de XX-APO_VALEURS-XX") from exc
|
||||
self.header = data[:idx]
|
||||
|
||||
# 2-- Titres:
|
||||
|
@ -779,13 +783,13 @@ class ApoData(object):
|
|||
# 3-- La section XX-APO_TYP_RES-XX est ignorée:
|
||||
line = f.readline().strip()
|
||||
if line != "XX-APO_TYP_RES-XX":
|
||||
raise FormatError("format incorrect: pas de XX-APO_TYP_RES-XX")
|
||||
raise ScoFormatError("format incorrect: pas de XX-APO_TYP_RES-XX")
|
||||
_apo_skip_section(f)
|
||||
|
||||
# 4-- Définition de colonnes: (on y trouve aussi l'étape)
|
||||
line = f.readline().strip()
|
||||
if line != "XX-APO_COLONNES-XX":
|
||||
raise FormatError("format incorrect: pas de XX-APO_COLONNES-XX")
|
||||
raise ScoFormatError("format incorrect: pas de XX-APO_COLONNES-XX")
|
||||
self.cols = _apo_read_cols(f)
|
||||
self.apo_elts = self._group_elt_cols(self.cols)
|
||||
|
||||
|
@ -794,7 +798,7 @@ class ApoData(object):
|
|||
while True: # skip
|
||||
line = f.readline()
|
||||
if not line:
|
||||
raise FormatError("format incorrect: pas de XX-APO_VALEURS-XX")
|
||||
raise ScoFormatError("format incorrect: pas de XX-APO_VALEURS-XX")
|
||||
if line.strip() == "XX-APO_VALEURS-XX":
|
||||
break
|
||||
self.column_titles = f.readline()
|
||||
|
@ -885,7 +889,7 @@ class ApoData(object):
|
|||
"""
|
||||
m = re.match("[12][0-9]{3}", self.titles["apoC_annee"])
|
||||
if not m:
|
||||
raise FormatError(
|
||||
raise ScoFormatError(
|
||||
'Annee scolaire (apoC_annee) invalide: "%s"' % self.titles["apoC_annee"]
|
||||
)
|
||||
return int(m.group(0))
|
||||
|
@ -943,7 +947,7 @@ class ApoData(object):
|
|||
log("Fichier Apogee invalide:")
|
||||
log("Colonnes declarees: %s" % declared)
|
||||
log("Colonnes presentes: %s" % present)
|
||||
raise FormatError(
|
||||
raise ScoFormatError(
|
||||
"""Fichier Apogee invalide<br/>Colonnes declarees: <tt>%s</tt>
|
||||
<br/>Colonnes presentes: <tt>%s</tt>"""
|
||||
% (declared, present)
|
||||
|
@ -1032,7 +1036,7 @@ def _apo_read_cols(f):
|
|||
line = f.readline().strip(" " + APO_NEWLINE)
|
||||
fs = line.split(APO_SEP)
|
||||
if fs[0] != "apoL_a01_code":
|
||||
raise FormatError("invalid line: %s (expecting apoL_a01_code)" % line)
|
||||
raise ScoFormatError("invalid line: %s (expecting apoL_a01_code)" % line)
|
||||
col_keys = fs
|
||||
|
||||
while True: # skip premiere partie (apoL_a02_nom, ...)
|
||||
|
@ -1052,14 +1056,14 @@ def _apo_read_cols(f):
|
|||
# sanity check
|
||||
col_id = fs[0] # apoL_c0001, ...
|
||||
if col_id in cols:
|
||||
raise FormatError("duplicate column definition: %s" % col_id)
|
||||
raise ScoFormatError("duplicate column definition: %s" % col_id)
|
||||
m = re.match(r"^apoL_c([0-9]{4})$", col_id)
|
||||
if not m:
|
||||
raise FormatError(
|
||||
raise ScoFormatError(
|
||||
"invalid column id: %s (expecting apoL_c%04d)" % (line, col_id)
|
||||
)
|
||||
if int(m.group(1)) != i:
|
||||
raise FormatError("invalid column id: %s for index %s" % (col_id, i))
|
||||
raise ScoFormatError("invalid column id: %s for index %s" % (col_id, i))
|
||||
|
||||
cols[col_id] = DictCol(list(zip(col_keys, fs)))
|
||||
cols[col_id].lineno = f.lineno # for debuging purpose
|
||||
|
@ -1083,14 +1087,14 @@ def _apo_read_TITRES(f):
|
|||
else:
|
||||
log("Error read CSV: \nline=%s\nfields=%s" % (line, fields))
|
||||
log(dir(f))
|
||||
raise FormatError(
|
||||
raise ScoFormatError(
|
||||
"Fichier Apogee incorrect (section titres, %d champs au lieu de 2)"
|
||||
% len(fields)
|
||||
)
|
||||
d[k] = v
|
||||
#
|
||||
if not d.get("apoC_Fichier_Exp", None):
|
||||
raise FormatError("Fichier Apogee incorrect: pas de titre apoC_Fichier_Exp")
|
||||
raise ScoFormatError("Fichier Apogee incorrect: pas de titre apoC_Fichier_Exp")
|
||||
# keep only basename: may be a windows or unix pathname
|
||||
s = d["apoC_Fichier_Exp"].split("/")[-1]
|
||||
s = s.split("\\")[-1] # for DOS paths, eg C:\TEMP\VL4RT_V3ASR.TXT
|
||||
|
@ -1178,7 +1182,7 @@ def nar_etuds_table(apo_data, NAR_Etuds):
|
|||
|
||||
|
||||
def export_csv_to_apogee(
|
||||
apo_csv_data,
|
||||
apo_csv_data: str,
|
||||
periode=None,
|
||||
dest_zip=None,
|
||||
export_res_etape=True,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -29,37 +29,41 @@
|
|||
|
||||
|
||||
Archives are plain files, stored in
|
||||
<SCODOC_VAR_DIR>/archives/<deptid>
|
||||
(where <SCODOC_VAR_DIR> is usually /opt/scodoc-data, and <deptid> a departement id)
|
||||
<SCODOC_VAR_DIR>/archives/<dept_id>
|
||||
(where <SCODOC_VAR_DIR> is usually /opt/scodoc-data, and <dept_id> a departement id (int))
|
||||
|
||||
Les PV de jurys et documents associés sont stockées dans un sous-repertoire de la forme
|
||||
<archivedir>/<dept>/<formsemestre_id>/<YYYY-MM-DD-HH-MM-SS>
|
||||
(formsemestre_id est ici FormSemestre.scodoc7_id ou à défaut FormSemestre.id)
|
||||
(formsemestre_id est ici FormSemestre.id)
|
||||
|
||||
Les documents liés à l'étudiant sont dans
|
||||
<archivedir>/docetuds/<dept>/<etudid>/<YYYY-MM-DD-HH-MM-SS>
|
||||
(etudid est ici soit Identite.scodoc7id, soit à défaut Identite.id)
|
||||
<archivedir>/docetuds/<dept_id>/<etudid>/<YYYY-MM-DD-HH-MM-SS>
|
||||
(etudid est ici Identite.id)
|
||||
|
||||
Les maquettes Apogée pour l'export des notes sont dans
|
||||
<archivedir>/apo_csv/<dept>/<annee_scolaire>-<sem_id>/<YYYY-MM-DD-HH-MM-SS>/<code_etape>.csv
|
||||
<archivedir>/apo_csv/<dept_id>/<annee_scolaire>-<sem_id>/<YYYY-MM-DD-HH-MM-SS>/<code_etape>.csv
|
||||
|
||||
Un répertoire d'archive contient des fichiers quelconques, et un fichier texte nommé _description.txt
|
||||
qui est une description (humaine, format libre) de l'archive.
|
||||
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import chardet
|
||||
import datetime
|
||||
import glob
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import glob
|
||||
import time
|
||||
|
||||
import flask
|
||||
from flask import g
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from config import Config
|
||||
from app import log
|
||||
from app.models import Departement
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
from app.scodoc.sco_exceptions import (
|
||||
AccessDenied,
|
||||
|
@ -108,7 +112,8 @@ class BaseArchiver(object):
|
|||
If directory does not yet exist, create it.
|
||||
"""
|
||||
self.initialize()
|
||||
dept_dir = os.path.join(self.root, g.scodoc_dept)
|
||||
dept = Departement.query.filter_by(acronym=g.scodoc_dept).first()
|
||||
dept_dir = os.path.join(self.root, str(dept.id))
|
||||
try:
|
||||
scu.GSL.acquire()
|
||||
if not os.path.isdir(dept_dir):
|
||||
|
@ -127,7 +132,8 @@ class BaseArchiver(object):
|
|||
:return: list of archive oids
|
||||
"""
|
||||
self.initialize()
|
||||
base = os.path.join(self.root, g.scodoc_dept) + os.path.sep
|
||||
dept = Departement.query.filter_by(acronym=g.scodoc_dept).first()
|
||||
base = os.path.join(self.root, str(dept.id)) + os.path.sep
|
||||
dirs = glob.glob(base + "*")
|
||||
return [os.path.split(x)[1] for x in dirs]
|
||||
|
||||
|
@ -198,7 +204,17 @@ class BaseArchiver(object):
|
|||
def get_archive_description(self, archive_id):
|
||||
"""Return description of archive"""
|
||||
self.initialize()
|
||||
return open(os.path.join(archive_id, "_description.txt")).read()
|
||||
filename = os.path.join(archive_id, "_description.txt")
|
||||
try:
|
||||
with open(filename) as f:
|
||||
descr = f.read()
|
||||
except UnicodeDecodeError:
|
||||
# some (old) files may have saved under exotic encodings
|
||||
with open(filename, "rb") as f:
|
||||
data = f.read()
|
||||
descr = data.decode(chardet.detect(data)["encoding"])
|
||||
|
||||
return descr
|
||||
|
||||
def create_obj_archive(self, oid: int, description: str):
|
||||
"""Creates a new archive for this object and returns its id."""
|
||||
|
@ -227,9 +243,8 @@ class BaseArchiver(object):
|
|||
try:
|
||||
scu.GSL.acquire()
|
||||
fname = os.path.join(archive_id, filename)
|
||||
f = open(fname, "wb")
|
||||
f.write(data)
|
||||
f.close()
|
||||
with open(fname, "wb") as f:
|
||||
f.write(data)
|
||||
finally:
|
||||
scu.GSL.release()
|
||||
return filename
|
||||
|
@ -242,33 +257,19 @@ class BaseArchiver(object):
|
|||
raise ValueError("invalid filename")
|
||||
fname = os.path.join(archive_id, filename)
|
||||
log("reading archive file %s" % fname)
|
||||
return open(fname, "rb").read()
|
||||
with open(fname, "rb") as f:
|
||||
data = f.read()
|
||||
return data
|
||||
|
||||
def get_archived_file(self, REQUEST, oid, archive_name, filename):
|
||||
def get_archived_file(self, oid, archive_name, filename):
|
||||
"""Recupere donnees du fichier indiqué et envoie au client"""
|
||||
# XXX très incomplet: devrait inférer et assigner un type MIME
|
||||
archive_id = self.get_id_from_name(oid, archive_name)
|
||||
data = self.get(archive_id, filename)
|
||||
ext = os.path.splitext(filename.lower())[1]
|
||||
if ext == ".html" or ext == ".htm":
|
||||
return data
|
||||
elif ext == ".xml":
|
||||
REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE)
|
||||
return data
|
||||
elif ext == ".xls":
|
||||
return sco_excel.send_excel_file(
|
||||
REQUEST, data, filename, mime=scu.XLS_MIMETYPE
|
||||
)
|
||||
elif ext == ".xlsx":
|
||||
return sco_excel.send_excel_file(
|
||||
REQUEST, data, filename, mime=scu.XLSX_MIMETYPE
|
||||
)
|
||||
elif ext == ".csv":
|
||||
return scu.sendCSVFile(REQUEST, data, filename)
|
||||
elif ext == ".pdf":
|
||||
return scu.sendPDFFile(REQUEST, data, filename)
|
||||
REQUEST.RESPONSE.setHeader("content-type", "application/octet-stream")
|
||||
return data # should set mimetype for known files like images
|
||||
mime = mimetypes.guess_type(filename)[0]
|
||||
if mime is None:
|
||||
mime = "application/octet-stream"
|
||||
|
||||
return scu.send_file(data, filename, mime=mime)
|
||||
|
||||
|
||||
class SemsArchiver(BaseArchiver):
|
||||
|
@ -283,7 +284,6 @@ PVArchive = SemsArchiver()
|
|||
|
||||
|
||||
def do_formsemestre_archive(
|
||||
REQUEST,
|
||||
formsemestre_id,
|
||||
group_ids=[], # si indiqué, ne prend que ces groupes
|
||||
description="",
|
||||
|
@ -305,7 +305,7 @@ def do_formsemestre_archive(
|
|||
from app.scodoc.sco_recapcomplet import make_formsemestre_recapcomplet
|
||||
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
sem_archive_id = sem["scodoc7_id"] or formsemestre_id
|
||||
sem_archive_id = formsemestre_id
|
||||
archive_id = PVArchive.create_obj_archive(sem_archive_id, description)
|
||||
date = PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M")
|
||||
|
||||
|
@ -351,14 +351,12 @@ def do_formsemestre_archive(
|
|||
data = data.encode(scu.SCO_ENCODING)
|
||||
PVArchive.store(archive_id, "Bulletins.xml", data)
|
||||
# Decisions de jury, en XLS
|
||||
data = sco_pvjury.formsemestre_pvjury(
|
||||
formsemestre_id, format="xls", REQUEST=REQUEST, publish=False
|
||||
)
|
||||
data = sco_pvjury.formsemestre_pvjury(formsemestre_id, format="xls", publish=False)
|
||||
if data:
|
||||
PVArchive.store(archive_id, "Decisions_Jury" + scu.XLSX_SUFFIX, data)
|
||||
# Classeur bulletins (PDF)
|
||||
data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
|
||||
formsemestre_id, REQUEST, version=bulVersion
|
||||
formsemestre_id, version=bulVersion
|
||||
)
|
||||
if data:
|
||||
PVArchive.store(archive_id, "Bulletins.pdf", data)
|
||||
|
@ -389,14 +387,12 @@ def do_formsemestre_archive(
|
|||
PVArchive.store(archive_id, "PV_Jury%s.pdf" % groups_filename, data)
|
||||
|
||||
|
||||
def formsemestre_archive(REQUEST, formsemestre_id, group_ids=[]):
|
||||
def formsemestre_archive(formsemestre_id, group_ids=[]):
|
||||
"""Make and store new archive for this formsemestre.
|
||||
(all students or only selected groups)
|
||||
"""
|
||||
if not sco_permissions_check.can_edit_pv(formsemestre_id):
|
||||
raise AccessDenied(
|
||||
"opération non autorisée pour %s" % str(REQUEST.AUTHENTICATED_USER)
|
||||
)
|
||||
raise AccessDenied("opération non autorisée pour %s" % str(current_user))
|
||||
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
if not group_ids:
|
||||
|
@ -408,7 +404,6 @@ def formsemestre_archive(REQUEST, formsemestre_id, group_ids=[]):
|
|||
|
||||
H = [
|
||||
html_sco_header.html_sem_header(
|
||||
REQUEST,
|
||||
"Archiver les PV et résultats du semestre",
|
||||
sem=sem,
|
||||
javascripts=sco_groups_view.JAVASCRIPTS,
|
||||
|
@ -469,8 +464,8 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
|
|||
)
|
||||
|
||||
tf = TrivialFormulator(
|
||||
REQUEST.URL0,
|
||||
REQUEST.form,
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
descr,
|
||||
cancelbutton="Annuler",
|
||||
method="POST",
|
||||
|
@ -492,7 +487,6 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
|
|||
else:
|
||||
tf[2]["anonymous"] = False
|
||||
do_formsemestre_archive(
|
||||
REQUEST,
|
||||
formsemestre_id,
|
||||
group_ids=group_ids,
|
||||
description=tf[2]["description"],
|
||||
|
@ -516,10 +510,10 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
|
|||
)
|
||||
|
||||
|
||||
def formsemestre_list_archives(REQUEST, formsemestre_id):
|
||||
def formsemestre_list_archives(formsemestre_id):
|
||||
"""Page listing archives"""
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
sem_archive_id = sem["scodoc7_id"] or formsemestre_id
|
||||
sem_archive_id = formsemestre_id
|
||||
L = []
|
||||
for archive_id in PVArchive.list_obj_archives(sem_archive_id):
|
||||
a = {
|
||||
|
@ -530,7 +524,7 @@ def formsemestre_list_archives(REQUEST, formsemestre_id):
|
|||
}
|
||||
L.append(a)
|
||||
|
||||
H = [html_sco_header.html_sem_header(REQUEST, "Archive des PV et résultats ", sem)]
|
||||
H = [html_sco_header.html_sem_header("Archive des PV et résultats ", sem)]
|
||||
if not L:
|
||||
H.append("<p>aucune archive enregistrée</p>")
|
||||
else:
|
||||
|
@ -559,23 +553,19 @@ def formsemestre_list_archives(REQUEST, formsemestre_id):
|
|||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
|
||||
def formsemestre_get_archived_file(REQUEST, formsemestre_id, archive_name, filename):
|
||||
def formsemestre_get_archived_file(formsemestre_id, archive_name, filename):
|
||||
"""Send file to client."""
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
sem_archive_id = sem["scodoc7_id"] or formsemestre_id
|
||||
return PVArchive.get_archived_file(REQUEST, sem_archive_id, archive_name, filename)
|
||||
sem_archive_id = formsemestre_id
|
||||
return PVArchive.get_archived_file(sem_archive_id, archive_name, filename)
|
||||
|
||||
|
||||
def formsemestre_delete_archive(
|
||||
REQUEST, formsemestre_id, archive_name, dialog_confirmed=False
|
||||
):
|
||||
def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed=False):
|
||||
"""Delete an archive"""
|
||||
if not sco_permissions_check.can_edit_pv(formsemestre_id):
|
||||
raise AccessDenied(
|
||||
"opération non autorisée pour %s" % str(REQUEST.AUTHENTICATED_USER)
|
||||
)
|
||||
raise AccessDenied("opération non autorisée pour %s" % str(current_user))
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
sem_archive_id = sem["scodoc7_id"] or formsemestre_id
|
||||
sem_archive_id = formsemestre_id
|
||||
archive_id = PVArchive.get_id_from_name(sem_archive_id, archive_name)
|
||||
|
||||
dest_url = "formsemestre_list_archives?formsemestre_id=%s" % (formsemestre_id)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -30,7 +30,9 @@
|
|||
les dossiers d'admission et autres pièces utiles.
|
||||
"""
|
||||
import flask
|
||||
from flask import url_for, g
|
||||
from flask import url_for, render_template
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_import_etuds
|
||||
|
@ -58,14 +60,14 @@ def can_edit_etud_archive(authuser):
|
|||
return authuser.has_permission(Permission.ScoEtudAddAnnotations)
|
||||
|
||||
|
||||
def etud_list_archives_html(REQUEST, etudid):
|
||||
def etud_list_archives_html(etudid):
|
||||
"""HTML snippet listing archives"""
|
||||
can_edit = can_edit_etud_archive(REQUEST.AUTHENTICATED_USER)
|
||||
can_edit = can_edit_etud_archive(current_user)
|
||||
etuds = sco_etud.get_etud_info(etudid=etudid)
|
||||
if not etuds:
|
||||
raise ScoValueError("étudiant inexistant")
|
||||
etud = etuds[0]
|
||||
etud_archive_id = etud["scodoc7_id"] or etudid
|
||||
etud_archive_id = etudid
|
||||
L = []
|
||||
for archive_id in EtudsArchive.list_obj_archives(etud_archive_id):
|
||||
a = {
|
||||
|
@ -118,7 +120,7 @@ def add_archives_info_to_etud_list(etuds):
|
|||
"""
|
||||
for etud in etuds:
|
||||
l = []
|
||||
etud_archive_id = etud["scodoc7_id"] or etud["etudid"]
|
||||
etud_archive_id = etud["etudid"]
|
||||
for archive_id in EtudsArchive.list_obj_archives(etud_archive_id):
|
||||
l.append(
|
||||
"%s (%s)"
|
||||
|
@ -130,13 +132,11 @@ def add_archives_info_to_etud_list(etuds):
|
|||
etud["etudarchive"] = ", ".join(l)
|
||||
|
||||
|
||||
def etud_upload_file_form(REQUEST, etudid):
|
||||
def etud_upload_file_form(etudid):
|
||||
"""Page with a form to choose and upload a file, with a description."""
|
||||
# check permission
|
||||
if not can_edit_etud_archive(REQUEST.AUTHENTICATED_USER):
|
||||
raise AccessDenied(
|
||||
"opération non autorisée pour %s" % REQUEST.AUTHENTICATED_USER
|
||||
)
|
||||
if not can_edit_etud_archive(current_user):
|
||||
raise AccessDenied("opération non autorisée pour %s" % current_user)
|
||||
etuds = sco_etud.get_etud_info(filled=True)
|
||||
if not etuds:
|
||||
raise ScoValueError("étudiant inexistant")
|
||||
|
@ -153,8 +153,8 @@ def etud_upload_file_form(REQUEST, etudid):
|
|||
% (scu.CONFIG.ETUD_MAX_FILE_SIZE // (1024 * 1024)),
|
||||
]
|
||||
tf = TrivialFormulator(
|
||||
REQUEST.URL0,
|
||||
REQUEST.form,
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
(
|
||||
("etudid", {"default": etudid, "input_type": "hidden"}),
|
||||
("datafile", {"input_type": "file", "title": "Fichier", "size": 30}),
|
||||
|
@ -181,7 +181,7 @@ def etud_upload_file_form(REQUEST, etudid):
|
|||
data = tf[2]["datafile"].read()
|
||||
descr = tf[2]["description"]
|
||||
filename = tf[2]["datafile"].filename
|
||||
etud_archive_id = etud["scodoc7_id"] or etud["etudid"]
|
||||
etud_archive_id = etud["etudid"]
|
||||
_store_etud_file_to_new_archive(
|
||||
etud_archive_id, data, filename, description=descr
|
||||
)
|
||||
|
@ -199,18 +199,16 @@ def _store_etud_file_to_new_archive(etud_archive_id, data, filename, description
|
|||
EtudsArchive.store(archive_id, filename, data)
|
||||
|
||||
|
||||
def etud_delete_archive(REQUEST, etudid, archive_name, dialog_confirmed=False):
|
||||
def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
|
||||
"""Delete an archive"""
|
||||
# check permission
|
||||
if not can_edit_etud_archive(REQUEST.AUTHENTICATED_USER):
|
||||
raise AccessDenied(
|
||||
"opération non autorisée pour %s" % str(REQUEST.AUTHENTICATED_USER)
|
||||
)
|
||||
if not can_edit_etud_archive(current_user):
|
||||
raise AccessDenied("opération non autorisée pour %s" % str(current_user))
|
||||
etuds = sco_etud.get_etud_info(filled=True)
|
||||
if not etuds:
|
||||
raise ScoValueError("étudiant inexistant")
|
||||
etud = etuds[0]
|
||||
etud_archive_id = etud["scodoc7_id"] or etud["etudid"]
|
||||
etud_archive_id = etud["etudid"]
|
||||
archive_id = EtudsArchive.get_id_from_name(etud_archive_id, archive_name)
|
||||
if not dialog_confirmed:
|
||||
return scu.confirm_dialog(
|
||||
|
@ -242,20 +240,18 @@ def etud_delete_archive(REQUEST, etudid, archive_name, dialog_confirmed=False):
|
|||
)
|
||||
|
||||
|
||||
def etud_get_archived_file(REQUEST, etudid, archive_name, filename):
|
||||
def etud_get_archived_file(etudid, archive_name, filename):
|
||||
"""Send file to client."""
|
||||
etuds = sco_etud.get_etud_info(filled=True)
|
||||
etuds = sco_etud.get_etud_info(etudid=etudid, filled=True)
|
||||
if not etuds:
|
||||
raise ScoValueError("étudiant inexistant")
|
||||
etud = etuds[0]
|
||||
etud_archive_id = etud["scodoc7_id"] or etud["etudid"]
|
||||
return EtudsArchive.get_archived_file(
|
||||
REQUEST, etud_archive_id, archive_name, filename
|
||||
)
|
||||
etud_archive_id = etud["etudid"]
|
||||
return EtudsArchive.get_archived_file(etud_archive_id, archive_name, filename)
|
||||
|
||||
|
||||
# --- Upload d'un ensemble de fichiers (pour un groupe d'étudiants)
|
||||
def etudarchive_generate_excel_sample(group_id=None, REQUEST=None):
|
||||
def etudarchive_generate_excel_sample(group_id=None):
|
||||
"""Feuille excel pour import fichiers etudiants (utilisé pour admissions)"""
|
||||
fmt = sco_import_etuds.sco_import_format()
|
||||
data = sco_import_etuds.sco_import_generate_excel_sample(
|
||||
|
@ -271,12 +267,15 @@ def etudarchive_generate_excel_sample(group_id=None, REQUEST=None):
|
|||
],
|
||||
extra_cols=["fichier_a_charger"],
|
||||
)
|
||||
return sco_excel.send_excel_file(
|
||||
REQUEST, data, "ImportFichiersEtudiants" + scu.XLSX_SUFFIX
|
||||
return scu.send_file(
|
||||
data,
|
||||
"ImportFichiersEtudiants",
|
||||
suffix=scu.XLSX_SUFFIX,
|
||||
mime=scu.XLSX_MIMETYPE,
|
||||
)
|
||||
|
||||
|
||||
def etudarchive_import_files_form(group_id, REQUEST=None):
|
||||
def etudarchive_import_files_form(group_id):
|
||||
"""Formulaire pour importation fichiers d'un groupe"""
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
|
@ -310,8 +309,8 @@ def etudarchive_import_files_form(group_id, REQUEST=None):
|
|||
]
|
||||
F = html_sco_header.sco_footer()
|
||||
tf = TrivialFormulator(
|
||||
REQUEST.URL0,
|
||||
REQUEST.form,
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
(
|
||||
("xlsfile", {"title": "Fichier Excel:", "input_type": "file", "size": 40}),
|
||||
("zipfile", {"title": "Fichier zip:", "input_type": "file", "size": 40}),
|
||||
|
@ -330,9 +329,9 @@ def etudarchive_import_files_form(group_id, REQUEST=None):
|
|||
|
||||
if tf[0] == 0:
|
||||
return "\n".join(H) + tf[1] + "</li></ol>" + F
|
||||
elif tf[0] == -1:
|
||||
# retrouve le semestre à partir du groupe:
|
||||
group = sco_groups.get_group(group_id)
|
||||
# retrouve le semestre à partir du groupe:
|
||||
group = sco_groups.get_group(group_id)
|
||||
if tf[0] == -1:
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.formsemestre_status",
|
||||
|
@ -342,21 +341,41 @@ def etudarchive_import_files_form(group_id, REQUEST=None):
|
|||
)
|
||||
else:
|
||||
return etudarchive_import_files(
|
||||
group_id=tf[2]["group_id"],
|
||||
formsemestre_id=group["formsemestre_id"],
|
||||
xlsfile=tf[2]["xlsfile"],
|
||||
zipfile=tf[2]["zipfile"],
|
||||
description=tf[2]["description"],
|
||||
)
|
||||
|
||||
|
||||
def etudarchive_import_files(group_id=None, xlsfile=None, zipfile=None, description=""):
|
||||
def etudarchive_import_files(
|
||||
formsemestre_id=None, xlsfile=None, zipfile=None, description=""
|
||||
):
|
||||
"Importe des fichiers"
|
||||
|
||||
def callback(etud, data, filename):
|
||||
_store_etud_file_to_new_archive(etud["etudid"], data, filename, description)
|
||||
|
||||
filename_title = "fichier_a_charger"
|
||||
page_title = "Téléchargement de fichiers associés aux étudiants"
|
||||
# Utilise la fontion au depart developpee pour les photos
|
||||
r = sco_trombino.zip_excel_import_files(
|
||||
xlsfile, zipfile, callback, filename_title, page_title
|
||||
# Utilise la fontion developpée au depart pour les photos
|
||||
(
|
||||
ignored_zipfiles,
|
||||
unmatched_files,
|
||||
stored_etud_filename,
|
||||
) = sco_trombino.zip_excel_import_files(
|
||||
xlsfile=xlsfile,
|
||||
zipfile=zipfile,
|
||||
callback=callback,
|
||||
filename_title="fichier_a_charger",
|
||||
)
|
||||
return render_template(
|
||||
"scolar/photos_import_files.html",
|
||||
page_title="Téléchargement de fichiers associés aux étudiants",
|
||||
ignored_zipfiles=ignored_zipfiles,
|
||||
unmatched_files=unmatched_files,
|
||||
stored_etud_filename=stored_etud_filename,
|
||||
next_page=url_for(
|
||||
"scolar.groups_view",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
),
|
||||
)
|
||||
return r + html_sco_header.sco_footer()
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -28,6 +28,7 @@
|
|||
"""Génération des bulletins de notes
|
||||
|
||||
"""
|
||||
from app.models import formsemestre
|
||||
import time
|
||||
import pprint
|
||||
import email
|
||||
|
@ -35,20 +36,20 @@ from email.mime.multipart import MIMEMultipart
|
|||
from email.mime.text import MIMEText
|
||||
from email.mime.base import MIMEBase
|
||||
from email.header import Header
|
||||
|
||||
from reportlab.lib.colors import Color
|
||||
import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error
|
||||
import urllib
|
||||
|
||||
from flask import g
|
||||
from flask import g, request
|
||||
from flask import url_for
|
||||
from flask_login import current_user
|
||||
from flask_mail import Message
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_exceptions import AccessDenied
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import htmlutils
|
||||
from app.scodoc import sco_abs
|
||||
|
@ -59,7 +60,7 @@ from app.scodoc import sco_bulletins_xml
|
|||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
|
@ -121,9 +122,7 @@ def make_context_dict(sem, etud):
|
|||
return C
|
||||
|
||||
|
||||
def formsemestre_bulletinetud_dict(
|
||||
formsemestre_id, etudid, version="long", REQUEST=None
|
||||
):
|
||||
def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
||||
"""Collecte informations pour bulletin de notes
|
||||
Retourne un dictionnaire (avec valeur par défaut chaine vide).
|
||||
Le contenu du dictionnaire dépend des options (rangs, ...)
|
||||
|
@ -138,15 +137,13 @@ def formsemestre_bulletinetud_dict(
|
|||
|
||||
prefs = sco_preferences.SemPreferences(formsemestre_id)
|
||||
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes
|
||||
|
||||
if not nt.get_etud_etat(etudid):
|
||||
raise ScoValueError("Etudiant non inscrit à ce semestre")
|
||||
I = scu.DictDefault(defaultvalue="")
|
||||
I["etudid"] = etudid
|
||||
I["formsemestre_id"] = formsemestre_id
|
||||
I["sem"] = nt.sem
|
||||
if REQUEST:
|
||||
I["server_name"] = REQUEST.BASE0
|
||||
else:
|
||||
I["server_name"] = ""
|
||||
I["server_name"] = request.url_root
|
||||
|
||||
# Formation et parcours
|
||||
I["formation"] = sco_formations.formation_list(
|
||||
|
@ -432,7 +429,7 @@ def _ue_mod_bulletin(etudid, formsemestre_id, ue_id, modimpls, nt, version):
|
|||
mod_moy = nt.get_etud_mod_moy(
|
||||
modimpl["moduleimpl_id"], etudid
|
||||
) # peut etre 'NI'
|
||||
is_malus = mod["module"]["module_type"] == scu.MODULE_MALUS
|
||||
is_malus = mod["module"]["module_type"] == ModuleType.MALUS
|
||||
if bul_show_abs_modules:
|
||||
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem)
|
||||
mod_abs = [nbabs, nbabsjust]
|
||||
|
@ -562,7 +559,7 @@ def _ue_mod_bulletin(etudid, formsemestre_id, ue_id, modimpls, nt, version):
|
|||
mod["evaluations_incompletes"] = []
|
||||
if sco_preferences.get_preference("bul_show_all_evals", formsemestre_id):
|
||||
complete_eval_ids = set([e["evaluation_id"] for e in evals])
|
||||
all_evals = sco_evaluations.do_evaluation_list(
|
||||
all_evals = sco_evaluation_db.do_evaluation_list(
|
||||
args={"moduleimpl_id": modimpl["moduleimpl_id"]}
|
||||
)
|
||||
all_evals.reverse() # plus ancienne d'abord
|
||||
|
@ -771,14 +768,16 @@ def formsemestre_bulletinetud(
|
|||
xml_with_decisions=False,
|
||||
force_publishing=False, # force publication meme si semestre non publie sur "portail"
|
||||
prefer_mail_perso=False,
|
||||
REQUEST=None,
|
||||
):
|
||||
"page bulletin de notes"
|
||||
try:
|
||||
etud = sco_etud.get_etud_info(filled=True)[0]
|
||||
etudid = etud["etudid"]
|
||||
except:
|
||||
return scu.log_unknown_etud(REQUEST, format=format)
|
||||
sco_etud.log_unknown_etud()
|
||||
raise ScoValueError("étudiant inconnu")
|
||||
# API, donc erreurs admises en ScoValueError
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
|
||||
|
||||
bulletin = do_formsemestre_bulletinetud(
|
||||
formsemestre_id,
|
||||
|
@ -788,15 +787,15 @@ def formsemestre_bulletinetud(
|
|||
xml_with_decisions=xml_with_decisions,
|
||||
force_publishing=force_publishing,
|
||||
prefer_mail_perso=prefer_mail_perso,
|
||||
REQUEST=REQUEST,
|
||||
)[0]
|
||||
if format not in {"html", "pdfmail"}:
|
||||
return bulletin
|
||||
filename = scu.bul_filename(sem, etud, format)
|
||||
return scu.send_file(bulletin, filename, mime=scu.get_mime_suffix(format)[0])
|
||||
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
H = [
|
||||
_formsemestre_bulletinetud_header_html(
|
||||
etud, etudid, sem, formsemestre_id, format, version, REQUEST
|
||||
etud, etudid, sem, formsemestre_id, format, version
|
||||
),
|
||||
bulletin,
|
||||
]
|
||||
|
@ -854,7 +853,6 @@ def do_formsemestre_bulletinetud(
|
|||
etudid,
|
||||
version="long", # short, long, selectedevals
|
||||
format="html",
|
||||
REQUEST=None,
|
||||
nohtml=False,
|
||||
xml_with_decisions=False, # force decisions dans XML
|
||||
force_publishing=False, # force publication meme si semestre non publie sur "portail"
|
||||
|
@ -862,14 +860,13 @@ def do_formsemestre_bulletinetud(
|
|||
):
|
||||
"""Génère le bulletin au format demandé.
|
||||
Retourne: (bul, filigranne)
|
||||
où bul est au format demandé (html, pdf, pdfmail, pdfpart, xml)
|
||||
où bul est str ou bytes au format demandé (html, pdf, pdfmail, pdfpart, xml, json)
|
||||
et filigranne est un message à placer en "filigranne" (eg "Provisoire").
|
||||
"""
|
||||
if format == "xml":
|
||||
bul = sco_bulletins_xml.make_xml_formsemestre_bulletinetud(
|
||||
formsemestre_id,
|
||||
etudid,
|
||||
REQUEST=REQUEST,
|
||||
xml_with_decisions=xml_with_decisions,
|
||||
force_publishing=force_publishing,
|
||||
version=version,
|
||||
|
@ -881,19 +878,18 @@ def do_formsemestre_bulletinetud(
|
|||
bul = sco_bulletins_json.make_json_formsemestre_bulletinetud(
|
||||
formsemestre_id,
|
||||
etudid,
|
||||
REQUEST=REQUEST,
|
||||
xml_with_decisions=xml_with_decisions,
|
||||
force_publishing=force_publishing,
|
||||
version=version,
|
||||
)
|
||||
return bul, ""
|
||||
|
||||
I = formsemestre_bulletinetud_dict(formsemestre_id, etudid, REQUEST=REQUEST)
|
||||
I = formsemestre_bulletinetud_dict(formsemestre_id, etudid)
|
||||
etud = I["etud"]
|
||||
|
||||
if format == "html":
|
||||
htm, _ = sco_bulletins_generator.make_formsemestre_bulletinetud(
|
||||
I, version=version, format="html", REQUEST=REQUEST
|
||||
I, version=version, format="html"
|
||||
)
|
||||
return htm, I["filigranne"]
|
||||
|
||||
|
@ -903,11 +899,10 @@ def do_formsemestre_bulletinetud(
|
|||
version=version,
|
||||
format="pdf",
|
||||
stand_alone=(format != "pdfpart"),
|
||||
REQUEST=REQUEST,
|
||||
)
|
||||
if format == "pdf":
|
||||
return (
|
||||
scu.sendPDFFile(REQUEST, bul, filename),
|
||||
scu.sendPDFFile(bul, filename),
|
||||
I["filigranne"],
|
||||
) # unused ret. value
|
||||
else:
|
||||
|
@ -923,11 +918,11 @@ def do_formsemestre_bulletinetud(
|
|||
htm = "" # speed up if html version not needed
|
||||
else:
|
||||
htm, _ = sco_bulletins_generator.make_formsemestre_bulletinetud(
|
||||
I, version=version, format="html", REQUEST=REQUEST
|
||||
I, version=version, format="html"
|
||||
)
|
||||
|
||||
pdfdata, filename = sco_bulletins_generator.make_formsemestre_bulletinetud(
|
||||
I, version=version, format="pdf", REQUEST=REQUEST
|
||||
I, version=version, format="pdf"
|
||||
)
|
||||
|
||||
if prefer_mail_perso:
|
||||
|
@ -993,12 +988,11 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr):
|
|||
bcc = copy_addr.strip()
|
||||
else:
|
||||
bcc = ""
|
||||
msg = Message(subject, sender=sender, recipients=recipients, bcc=bcc)
|
||||
msg = Message(subject, sender=sender, recipients=recipients, bcc=[bcc])
|
||||
msg.body = hea
|
||||
|
||||
# Attach pdf
|
||||
msg.attach(filename, scu.PDF_MIMETYPE, pdfdata)
|
||||
|
||||
log("mail bulletin a %s" % recipient_addr)
|
||||
email.send_message(msg)
|
||||
|
||||
|
@ -1010,7 +1004,6 @@ def _formsemestre_bulletinetud_header_html(
|
|||
formsemestre_id=None,
|
||||
format=None,
|
||||
version=None,
|
||||
REQUEST=None,
|
||||
):
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
|
@ -1033,7 +1026,7 @@ def _formsemestre_bulletinetud_header_html(
|
|||
),
|
||||
"""
|
||||
<form name="f" method="GET" action="%s">"""
|
||||
% REQUEST.URL0,
|
||||
% request.base_url,
|
||||
f"""Bulletin <span class="bull_liensemestre"><a href="{
|
||||
url_for("notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
|
@ -1063,14 +1056,20 @@ def _formsemestre_bulletinetud_header_html(
|
|||
H.append("""</select></td>""")
|
||||
# Menu
|
||||
endpoint = "notes.formsemestre_bulletinetud"
|
||||
url = REQUEST.URL0
|
||||
qurl = six.moves.urllib.parse.quote_plus(url + "?" + REQUEST.QUERY_STRING)
|
||||
|
||||
menuBul = [
|
||||
{
|
||||
"title": "Réglages bulletins",
|
||||
"endpoint": "notes.formsemestre_edit_options",
|
||||
"args": {"formsemestre_id": formsemestre_id, "target_url": qurl},
|
||||
"args": {
|
||||
"formsemestre_id": formsemestre_id,
|
||||
# "target_url": url_for(
|
||||
# "notes.formsemestre_bulletinetud",
|
||||
# scodoc_dept=g.scodoc_dept,
|
||||
# formsemestre_id=formsemestre_id,
|
||||
# etudid=etudid,
|
||||
# ),
|
||||
},
|
||||
"enabled": (current_user.id in sem["responsables"])
|
||||
or current_user.has_permission(Permission.ScoImplement),
|
||||
},
|
||||
|
@ -1113,6 +1112,16 @@ def _formsemestre_bulletinetud_header_html(
|
|||
"enabled": etud["emailperso"]
|
||||
and can_send_bulletin_by_mail(formsemestre_id),
|
||||
},
|
||||
{
|
||||
"title": "Version json",
|
||||
"endpoint": endpoint,
|
||||
"args": {
|
||||
"formsemestre_id": formsemestre_id,
|
||||
"etudid": etudid,
|
||||
"version": version,
|
||||
"format": "json",
|
||||
},
|
||||
},
|
||||
{
|
||||
"title": "Version XML",
|
||||
"endpoint": endpoint,
|
||||
|
@ -1188,9 +1197,14 @@ def _formsemestre_bulletinetud_header_html(
|
|||
H.append(
|
||||
'<td> <a href="%s">%s</a></td>'
|
||||
% (
|
||||
url
|
||||
+ "?formsemestre_id=%s&etudid=%s&format=pdf&version=%s"
|
||||
% (formsemestre_id, etudid, version),
|
||||
url_for(
|
||||
"notes.formsemestre_bulletinetud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
etudid=etudid,
|
||||
format="pdf",
|
||||
version=version,
|
||||
),
|
||||
scu.ICON_PDF,
|
||||
)
|
||||
)
|
||||
|
@ -1201,9 +1215,7 @@ def _formsemestre_bulletinetud_header_html(
|
|||
"""
|
||||
% (
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
sco_photos.etud_photo_html(
|
||||
etud, title="fiche de " + etud["nom"], REQUEST=REQUEST
|
||||
),
|
||||
sco_photos.etud_photo_html(etud, title="fiche de " + etud["nom"]),
|
||||
)
|
||||
)
|
||||
H.append(
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -52,6 +52,9 @@ import reportlab
|
|||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak
|
||||
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
|
||||
|
||||
from flask import request
|
||||
from flask_login import current_user
|
||||
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import NoteProcessError
|
||||
from app import log
|
||||
|
@ -148,14 +151,7 @@ class BulletinGenerator(object):
|
|||
def get_filename(self):
|
||||
"""Build a filename to be proposed to the web client"""
|
||||
sem = sco_formsemestre.get_formsemestre(self.infos["formsemestre_id"])
|
||||
dt = time.strftime("%Y-%m-%d")
|
||||
filename = "bul-%s-%s-%s.pdf" % (
|
||||
sem["titre_num"],
|
||||
dt,
|
||||
self.infos["etud"]["nom"],
|
||||
)
|
||||
filename = scu.unescape_html(filename).replace(" ", "_").replace("&", "")
|
||||
return filename
|
||||
return scu.bul_filename(sem, self.infos["etud"], "pdf")
|
||||
|
||||
def generate(self, format="", stand_alone=True):
|
||||
"""Return bulletin in specified format"""
|
||||
|
@ -260,7 +256,6 @@ def make_formsemestre_bulletinetud(
|
|||
version="long", # short, long, selectedevals
|
||||
format="pdf", # html, pdf
|
||||
stand_alone=True,
|
||||
REQUEST=None,
|
||||
):
|
||||
"""Bulletin de notes
|
||||
|
||||
|
@ -286,10 +281,10 @@ def make_formsemestre_bulletinetud(
|
|||
PDFLOCK.acquire()
|
||||
bul_generator = gen_class(
|
||||
infos,
|
||||
authuser=REQUEST.AUTHENTICATED_USER,
|
||||
authuser=current_user,
|
||||
version=version,
|
||||
filigranne=infos["filigranne"],
|
||||
server_name=REQUEST.BASE0,
|
||||
server_name=request.url_root,
|
||||
)
|
||||
if format not in bul_generator.supported_formats:
|
||||
# use standard generator
|
||||
|
@ -301,10 +296,10 @@ def make_formsemestre_bulletinetud(
|
|||
gen_class = bulletin_get_class(bul_class_name)
|
||||
bul_generator = gen_class(
|
||||
infos,
|
||||
authuser=REQUEST.AUTHENTICATED_USER,
|
||||
authuser=current_user,
|
||||
version=version,
|
||||
filigranne=infos["filigranne"],
|
||||
server_name=REQUEST.BASE0,
|
||||
server_name=request.url_root,
|
||||
)
|
||||
|
||||
data = bul_generator.generate(format=format, stand_alone=stand_alone)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -31,12 +31,16 @@
|
|||
import datetime
|
||||
import json
|
||||
|
||||
from app.but import bulletin_but
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.etudiants import Identite
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_photos
|
||||
|
@ -47,27 +51,22 @@ from app.scodoc import sco_etud
|
|||
|
||||
|
||||
def make_json_formsemestre_bulletinetud(
|
||||
formsemestre_id,
|
||||
etudid,
|
||||
REQUEST=None,
|
||||
formsemestre_id: int,
|
||||
etudid: int,
|
||||
xml_with_decisions=False,
|
||||
version="long",
|
||||
force_publishing=False, # force publication meme si semestre non publie sur "portail"
|
||||
):
|
||||
) -> str:
|
||||
"""Renvoie bulletin en chaine JSON"""
|
||||
|
||||
d = formsemestre_bulletinetud_published_dict(
|
||||
formsemestre_id,
|
||||
etudid,
|
||||
force_publishing=force_publishing,
|
||||
REQUEST=REQUEST,
|
||||
xml_with_decisions=xml_with_decisions,
|
||||
version=version,
|
||||
)
|
||||
|
||||
if REQUEST:
|
||||
REQUEST.RESPONSE.setHeader("content-type", scu.JSON_MIMETYPE)
|
||||
|
||||
return json.dumps(d, cls=scu.ScoDocJSONEncoder)
|
||||
|
||||
|
||||
|
@ -79,7 +78,6 @@ def formsemestre_bulletinetud_published_dict(
|
|||
etudid,
|
||||
force_publishing=False,
|
||||
xml_nodate=False,
|
||||
REQUEST=None,
|
||||
xml_with_decisions=False, # inclue les decisions même si non publiées
|
||||
version="long",
|
||||
):
|
||||
|
@ -88,9 +86,17 @@ def formsemestre_bulletinetud_published_dict(
|
|||
"""
|
||||
from app.scodoc import sco_bulletins
|
||||
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
etud = Identite.query.get(etudid)
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
|
||||
if formsemestre.formation.is_apc():
|
||||
nt = bulletin_but.APCNotesTableCompat(formsemestre)
|
||||
else:
|
||||
nt = sco_cache.NotesTableCache.get(formsemestre_id)
|
||||
|
||||
d = {}
|
||||
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
if (not sem["bul_hide_xml"]) or force_publishing:
|
||||
published = 1
|
||||
else:
|
||||
|
@ -135,6 +141,11 @@ def formsemestre_bulletinetud_published_dict(
|
|||
if not published:
|
||||
return d # stop !
|
||||
|
||||
etat_inscription = etud.etat_inscription(formsemestre.id)
|
||||
if etat_inscription != scu.INSCRIT:
|
||||
d.update(dict_decision_jury(etudid, formsemestre_id, with_decisions=True))
|
||||
return d
|
||||
|
||||
# Groupes:
|
||||
partitions = sco_groups.get_partitions_list(formsemestre_id, with_default=False)
|
||||
partitions_etud_groups = {} # { partition_id : { etudid : group } }
|
||||
|
@ -142,7 +153,6 @@ def formsemestre_bulletinetud_published_dict(
|
|||
pid = partition["partition_id"]
|
||||
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
|
||||
|
||||
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes
|
||||
ues = nt.get_ues()
|
||||
modimpls = nt.get_modimpls()
|
||||
nbetuds = len(nt.rangs)
|
||||
|
@ -283,7 +293,7 @@ def formsemestre_bulletinetud_published_dict(
|
|||
if sco_preferences.get_preference(
|
||||
"bul_show_all_evals", formsemestre_id
|
||||
):
|
||||
all_evals = sco_evaluations.do_evaluation_list(
|
||||
all_evals = sco_evaluation_db.do_evaluation_list(
|
||||
args={"moduleimpl_id": modimpl["moduleimpl_id"]}
|
||||
)
|
||||
all_evals.reverse() # plus ancienne d'abord
|
||||
|
@ -331,60 +341,9 @@ def formsemestre_bulletinetud_published_dict(
|
|||
d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust)
|
||||
|
||||
# --- Decision Jury
|
||||
if (
|
||||
sco_preferences.get_preference("bul_show_decision", formsemestre_id)
|
||||
or xml_with_decisions
|
||||
):
|
||||
infos, dpv = sco_bulletins.etud_descr_situation_semestre(
|
||||
etudid,
|
||||
formsemestre_id,
|
||||
format="xml",
|
||||
show_uevalid=sco_preferences.get_preference(
|
||||
"bul_show_uevalid", formsemestre_id
|
||||
),
|
||||
)
|
||||
d["situation"] = scu.quote_xml_attr(infos["situation"])
|
||||
if dpv:
|
||||
decision = dpv["decisions"][0]
|
||||
etat = decision["etat"]
|
||||
if decision["decision_sem"]:
|
||||
code = decision["decision_sem"]["code"]
|
||||
else:
|
||||
code = ""
|
||||
|
||||
d["decision"] = dict(code=code, etat=etat)
|
||||
if (
|
||||
decision["decision_sem"]
|
||||
and "compense_formsemestre_id" in decision["decision_sem"]
|
||||
):
|
||||
d["decision"]["compense_formsemestre_id"] = decision["decision_sem"][
|
||||
"compense_formsemestre_id"
|
||||
]
|
||||
|
||||
d["decision_ue"] = []
|
||||
if decision[
|
||||
"decisions_ue"
|
||||
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
|
||||
for ue_id in decision["decisions_ue"].keys():
|
||||
ue = sco_edit_ue.do_ue_list({"ue_id": ue_id})[0]
|
||||
d["decision_ue"].append(
|
||||
dict(
|
||||
ue_id=ue["ue_id"],
|
||||
numero=scu.quote_xml_attr(ue["numero"]),
|
||||
acronyme=scu.quote_xml_attr(ue["acronyme"]),
|
||||
titre=scu.quote_xml_attr(ue["titre"]),
|
||||
code=decision["decisions_ue"][ue_id]["code"],
|
||||
ects=scu.quote_xml_attr(ue["ects"] or ""),
|
||||
)
|
||||
)
|
||||
d["autorisation_inscription"] = []
|
||||
for aut in decision["autorisations"]:
|
||||
d["autorisation_inscription"].append(
|
||||
dict(semestre_id=aut["semestre_id"])
|
||||
)
|
||||
else:
|
||||
d["decision"] = dict(code="", etat="DEM")
|
||||
|
||||
d.update(
|
||||
dict_decision_jury(etudid, formsemestre_id, with_decisions=xml_with_decisions)
|
||||
)
|
||||
# --- Appreciations
|
||||
cnx = ndb.GetDBConnexion()
|
||||
apprecs = sco_etud.appreciations_list(
|
||||
|
@ -401,3 +360,71 @@ def formsemestre_bulletinetud_published_dict(
|
|||
|
||||
#
|
||||
return d
|
||||
|
||||
|
||||
def dict_decision_jury(etudid, formsemestre_id, with_decisions=False):
|
||||
"dict avec decision pour bulletins json"
|
||||
from app.scodoc import sco_bulletins
|
||||
|
||||
d = {}
|
||||
if (
|
||||
sco_preferences.get_preference("bul_show_decision", formsemestre_id)
|
||||
or with_decisions
|
||||
):
|
||||
infos, dpv = sco_bulletins.etud_descr_situation_semestre(
|
||||
etudid,
|
||||
formsemestre_id,
|
||||
show_uevalid=sco_preferences.get_preference(
|
||||
"bul_show_uevalid", formsemestre_id
|
||||
),
|
||||
)
|
||||
d["situation"] = infos["situation"]
|
||||
if dpv:
|
||||
decision = dpv["decisions"][0]
|
||||
etat = decision["etat"]
|
||||
if decision["decision_sem"]:
|
||||
code = decision["decision_sem"]["code"]
|
||||
date = ndb.DateDMYtoISO(
|
||||
dpv["decisions"][0]["decision_sem"]["event_date"]
|
||||
)
|
||||
else:
|
||||
code = ""
|
||||
date = ""
|
||||
|
||||
d["decision"] = dict(
|
||||
code=code,
|
||||
etat=etat,
|
||||
date=date,
|
||||
)
|
||||
if (
|
||||
decision["decision_sem"]
|
||||
and "compense_formsemestre_id" in decision["decision_sem"]
|
||||
):
|
||||
d["decision"]["compense_formsemestre_id"] = decision["decision_sem"][
|
||||
"compense_formsemestre_id"
|
||||
]
|
||||
|
||||
d["decision_ue"] = []
|
||||
if decision[
|
||||
"decisions_ue"
|
||||
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
|
||||
for ue_id in decision["decisions_ue"].keys():
|
||||
ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
|
||||
d["decision_ue"].append(
|
||||
dict(
|
||||
ue_id=ue["ue_id"],
|
||||
numero=ue["numero"],
|
||||
acronyme=ue["acronyme"],
|
||||
titre=ue["titre"],
|
||||
code=decision["decisions_ue"][ue_id]["code"],
|
||||
ects=ue["ects"] or "",
|
||||
)
|
||||
)
|
||||
d["autorisation_inscription"] = []
|
||||
for aut in decision["autorisations"]:
|
||||
d["autorisation_inscription"].append(
|
||||
dict(semestre_id=aut["semestre_id"])
|
||||
)
|
||||
else:
|
||||
d["decision"] = dict(code="", etat="DEM")
|
||||
return d
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -51,23 +51,24 @@ Chaque semestre peut si nécessaire utiliser un type de bulletin différent.
|
|||
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
from pydoc import html
|
||||
|
||||
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
|
||||
|
||||
from flask import g, url_for
|
||||
from flask import g, request
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app import log, ScoValueError
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_pdf
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_etud
|
||||
import sco_version
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
|
||||
|
||||
def pdfassemblebulletins(
|
||||
|
@ -110,6 +111,17 @@ def pdfassemblebulletins(
|
|||
return data
|
||||
|
||||
|
||||
def replacement_function(match):
|
||||
balise = match.group(1)
|
||||
name = match.group(3)
|
||||
logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id)
|
||||
if logo is not None:
|
||||
return r'<img %s src="%s"%s/>' % (match.group(2), logo.filepath, match.group(4))
|
||||
raise ScoValueError(
|
||||
'balise "%s": logo "%s" introuvable' % (html.escape(balise), html.escape(name))
|
||||
)
|
||||
|
||||
|
||||
def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
|
||||
"""Process a field given in preferences, returns
|
||||
- if format = 'pdf': a list of Platypus objects
|
||||
|
@ -141,30 +153,24 @@ def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
|
|||
return text
|
||||
# --- PDF format:
|
||||
# handle logos:
|
||||
image_dir = scu.SCODOC_LOGOS_DIR + "/logos_" + g.scodoc_dept + "/"
|
||||
if not os.path.exists(image_dir):
|
||||
image_dir = scu.SCODOC_LOGOS_DIR + "/" # use global logos
|
||||
if not os.path.exists(image_dir):
|
||||
log(f"Warning: missing global logo directory ({image_dir})")
|
||||
image_dir = None
|
||||
|
||||
text = re.sub(
|
||||
r"<(\s*)logo(.*?)src\s*=\s*(.*?)>", r"<\1logo\2\3>", text
|
||||
) # remove forbidden src attribute
|
||||
if image_dir is not None:
|
||||
text = re.sub(
|
||||
r'<\s*logo(.*?)name\s*=\s*"(\w*?)"(.*?)/?>',
|
||||
r'<img\1src="%s/logo_\2.jpg"\3/>' % image_dir,
|
||||
text,
|
||||
)
|
||||
# nota: le match sur \w*? donne le nom du logo et interdit les .. et autres
|
||||
# tentatives d'acceder à d'autres fichiers !
|
||||
text = re.sub(
|
||||
r'(<\s*logo(.*?)name\s*=\s*"(\w*?)"(.*?)/?>)',
|
||||
replacement_function,
|
||||
text,
|
||||
)
|
||||
# nota: le match sur \w*? donne le nom du logo et interdit les .. et autres
|
||||
# tentatives d'acceder à d'autres fichiers !
|
||||
# la protection contre des noms malveillants est aussi assurée par l'utilisation de
|
||||
# secure_filename dans la classe Logo
|
||||
|
||||
# log('field: %s' % (text))
|
||||
return sco_pdf.makeParas(text, style, suppress_empty=suppress_empty_pars)
|
||||
|
||||
|
||||
def get_formsemestre_bulletins_pdf(formsemestre_id, REQUEST, version="selectedevals"):
|
||||
def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
|
||||
"document pdf et filename"
|
||||
from app.scodoc import sco_bulletins
|
||||
|
||||
|
@ -184,7 +190,6 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, REQUEST, version="selectedev
|
|||
etudid,
|
||||
format="pdfpart",
|
||||
version=version,
|
||||
REQUEST=REQUEST,
|
||||
)
|
||||
fragments += frag
|
||||
filigrannes[i] = filigranne
|
||||
|
@ -192,8 +197,8 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, REQUEST, version="selectedev
|
|||
i = i + 1
|
||||
#
|
||||
infos = {"DeptName": sco_preferences.get_preference("DeptName", formsemestre_id)}
|
||||
if REQUEST:
|
||||
server_name = REQUEST.BASE0
|
||||
if request:
|
||||
server_name = request.url_root
|
||||
else:
|
||||
server_name = ""
|
||||
try:
|
||||
|
@ -220,7 +225,7 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, REQUEST, version="selectedev
|
|||
return pdfdoc, filename
|
||||
|
||||
|
||||
def get_etud_bulletins_pdf(etudid, REQUEST, version="selectedevals"):
|
||||
def get_etud_bulletins_pdf(etudid, version="selectedevals"):
|
||||
"Bulletins pdf de tous les semestres de l'étudiant, et filename"
|
||||
from app.scodoc import sco_bulletins
|
||||
|
||||
|
@ -235,15 +240,14 @@ def get_etud_bulletins_pdf(etudid, REQUEST, version="selectedevals"):
|
|||
etudid,
|
||||
format="pdfpart",
|
||||
version=version,
|
||||
REQUEST=REQUEST,
|
||||
)
|
||||
fragments += frag
|
||||
filigrannes[i] = filigranne
|
||||
bookmarks[i] = sem["session_id"] # eg RT-DUT-FI-S1-2015
|
||||
i = i + 1
|
||||
infos = {"DeptName": sco_preferences.get_preference("DeptName")}
|
||||
if REQUEST:
|
||||
server_name = REQUEST.BASE0
|
||||
if request:
|
||||
server_name = request.url_root
|
||||
else:
|
||||
server_name = ""
|
||||
try:
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -56,7 +56,7 @@ et sur page "réglages bulletin" (avec formsemestre_id)
|
|||
# import os
|
||||
|
||||
|
||||
# def form_change_bul_sig(side, formsemestre_id=None, REQUEST=None):
|
||||
# def form_change_bul_sig(side, formsemestre_id=None):
|
||||
# """Change pdf signature"""
|
||||
# filename = _get_sig_existing_filename(
|
||||
# side, formsemestre_id=formsemestre_id
|
||||
|
@ -69,7 +69,7 @@ et sur page "réglages bulletin" (avec formsemestre_id)
|
|||
# raise ValueError("invalid value for 'side' parameter")
|
||||
# signatureloc = get_bul_sig_img()
|
||||
# H = [
|
||||
# self.sco_header(REQUEST, page_title="Changement de signature"),
|
||||
# self.sco_header(page_title="Changement de signature"),
|
||||
# """<h2>Changement de la signature bulletin de %(sidetxt)s</h2>
|
||||
# """
|
||||
# % (sidetxt,),
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -47,11 +47,13 @@ from xml.etree.ElementTree import Element
|
|||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
from app.but.bulletin_but_xml_compat import bulletin_but_xml_compat
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_photos
|
||||
|
@ -69,22 +71,31 @@ def make_xml_formsemestre_bulletinetud(
|
|||
doc=None, # XML document
|
||||
force_publishing=False,
|
||||
xml_nodate=False,
|
||||
REQUEST=None,
|
||||
xml_with_decisions=False, # inclue les decisions même si non publiées
|
||||
version="long",
|
||||
):
|
||||
) -> str:
|
||||
"bulletin au format XML"
|
||||
from app.scodoc import sco_bulletins
|
||||
|
||||
log("xml_bulletin( formsemestre_id=%s, etudid=%s )" % (formsemestre_id, etudid))
|
||||
if REQUEST:
|
||||
REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE)
|
||||
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
if formsemestre.formation.is_apc():
|
||||
return bulletin_but_xml_compat(
|
||||
formsemestre_id,
|
||||
etudid,
|
||||
doc=doc,
|
||||
force_publishing=force_publishing,
|
||||
xml_nodate=xml_nodate,
|
||||
xml_with_decisions=xml_with_decisions, # inclue les decisions même si non publiées
|
||||
version=version,
|
||||
)
|
||||
|
||||
if (not sem["bul_hide_xml"]) or force_publishing:
|
||||
published = "1"
|
||||
published = 1
|
||||
else:
|
||||
published = "0"
|
||||
published = 0
|
||||
if xml_nodate:
|
||||
docdate = ""
|
||||
else:
|
||||
|
@ -94,7 +105,7 @@ def make_xml_formsemestre_bulletinetud(
|
|||
"etudid": str(etudid),
|
||||
"formsemestre_id": str(formsemestre_id),
|
||||
"date": docdate,
|
||||
"publie": published,
|
||||
"publie": str(published),
|
||||
}
|
||||
if sem["etapes"]:
|
||||
el["etape_apo"] = str(sem["etapes"][0]) or ""
|
||||
|
@ -130,7 +141,9 @@ def make_xml_formsemestre_bulletinetud(
|
|||
|
||||
# Disponible pour publication ?
|
||||
if not published:
|
||||
return doc # stop !
|
||||
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(
|
||||
scu.SCO_ENCODING
|
||||
) # stop !
|
||||
|
||||
# Groupes:
|
||||
partitions = sco_groups.get_partitions_list(formsemestre_id, with_default=False)
|
||||
|
@ -292,7 +305,7 @@ def make_xml_formsemestre_bulletinetud(
|
|||
if sco_preferences.get_preference(
|
||||
"bul_show_all_evals", formsemestre_id
|
||||
):
|
||||
all_evals = sco_evaluations.do_evaluation_list(
|
||||
all_evals = sco_evaluation_db.do_evaluation_list(
|
||||
args={"moduleimpl_id": modimpl["moduleimpl_id"]}
|
||||
)
|
||||
all_evals.reverse() # plus ancienne d'abord
|
||||
|
@ -388,7 +401,7 @@ def make_xml_formsemestre_bulletinetud(
|
|||
"decisions_ue"
|
||||
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
|
||||
for ue_id in decision["decisions_ue"].keys():
|
||||
ue = sco_edit_ue.do_ue_list({"ue_id": ue_id})[0]
|
||||
ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
|
||||
doc.append(
|
||||
Element(
|
||||
"decision_ue",
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -29,7 +29,7 @@
|
|||
|
||||
Ré-écrite pour ScoDoc8, utilise flask_caching et REDIS
|
||||
|
||||
ScoDoc est maintenant multiprocessus / mono-thread, avec un cache en mémoire partagé.
|
||||
ScoDoc est maintenant multiprocessus / mono-thread, avec un cache partagé.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -46,9 +46,9 @@
|
|||
# sco_cache.NotesTableCache.delete_many(formsemestre_id_list)
|
||||
#
|
||||
# Bulletins PDF:
|
||||
# sco_cache.PDFBulCache.get(formsemestre_id, version)
|
||||
# sco_cache.PDFBulCache.set(formsemestre_id, version, filename, pdfdoc)
|
||||
# sco_cache.PDFBulCache.delete(formsemestre_id) suppr. toutes les versions
|
||||
# sco_cache.SemBulletinsPDFCache.get(formsemestre_id, version)
|
||||
# sco_cache.SemBulletinsPDFCache.set(formsemestre_id, version, filename, pdfdoc)
|
||||
# sco_cache.SemBulletinsPDFCache.delete(formsemestre_id) suppr. toutes les versions
|
||||
|
||||
# Evaluations:
|
||||
# sco_cache.EvaluationCache.get(evaluation_id), set(evaluation_id, value), delete(evaluation_id),
|
||||
|
@ -155,9 +155,19 @@ class EvaluationCache(ScoDocCache):
|
|||
cls.delete_many(evaluation_ids)
|
||||
|
||||
|
||||
class ResultatsSemestreBUTCache(ScoDocCache):
|
||||
"""Cache pour les résultats ResultatsSemestreBUT.
|
||||
Clé: formsemestre_id
|
||||
Valeur: { un paquet de dataframes }
|
||||
"""
|
||||
|
||||
prefix = "RBUT"
|
||||
timeout = 1 * 60 # ttl 1 minutes (en phase de mise au point)
|
||||
|
||||
|
||||
class AbsSemEtudCache(ScoDocCache):
|
||||
"""Cache pour les comptes d'absences d'un étudiant dans un semestre.
|
||||
Ce cache étant indépendant des semestre, le compte peut être faux lorsqu'on
|
||||
Ce cache étant indépendant des semestres, le compte peut être faux lorsqu'on
|
||||
change les dates début/fin d'un semestre.
|
||||
C'est pourquoi il expire après timeout secondes.
|
||||
Le timeout evite aussi d'éliminer explicitement ces éléments cachés lors
|
||||
|
@ -289,10 +299,11 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
|||
SemInscriptionsCache.delete_many(formsemestre_ids)
|
||||
|
||||
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
|
||||
ResultatsSemestreBUTCache.delete_many(formsemestre_ids)
|
||||
|
||||
|
||||
class DefferedSemCacheManager:
|
||||
"""Experimental: pour effectuer des opérations indépendantes dans la
|
||||
"""Contexte pour effectuer des opérations indépendantes dans la
|
||||
même requete qui invalident le cache. Par exemple, quand on inscrit
|
||||
des étudiants un par un à un semestre, chaque inscription va invalider
|
||||
le cache, et la suivante va le reconstruire... pour l'invalider juste après.
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -28,7 +28,41 @@
|
|||
"""Semestres: Codes gestion parcours (constantes)
|
||||
"""
|
||||
import collections
|
||||
from six.moves import range
|
||||
import enum
|
||||
|
||||
from app import log
|
||||
|
||||
|
||||
@enum.unique
|
||||
class CodesParcours(enum.IntEnum):
|
||||
"""Codes numériques de sparcours, enregistrés en base
|
||||
dans notes_formations.type_parcours
|
||||
Ne pas modifier.
|
||||
"""
|
||||
|
||||
Legacy = 0
|
||||
DUT = 100
|
||||
DUT4 = 110
|
||||
DUTMono = 120
|
||||
DUT2 = 130
|
||||
LP = 200
|
||||
LP2sem = 210
|
||||
LP2semEvry = 220
|
||||
LP2014 = 230
|
||||
LP2sem2014 = 240
|
||||
M2 = 250
|
||||
M2noncomp = 251
|
||||
Mono = 300
|
||||
MasterLMD = 402
|
||||
MasterIG = 403
|
||||
LicenceUCAC3 = 501
|
||||
MasterUCAC2 = 502
|
||||
MonoUCAC = 503
|
||||
GEN_6_SEM = 600
|
||||
BUT = 700
|
||||
ISCID6 = 1001
|
||||
ISCID4 = 1002
|
||||
|
||||
|
||||
NOTES_TOLERANCE = 0.00499999999999 # si note >= (BARRE-TOLERANCE), considere ok
|
||||
# (permet d'eviter d'afficher 10.00 sous barre alors que la moyenne vaut 9.999)
|
||||
|
@ -44,6 +78,7 @@ UE_STAGE_LP = 2 # ue "projet tuteuré et stage" dans les Lic. Pro.
|
|||
UE_STAGE_10 = 3 # ue "stage" avec moyenne requise > 10
|
||||
UE_ELECTIVE = 4 # UE "élective" dans certains parcours (UCAC?, ISCID)
|
||||
UE_PROFESSIONNELLE = 5 # UE "professionnelle" (ISCID, ...)
|
||||
UE_OPTIONNELLE = 6 # UE non fondamentales (ILEPS, ...)
|
||||
|
||||
|
||||
def UE_is_fondamentale(ue_type):
|
||||
|
@ -62,7 +97,8 @@ UE_TYPE_NAME = {
|
|||
UE_STAGE_LP: "Projet tuteuré et stage (Lic. Pro.)",
|
||||
UE_STAGE_10: "Stage (moyenne min. 10/20)",
|
||||
UE_ELECTIVE: "Elective (ISCID)",
|
||||
UE_PROFESSIONNELLE: "Professionnelle (ISCID)"
|
||||
UE_PROFESSIONNELLE: "Professionnelle (ISCID)",
|
||||
UE_OPTIONNELLE: "Optionnelle",
|
||||
# UE_FONDAMENTALE : '"Fondamentale" (eg UCAC)',
|
||||
# UE_OPTIONNELLE : '"Optionnelle" (UCAC)'
|
||||
}
|
||||
|
@ -215,6 +251,8 @@ class TypeParcours(object):
|
|||
ALLOWED_UE_TYPES = list(
|
||||
UE_TYPE_NAME.keys()
|
||||
) # par defaut, autorise tous les types d'UE
|
||||
APC_SAE = False # Approche par compétences avec ressources et SAÉs
|
||||
USE_REFERENTIEL_COMPETENCES = False # Lien avec ref. comp.
|
||||
|
||||
def check(self, formation=None):
|
||||
return True, "" # status, diagnostic_message
|
||||
|
@ -254,13 +292,27 @@ class TypeParcours(object):
|
|||
return False, """<b>%d UE sous la barre</b>""" % n
|
||||
|
||||
|
||||
TYPES_PARCOURS = (
|
||||
collections.OrderedDict()
|
||||
) # liste des parcours définis (instances de sous-classes de TypeParcours)
|
||||
# Parcours définis (instances de sous-classes de TypeParcours):
|
||||
TYPES_PARCOURS = collections.OrderedDict() # type : Parcours
|
||||
|
||||
|
||||
def register_parcours(Parcours):
|
||||
TYPES_PARCOURS[Parcours.TYPE_PARCOURS] = Parcours
|
||||
TYPES_PARCOURS[int(Parcours.TYPE_PARCOURS)] = Parcours
|
||||
|
||||
|
||||
class ParcoursBUT(TypeParcours):
|
||||
"""BUT Bachelor Universitaire de Technologie"""
|
||||
|
||||
TYPE_PARCOURS = 700
|
||||
NAME = "BUT"
|
||||
NB_SEM = 6
|
||||
COMPENSATION_UE = False
|
||||
APC_SAE = True
|
||||
USE_REFERENTIEL_COMPETENCES = True
|
||||
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT]
|
||||
|
||||
|
||||
register_parcours(ParcoursBUT())
|
||||
|
||||
|
||||
class ParcoursDUT(TypeParcours):
|
||||
|
@ -303,7 +355,7 @@ register_parcours(ParcoursDUTMono())
|
|||
class ParcoursDUT2(ParcoursDUT):
|
||||
"""DUT en deux semestres (par ex.: années spéciales semestrialisées)"""
|
||||
|
||||
TYPE_PARCOURS = 130
|
||||
TYPE_PARCOURS = CodesParcours.DUT2
|
||||
NAME = "DUT2"
|
||||
NB_SEM = 2
|
||||
|
||||
|
@ -316,7 +368,7 @@ class ParcoursLP(TypeParcours):
|
|||
(pour anciennes LP. Après 2014, préférer ParcoursLP2014)
|
||||
"""
|
||||
|
||||
TYPE_PARCOURS = 200
|
||||
TYPE_PARCOURS = CodesParcours.LP
|
||||
NAME = "LP"
|
||||
NB_SEM = 1
|
||||
COMPENSATION_UE = False
|
||||
|
@ -333,7 +385,7 @@ register_parcours(ParcoursLP())
|
|||
class ParcoursLP2sem(ParcoursLP):
|
||||
"""Licence Pro (en deux "semestres")"""
|
||||
|
||||
TYPE_PARCOURS = 210
|
||||
TYPE_PARCOURS = CodesParcours.LP2sem
|
||||
NAME = "LP2sem"
|
||||
NB_SEM = 2
|
||||
COMPENSATION_UE = True
|
||||
|
@ -346,7 +398,7 @@ register_parcours(ParcoursLP2sem())
|
|||
class ParcoursLP2semEvry(ParcoursLP):
|
||||
"""Licence Pro (en deux "semestres", U. Evry)"""
|
||||
|
||||
TYPE_PARCOURS = 220
|
||||
TYPE_PARCOURS = CodesParcours.LP2semEvry
|
||||
NAME = "LP2semEvry"
|
||||
NB_SEM = 2
|
||||
COMPENSATION_UE = True
|
||||
|
@ -372,7 +424,7 @@ class ParcoursLP2014(TypeParcours):
|
|||
# l'établissement d'un coefficient qui peut varier dans un rapport de 1 à 3. ", etc ne sont _pas_
|
||||
# vérifiés par ScoDoc)
|
||||
|
||||
TYPE_PARCOURS = 230
|
||||
TYPE_PARCOURS = CodesParcours.LP2014
|
||||
NAME = "LP2014"
|
||||
NB_SEM = 1
|
||||
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT, UE_STAGE_LP]
|
||||
|
@ -416,7 +468,7 @@ register_parcours(ParcoursLP2014())
|
|||
class ParcoursLP2sem2014(ParcoursLP):
|
||||
"""Licence Pro (en deux "semestres", selon arrêté du 22/01/2014)"""
|
||||
|
||||
TYPE_PARCOURS = 240
|
||||
TYPE_PARCOURS = CodesParcours.LP2sem2014
|
||||
NAME = "LP2014_2sem"
|
||||
NB_SEM = 2
|
||||
|
||||
|
@ -428,7 +480,7 @@ register_parcours(ParcoursLP2sem2014())
|
|||
class ParcoursM2(TypeParcours):
|
||||
"""Master 2 (en deux "semestres")"""
|
||||
|
||||
TYPE_PARCOURS = 250
|
||||
TYPE_PARCOURS = CodesParcours.M2
|
||||
NAME = "M2sem"
|
||||
NB_SEM = 2
|
||||
COMPENSATION_UE = True
|
||||
|
@ -441,7 +493,7 @@ register_parcours(ParcoursM2())
|
|||
class ParcoursM2noncomp(ParcoursM2):
|
||||
"""Master 2 (en deux "semestres") sans compensation"""
|
||||
|
||||
TYPE_PARCOURS = 251
|
||||
TYPE_PARCOURS = CodesParcours.M2noncomp
|
||||
NAME = "M2noncomp"
|
||||
COMPENSATION_UE = False
|
||||
UNUSED_CODES = set((ADC, ATT, ATB))
|
||||
|
@ -453,7 +505,7 @@ register_parcours(ParcoursM2noncomp())
|
|||
class ParcoursMono(TypeParcours):
|
||||
"""Formation générique en une session"""
|
||||
|
||||
TYPE_PARCOURS = 300
|
||||
TYPE_PARCOURS = CodesParcours.Mono
|
||||
NAME = "Mono"
|
||||
NB_SEM = 1
|
||||
COMPENSATION_UE = False
|
||||
|
@ -466,7 +518,7 @@ register_parcours(ParcoursMono())
|
|||
class ParcoursLegacy(TypeParcours):
|
||||
"""DUT (ancien ScoDoc, ne plus utiliser)"""
|
||||
|
||||
TYPE_PARCOURS = 0
|
||||
TYPE_PARCOURS = CodesParcours.Legacy
|
||||
NAME = "DUT"
|
||||
NB_SEM = 4
|
||||
COMPENSATION_UE = None # backward compat: defini dans formsemestre
|
||||
|
@ -500,7 +552,7 @@ class ParcoursBachelorISCID6(ParcoursISCID):
|
|||
"""ISCID: Bachelor en 3 ans (6 sem.)"""
|
||||
|
||||
NAME = "ParcoursBachelorISCID6"
|
||||
TYPE_PARCOURS = 1001
|
||||
TYPE_PARCOURS = CodesParcours.ISCID6
|
||||
NAME = ""
|
||||
NB_SEM = 6
|
||||
ECTS_PROF_DIPL = 8 # crédits professionnels requis pour obtenir le diplôme
|
||||
|
@ -511,7 +563,7 @@ register_parcours(ParcoursBachelorISCID6())
|
|||
|
||||
class ParcoursMasterISCID4(ParcoursISCID):
|
||||
"ISCID: Master en 2 ans (4 sem.)"
|
||||
TYPE_PARCOURS = 1002
|
||||
TYPE_PARCOURS = CodesParcours.ISCID4
|
||||
NAME = "ParcoursMasterISCID4"
|
||||
NB_SEM = 4
|
||||
ECTS_PROF_DIPL = 15 # crédits professionnels requis pour obtenir le diplôme
|
||||
|
@ -520,6 +572,34 @@ class ParcoursMasterISCID4(ParcoursISCID):
|
|||
register_parcours(ParcoursMasterISCID4())
|
||||
|
||||
|
||||
class ParcoursILEPS(TypeParcours):
|
||||
"""Superclasse pour les parcours de l'ILEPS"""
|
||||
|
||||
# SESSION_NAME = "année"
|
||||
# SESSION_NAME_A = "de l'"
|
||||
# SESSION_ABBRV = 'A' # A1, A2, ...
|
||||
COMPENSATION_UE = False
|
||||
UNUSED_CODES = set((ADC, ATT, ATB, ATJ))
|
||||
ALLOWED_UE_TYPES = [UE_STANDARD, UE_OPTIONNELLE]
|
||||
# Barre moy gen. pour validation semestre:
|
||||
BARRE_MOY = 10.0
|
||||
# Barre pour UE ILEPS: 8/20 pour UE standards ("fondamentales")
|
||||
# et pas de barre (-1.) pour UE élective.
|
||||
BARRE_UE = {UE_STANDARD: 8.0, UE_OPTIONNELLE: 0.0}
|
||||
BARRE_UE_DEFAULT = 0.0 # pas de barre sur les autres UE
|
||||
|
||||
|
||||
class ParcoursLicenceILEPS6(ParcoursILEPS):
|
||||
"""ILEPS: Licence 6 semestres"""
|
||||
|
||||
TYPE_PARCOURS = 1010
|
||||
NAME = "LicenceILEPS6"
|
||||
NB_SEM = 6
|
||||
|
||||
|
||||
register_parcours(ParcoursLicenceILEPS6())
|
||||
|
||||
|
||||
class ParcoursUCAC(TypeParcours):
|
||||
"""Règles de validation UCAC"""
|
||||
|
||||
|
@ -537,7 +617,7 @@ class ParcoursUCAC(TypeParcours):
|
|||
class ParcoursLicenceUCAC3(ParcoursUCAC):
|
||||
"""UCAC: Licence en 3 sessions d'un an"""
|
||||
|
||||
TYPE_PARCOURS = 501
|
||||
TYPE_PARCOURS = CodesParcours.LicenceUCAC3
|
||||
NAME = "Licence UCAC en 3 sessions d'un an"
|
||||
NB_SEM = 3
|
||||
|
||||
|
@ -548,7 +628,7 @@ register_parcours(ParcoursLicenceUCAC3())
|
|||
class ParcoursMasterUCAC2(ParcoursUCAC):
|
||||
"""UCAC: Master en 2 sessions d'un an"""
|
||||
|
||||
TYPE_PARCOURS = 502
|
||||
TYPE_PARCOURS = CodesParcours.MasterUCAC2
|
||||
NAME = "Master UCAC en 2 sessions d'un an"
|
||||
NB_SEM = 2
|
||||
|
||||
|
@ -559,7 +639,7 @@ register_parcours(ParcoursMasterUCAC2())
|
|||
class ParcoursMonoUCAC(ParcoursUCAC):
|
||||
"""UCAC: Formation en 1 session de durée variable"""
|
||||
|
||||
TYPE_PARCOURS = 503
|
||||
TYPE_PARCOURS = CodesParcours.MonoUCAC
|
||||
NAME = "Formation UCAC en 1 session de durée variable"
|
||||
NB_SEM = 1
|
||||
UNUSED_CODES = set((ADC, ATT, ATB))
|
||||
|
@ -571,7 +651,7 @@ register_parcours(ParcoursMonoUCAC())
|
|||
class Parcours6Sem(TypeParcours):
|
||||
"""Parcours générique en 6 semestres"""
|
||||
|
||||
TYPE_PARCOURS = 600
|
||||
TYPE_PARCOURS = CodesParcours.GEN_6_SEM
|
||||
NAME = "Formation en 6 semestres"
|
||||
NB_SEM = 6
|
||||
COMPENSATION_UE = True
|
||||
|
@ -593,7 +673,7 @@ register_parcours(Parcours6Sem())
|
|||
class ParcoursMasterLMD(TypeParcours):
|
||||
"""Master générique en 4 semestres dans le LMD"""
|
||||
|
||||
TYPE_PARCOURS = 402
|
||||
TYPE_PARCOURS = CodesParcours.MasterLMD
|
||||
NAME = "Master LMD"
|
||||
NB_SEM = 4
|
||||
COMPENSATION_UE = True # variabale inutilisée
|
||||
|
@ -606,7 +686,7 @@ register_parcours(ParcoursMasterLMD())
|
|||
class ParcoursMasterIG(ParcoursMasterLMD):
|
||||
"""Master de l'Institut Galilée (U. Paris 13) en 4 semestres (LMD)"""
|
||||
|
||||
TYPE_PARCOURS = 403
|
||||
TYPE_PARCOURS = CodesParcours.MasterIG
|
||||
NAME = "Master IG P13"
|
||||
BARRE_MOY = 10.0
|
||||
NOTES_BARRE_VALID_UE_TH = 10.0 # seuil pour valider UE
|
||||
|
@ -673,4 +753,9 @@ FORMATION_PARCOURS_TYPES = [p[0] for p in _tp] # codes numeriques (TYPE_PARCOUR
|
|||
|
||||
|
||||
def get_parcours_from_code(code_parcours):
|
||||
return TYPES_PARCOURS[code_parcours]
|
||||
parcours = TYPES_PARCOURS.get(code_parcours)
|
||||
if parcours is None:
|
||||
log(f"Warning: invalid code_parcours: {code_parcours}")
|
||||
# default to legacy
|
||||
parcours = TYPES_PARCOURS.get(0)
|
||||
return parcours
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -27,24 +27,25 @@
|
|||
|
||||
"""Calcul des moyennes de module
|
||||
"""
|
||||
|
||||
import traceback
|
||||
import pprint
|
||||
import traceback
|
||||
|
||||
from flask import url_for, g
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_utils import (
|
||||
ModuleType,
|
||||
NOTES_ATTENTE,
|
||||
NOTES_NEUTRALISE,
|
||||
EVALUATION_NORMALE,
|
||||
EVALUATION_RATTRAPAGE,
|
||||
EVALUATION_SESSION2,
|
||||
)
|
||||
from app.scodoc.sco_exceptions import ScoException
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app import log
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_formulas
|
||||
|
@ -65,7 +66,8 @@ def moduleimpl_has_expression(mod):
|
|||
|
||||
def formsemestre_expressions_use_abscounts(formsemestre_id):
|
||||
"""True si les notes de ce semestre dépendent des compteurs d'absences.
|
||||
Cela n'est normalement pas le cas, sauf si des formules utilisateur utilisent ces compteurs.
|
||||
Cela n'est normalement pas le cas, sauf si des formules utilisateur
|
||||
utilisent ces compteurs.
|
||||
"""
|
||||
# check presence of 'nbabs' in expressions
|
||||
ab = "nb_abs" # chaine recherchée
|
||||
|
@ -79,7 +81,7 @@ def formsemestre_expressions_use_abscounts(formsemestre_id):
|
|||
if expr and expr[0] != "#" and ab in expr:
|
||||
return True
|
||||
# 2- moyennes de modules
|
||||
for mod in sco_moduleimpl.do_moduleimpl_list(formsemestre_id=formsemestre_id):
|
||||
for mod in sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id):
|
||||
if moduleimpl_has_expression(mod) and ab in mod["computation_expr"]:
|
||||
return True
|
||||
return False
|
||||
|
@ -102,8 +104,9 @@ formsemestre_ue_computation_expr_list = _formsemestre_ue_computation_exprEditor.
|
|||
formsemestre_ue_computation_expr_edit = _formsemestre_ue_computation_exprEditor.edit
|
||||
|
||||
|
||||
def get_ue_expression(formsemestre_id, ue_id, cnx, html_quote=False):
|
||||
def get_ue_expression(formsemestre_id, ue_id, html_quote=False):
|
||||
"""Returns UE expression (formula), or None if no expression has been defined"""
|
||||
cnx = ndb.GetDBConnexion()
|
||||
el = formsemestre_ue_computation_expr_list(
|
||||
cnx, {"formsemestre_id": formsemestre_id, "ue_id": ue_id}
|
||||
)
|
||||
|
@ -128,7 +131,7 @@ def compute_user_formula(
|
|||
coefs,
|
||||
coefs_mask,
|
||||
formula,
|
||||
diag_info={}, # infos supplementaires a placer ds messages d'erreur
|
||||
diag_info=None, # infos supplementaires a placer ds messages d'erreur
|
||||
use_abs=True,
|
||||
):
|
||||
"""Calcul moyenne a partir des notes et coefs, en utilisant la formule utilisateur (une chaine).
|
||||
|
@ -159,14 +162,19 @@ def compute_user_formula(
|
|||
# log('expression : %s\nvariables=%s\n' % (formula, variables)) # debug
|
||||
user_moy = sco_formulas.eval_user_expression(formula, variables)
|
||||
# log('user_moy=%s' % user_moy)
|
||||
if user_moy != "NA0" and user_moy != "NA":
|
||||
if user_moy != "NA":
|
||||
user_moy = float(user_moy)
|
||||
if (user_moy > 20) or (user_moy < 0):
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
|
||||
raise ScoException(
|
||||
"""valeur moyenne %s hors limite pour <a href="formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s">%s</a>"""
|
||||
% (user_moy, sem["formsemestre_id"], etudid, etud["nomprenom"])
|
||||
raise ScoValueError(
|
||||
f"""
|
||||
Valeur moyenne {user_moy} hors limite pour
|
||||
<a href="{url_for('notes.formsemestre_bulletinetud',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=sem["formsemestre_id"],
|
||||
etudid=etudid
|
||||
)}">{etud["nomprenom"]}</a>"""
|
||||
)
|
||||
except:
|
||||
log(
|
||||
|
@ -183,7 +191,8 @@ def compute_user_formula(
|
|||
return user_moy
|
||||
|
||||
|
||||
def do_moduleimpl_moyennes(nt, mod):
|
||||
# XXX OBSOLETE
|
||||
def compute_moduleimpl_moyennes(nt, modimpl):
|
||||
"""Retourne dict { etudid : note_moyenne } pour tous les etuds inscrits
|
||||
au moduleimpl mod, la liste des evaluations "valides" (toutes notes entrées
|
||||
ou en attente), et att (vrai s'il y a des notes en attente dans ce module).
|
||||
|
@ -193,13 +202,13 @@ def do_moduleimpl_moyennes(nt, mod):
|
|||
S'il manque des notes et que le coef n'est pas nul,
|
||||
la moyenne n'est pas calculée: NA
|
||||
Ne prend en compte que les evaluations où toutes les notes sont entrées.
|
||||
Le résultat est une note sur 20.
|
||||
Le résultat note_moyenne est une note sur 20.
|
||||
"""
|
||||
diag_info = {} # message d'erreur formule
|
||||
moduleimpl_id = mod["moduleimpl_id"]
|
||||
is_malus = mod["module"]["module_type"] == scu.MODULE_MALUS
|
||||
sem = sco_formsemestre.get_formsemestre(mod["formsemestre_id"])
|
||||
etudids = sco_moduleimpl.do_moduleimpl_listeetuds(
|
||||
moduleimpl_id = modimpl["moduleimpl_id"]
|
||||
is_malus = modimpl["module"]["module_type"] == ModuleType.MALUS
|
||||
sem = sco_formsemestre.get_formsemestre(modimpl["formsemestre_id"])
|
||||
etudids = sco_moduleimpl.moduleimpl_listeetuds(
|
||||
moduleimpl_id
|
||||
) # tous, y compris demissions
|
||||
# Inscrits au semestre (pour traiter les demissions):
|
||||
|
@ -207,7 +216,7 @@ def do_moduleimpl_moyennes(nt, mod):
|
|||
[
|
||||
x["etudid"]
|
||||
for x in sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits(
|
||||
mod["formsemestre_id"]
|
||||
modimpl["formsemestre_id"]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
@ -218,24 +227,25 @@ def do_moduleimpl_moyennes(nt, mod):
|
|||
key=lambda x: (x["numero"], x["jour"], x["heure_debut"])
|
||||
) # la plus ancienne en tête
|
||||
|
||||
user_expr = moduleimpl_has_expression(mod)
|
||||
user_expr = moduleimpl_has_expression(modimpl)
|
||||
attente = False
|
||||
# recupere les notes de toutes les evaluations
|
||||
# récupere les notes de toutes les evaluations
|
||||
eval_rattr = None
|
||||
for e in evals:
|
||||
e["nb_inscrits"] = e["etat"]["nb_inscrits"]
|
||||
NotesDB = sco_evaluations.do_evaluation_get_all_notes(
|
||||
# XXX OBSOLETE
|
||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
|
||||
e["evaluation_id"]
|
||||
) # toutes, y compris demissions
|
||||
# restreint aux étudiants encore inscrits à ce module
|
||||
notes = [
|
||||
NotesDB[etudid]["value"] for etudid in NotesDB if (etudid in insmod_set)
|
||||
notes_db[etudid]["value"] for etudid in notes_db if (etudid in insmod_set)
|
||||
]
|
||||
e["nb_notes"] = len(notes)
|
||||
e["nb_abs"] = len([x for x in notes if x is None])
|
||||
e["nb_neutre"] = len([x for x in notes if x == NOTES_NEUTRALISE])
|
||||
e["nb_att"] = len([x for x in notes if x == NOTES_ATTENTE])
|
||||
e["notes"] = NotesDB
|
||||
e["notes"] = notes_db
|
||||
|
||||
if e["etat"]["evalattente"]:
|
||||
attente = True
|
||||
|
@ -268,7 +278,7 @@ def do_moduleimpl_moyennes(nt, mod):
|
|||
]
|
||||
#
|
||||
R = {}
|
||||
formula = scu.unescape_html(mod["computation_expr"])
|
||||
formula = scu.unescape_html(modimpl["computation_expr"])
|
||||
formula_use_abs = "abs" in formula
|
||||
|
||||
for etudid in insmod_set: # inscrits au semestre et au module
|
||||
|
@ -289,15 +299,17 @@ def do_moduleimpl_moyennes(nt, mod):
|
|||
# il manque une note ! (si publish_incomplete, cela peut arriver, on ignore)
|
||||
if e["coefficient"] > 0 and not e["publish_incomplete"]:
|
||||
nb_missing += 1
|
||||
# ne devrait pas arriver ?
|
||||
log("\nXXX SCM298\n")
|
||||
if nb_missing == 0 and sum_coefs > 0:
|
||||
if sum_coefs > 0:
|
||||
R[etudid] = sum_notes / sum_coefs
|
||||
moy_valid = True
|
||||
else:
|
||||
R[etudid] = "na"
|
||||
R[etudid] = "NA"
|
||||
moy_valid = False
|
||||
else:
|
||||
R[etudid] = "NA%d" % nb_missing
|
||||
R[etudid] = "NA"
|
||||
moy_valid = False
|
||||
|
||||
if user_expr:
|
||||
|
@ -348,14 +360,14 @@ def do_moduleimpl_moyennes(nt, mod):
|
|||
if etudid in eval_rattr["notes"]:
|
||||
note = eval_rattr["notes"][etudid]["value"]
|
||||
if note != None and note != NOTES_NEUTRALISE and note != NOTES_ATTENTE:
|
||||
if isinstance(R[etudid], float):
|
||||
if not isinstance(R[etudid], float):
|
||||
R[etudid] = note
|
||||
else:
|
||||
note_sur_20 = note * 20.0 / eval_rattr["note_max"]
|
||||
if eval_rattr["evaluation_type"] == EVALUATION_RATTRAPAGE:
|
||||
# rattrapage classique: prend la meilleure note entre moyenne
|
||||
# module et note eval rattrapage
|
||||
if (R[etudid] == "NA0") or (note_sur_20 > R[etudid]):
|
||||
if (R[etudid] == "NA") or (note_sur_20 > R[etudid]):
|
||||
# log('note_sur_20=%s' % note_sur_20)
|
||||
R[etudid] = note_sur_20
|
||||
elif eval_rattr["evaluation_type"] == EVALUATION_SESSION2:
|
||||
|
@ -365,7 +377,7 @@ def do_moduleimpl_moyennes(nt, mod):
|
|||
return R, valid_evals, attente, diag_info
|
||||
|
||||
|
||||
def do_formsemestre_moyennes(nt, formsemestre_id):
|
||||
def formsemestre_compute_modimpls_moyennes(nt, formsemestre_id):
|
||||
"""retourne dict { moduleimpl_id : { etudid, note_moyenne_dans_ce_module } },
|
||||
la liste des moduleimpls, la liste des evaluations valides,
|
||||
liste des moduleimpls avec notes en attente.
|
||||
|
@ -375,7 +387,7 @@ def do_formsemestre_moyennes(nt, formsemestre_id):
|
|||
# args={"formsemestre_id": formsemestre_id}
|
||||
# )
|
||||
# etudids = [x["etudid"] for x in inscr]
|
||||
modimpls = sco_moduleimpl.do_moduleimpl_list(formsemestre_id=formsemestre_id)
|
||||
modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
|
||||
# recupere les moyennes des etudiants de tous les modules
|
||||
D = {}
|
||||
valid_evals = []
|
||||
|
@ -383,15 +395,16 @@ def do_formsemestre_moyennes(nt, formsemestre_id):
|
|||
mods_att = []
|
||||
expr_diags = []
|
||||
for modimpl in modimpls:
|
||||
mod = sco_edit_module.do_module_list(args={"module_id": modimpl["module_id"]})[
|
||||
0
|
||||
]
|
||||
mod = sco_edit_module.module_list(args={"module_id": modimpl["module_id"]})[0]
|
||||
modimpl["module"] = mod # add module dict to moduleimpl (used by nt)
|
||||
moduleimpl_id = modimpl["moduleimpl_id"]
|
||||
assert moduleimpl_id not in D
|
||||
D[moduleimpl_id], valid_evals_mod, attente, expr_diag = do_moduleimpl_moyennes(
|
||||
nt, modimpl
|
||||
)
|
||||
(
|
||||
D[moduleimpl_id],
|
||||
valid_evals_mod,
|
||||
attente,
|
||||
expr_diag,
|
||||
) = compute_moduleimpl_moyennes(nt, modimpl)
|
||||
valid_evals_per_mod[moduleimpl_id] = valid_evals_mod
|
||||
valid_evals += valid_evals_mod
|
||||
if attente:
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
|
||||
"""
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.scodoc.sco_logos import write_logo, find_logo, delete_logo
|
||||
import app
|
||||
from flask import current_app
|
||||
|
||||
|
||||
class Action:
|
||||
"""Base class for all classes describing an action from from config form."""
|
||||
|
||||
def __init__(self, message, parameters):
|
||||
self.message = message
|
||||
self.parameters = parameters
|
||||
|
||||
@staticmethod
|
||||
def build_action(parameters, stream=None):
|
||||
"""Check (from parameters) if some action has to be done and
|
||||
then return list of action (or else return empty list)."""
|
||||
raise NotImplementedError
|
||||
|
||||
def display(self):
|
||||
"""return a str describing the action to be done"""
|
||||
return self.message.format_map(self.parameters)
|
||||
|
||||
def execute(self):
|
||||
"""Executes the action"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
GLOBAL = "_"
|
||||
|
||||
|
||||
class LogoUpdate(Action):
|
||||
"""Action: change a logo
|
||||
dept_id: dept_id or '_',
|
||||
logo_id: logo_id,
|
||||
upload: image file replacement
|
||||
"""
|
||||
|
||||
def __init__(self, parameters):
|
||||
super().__init__(
|
||||
f"Modification du logo {parameters['logo_id']} pour le département {parameters['dept_id']}",
|
||||
parameters,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def build_action(parameters):
|
||||
dept_id = parameters["dept_key"]
|
||||
if dept_id == GLOBAL:
|
||||
dept_id = None
|
||||
parameters["dept_id"] = dept_id
|
||||
if parameters["upload"] is not None:
|
||||
return LogoUpdate(parameters)
|
||||
return None
|
||||
|
||||
def execute(self):
|
||||
current_app.logger.info(self.message)
|
||||
write_logo(
|
||||
stream=self.parameters["upload"],
|
||||
dept_id=self.parameters["dept_id"],
|
||||
name=self.parameters["logo_id"],
|
||||
)
|
||||
|
||||
|
||||
class LogoDelete(Action):
|
||||
"""Action: Delete an existing logo
|
||||
dept_id: dept_id or '_',
|
||||
logo_id: logo_id
|
||||
"""
|
||||
|
||||
def __init__(self, parameters):
|
||||
super().__init__(
|
||||
f"Suppression du logo {parameters['logo_id']} pour le département {parameters['dept_id'] or 'tous'}.",
|
||||
parameters,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def build_action(parameters):
|
||||
parameters["dept_id"] = parameters["dept_key"]
|
||||
if parameters["dept_key"] == GLOBAL:
|
||||
parameters["dept_id"] = None
|
||||
if parameters["do_delete"]:
|
||||
return LogoDelete(parameters)
|
||||
return None
|
||||
|
||||
def execute(self):
|
||||
current_app.logger.info(self.message)
|
||||
delete_logo(name=self.parameters["logo_id"], dept_id=self.parameters["dept_id"])
|
||||
|
||||
|
||||
class LogoInsert(Action):
|
||||
"""Action: add a new logo
|
||||
dept_key: dept_id or '_',
|
||||
logo_id: logo_id,
|
||||
upload: image file replacement
|
||||
"""
|
||||
|
||||
def __init__(self, parameters):
|
||||
super().__init__(
|
||||
f"Ajout du logo {parameters['name']} pour le département {parameters['dept_key']} ({parameters['upload'].filename}).",
|
||||
parameters,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def build_action(parameters):
|
||||
if parameters["dept_key"] == GLOBAL:
|
||||
parameters["dept_id"] = None
|
||||
if parameters["upload"] and parameters["name"]:
|
||||
logo = find_logo(
|
||||
logoname=parameters["name"], dept_id=parameters["dept_key"]
|
||||
)
|
||||
if logo is None:
|
||||
return LogoInsert(parameters)
|
||||
return None
|
||||
|
||||
def execute(self):
|
||||
dept_id = self.parameters["dept_key"]
|
||||
if dept_id == GLOBAL:
|
||||
dept_id = None
|
||||
current_app.logger.info(self.message)
|
||||
write_logo(
|
||||
stream=self.parameters["upload"],
|
||||
name=self.parameters["name"],
|
||||
dept_id=dept_id,
|
||||
)
|
||||
|
||||
|
||||
class BonusSportUpdate(Action):
|
||||
"""Action: Change bonus_sport_function_name.
|
||||
bonus_sport_function_name: the new value"""
|
||||
|
||||
def __init__(self, parameters):
|
||||
super().__init__(
|
||||
f"Changement du calcul de bonus sport pour ({parameters['bonus_sport_func_name']}).",
|
||||
parameters,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def build_action(parameters):
|
||||
if (
|
||||
parameters["bonus_sport_func_name"]
|
||||
!= ScoDocSiteConfig.get_bonus_sport_func_name()
|
||||
):
|
||||
return [BonusSportUpdate(parameters)]
|
||||
return []
|
||||
|
||||
def execute(self):
|
||||
current_app.logger.info(self.message)
|
||||
ScoDocSiteConfig.set_bonus_sport_func(self.parameters["bonus_sport_func_name"])
|
||||
app.clear_scodoc_cache()
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -30,6 +30,8 @@
|
|||
|
||||
(coût théorique en heures équivalent TD)
|
||||
"""
|
||||
from flask import request
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc import sco_formsemestre
|
||||
|
@ -45,7 +47,6 @@ def formsemestre_table_estim_cost(
|
|||
n_group_tp=1,
|
||||
coef_tp=1,
|
||||
coef_cours=1.5,
|
||||
REQUEST=None,
|
||||
):
|
||||
"""
|
||||
Rapports estimation coût de formation basé sur le programme pédagogique
|
||||
|
@ -58,9 +59,7 @@ def formsemestre_table_estim_cost(
|
|||
"""
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
sco_formsemestre_status.fill_formsemestre(sem)
|
||||
Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list(
|
||||
formsemestre_id=formsemestre_id
|
||||
)
|
||||
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
|
||||
T = []
|
||||
for M in Mlist:
|
||||
Mod = M["module"]
|
||||
|
@ -156,7 +155,6 @@ def formsemestre_estim_cost(
|
|||
coef_tp=1,
|
||||
coef_cours=1.5,
|
||||
format="html",
|
||||
REQUEST=None,
|
||||
):
|
||||
"""Page (formulaire) estimation coûts"""
|
||||
|
||||
|
@ -171,7 +169,6 @@ def formsemestre_estim_cost(
|
|||
n_group_tp=n_group_tp,
|
||||
coef_tp=coef_tp,
|
||||
coef_cours=coef_cours,
|
||||
REQUEST=REQUEST,
|
||||
)
|
||||
h = """
|
||||
<form name="f" method="get" action="%s">
|
||||
|
@ -182,7 +179,7 @@ def formsemestre_estim_cost(
|
|||
<br/>
|
||||
</form>
|
||||
""" % (
|
||||
REQUEST.URL0,
|
||||
request.base_url,
|
||||
formsemestre_id,
|
||||
n_group_td,
|
||||
n_group_tp,
|
||||
|
@ -190,11 +187,11 @@ def formsemestre_estim_cost(
|
|||
)
|
||||
tab.html_before_table = h
|
||||
tab.base_url = "%s?formsemestre_id=%s&n_group_td=%s&n_group_tp=%s&coef_tp=%s" % (
|
||||
REQUEST.URL0,
|
||||
request.base_url,
|
||||
formsemestre_id,
|
||||
n_group_td,
|
||||
n_group_tp,
|
||||
coef_tp,
|
||||
)
|
||||
|
||||
return tab.make_page(format=format, REQUEST=REQUEST)
|
||||
return tab.make_page(format=format)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -29,7 +29,7 @@
|
|||
Rapport (table) avec dernier semestre fréquenté et débouché de chaque étudiant
|
||||
"""
|
||||
import http
|
||||
from flask import url_for, g
|
||||
from flask import url_for, g, request
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
@ -47,10 +47,20 @@ from app.scodoc import sco_etud
|
|||
import sco_version
|
||||
|
||||
|
||||
def report_debouche_date(start_year=None, format="html", REQUEST=None):
|
||||
"""Rapport (table) pour les débouchés des étudiants sortis à partir de l'année indiquée."""
|
||||
def report_debouche_date(start_year=None, format="html"):
|
||||
"""Rapport (table) pour les débouchés des étudiants sortis
|
||||
à partir de l'année indiquée.
|
||||
"""
|
||||
if not start_year:
|
||||
return report_debouche_ask_date(REQUEST=REQUEST)
|
||||
return report_debouche_ask_date("Année de début de la recherche")
|
||||
else:
|
||||
try:
|
||||
start_year = int(start_year)
|
||||
except ValueError:
|
||||
return report_debouche_ask_date(
|
||||
"Année invalide. Année de début de la recherche"
|
||||
)
|
||||
|
||||
if format == "xls":
|
||||
keep_numeric = True # pas de conversion des notes en strings
|
||||
else:
|
||||
|
@ -64,13 +74,12 @@ def report_debouche_date(start_year=None, format="html", REQUEST=None):
|
|||
"Généré par %s le " % sco_version.SCONAME + scu.timedate_human_repr() + ""
|
||||
)
|
||||
tab.caption = "Récapitulatif débouchés à partir du 1/1/%s." % start_year
|
||||
tab.base_url = "%s?start_year=%s" % (REQUEST.URL0, start_year)
|
||||
tab.base_url = "%s?start_year=%s" % (request.base_url, start_year)
|
||||
return tab.make_page(
|
||||
title="""<h2 class="formsemestre">Débouchés étudiants </h2>""",
|
||||
init_qtip=True,
|
||||
javascripts=["js/etud_info.js"],
|
||||
format=format,
|
||||
REQUEST=REQUEST,
|
||||
with_html_headers=True,
|
||||
)
|
||||
|
||||
|
@ -97,8 +106,9 @@ def get_etudids_with_debouche(start_year):
|
|||
FROM notes_formsemestre_inscription i, notes_formsemestre s, itemsuivi it
|
||||
WHERE i.etudid = it.etudid
|
||||
AND i.formsemestre_id = s.id AND s.date_fin >= %(start_date)s
|
||||
AND s.dept_id = %(dept_id)s
|
||||
""",
|
||||
{"start_date": start_date},
|
||||
{"start_date": start_date, "dept_id": g.scodoc_dept_id},
|
||||
)
|
||||
|
||||
return [x["etudid"] for x in r]
|
||||
|
@ -194,15 +204,16 @@ def table_debouche_etudids(etudids, keep_numeric=True):
|
|||
return tab
|
||||
|
||||
|
||||
def report_debouche_ask_date(REQUEST=None):
|
||||
def report_debouche_ask_date(msg: str) -> str:
|
||||
"""Formulaire demande date départ"""
|
||||
return (
|
||||
html_sco_header.sco_header()
|
||||
+ """<form method="GET">
|
||||
Date de départ de la recherche: <input type="text" name="start_year" value="" size=10/>
|
||||
</form>"""
|
||||
+ html_sco_header.sco_footer()
|
||||
)
|
||||
return f"""{html_sco_header.sco_header()}
|
||||
<h2>Table des débouchés des étudiants</h2>
|
||||
<form method="GET">
|
||||
{msg}
|
||||
<input type="text" name="start_year" value="" size=10/>
|
||||
</form>
|
||||
{html_sco_header.sco_footer()}
|
||||
"""
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
|
@ -249,7 +260,7 @@ def itemsuivi_get(cnx, itemsuivi_id, ignore_errors=False):
|
|||
return None
|
||||
|
||||
|
||||
def itemsuivi_suppress(itemsuivi_id, REQUEST=None):
|
||||
def itemsuivi_suppress(itemsuivi_id):
|
||||
"""Suppression d'un item"""
|
||||
if not sco_permissions_check.can_edit_suivi():
|
||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||
|
@ -259,9 +270,10 @@ def itemsuivi_suppress(itemsuivi_id, REQUEST=None):
|
|||
_itemsuivi_delete(cnx, itemsuivi_id)
|
||||
logdb(cnx, method="itemsuivi_suppress", etudid=item["etudid"])
|
||||
log("suppressed itemsuivi %s" % (itemsuivi_id,))
|
||||
return ("", 204)
|
||||
|
||||
|
||||
def itemsuivi_create(etudid, item_date=None, situation="", REQUEST=None, format=None):
|
||||
def itemsuivi_create(etudid, item_date=None, situation="", format=None):
|
||||
"""Creation d'un item"""
|
||||
if not sco_permissions_check.can_edit_suivi():
|
||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||
|
@ -273,11 +285,11 @@ def itemsuivi_create(etudid, item_date=None, situation="", REQUEST=None, format=
|
|||
log("created itemsuivi %s for %s" % (itemsuivi_id, etudid))
|
||||
item = itemsuivi_get(cnx, itemsuivi_id)
|
||||
if format == "json":
|
||||
return scu.sendJSON(REQUEST, item)
|
||||
return scu.sendJSON(item)
|
||||
return item
|
||||
|
||||
|
||||
def itemsuivi_set_date(itemsuivi_id, item_date, REQUEST=None):
|
||||
def itemsuivi_set_date(itemsuivi_id, item_date):
|
||||
"""set item date
|
||||
item_date is a string dd/mm/yyyy
|
||||
"""
|
||||
|
@ -288,9 +300,10 @@ def itemsuivi_set_date(itemsuivi_id, item_date, REQUEST=None):
|
|||
item = itemsuivi_get(cnx, itemsuivi_id)
|
||||
item["item_date"] = item_date
|
||||
_itemsuivi_edit(cnx, item)
|
||||
return ("", 204)
|
||||
|
||||
|
||||
def itemsuivi_set_situation(object, value, REQUEST=None):
|
||||
def itemsuivi_set_situation(object, value):
|
||||
"""set situation"""
|
||||
if not sco_permissions_check.can_edit_suivi():
|
||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||
|
@ -304,14 +317,14 @@ def itemsuivi_set_situation(object, value, REQUEST=None):
|
|||
return situation or scu.IT_SITUATION_MISSING_STR
|
||||
|
||||
|
||||
def itemsuivi_list_etud(etudid, format=None, REQUEST=None):
|
||||
def itemsuivi_list_etud(etudid, format=None):
|
||||
"""Liste des items pour cet étudiant, avec tags"""
|
||||
cnx = ndb.GetDBConnexion()
|
||||
items = _itemsuivi_list(cnx, {"etudid": etudid})
|
||||
for it in items:
|
||||
it["tags"] = ", ".join(itemsuivi_tag_list(it["itemsuivi_id"]))
|
||||
if format == "json":
|
||||
return scu.sendJSON(REQUEST, items)
|
||||
return scu.sendJSON(items)
|
||||
return items
|
||||
|
||||
|
||||
|
@ -328,7 +341,7 @@ def itemsuivi_tag_list(itemsuivi_id):
|
|||
return [x["title"] for x in r]
|
||||
|
||||
|
||||
def itemsuivi_tag_search(term, REQUEST=None):
|
||||
def itemsuivi_tag_search(term):
|
||||
"""List all used tag names (for auto-completion)"""
|
||||
# restrict charset to avoid injections
|
||||
if not scu.ALPHANUM_EXP.match(term):
|
||||
|
@ -343,10 +356,10 @@ def itemsuivi_tag_search(term, REQUEST=None):
|
|||
)
|
||||
data = [x["title"] for x in r]
|
||||
|
||||
return scu.sendJSON(REQUEST, data)
|
||||
return scu.sendJSON(data)
|
||||
|
||||
|
||||
def itemsuivi_tag_set(itemsuivi_id="", taglist=[], REQUEST=None):
|
||||
def itemsuivi_tag_set(itemsuivi_id="", taglist=None):
|
||||
"""taglist may either be:
|
||||
a string with tag names separated by commas ("un;deux")
|
||||
or a list of strings (["un", "deux"])
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -28,7 +28,7 @@
|
|||
"""Page accueil département (liste des semestres, etc)
|
||||
"""
|
||||
|
||||
from flask import g
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
||||
import app
|
||||
|
@ -46,8 +46,9 @@ from app.scodoc import sco_up_to_date
|
|||
from app.scodoc import sco_users
|
||||
|
||||
|
||||
def index_html(REQUEST=None, showcodes=0, showsemtable=0):
|
||||
def index_html(showcodes=0, showsemtable=0):
|
||||
"Page accueil département (liste des semestres)"
|
||||
showcodes = int(showcodes)
|
||||
showsemtable = int(showsemtable)
|
||||
H = []
|
||||
|
||||
|
@ -78,7 +79,7 @@ def index_html(REQUEST=None, showcodes=0, showsemtable=0):
|
|||
# Responsable de formation:
|
||||
sco_formsemestre.sem_set_responsable_name(sem)
|
||||
|
||||
if showcodes == "1":
|
||||
if showcodes:
|
||||
sem["tmpcode"] = "<td><tt>%s</tt></td>" % sem["formsemestre_id"]
|
||||
else:
|
||||
sem["tmpcode"] = ""
|
||||
|
@ -126,12 +127,12 @@ def index_html(REQUEST=None, showcodes=0, showsemtable=0):
|
|||
"""
|
||||
% sco_preferences.get_preference("DeptName")
|
||||
)
|
||||
H.append(_sem_table_gt(sems).html())
|
||||
H.append(_sem_table_gt(sems, showcodes=showcodes).html())
|
||||
H.append("</table>")
|
||||
if not showsemtable:
|
||||
H.append(
|
||||
'<hr/><p><a href="%s?showsemtable=1">Voir tous les semestres</a></p>'
|
||||
% REQUEST.URL0
|
||||
% request.base_url
|
||||
)
|
||||
|
||||
H.append(
|
||||
|
@ -242,7 +243,7 @@ def _sem_table_gt(sems, showcodes=False):
|
|||
rows=sems,
|
||||
html_class="table_leftalign semlist",
|
||||
html_sortable=True,
|
||||
# base_url = '%s?formsemestre_id=%s' % (REQUEST.URL0, formsemestre_id),
|
||||
# base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id),
|
||||
# caption='Maquettes enregistrées',
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -51,6 +51,7 @@ import fcntl
|
|||
import subprocess
|
||||
import requests
|
||||
|
||||
from flask_login import current_user
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
@ -64,7 +65,7 @@ from app.scodoc.sco_exceptions import ScoValueError
|
|||
SCO_DUMP_LOCK = "/tmp/scodump.lock"
|
||||
|
||||
|
||||
def sco_dump_and_send_db(REQUEST=None):
|
||||
def sco_dump_and_send_db():
|
||||
"""Dump base de données et l'envoie anonymisée pour debug"""
|
||||
H = [html_sco_header.sco_header(page_title="Assistance technique")]
|
||||
# get currect (dept) DB name:
|
||||
|
@ -93,7 +94,7 @@ def sco_dump_and_send_db(REQUEST=None):
|
|||
_anonymize_db(ano_db_name)
|
||||
|
||||
# Send
|
||||
r = _send_db(REQUEST, ano_db_name)
|
||||
r = _send_db(ano_db_name)
|
||||
if (
|
||||
r.status_code
|
||||
== requests.codes.INSUFFICIENT_STORAGE # pylint: disable=no-member
|
||||
|
@ -166,34 +167,33 @@ def _anonymize_db(ano_db_name):
|
|||
|
||||
def _get_scodoc_serial():
|
||||
try:
|
||||
return int(open(os.path.join(scu.SCODOC_VERSION_DIR, "scodoc.sn")).read())
|
||||
with open(os.path.join(scu.SCODOC_VERSION_DIR, "scodoc.sn")) as f:
|
||||
return int(f.read())
|
||||
except:
|
||||
return 0
|
||||
|
||||
|
||||
def _send_db(REQUEST, ano_db_name):
|
||||
def _send_db(ano_db_name):
|
||||
"""Dump this (anonymized) database and send it to tech support"""
|
||||
log("dumping anonymized database {}".format(ano_db_name))
|
||||
log(f"dumping anonymized database {ano_db_name}")
|
||||
try:
|
||||
data = subprocess.check_output("pg_dump {} | gzip".format(ano_db_name), shell=1)
|
||||
except subprocess.CalledProcessError as e:
|
||||
log("sco_dump_and_send_db: exception in anonymisation: {}".format(e))
|
||||
raise ScoValueError(
|
||||
"erreur lors de l'anonymisation de la base {}".format(ano_db_name)
|
||||
dump = subprocess.check_output(
|
||||
f"pg_dump --format=custom {ano_db_name}", shell=1
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
log(f"sco_dump_and_send_db: exception in anonymisation: {e}")
|
||||
raise ScoValueError(f"erreur lors de l'anonymisation de la base {ano_db_name}")
|
||||
|
||||
log("uploading anonymized dump...")
|
||||
files = {"file": (ano_db_name + ".gz", data)}
|
||||
files = {"file": (ano_db_name + ".dump", dump)}
|
||||
r = requests.post(
|
||||
scu.SCO_DUMP_UP_URL,
|
||||
files=files,
|
||||
data={
|
||||
"dept_name": sco_preferences.get_preference("DeptName"),
|
||||
"serial": _get_scodoc_serial(),
|
||||
"sco_user": str(REQUEST.AUTHENTICATED_USER),
|
||||
"sent_by": sco_users.user_info(str(REQUEST.AUTHENTICATED_USER))[
|
||||
"nomcomplet"
|
||||
],
|
||||
"sco_user": str(current_user),
|
||||
"sent_by": sco_users.user_info(str(current_user))["nomcomplet"],
|
||||
"sco_version": sco_version.SCOVERSION,
|
||||
"sco_fullversion": scu.get_scodoc_version(),
|
||||
},
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
##############################################################################
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Édition formation APC (BUT)
|
||||
"""
|
||||
import flask
|
||||
from flask import url_for
|
||||
from flask.templating import render_template
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db
|
||||
from app.models import Formation, UniteEns, Matiere, Module, FormSemestre, ModuleImpl
|
||||
from app.models.notes import ScolarFormSemestreValidation
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
def html_edit_formation_apc(
|
||||
formation,
|
||||
semestre_idx=None,
|
||||
editable=True,
|
||||
tag_editable=True,
|
||||
):
|
||||
"""Formulaire html pour visualisation ou édition d'une formation APC.
|
||||
- Les UEs
|
||||
- Les ressources
|
||||
- Les SAÉs
|
||||
"""
|
||||
parcours = formation.get_parcours()
|
||||
assert parcours.APC_SAE
|
||||
ressources = formation.modules.filter_by(module_type=ModuleType.RESSOURCE).order_by(
|
||||
Module.semestre_id, Module.numero, Module.code
|
||||
)
|
||||
saes = formation.modules.filter_by(module_type=ModuleType.SAE).order_by(
|
||||
Module.semestre_id, Module.numero, Module.code
|
||||
)
|
||||
if semestre_idx is None:
|
||||
semestre_ids = range(1, parcours.NB_SEM + 1)
|
||||
else:
|
||||
semestre_ids = [semestre_idx]
|
||||
other_modules = formation.modules.filter(
|
||||
Module.module_type.is_distinct_from(ModuleType.SAE),
|
||||
Module.module_type.is_distinct_from(ModuleType.RESSOURCE),
|
||||
).order_by(
|
||||
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
|
||||
)
|
||||
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
|
||||
|
||||
icons = {
|
||||
"arrow_up": arrow_up,
|
||||
"arrow_down": arrow_down,
|
||||
"arrow_none": arrow_none,
|
||||
"delete": scu.icontag(
|
||||
"delete_small_img",
|
||||
title="Supprimer (module inutilisé)",
|
||||
alt="supprimer",
|
||||
),
|
||||
"delete_disabled": scu.icontag(
|
||||
"delete_small_dis_img",
|
||||
title="Suppression impossible (utilisé dans des semestres)",
|
||||
),
|
||||
}
|
||||
|
||||
H = [
|
||||
render_template(
|
||||
"pn/form_ues.html",
|
||||
formation=formation,
|
||||
semestre_ids=semestre_ids,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
UniteEns=UniteEns,
|
||||
),
|
||||
]
|
||||
for semestre_idx in semestre_ids:
|
||||
ressources_in_sem = ressources.filter_by(semestre_id=semestre_idx)
|
||||
saes_in_sem = saes.filter_by(semestre_id=semestre_idx)
|
||||
other_modules_in_sem = other_modules.filter_by(semestre_id=semestre_idx)
|
||||
H += [
|
||||
render_template(
|
||||
"pn/form_mods.html",
|
||||
formation=formation,
|
||||
titre=f"Ressources du S{semestre_idx}",
|
||||
create_element_msg="créer une nouvelle ressource",
|
||||
modules=ressources_in_sem,
|
||||
module_type=ModuleType.RESSOURCE,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
scu=scu,
|
||||
),
|
||||
render_template(
|
||||
"pn/form_mods.html",
|
||||
formation=formation,
|
||||
titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}",
|
||||
create_element_msg="créer une nouvelle SAÉ",
|
||||
modules=saes_in_sem,
|
||||
module_type=ModuleType.SAE,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
scu=scu,
|
||||
),
|
||||
render_template(
|
||||
"pn/form_mods.html",
|
||||
formation=formation,
|
||||
titre=f"Autres modules (non BUT) du S{semestre_idx}",
|
||||
create_element_msg="créer un nouveau module",
|
||||
modules=other_modules_in_sem,
|
||||
module_type=ModuleType.STANDARD,
|
||||
editable=editable,
|
||||
tag_editable=tag_editable,
|
||||
icons=icons,
|
||||
scu=scu,
|
||||
),
|
||||
]
|
||||
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def html_ue_infos(ue):
|
||||
"""page d'information sur une UE"""
|
||||
from app.views import ScoData
|
||||
|
||||
formsemestres = (
|
||||
db.session.query(FormSemestre)
|
||||
.filter(
|
||||
ue.id == Module.ue_id,
|
||||
Module.id == ModuleImpl.module_id,
|
||||
FormSemestre.id == ModuleImpl.formsemestre_id,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
nb_etuds_valid_ue = ScolarFormSemestreValidation.query.filter_by(
|
||||
ue_id=ue.id
|
||||
).count()
|
||||
can_safely_be_suppressed = (
|
||||
(nb_etuds_valid_ue == 0)
|
||||
and (len(formsemestres) == 0)
|
||||
and ue.modules.count() == 0
|
||||
and ue.matieres.count() == 0
|
||||
)
|
||||
return render_template(
|
||||
"pn/ue_infos.html",
|
||||
# "pn/tmp.html",
|
||||
titre=f"UE {ue.acronyme} {ue.titre}",
|
||||
ue=ue,
|
||||
formsemestres=formsemestres,
|
||||
nb_etuds_valid_ue=nb_etuds_valid_ue,
|
||||
can_safely_be_suppressed=can_safely_be_suppressed,
|
||||
sco=ScoData(),
|
||||
)
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -29,11 +29,16 @@
|
|||
(portage from DTML)
|
||||
"""
|
||||
import flask
|
||||
from flask import g, url_for
|
||||
from flask import g, url_for, request
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models.formations import Formation
|
||||
from app.models.modules import Module
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
||||
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError
|
||||
|
||||
|
@ -47,7 +52,7 @@ from app.scodoc import sco_formsemestre
|
|||
from app.scodoc import sco_news
|
||||
|
||||
|
||||
def formation_delete(formation_id=None, dialog_confirmed=False, REQUEST=None):
|
||||
def formation_delete(formation_id=None, dialog_confirmed=False):
|
||||
"""Delete a formation"""
|
||||
F = sco_formations.formation_list(args={"formation_id": formation_id})
|
||||
if not F:
|
||||
|
@ -104,7 +109,7 @@ def do_formation_delete(oid):
|
|||
raise ScoLockedFormError()
|
||||
cnx = ndb.GetDBConnexion()
|
||||
# delete all UE in this formation
|
||||
ues = sco_edit_ue.do_ue_list({"formation_id": oid})
|
||||
ues = sco_edit_ue.ue_list({"formation_id": oid})
|
||||
for ue in ues:
|
||||
sco_edit_ue.do_ue_delete(ue["ue_id"], force=True)
|
||||
|
||||
|
@ -119,12 +124,12 @@ def do_formation_delete(oid):
|
|||
)
|
||||
|
||||
|
||||
def formation_create(REQUEST=None):
|
||||
def formation_create():
|
||||
"""Creation d'une formation"""
|
||||
return formation_edit(create=True, REQUEST=REQUEST)
|
||||
return formation_edit(create=True)
|
||||
|
||||
|
||||
def formation_edit(formation_id=None, create=False, REQUEST=None):
|
||||
def formation_edit(formation_id=None, create=False):
|
||||
"""Edit or create a formation"""
|
||||
if create:
|
||||
H = [
|
||||
|
@ -159,8 +164,8 @@ def formation_edit(formation_id=None, create=False, REQUEST=None):
|
|||
)
|
||||
|
||||
tf = TrivialFormulator(
|
||||
REQUEST.URL0,
|
||||
REQUEST.form,
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
(
|
||||
("formation_id", {"default": formation_id, "input_type": "hidden"}),
|
||||
(
|
||||
|
@ -205,6 +210,7 @@ def formation_edit(formation_id=None, create=False, REQUEST=None):
|
|||
"size": 12,
|
||||
"title": "Code formation",
|
||||
"explanation": "code interne. Toutes les formations partageant le même code sont compatibles (compensation de semestres, capitalisation d'UE). Laisser vide si vous ne savez pas, ou entrer le code d'une formation existante.",
|
||||
"validator": lambda val, _: len(val) < SHORT_STR_LEN,
|
||||
},
|
||||
),
|
||||
(
|
||||
|
@ -252,7 +258,7 @@ def formation_edit(formation_id=None, create=False, REQUEST=None):
|
|||
do_formation_edit(tf[2])
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.ue_list", scodoc_dept=g.scodoc_dept, formation_id=formation_id
|
||||
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -298,66 +304,64 @@ def do_formation_edit(args):
|
|||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
sco_formations._formationEditor.edit(cnx, args)
|
||||
invalidate_sems_in_formation(args["formation_id"])
|
||||
formation: Formation = Formation.query.get(args["formation_id"])
|
||||
formation.invalidate_cached_sems()
|
||||
|
||||
|
||||
def invalidate_sems_in_formation(formation_id):
|
||||
"Invalide les semestres utilisant cette formation"
|
||||
for sem in sco_formsemestre.do_formsemestre_list(
|
||||
args={"formation_id": formation_id}
|
||||
):
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=sem["formsemestre_id"]
|
||||
) # > formation modif.
|
||||
|
||||
|
||||
def module_move(module_id, after=0, REQUEST=None, redirect=1):
|
||||
def module_move(module_id, after=0, redirect=True):
|
||||
"""Move before/after previous one (decrement/increment numero)"""
|
||||
module = sco_edit_module.do_module_list({"module_id": module_id})[0]
|
||||
redirect = int(redirect)
|
||||
redirect = bool(redirect)
|
||||
module = Module.query.get_or_404(module_id)
|
||||
after = int(after) # 0: deplace avant, 1 deplace apres
|
||||
if after not in (0, 1):
|
||||
raise ValueError('invalid value for "after"')
|
||||
formation_id = module["formation_id"]
|
||||
others = sco_edit_module.do_module_list({"matiere_id": module["matiere_id"]})
|
||||
# log('others=%s' % others)
|
||||
if len(others) > 1:
|
||||
idx = [p["module_id"] for p in others].index(module_id)
|
||||
# log('module_move: after=%s idx=%s' % (after, idx))
|
||||
raise ValueError(f'invalid value for "after" ({after})')
|
||||
if module.formation.is_apc():
|
||||
# pas de matières, mais on prend tous les modules de même type de la formation
|
||||
query = Module.query.filter_by(
|
||||
semestre_id=module.semestre_id,
|
||||
formation=module.formation,
|
||||
module_type=module.module_type,
|
||||
)
|
||||
else:
|
||||
query = Module.query.filter_by(matiere=module.matiere)
|
||||
modules = query.order_by(Module.numero, Module.code).all()
|
||||
if len({o.numero for o in modules}) != len(modules):
|
||||
# il y a des numeros identiques !
|
||||
scu.objects_renumber(db, modules)
|
||||
if len(modules) > 1:
|
||||
idx = [m.id for m in modules].index(module.id)
|
||||
neigh = None # object to swap with
|
||||
if after == 0 and idx > 0:
|
||||
neigh = others[idx - 1]
|
||||
elif after == 1 and idx < len(others) - 1:
|
||||
neigh = others[idx + 1]
|
||||
if neigh: #
|
||||
# swap numero between partition and its neighbor
|
||||
# log('moving module %s' % module_id)
|
||||
cnx = ndb.GetDBConnexion()
|
||||
module["numero"], neigh["numero"] = neigh["numero"], module["numero"]
|
||||
if module["numero"] == neigh["numero"]:
|
||||
neigh["numero"] -= 2 * after - 1
|
||||
sco_edit_module._moduleEditor.edit(cnx, module)
|
||||
sco_edit_module._moduleEditor.edit(cnx, neigh)
|
||||
|
||||
neigh = modules[idx - 1]
|
||||
elif after == 1 and idx < len(modules) - 1:
|
||||
neigh = modules[idx + 1]
|
||||
if neigh: # échange les numéros
|
||||
module.numero, neigh.numero = neigh.numero, module.numero
|
||||
db.session.add(module)
|
||||
db.session.add(neigh)
|
||||
db.session.commit()
|
||||
# redirect to ue_list page:
|
||||
if redirect:
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.ue_list", scodoc_dept=g.scodoc_dept, formation_id=formation_id
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=module.formation.id,
|
||||
semestre_idx=module.ue.semestre_idx,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def ue_move(ue_id, after=0, redirect=1):
|
||||
"""Move UE before/after previous one (decrement/increment numero)"""
|
||||
o = sco_edit_ue.do_ue_list({"ue_id": ue_id})[0]
|
||||
o = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
|
||||
# log('ue_move %s (#%s) after=%s' % (ue_id, o['numero'], after))
|
||||
redirect = int(redirect)
|
||||
after = int(after) # 0: deplace avant, 1 deplace apres
|
||||
if after not in (0, 1):
|
||||
raise ValueError('invalid value for "after"')
|
||||
formation_id = o["formation_id"]
|
||||
others = sco_edit_ue.do_ue_list({"formation_id": formation_id})
|
||||
others = sco_edit_ue.ue_list({"formation_id": formation_id})
|
||||
if len(others) > 1:
|
||||
idx = [p["ue_id"] for p in others].index(ue_id)
|
||||
neigh = None # object to swap with
|
||||
|
@ -378,8 +382,9 @@ def ue_move(ue_id, after=0, redirect=1):
|
|||
if redirect:
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.ue_list",
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=o["formation_id"],
|
||||
semestre_idx=o["semestre_idx"],
|
||||
)
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -29,13 +29,19 @@
|
|||
(portage from DTML)
|
||||
"""
|
||||
import flask
|
||||
from flask import g, url_for
|
||||
from flask import g, url_for, request
|
||||
from app.models.formations import Matiere
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.models import Formation
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
||||
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError
|
||||
from app.scodoc.sco_exceptions import (
|
||||
ScoValueError,
|
||||
ScoLockedFormError,
|
||||
ScoNonEmptyFormationObject,
|
||||
)
|
||||
from app.scodoc import html_sco_header
|
||||
|
||||
_matiereEditor = ndb.EditableTable(
|
||||
|
@ -47,7 +53,7 @@ _matiereEditor = ndb.EditableTable(
|
|||
)
|
||||
|
||||
|
||||
def do_matiere_list(*args, **kw):
|
||||
def matiere_list(*args, **kw):
|
||||
"list matieres"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
return _matiereEditor.list(cnx, *args, **kw)
|
||||
|
@ -60,13 +66,13 @@ def do_matiere_edit(*args, **kw):
|
|||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
# check
|
||||
mat = do_matiere_list({"matiere_id": args[0]["matiere_id"]})[0]
|
||||
mat = matiere_list({"matiere_id": args[0]["matiere_id"]})[0]
|
||||
if matiere_is_locked(mat["matiere_id"]):
|
||||
raise ScoLockedFormError()
|
||||
# edit
|
||||
_matiereEditor.edit(cnx, *args, **kw)
|
||||
formation_id = sco_edit_ue.do_ue_list({"ue_id": mat["ue_id"]})[0]["formation_id"]
|
||||
sco_edit_formation.invalidate_sems_in_formation(formation_id)
|
||||
formation_id = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]["formation_id"]
|
||||
Formation.query.get(formation_id).invalidate_cached_sems()
|
||||
|
||||
|
||||
def do_matiere_create(args):
|
||||
|
@ -77,7 +83,7 @@ def do_matiere_create(args):
|
|||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
# check
|
||||
ue = sco_edit_ue.do_ue_list({"ue_id": args["ue_id"]})[0]
|
||||
ue = sco_edit_ue.ue_list({"ue_id": args["ue_id"]})[0]
|
||||
# create matiere
|
||||
r = _matiereEditor.create(cnx, args)
|
||||
|
||||
|
@ -92,11 +98,11 @@ def do_matiere_create(args):
|
|||
return r
|
||||
|
||||
|
||||
def matiere_create(ue_id=None, REQUEST=None):
|
||||
def matiere_create(ue_id=None):
|
||||
"""Creation d'une matiere"""
|
||||
from app.scodoc import sco_edit_ue
|
||||
|
||||
UE = sco_edit_ue.do_ue_list(args={"ue_id": ue_id})[0]
|
||||
UE = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Création d'une matière"),
|
||||
"""<h2>Création d'une matière dans l'UE %(titre)s (%(acronyme)s)</h2>""" % UE,
|
||||
|
@ -116,8 +122,8 @@ associé.
|
|||
</p>""",
|
||||
]
|
||||
tf = TrivialFormulator(
|
||||
REQUEST.URL0,
|
||||
REQUEST.form,
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
(
|
||||
("ue_id", {"input_type": "hidden", "default": ue_id}),
|
||||
("titre", {"size": 30, "explanation": "nom de la matière."}),
|
||||
|
@ -134,7 +140,7 @@ associé.
|
|||
)
|
||||
|
||||
dest_url = url_for(
|
||||
"notes.ue_list", scodoc_dept=g.scodoc_dept, formation_id=UE["formation_id"]
|
||||
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=UE["formation_id"]
|
||||
)
|
||||
|
||||
if tf[0] == 0:
|
||||
|
@ -143,7 +149,7 @@ associé.
|
|||
return flask.redirect(dest_url)
|
||||
else:
|
||||
# check unicity
|
||||
mats = do_matiere_list(args={"ue_id": ue_id, "titre": tf[2]["titre"]})
|
||||
mats = matiere_list(args={"ue_id": ue_id, "titre": tf[2]["titre"]})
|
||||
if mats:
|
||||
return (
|
||||
"\n".join(H)
|
||||
|
@ -155,6 +161,16 @@ associé.
|
|||
return flask.redirect(dest_url)
|
||||
|
||||
|
||||
def can_delete_matiere(matiere: Matiere) -> tuple[bool, str]:
|
||||
"True si la matiere n'est pas utilisée dans des formsemestre"
|
||||
locked = matiere_is_locked(matiere.id)
|
||||
if locked:
|
||||
return False
|
||||
if any(m.modimpls.all() for m in matiere.modules):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def do_matiere_delete(oid):
|
||||
"delete matiere and attached modules"
|
||||
from app.scodoc import sco_formations
|
||||
|
@ -164,17 +180,16 @@ def do_matiere_delete(oid):
|
|||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
# check
|
||||
mat = do_matiere_list({"matiere_id": oid})[0]
|
||||
ue = sco_edit_ue.do_ue_list({"ue_id": mat["ue_id"]})[0]
|
||||
locked = matiere_is_locked(mat["matiere_id"])
|
||||
if locked:
|
||||
log("do_matiere_delete: mat=%s" % mat)
|
||||
log("do_matiere_delete: ue=%s" % ue)
|
||||
log("do_matiere_delete: locked sems: %s" % locked)
|
||||
raise ScoLockedFormError()
|
||||
log("do_matiere_delete: matiere_id=%s" % oid)
|
||||
matiere = Matiere.query.get_or_404(oid)
|
||||
mat = matiere_list({"matiere_id": oid})[0] # compat sco7
|
||||
ue = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]
|
||||
if not can_delete_matiere(matiere):
|
||||
# il y a au moins un modimpl dans un module de cette matière
|
||||
raise ScoNonEmptyFormationObject("Matière", matiere.titre)
|
||||
|
||||
log("do_matiere_delete: matiere_id=%s" % matiere.id)
|
||||
# delete all modules in this matiere
|
||||
mods = sco_edit_module.do_module_list({"matiere_id": oid})
|
||||
mods = sco_edit_module.module_list({"matiere_id": matiere.id})
|
||||
for mod in mods:
|
||||
sco_edit_module.do_module_delete(mod["module_id"])
|
||||
_matiereEditor.delete(cnx, oid)
|
||||
|
@ -189,23 +204,41 @@ def do_matiere_delete(oid):
|
|||
)
|
||||
|
||||
|
||||
def matiere_delete(matiere_id=None, REQUEST=None):
|
||||
"""Delete an UE"""
|
||||
def matiere_delete(matiere_id=None):
|
||||
"""Delete matière"""
|
||||
from app.scodoc import sco_edit_ue
|
||||
|
||||
M = do_matiere_list(args={"matiere_id": matiere_id})[0]
|
||||
UE = sco_edit_ue.do_ue_list(args={"ue_id": M["ue_id"]})[0]
|
||||
matiere = Matiere.query.get_or_404(matiere_id)
|
||||
if not can_delete_matiere(matiere):
|
||||
# il y a au moins un modimpl dans un module de cette matière
|
||||
raise ScoNonEmptyFormationObject(
|
||||
"Matière",
|
||||
matiere.titre,
|
||||
dest_url=url_for(
|
||||
"notes.ue_table",
|
||||
formation_id=matiere.ue.formation_id,
|
||||
semestre_idx=matiere.ue.semestre_idx,
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
),
|
||||
)
|
||||
|
||||
mat = matiere_list(args={"matiere_id": matiere_id})[0]
|
||||
UE = sco_edit_ue.ue_list(args={"ue_id": mat["ue_id"]})[0]
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Suppression d'une matière"),
|
||||
"<h2>Suppression de la matière %(titre)s" % M,
|
||||
"<h2>Suppression de la matière %(titre)s" % mat,
|
||||
" dans l'UE (%(acronyme)s))</h2>" % UE,
|
||||
]
|
||||
dest_url = scu.NotesURL() + "/ue_list?formation_id=" + str(UE["formation_id"])
|
||||
dest_url = url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=str(UE["formation_id"]),
|
||||
)
|
||||
tf = TrivialFormulator(
|
||||
REQUEST.URL0,
|
||||
REQUEST.form,
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
(("matiere_id", {"input_type": "hidden"}),),
|
||||
initvalues=M,
|
||||
initvalues=mat,
|
||||
submitlabel="Confirmer la suppression",
|
||||
cancelbutton="Annuler",
|
||||
)
|
||||
|
@ -218,22 +251,22 @@ def matiere_delete(matiere_id=None, REQUEST=None):
|
|||
return flask.redirect(dest_url)
|
||||
|
||||
|
||||
def matiere_edit(matiere_id=None, REQUEST=None):
|
||||
def matiere_edit(matiere_id=None):
|
||||
"""Edit matiere"""
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_edit_ue
|
||||
|
||||
F = do_matiere_list(args={"matiere_id": matiere_id})
|
||||
F = matiere_list(args={"matiere_id": matiere_id})
|
||||
if not F:
|
||||
raise ScoValueError("Matière inexistante !")
|
||||
F = F[0]
|
||||
U = sco_edit_ue.do_ue_list(args={"ue_id": F["ue_id"]})
|
||||
if not F:
|
||||
ues = sco_edit_ue.ue_list(args={"ue_id": F["ue_id"]})
|
||||
if not ues:
|
||||
raise ScoValueError("UE inexistante !")
|
||||
U = U[0]
|
||||
Fo = sco_formations.formation_list(args={"formation_id": U["formation_id"]})[0]
|
||||
ue = ues[0]
|
||||
Fo = sco_formations.formation_list(args={"formation_id": ue["formation_id"]})[0]
|
||||
|
||||
ues = sco_edit_ue.do_ue_list(args={"formation_id": U["formation_id"]})
|
||||
ues = sco_edit_ue.ue_list(args={"formation_id": ue["formation_id"]})
|
||||
ue_names = ["%(acronyme)s (%(titre)s)" % u for u in ues]
|
||||
ue_ids = [u["ue_id"] for u in ues]
|
||||
H = [
|
||||
|
@ -256,8 +289,8 @@ des notes.</em>
|
|||
associé.
|
||||
</p>"""
|
||||
tf = TrivialFormulator(
|
||||
REQUEST.URL0,
|
||||
REQUEST.form,
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
(
|
||||
("matiere_id", {"input_type": "hidden"}),
|
||||
(
|
||||
|
@ -278,15 +311,18 @@ associé.
|
|||
submitlabel="Modifier les valeurs",
|
||||
)
|
||||
|
||||
dest_url = scu.NotesURL() + "/ue_list?formation_id=" + str(U["formation_id"])
|
||||
|
||||
dest_url = url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=str(ue["formation_id"]),
|
||||
)
|
||||
if tf[0] == 0:
|
||||
return "\n".join(H) + tf[1] + help + html_sco_header.sco_footer()
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(dest_url)
|
||||
else:
|
||||
# check unicity
|
||||
mats = do_matiere_list(args={"ue_id": tf[2]["ue_id"], "titre": tf[2]["titre"]})
|
||||
mats = matiere_list(args={"ue_id": tf[2]["ue_id"], "titre": tf[2]["titre"]})
|
||||
if len(mats) > 1 or (len(mats) == 1 and mats[0]["matiere_id"] != matiere_id):
|
||||
return (
|
||||
"\n".join(H)
|
||||
|
@ -323,4 +359,4 @@ def matiere_is_locked(matiere_id):
|
|||
""",
|
||||
{"matiere_id": matiere_id},
|
||||
)
|
||||
return len(r) > 0
|
||||
return len(r) > 0
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -29,36 +29,32 @@
|
|||
(portage from DTML)
|
||||
"""
|
||||
import flask
|
||||
from flask import url_for, g
|
||||
from flask import url_for, render_template
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import Matiere, Module, UniteEns
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
from app import log
|
||||
from app import models
|
||||
from app.models import Formation
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError, ScoGenError
|
||||
from app.scodoc.sco_exceptions import (
|
||||
ScoValueError,
|
||||
ScoLockedFormError,
|
||||
ScoGenError,
|
||||
ScoNonEmptyFormationObject,
|
||||
)
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_edit_matiere
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_news
|
||||
|
||||
_MODULE_HELP = """<p class="help">
|
||||
Les modules sont décrits dans le programme pédagogique. Un module est pour ce
|
||||
logiciel l'unité pédagogique élémentaire. On va lui associer une note
|
||||
à travers des <em>évaluations</em>. <br/>
|
||||
Cette note (moyenne de module) sera utilisée pour calculer la moyenne
|
||||
générale (et la moyenne de l'UE à laquelle appartient le module). Pour
|
||||
cela, on utilisera le <em>coefficient</em> associé au module.
|
||||
</p>
|
||||
|
||||
<p class="help">Un module possède un enseignant responsable
|
||||
(typiquement celui qui dispense le cours magistral). On peut associer
|
||||
au module une liste d'enseignants (typiquement les chargés de TD).
|
||||
Tous ces enseignants, plus le responsable du semestre, pourront
|
||||
saisir et modifier les notes de ce module.
|
||||
</p> """
|
||||
|
||||
_moduleEditor = ndb.EditableTable(
|
||||
"notes_modules",
|
||||
"module_id",
|
||||
|
@ -93,14 +89,14 @@ _moduleEditor = ndb.EditableTable(
|
|||
)
|
||||
|
||||
|
||||
def do_module_list(*args, **kw):
|
||||
def module_list(*args, **kw):
|
||||
"list modules"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
return _moduleEditor.list(cnx, *args, **kw)
|
||||
|
||||
|
||||
def do_module_create(args) -> int:
|
||||
"create a module"
|
||||
"Create a module. Returns id of new object."
|
||||
# create
|
||||
from app.scodoc import sco_formations
|
||||
|
||||
|
@ -118,77 +114,164 @@ def do_module_create(args) -> int:
|
|||
return r
|
||||
|
||||
|
||||
def module_create(matiere_id=None, REQUEST=None):
|
||||
"""Creation d'un module"""
|
||||
def module_create(matiere_id=None, module_type=None, semestre_id=None):
|
||||
"""Création d'un module"""
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_edit_ue
|
||||
|
||||
if matiere_id is None:
|
||||
matiere = Matiere.query.get_or_404(matiere_id)
|
||||
if matiere is None:
|
||||
raise ScoValueError("invalid matiere !")
|
||||
M = sco_edit_matiere.do_matiere_list(args={"matiere_id": matiere_id})[0]
|
||||
UE = sco_edit_ue.do_ue_list(args={"ue_id": M["ue_id"]})[0]
|
||||
Fo = sco_formations.formation_list(args={"formation_id": UE["formation_id"]})[0]
|
||||
parcours = sco_codes_parcours.get_parcours_from_code(Fo["type_parcours"])
|
||||
semestres_indices = list(range(1, parcours.NB_SEM + 1))
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Création d'un module"),
|
||||
"""<h2>Création d'un module dans la matière %(titre)s""" % M,
|
||||
""" (UE %(acronyme)s)</h2>""" % UE,
|
||||
_MODULE_HELP,
|
||||
]
|
||||
# cherche le numero adequat (pour placer le module en fin de liste)
|
||||
Mods = do_module_list(args={"matiere_id": matiere_id})
|
||||
if Mods:
|
||||
default_num = max([m["numero"] for m in Mods]) + 10
|
||||
ue = matiere.ue
|
||||
parcours = ue.formation.get_parcours()
|
||||
is_apc = parcours.APC_SAE
|
||||
ues = ue.formation.ues.order_by(
|
||||
UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme
|
||||
).all()
|
||||
# cherche le numero adéquat (pour placer le module en fin de liste)
|
||||
modules = matiere.ue.formation.modules.all()
|
||||
if modules:
|
||||
default_num = max([m.numero or 0 for m in modules]) + 10
|
||||
else:
|
||||
default_num = 10
|
||||
tf = TrivialFormulator(
|
||||
REQUEST.URL0,
|
||||
REQUEST.form,
|
||||
|
||||
if is_apc and module_type is not None:
|
||||
object_name = scu.MODULE_TYPE_NAMES[module_type]
|
||||
else:
|
||||
object_name = "Module"
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title=f"Création {object_name}"),
|
||||
]
|
||||
if is_apc:
|
||||
H += [
|
||||
f"""<h2>Création {object_name} dans la formation {ue.formation.acronyme}</h2>"""
|
||||
]
|
||||
else:
|
||||
H += [
|
||||
f"""<h2>Création {object_name} dans la matière {matiere.titre},
|
||||
(UE {ue.acronyme})</h2>
|
||||
"""
|
||||
]
|
||||
|
||||
H += [
|
||||
render_template(
|
||||
"scodoc/help/modules.html",
|
||||
is_apc=is_apc,
|
||||
ue=ue,
|
||||
semestre_id=semestre_id,
|
||||
)
|
||||
]
|
||||
|
||||
descr = [
|
||||
(
|
||||
"code",
|
||||
{
|
||||
"size": 10,
|
||||
"explanation": "code du module, ressource ou SAÉ. Exemple M1203, R2.01, ou SAÉ 3.4. Ce code doit être unique dans la formation.",
|
||||
"allow_null": False,
|
||||
"validator": lambda val, field, formation_id=ue.formation_id: check_module_code_unicity(
|
||||
val, field, formation_id
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"titre",
|
||||
{
|
||||
"size": 30,
|
||||
"explanation": "nom du module. Exemple: <em>Introduction à la démarche ergonomique</em>",
|
||||
},
|
||||
),
|
||||
(
|
||||
"abbrev",
|
||||
{
|
||||
"size": 20,
|
||||
"explanation": "nom abrégé (pour les bulletins). Exemple: <em>Intro. à l'ergonomie</em>",
|
||||
},
|
||||
),
|
||||
]
|
||||
semestres_indices = list(range(1, parcours.NB_SEM + 1))
|
||||
if is_apc: # BUT: choix de l'UE de rattachement (qui donnera le semestre)
|
||||
descr += [
|
||||
(
|
||||
"code",
|
||||
{
|
||||
"size": 10,
|
||||
"explanation": "code du module (doit être unique dans la formation)",
|
||||
"allow_null": False,
|
||||
"validator": lambda val, field, formation_id=Fo[
|
||||
"formation_id"
|
||||
]: check_module_code_unicity(val, field, formation_id),
|
||||
},
|
||||
),
|
||||
("titre", {"size": 30, "explanation": "nom du module"}),
|
||||
("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}),
|
||||
(
|
||||
"module_type",
|
||||
"ue_id",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"title": "Type",
|
||||
"explanation": "",
|
||||
"labels": ("Standard", "Malus"),
|
||||
"allowed_values": (str(scu.MODULE_STANDARD), str(scu.MODULE_MALUS)),
|
||||
"type": "int",
|
||||
"title": "UE de rattachement",
|
||||
"explanation": "utilisée pour la présentation dans certains documents",
|
||||
"labels": [f"{u.acronyme} {u.titre}" for u in ues],
|
||||
"allowed_values": [u.id for u in ues],
|
||||
},
|
||||
),
|
||||
]
|
||||
else:
|
||||
# Formations classiques: choix du semestre
|
||||
descr += [
|
||||
(
|
||||
"heures_cours",
|
||||
{"size": 4, "type": "float", "explanation": "nombre d'heures de cours"},
|
||||
),
|
||||
(
|
||||
"heures_td",
|
||||
"semestre_id",
|
||||
{
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"explanation": "nombre d'heures de Travaux Dirigés",
|
||||
"input_type": "menu",
|
||||
"type": "int",
|
||||
"title": parcours.SESSION_NAME.capitalize(),
|
||||
"explanation": "%s du module" % parcours.SESSION_NAME,
|
||||
"labels": [str(x) for x in semestres_indices],
|
||||
"allowed_values": semestres_indices,
|
||||
},
|
||||
),
|
||||
]
|
||||
descr += [
|
||||
(
|
||||
"module_type",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"title": "Type",
|
||||
"explanation": "",
|
||||
"labels": [x.name.capitalize() for x in scu.ModuleType],
|
||||
"allowed_values": [str(int(x)) for x in scu.ModuleType],
|
||||
},
|
||||
),
|
||||
(
|
||||
"heures_cours",
|
||||
{
|
||||
"title": "Heures de cours",
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"explanation": "nombre d'heures de cours (optionnel)",
|
||||
},
|
||||
),
|
||||
(
|
||||
"heures_td",
|
||||
{
|
||||
"title": "Heures de TD",
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"explanation": "nombre d'heures de Travaux Dirigés (optionnel)",
|
||||
},
|
||||
),
|
||||
(
|
||||
"heures_tp",
|
||||
{
|
||||
"title": "Heures de TP",
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"explanation": "nombre d'heures de Travaux Pratiques (optionnel)",
|
||||
},
|
||||
),
|
||||
]
|
||||
if is_apc:
|
||||
descr += [
|
||||
(
|
||||
"heures_tp",
|
||||
"sep_ue_coefs",
|
||||
{
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"explanation": "nombre d'heures de Travaux Pratiques",
|
||||
"input_type": "separator",
|
||||
"title": """
|
||||
<div>(<em>les coefficients vers les UE se fixent sur la page dédiée</em>)
|
||||
</div>""",
|
||||
},
|
||||
),
|
||||
]
|
||||
else:
|
||||
descr += [
|
||||
(
|
||||
"coefficient",
|
||||
{
|
||||
|
@ -198,72 +281,95 @@ def module_create(matiere_id=None, REQUEST=None):
|
|||
"allow_null": False,
|
||||
},
|
||||
),
|
||||
# ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS (inutilisés: les crédits sont associés aux UE)' }),
|
||||
("formation_id", {"default": UE["formation_id"], "input_type": "hidden"}),
|
||||
("ue_id", {"default": M["ue_id"], "input_type": "hidden"}),
|
||||
("matiere_id", {"default": M["matiere_id"], "input_type": "hidden"}),
|
||||
(
|
||||
"semestre_id",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"type": "int",
|
||||
"title": parcours.SESSION_NAME.capitalize(),
|
||||
"explanation": "%s de début du module dans la formation standard"
|
||||
% parcours.SESSION_NAME,
|
||||
"labels": [str(x) for x in semestres_indices],
|
||||
"allowed_values": semestres_indices,
|
||||
},
|
||||
),
|
||||
(
|
||||
"code_apogee",
|
||||
{
|
||||
"title": "Code Apogée",
|
||||
"size": 25,
|
||||
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
|
||||
},
|
||||
),
|
||||
(
|
||||
"numero",
|
||||
{
|
||||
"size": 2,
|
||||
"explanation": "numéro (1,2,3,4...) pour ordre d'affichage",
|
||||
"type": "int",
|
||||
"default": default_num,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
descr += [
|
||||
# ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS (inutilisés: les crédits sont associés aux UE)' }),
|
||||
("formation_id", {"default": ue.formation_id, "input_type": "hidden"}),
|
||||
("ue_id", {"default": ue.id, "input_type": "hidden"}),
|
||||
("matiere_id", {"default": matiere.id, "input_type": "hidden"}),
|
||||
(
|
||||
"code_apogee",
|
||||
{
|
||||
"title": "Code Apogée",
|
||||
"size": 25,
|
||||
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
|
||||
},
|
||||
),
|
||||
(
|
||||
"numero",
|
||||
{
|
||||
"size": 2,
|
||||
"explanation": "numéro (1,2,3,4...) pour ordre d'affichage",
|
||||
"type": "int",
|
||||
"default": default_num,
|
||||
},
|
||||
),
|
||||
]
|
||||
args = scu.get_request_args()
|
||||
tf = TrivialFormulator(
|
||||
request.base_url,
|
||||
args,
|
||||
descr,
|
||||
submitlabel="Créer ce module",
|
||||
)
|
||||
if tf[0] == 0:
|
||||
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
|
||||
else:
|
||||
do_module_create(tf[2])
|
||||
if is_apc:
|
||||
# BUT: l'UE indique le semestre
|
||||
selected_ue = UniteEns.query.get(tf[2]["ue_id"])
|
||||
if selected_ue is None:
|
||||
raise ValueError("UE invalide")
|
||||
tf[2]["semestre_id"] = selected_ue.semestre_idx
|
||||
|
||||
_ = do_module_create(tf[2])
|
||||
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.ue_list",
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=UE["formation_id"],
|
||||
formation_id=ue.formation_id,
|
||||
semestre_idx=tf[2]["semestre_id"],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def can_delete_module(module):
|
||||
"True si le module n'est pas utilisée dans des formsemestre"
|
||||
return len(module.modimpls.all()) == 0
|
||||
|
||||
|
||||
def do_module_delete(oid):
|
||||
"delete module"
|
||||
from app.scodoc import sco_formations
|
||||
|
||||
mod = do_module_list({"module_id": oid})[0]
|
||||
if module_is_locked(mod["module_id"]):
|
||||
module = Module.query.get_or_404(oid)
|
||||
mod = module_list({"module_id": oid})[0] # sco7
|
||||
if module_is_locked(module.id):
|
||||
raise ScoLockedFormError()
|
||||
if not can_delete_module(module):
|
||||
raise ScoNonEmptyFormationObject(
|
||||
"Module",
|
||||
msg=module.titre,
|
||||
dest_url=url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=module.formation_id,
|
||||
semestre_idx=module.ue.semestre_idx,
|
||||
),
|
||||
)
|
||||
|
||||
# S'il y a des moduleimpls, on ne peut pas detruire le module !
|
||||
mods = sco_moduleimpl.do_moduleimpl_list(module_id=oid)
|
||||
mods = sco_moduleimpl.moduleimpl_list(module_id=oid)
|
||||
if mods:
|
||||
err_page = scu.confirm_dialog(
|
||||
message="""<h3>Destruction du module impossible car il est utilisé dans des semestres existants !</h3>""",
|
||||
helpmsg="""Il faut d'abord supprimer le semestre. Mais il est peut être préférable de laisser ce programme intact et d'en créer une nouvelle version pour la modifier.""",
|
||||
dest_url="ue_list",
|
||||
parameters={"formation_id": mod["formation_id"]},
|
||||
)
|
||||
err_page = f"""<h3>Destruction du module impossible car il est utilisé dans des semestres existants !</h3>
|
||||
<p class="help">Il faut d'abord supprimer le semestre (ou en retirer ce module). Mais il est peut être préférable de
|
||||
laisser ce programme intact et d'en créer une nouvelle version pour la modifier sans affecter les semestres déjà en place.
|
||||
</p>
|
||||
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
|
||||
formation_id=mod["formation_id"])}">reprendre</a>
|
||||
"""
|
||||
raise ScoGenError(err_page)
|
||||
# delete
|
||||
cnx = ndb.GetDBConnexion()
|
||||
|
@ -279,25 +385,38 @@ def do_module_delete(oid):
|
|||
)
|
||||
|
||||
|
||||
def module_delete(module_id=None, REQUEST=None):
|
||||
def module_delete(module_id=None):
|
||||
"""Delete a module"""
|
||||
if not module_id:
|
||||
raise ScoValueError("invalid module !")
|
||||
Mods = do_module_list(args={"module_id": module_id})
|
||||
if not Mods:
|
||||
raise ScoValueError("Module inexistant !")
|
||||
Mod = Mods[0]
|
||||
module = Module.query.get_or_404(module_id)
|
||||
mod = module_list(args={"module_id": module_id})[0] # sco7
|
||||
|
||||
if not can_delete_module(module):
|
||||
raise ScoNonEmptyFormationObject(
|
||||
"Module",
|
||||
msg=module.titre,
|
||||
dest_url=url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=module.formation_id,
|
||||
semestre_idx=module.ue.semestre_idx,
|
||||
),
|
||||
)
|
||||
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Suppression d'un module"),
|
||||
"""<h2>Suppression du module %(titre)s (%(code)s)</h2>""" % Mod,
|
||||
"""<h2>Suppression du module %(titre)s (%(code)s)</h2>""" % mod,
|
||||
]
|
||||
|
||||
dest_url = scu.NotesURL() + "/ue_list?formation_id=" + str(Mod["formation_id"])
|
||||
dest_url = url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=str(mod["formation_id"]),
|
||||
)
|
||||
tf = TrivialFormulator(
|
||||
REQUEST.URL0,
|
||||
REQUEST.form,
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
(("module_id", {"input_type": "hidden"}),),
|
||||
initvalues=Mod,
|
||||
initvalues=mod,
|
||||
submitlabel="Confirmer la suppression",
|
||||
cancelbutton="Annuler",
|
||||
)
|
||||
|
@ -310,67 +429,68 @@ def module_delete(module_id=None, REQUEST=None):
|
|||
return flask.redirect(dest_url)
|
||||
|
||||
|
||||
def do_module_edit(val):
|
||||
def do_module_edit(vals: dict) -> None:
|
||||
"edit a module"
|
||||
from app.scodoc import sco_edit_formation
|
||||
|
||||
# check
|
||||
mod = do_module_list({"module_id": val["module_id"]})[0]
|
||||
mod = module_list({"module_id": vals["module_id"]})[0]
|
||||
if module_is_locked(mod["module_id"]):
|
||||
# formation verrouillée: empeche de modifier certains champs:
|
||||
protected_fields = ("coefficient", "ue_id", "matiere_id", "semestre_id")
|
||||
for f in protected_fields:
|
||||
if f in val:
|
||||
del val[f]
|
||||
if f in vals:
|
||||
del vals[f]
|
||||
# edit
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_moduleEditor.edit(cnx, val)
|
||||
sco_edit_formation.invalidate_sems_in_formation(mod["formation_id"])
|
||||
_moduleEditor.edit(cnx, vals)
|
||||
Formation.query.get(mod["formation_id"]).invalidate_cached_sems()
|
||||
|
||||
|
||||
def check_module_code_unicity(code, field, formation_id, module_id=None):
|
||||
"true si code module unique dans la formation"
|
||||
Mods = do_module_list(args={"code": code, "formation_id": formation_id})
|
||||
Mods = module_list(args={"code": code, "formation_id": formation_id})
|
||||
if module_id: # edition: supprime le module en cours
|
||||
Mods = [m for m in Mods if m["module_id"] != module_id]
|
||||
|
||||
return len(Mods) == 0
|
||||
|
||||
|
||||
def module_edit(module_id=None, REQUEST=None):
|
||||
def module_edit(module_id=None):
|
||||
"""Edit a module"""
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_tag_module
|
||||
|
||||
if not module_id:
|
||||
raise ScoValueError("invalid module !")
|
||||
Mod = do_module_list(args={"module_id": module_id})
|
||||
if not Mod:
|
||||
modules = module_list(args={"module_id": module_id})
|
||||
if not modules:
|
||||
raise ScoValueError("invalid module !")
|
||||
Mod = Mod[0]
|
||||
module = modules[0]
|
||||
a_module = models.Module.query.get(module_id)
|
||||
unlocked = not module_is_locked(module_id)
|
||||
Fo = sco_formations.formation_list(args={"formation_id": Mod["formation_id"]})[0]
|
||||
parcours = sco_codes_parcours.get_parcours_from_code(Fo["type_parcours"])
|
||||
M = ndb.SimpleDictFetch(
|
||||
formation_id = module["formation_id"]
|
||||
formation = sco_formations.formation_list(args={"formation_id": formation_id})[0]
|
||||
parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"])
|
||||
is_apc = parcours.APC_SAE
|
||||
ues_matieres = ndb.SimpleDictFetch(
|
||||
"""SELECT ue.acronyme, mat.*, mat.id AS matiere_id
|
||||
FROM notes_matieres mat, notes_ue ue
|
||||
WHERE mat.ue_id = ue.id
|
||||
AND ue.formation_id = %(formation_id)s
|
||||
ORDER BY ue.numero, mat.numero
|
||||
""",
|
||||
{"formation_id": Mod["formation_id"]},
|
||||
{"formation_id": formation_id},
|
||||
)
|
||||
Mnames = ["%s / %s" % (x["acronyme"], x["titre"]) for x in M]
|
||||
Mids = ["%s!%s" % (x["ue_id"], x["matiere_id"]) for x in M]
|
||||
Mod["ue_matiere_id"] = "%s!%s" % (Mod["ue_id"], Mod["matiere_id"])
|
||||
mat_names = ["%s / %s" % (x["acronyme"], x["titre"]) for x in ues_matieres]
|
||||
ue_mat_ids = ["%s!%s" % (x["ue_id"], x["matiere_id"]) for x in ues_matieres]
|
||||
module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"])
|
||||
|
||||
semestres_indices = list(range(1, parcours.NB_SEM + 1))
|
||||
|
||||
dest_url = scu.NotesURL() + "/ue_list?formation_id=" + str(Mod["formation_id"])
|
||||
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title="Modification du module %(titre)s" % Mod,
|
||||
page_title="Modification du module %(titre)s" % module,
|
||||
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
|
||||
javascripts=[
|
||||
"libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
|
||||
|
@ -378,65 +498,80 @@ def module_edit(module_id=None, REQUEST=None):
|
|||
"js/module_tag_editor.js",
|
||||
],
|
||||
),
|
||||
"""<h2>Modification du module %(titre)s""" % Mod,
|
||||
""" (formation %(acronyme)s, version %(version)s)</h2>""" % Fo,
|
||||
_MODULE_HELP,
|
||||
"""<h2>Modification du module %(titre)s""" % module,
|
||||
""" (formation %(acronyme)s, version %(version)s)</h2>""" % formation,
|
||||
render_template("scodoc/help/modules.html", is_apc=is_apc),
|
||||
]
|
||||
if not unlocked:
|
||||
H.append(
|
||||
"""<div class="ue_warning"><span>Formation verrouillée, seuls certains éléments peuvent être modifiés</span></div>"""
|
||||
)
|
||||
|
||||
tf = TrivialFormulator(
|
||||
REQUEST.URL0,
|
||||
REQUEST.form,
|
||||
descr = [
|
||||
(
|
||||
"code",
|
||||
{
|
||||
"size": 10,
|
||||
"explanation": "code du module (doit être unique dans la formation)",
|
||||
"allow_null": False,
|
||||
"validator": lambda val, field, formation_id=formation_id: check_module_code_unicity(
|
||||
val, field, formation_id, module_id=module_id
|
||||
),
|
||||
},
|
||||
),
|
||||
("titre", {"size": 30, "explanation": "nom du module"}),
|
||||
("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}),
|
||||
(
|
||||
"module_type",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"title": "Type",
|
||||
"explanation": "",
|
||||
"labels": [x.name.capitalize() for x in scu.ModuleType],
|
||||
"allowed_values": [str(int(x)) for x in scu.ModuleType],
|
||||
"enabled": unlocked,
|
||||
},
|
||||
),
|
||||
(
|
||||
"heures_cours",
|
||||
{"size": 4, "type": "float", "explanation": "nombre d'heures de cours"},
|
||||
),
|
||||
(
|
||||
"heures_td",
|
||||
{
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"explanation": "nombre d'heures de Travaux Dirigés",
|
||||
},
|
||||
),
|
||||
(
|
||||
"heures_tp",
|
||||
{
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"explanation": "nombre d'heures de Travaux Pratiques",
|
||||
},
|
||||
),
|
||||
]
|
||||
if is_apc:
|
||||
coefs_descr = a_module.ue_coefs_descr()
|
||||
if coefs_descr:
|
||||
coefs_descr_txt = ", ".join(["%s: %s" % x for x in coefs_descr])
|
||||
else:
|
||||
coefs_descr_txt = """<span class="missing_value">non définis</span>"""
|
||||
descr += [
|
||||
(
|
||||
"code",
|
||||
"ue_coefs",
|
||||
{
|
||||
"size": 10,
|
||||
"explanation": "code du module (doit être unique dans la formation)",
|
||||
"allow_null": False,
|
||||
"validator": lambda val, field, formation_id=Mod[
|
||||
"formation_id"
|
||||
]: check_module_code_unicity(
|
||||
val, field, formation_id, module_id=module_id
|
||||
),
|
||||
"readonly": True,
|
||||
"title": "Coefficients vers les UE",
|
||||
"default": coefs_descr_txt,
|
||||
"explanation": "passer par la page d'édition de la formation pour modifier les coefficients",
|
||||
},
|
||||
),
|
||||
("titre", {"size": 30, "explanation": "nom du module"}),
|
||||
("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}),
|
||||
(
|
||||
"module_type",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"title": "Type",
|
||||
"explanation": "",
|
||||
"labels": ("Standard", "Malus"),
|
||||
"allowed_values": (str(scu.MODULE_STANDARD), str(scu.MODULE_MALUS)),
|
||||
"enabled": unlocked,
|
||||
},
|
||||
),
|
||||
(
|
||||
"heures_cours",
|
||||
{"size": 4, "type": "float", "explanation": "nombre d'heures de cours"},
|
||||
),
|
||||
(
|
||||
"heures_td",
|
||||
{
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"explanation": "nombre d'heures de Travaux Dirigés",
|
||||
},
|
||||
),
|
||||
(
|
||||
"heures_tp",
|
||||
{
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"explanation": "nombre d'heures de Travaux Pratiques",
|
||||
},
|
||||
),
|
||||
)
|
||||
]
|
||||
else: # Module classique avec coef scalaire:
|
||||
descr += [
|
||||
(
|
||||
"coefficient",
|
||||
{
|
||||
|
@ -447,21 +582,39 @@ def module_edit(module_id=None, REQUEST=None):
|
|||
"enabled": unlocked,
|
||||
},
|
||||
),
|
||||
# ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS', 'enabled' : unlocked }),
|
||||
("formation_id", {"input_type": "hidden"}),
|
||||
("ue_id", {"input_type": "hidden"}),
|
||||
("module_id", {"input_type": "hidden"}),
|
||||
]
|
||||
descr += [
|
||||
("formation_id", {"input_type": "hidden"}),
|
||||
("ue_id", {"input_type": "hidden"}),
|
||||
("module_id", {"input_type": "hidden"}),
|
||||
(
|
||||
"ue_matiere_id",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"title": "Rattachement :" if is_apc else "Matière :",
|
||||
"explanation": "UE de rattachement, utilisée pour la présentation"
|
||||
if is_apc
|
||||
else "un module appartient à une seule matière.",
|
||||
"labels": mat_names,
|
||||
"allowed_values": ue_mat_ids,
|
||||
"enabled": unlocked,
|
||||
},
|
||||
),
|
||||
]
|
||||
if is_apc:
|
||||
# le semestre du module est toujours celui de son UE
|
||||
descr += [
|
||||
(
|
||||
"ue_matiere_id",
|
||||
"semestre_id",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"title": "Matière",
|
||||
"explanation": "un module appartient à une seule matière.",
|
||||
"labels": Mnames,
|
||||
"allowed_values": Mids,
|
||||
"enabled": unlocked,
|
||||
"input_type": "hidden",
|
||||
"type": "int",
|
||||
"readonly": True,
|
||||
},
|
||||
),
|
||||
)
|
||||
]
|
||||
else:
|
||||
descr += [
|
||||
(
|
||||
"semestre_id",
|
||||
{
|
||||
|
@ -474,52 +627,83 @@ def module_edit(module_id=None, REQUEST=None):
|
|||
"allowed_values": semestres_indices,
|
||||
"enabled": unlocked,
|
||||
},
|
||||
),
|
||||
(
|
||||
"code_apogee",
|
||||
{
|
||||
"title": "Code Apogée",
|
||||
"size": 25,
|
||||
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
|
||||
},
|
||||
),
|
||||
(
|
||||
"numero",
|
||||
{
|
||||
"size": 2,
|
||||
"explanation": "numéro (1,2,3,4...) pour ordre d'affichage",
|
||||
"type": "int",
|
||||
},
|
||||
),
|
||||
)
|
||||
]
|
||||
descr += [
|
||||
(
|
||||
"code_apogee",
|
||||
{
|
||||
"title": "Code Apogée",
|
||||
"size": 25,
|
||||
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
|
||||
"validator": lambda val, _: len(val) < APO_CODE_STR_LEN,
|
||||
},
|
||||
),
|
||||
(
|
||||
"numero",
|
||||
{
|
||||
"size": 2,
|
||||
"explanation": "numéro (1,2,3,4...) pour ordre d'affichage",
|
||||
"type": "int",
|
||||
},
|
||||
),
|
||||
]
|
||||
# force module semestre_idx to its UE
|
||||
if a_module.ue.semestre_idx:
|
||||
module["semestre_id"] = a_module.ue.semestre_idx
|
||||
# Filet de sécurité si jamais l'UE n'a pas non plus de semestre:
|
||||
if not module["semestre_id"]:
|
||||
module["semestre_id"] = 1
|
||||
tf = TrivialFormulator(
|
||||
request.base_url,
|
||||
scu.get_request_args(),
|
||||
descr,
|
||||
html_foot_markup="""<div style="width: 90%;"><span class="sco_tag_edit"><textarea data-module_id="{}" class="module_tag_editor">{}</textarea></span></div>""".format(
|
||||
module_id, ",".join(sco_tag_module.module_tag_list(module_id))
|
||||
),
|
||||
initvalues=Mod,
|
||||
initvalues=module,
|
||||
submitlabel="Modifier ce module",
|
||||
)
|
||||
|
||||
if tf[0] == 0:
|
||||
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(dest_url)
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=formation_id,
|
||||
semestre_idx=module["semestre_id"],
|
||||
)
|
||||
)
|
||||
else:
|
||||
# l'UE peut changer
|
||||
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
|
||||
# En APC, force le semestre égal à celui de l'UE
|
||||
if is_apc:
|
||||
selected_ue = UniteEns.query.get(tf[2]["ue_id"])
|
||||
if selected_ue is None:
|
||||
raise ValueError("UE invalide")
|
||||
tf[2]["semestre_id"] = selected_ue.semestre_idx
|
||||
# Check unicité code module dans la formation
|
||||
|
||||
do_module_edit(tf[2])
|
||||
return flask.redirect(dest_url)
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=formation_id,
|
||||
semestre_idx=tf[2]["semestre_id"],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Edition en ligne du code Apogee
|
||||
def edit_module_set_code_apogee(id=None, value=None, REQUEST=None):
|
||||
def edit_module_set_code_apogee(id=None, value=None):
|
||||
"Set UE code apogee"
|
||||
module_id = id
|
||||
value = value.strip("-_ \t")
|
||||
value = str(value).strip("-_ \t")
|
||||
log("edit_module_set_code_apogee: module_id=%s code_apogee=%s" % (module_id, value))
|
||||
|
||||
modules = do_module_list(args={"module_id": module_id})
|
||||
modules = module_list(args={"module_id": module_id})
|
||||
if not modules:
|
||||
return "module invalide" # should not occur
|
||||
|
||||
|
@ -529,7 +713,7 @@ def edit_module_set_code_apogee(id=None, value=None, REQUEST=None):
|
|||
return value
|
||||
|
||||
|
||||
def module_list(formation_id, REQUEST=None):
|
||||
def module_table(formation_id):
|
||||
"""Liste des modules de la formation
|
||||
(XXX inutile ou a revoir)
|
||||
"""
|
||||
|
@ -544,9 +728,9 @@ def module_list(formation_id, REQUEST=None):
|
|||
% F,
|
||||
'<ul class="notes_module_list">',
|
||||
]
|
||||
editable = REQUEST.AUTHENTICATED_USER.has_permission(Permission.ScoChangeFormation)
|
||||
editable = current_user.has_permission(Permission.ScoChangeFormation)
|
||||
|
||||
for Mod in do_module_list(args={"formation_id": formation_id}):
|
||||
for Mod in module_list(args={"formation_id": formation_id}):
|
||||
H.append('<li class="notes_module_list">%s' % Mod)
|
||||
if editable:
|
||||
H.append('<a href="module_edit?module_id=%(module_id)s">modifier</a>' % Mod)
|
||||
|
@ -578,37 +762,41 @@ def module_is_locked(module_id):
|
|||
|
||||
def module_count_moduleimpls(module_id):
|
||||
"Number of moduleimpls using this module"
|
||||
mods = sco_moduleimpl.do_moduleimpl_list(module_id=module_id)
|
||||
mods = sco_moduleimpl.moduleimpl_list(module_id=module_id)
|
||||
return len(mods)
|
||||
|
||||
|
||||
def formation_add_malus_modules(formation_id, titre=None, REQUEST=None):
|
||||
def formation_add_malus_modules(formation_id, titre=None, redirect=True):
|
||||
"""Création d'un module de "malus" dans chaque UE d'une formation"""
|
||||
from app.scodoc import sco_edit_ue
|
||||
|
||||
ue_list = sco_edit_ue.do_ue_list(args={"formation_id": formation_id})
|
||||
ues = sco_edit_ue.ue_list(args={"formation_id": formation_id})
|
||||
|
||||
for ue in ue_list:
|
||||
for ue in ues:
|
||||
# Un seul module de malus par UE:
|
||||
nb_mod_malus = len(
|
||||
[
|
||||
mod
|
||||
for mod in do_module_list(args={"ue_id": ue["ue_id"]})
|
||||
if mod["module_type"] == scu.MODULE_MALUS
|
||||
for mod in module_list(args={"ue_id": ue["ue_id"]})
|
||||
if mod["module_type"] == ModuleType.MALUS
|
||||
]
|
||||
)
|
||||
if nb_mod_malus == 0:
|
||||
ue_add_malus_module(ue["ue_id"], titre=titre, REQUEST=REQUEST)
|
||||
ue_add_malus_module(ue["ue_id"], titre=titre)
|
||||
|
||||
if REQUEST:
|
||||
return flask.redirect("ue_list?formation_id=" + str(formation_id))
|
||||
if redirect:
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def ue_add_malus_module(ue_id, titre=None, code=None, REQUEST=None):
|
||||
def ue_add_malus_module(ue_id, titre=None, code=None):
|
||||
"""Add a malus module in this ue"""
|
||||
from app.scodoc import sco_edit_ue
|
||||
|
||||
ue = sco_edit_ue.do_ue_list(args={"ue_id": ue_id})[0]
|
||||
ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
|
||||
|
||||
if titre is None:
|
||||
titre = ""
|
||||
|
@ -627,7 +815,7 @@ def ue_add_malus_module(ue_id, titre=None, code=None, REQUEST=None):
|
|||
)
|
||||
|
||||
# Matiere pour placer le module malus
|
||||
Matlist = sco_edit_matiere.do_matiere_list(args={"ue_id": ue_id})
|
||||
Matlist = sco_edit_matiere.matiere_list(args={"ue_id": ue_id})
|
||||
numero = max([mat["numero"] for mat in Matlist]) + 10
|
||||
matiere_id = sco_edit_matiere.do_matiere_create(
|
||||
{"ue_id": ue_id, "titre": "Malus", "numero": numero}
|
||||
|
@ -642,7 +830,7 @@ def ue_add_malus_module(ue_id, titre=None, code=None, REQUEST=None):
|
|||
"matiere_id": matiere_id,
|
||||
"formation_id": ue["formation_id"],
|
||||
"semestre_id": semestre_id,
|
||||
"module_type": scu.MODULE_MALUS,
|
||||
"module_type": ModuleType.MALUS,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -33,10 +33,10 @@ XXX incompatible avec les ics HyperPlanning Paris 13 (était pour GPU).
|
|||
|
||||
"""
|
||||
|
||||
import six.moves.urllib.request, six.moves.urllib.error, six.moves.urllib.parse
|
||||
import traceback
|
||||
import icalendar
|
||||
import pprint
|
||||
import traceback
|
||||
import urllib
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
|
@ -80,7 +80,7 @@ def formsemestre_load_ics(sem):
|
|||
ics_data = ""
|
||||
else:
|
||||
log("Loading edt from %s" % ics_url)
|
||||
f = six.moves.urllib.request.urlopen(
|
||||
f = urllib.request.urlopen(
|
||||
ics_url, timeout=5
|
||||
) # 5s TODO: add config parameter, eg for slow networks
|
||||
ics_data = f.read()
|
||||
|
@ -123,7 +123,7 @@ def get_edt_transcodage_groups(formsemestre_id):
|
|||
return edt2sco, sco2edt, msg
|
||||
|
||||
|
||||
def group_edt_json(group_id, start="", end="", REQUEST=None): # actuellement inutilisé
|
||||
def group_edt_json(group_id, start="", end=""): # actuellement inutilisé
|
||||
"""EDT complet du semestre, au format JSON
|
||||
TODO: indiquer un groupe
|
||||
TODO: utiliser start et end (2 dates au format ISO YYYY-MM-DD)
|
||||
|
@ -149,7 +149,7 @@ def group_edt_json(group_id, start="", end="", REQUEST=None): # actuellement in
|
|||
}
|
||||
J.append(d)
|
||||
|
||||
return scu.sendJSON(REQUEST, J)
|
||||
return scu.sendJSON(J)
|
||||
|
||||
|
||||
"""XXX
|
||||
|
@ -159,9 +159,7 @@ for e in events:
|
|||
"""
|
||||
|
||||
|
||||
def experimental_calendar(
|
||||
group_id=None, formsemestre_id=None, REQUEST=None
|
||||
): # inutilisé
|
||||
def experimental_calendar(group_id=None, formsemestre_id=None): # inutilisé
|
||||
"""experimental page"""
|
||||
return "\n".join(
|
||||
[
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -32,18 +32,18 @@
|
|||
Voir sco_apogee_csv.py pour la structure du fichier Apogée.
|
||||
|
||||
Stockage: utilise sco_archive.py
|
||||
=> /opt/scodoc/var/scodoc/archives/apo_csv/RT/2016-1/2016-07-03-16-12-19/V3ASR.csv
|
||||
=> /opt/scodoc/var/scodoc/archives/apo_csv/<dept_id>/2016-1/2016-07-03-16-12-19/V3ASR.csv
|
||||
pour une maquette de l'année scolaire 2016, semestre 1, etape V3ASR
|
||||
|
||||
ou bien (à partir de ScoDoc 1678) :
|
||||
/opt/scodoc/var/scodoc/archives/apo_csv/RT/2016-1/2016-07-03-16-12-19/V3ASR!111.csv
|
||||
/opt/scodoc/var/scodoc/archives/apo_csv/<dept_id>/2016-1/2016-07-03-16-12-19/V3ASR!111.csv
|
||||
pour une maquette de l'étape V3ASR version VDI 111.
|
||||
|
||||
La version VDI sera ignorée sauf si elle est indiquée dans l'étape du semestre.
|
||||
apo_csv_get()
|
||||
|
||||
API:
|
||||
apo_csv_store( annee_scolaire, sem_id)
|
||||
# apo_csv_store(csv_data, annee_scolaire, sem_id)
|
||||
store maq file (archive)
|
||||
|
||||
apo_csv_get(etape_apo, annee_scolaire, sem_id, vdi_apo=None)
|
||||
|
@ -101,7 +101,7 @@ ApoCSVArchive = ApoCSVArchiver()
|
|||
|
||||
def apo_csv_store(csv_data: str, annee_scolaire, sem_id):
|
||||
"""
|
||||
csv_data: maquette content (string)
|
||||
csv_data: maquette content (str))
|
||||
annee_scolaire: int (2016)
|
||||
sem_id: 0 (année ?), 1 (premier semestre de l'année) ou 2 (deuxième semestre)
|
||||
:return: etape_apo du fichier CSV stocké
|
||||
|
@ -378,7 +378,7 @@ e.associate_sco( apo_data)
|
|||
print apo_csv_list_stored_archives()
|
||||
|
||||
|
||||
apo_csv_store(csv_data, annee_scolaire, sem_id)
|
||||
# apo_csv_store(csv_data, annee_scolaire, sem_id)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -32,7 +32,7 @@ import io
|
|||
from zipfile import ZipFile
|
||||
|
||||
import flask
|
||||
from flask import url_for, g, send_file
|
||||
from flask import url_for, g, send_file, request
|
||||
|
||||
# from werkzeug.utils import send_file
|
||||
|
||||
|
@ -48,7 +48,7 @@ from app.scodoc import sco_preferences
|
|||
from app.scodoc import sco_semset
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_apogee_csv import APO_PORTAL_ENCODING, APO_INPUT_ENCODING
|
||||
from app.scodoc.sco_apogee_csv import APO_INPUT_ENCODING, APO_OUTPUT_ENCODING
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
|
@ -62,7 +62,6 @@ def apo_semset_maq_status(
|
|||
block_export_res_ues=False,
|
||||
block_export_res_modules=False,
|
||||
block_export_res_sdj=True,
|
||||
REQUEST=None,
|
||||
):
|
||||
"""Page statut / tableau de bord"""
|
||||
if not semset_id:
|
||||
|
@ -83,7 +82,7 @@ def apo_semset_maq_status(
|
|||
|
||||
prefs = sco_preferences.SemPreferences()
|
||||
|
||||
tab_archives = table_apo_csv_list(semset, REQUEST=REQUEST)
|
||||
tab_archives = table_apo_csv_list(semset)
|
||||
|
||||
(
|
||||
ok_for_export,
|
||||
|
@ -250,7 +249,7 @@ def apo_semset_maq_status(
|
|||
"""<form name="f" method="get" action="%s">
|
||||
<input type="hidden" name="semset_id" value="%s"></input>
|
||||
<div><input type="checkbox" name="allow_missing_apo" value="1" onchange="document.f.submit()" """
|
||||
% (REQUEST.URL0, semset_id)
|
||||
% (request.base_url, semset_id)
|
||||
)
|
||||
if allow_missing_apo:
|
||||
H.append("checked")
|
||||
|
@ -357,7 +356,7 @@ def apo_semset_maq_status(
|
|||
H.append(
|
||||
", ".join(
|
||||
[
|
||||
'<a class="stdlink" href="ue_list?formation_id=%(formation_id)s">%(acronyme)s v%(version)s</a>'
|
||||
'<a class="stdlink" href="ue_table?formation_id=%(formation_id)s">%(acronyme)s v%(version)s</a>'
|
||||
% f
|
||||
for f in formations
|
||||
]
|
||||
|
@ -430,7 +429,7 @@ def apo_semset_maq_status(
|
|||
return "\n".join(H)
|
||||
|
||||
|
||||
def table_apo_csv_list(semset, REQUEST=None):
|
||||
def table_apo_csv_list(semset):
|
||||
"""Table des archives (triée par date d'archivage)"""
|
||||
annee_scolaire = semset["annee_scolaire"]
|
||||
sem_id = semset["sem_id"]
|
||||
|
@ -476,7 +475,7 @@ def table_apo_csv_list(semset, REQUEST=None):
|
|||
rows=T,
|
||||
html_class="table_leftalign apo_maq_list",
|
||||
html_sortable=True,
|
||||
# base_url = '%s?formsemestre_id=%s' % (REQUEST.URL0, formsemestre_id),
|
||||
# base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id),
|
||||
# caption='Maquettes enregistrées',
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
)
|
||||
|
@ -484,7 +483,7 @@ def table_apo_csv_list(semset, REQUEST=None):
|
|||
return tab
|
||||
|
||||
|
||||
def view_apo_etuds(semset_id, title="", nip_list="", format="html", REQUEST=None):
|
||||
def view_apo_etuds(semset_id, title="", nip_list="", format="html"):
|
||||
"""Table des étudiants Apogée par nips
|
||||
nip_list est une chaine, codes nip séparés par des ,
|
||||
"""
|
||||
|
@ -517,11 +516,10 @@ def view_apo_etuds(semset_id, title="", nip_list="", format="html", REQUEST=None
|
|||
etuds=list(etuds.values()),
|
||||
keys=("nip", "etape_apo", "nom", "prenom", "inscriptions_scodoc"),
|
||||
format=format,
|
||||
REQUEST=REQUEST,
|
||||
)
|
||||
|
||||
|
||||
def view_scodoc_etuds(semset_id, title="", nip_list="", format="html", REQUEST=None):
|
||||
def view_scodoc_etuds(semset_id, title="", nip_list="", format="html"):
|
||||
"""Table des étudiants ScoDoc par nips ou etudids"""
|
||||
if not isinstance(nip_list, str):
|
||||
nip_list = str(nip_list)
|
||||
|
@ -541,13 +539,10 @@ def view_scodoc_etuds(semset_id, title="", nip_list="", format="html", REQUEST=N
|
|||
etuds=etuds,
|
||||
keys=("code_nip", "nom", "prenom"),
|
||||
format=format,
|
||||
REQUEST=REQUEST,
|
||||
)
|
||||
|
||||
|
||||
def _view_etuds_page(
|
||||
semset_id, title="", etuds=[], keys=(), format="html", REQUEST=None
|
||||
):
|
||||
def _view_etuds_page(semset_id, title="", etuds=[], keys=(), format="html"):
|
||||
# Tri les étudiants par nom:
|
||||
if etuds:
|
||||
etuds.sort(key=lambda x: (x["nom"], x["prenom"]))
|
||||
|
@ -578,7 +573,7 @@ def _view_etuds_page(
|
|||
preferences=sco_preferences.SemPreferences(),
|
||||
)
|
||||
if format != "html":
|
||||
return tab.make_page(format=format, REQUEST=REQUEST)
|
||||
return tab.make_page(format=format)
|
||||
|
||||
H.append(tab.html())
|
||||
|
||||
|
@ -590,9 +585,7 @@ def _view_etuds_page(
|
|||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
|
||||
def view_apo_csv_store(
|
||||
semset_id="", csvfile=None, data="", autodetect=False, REQUEST=None
|
||||
):
|
||||
def view_apo_csv_store(semset_id="", csvfile=None, data: bytes = "", autodetect=False):
|
||||
"""Store CSV data
|
||||
Le semset identifie l'annee scolaire et le semestre
|
||||
Si csvfile, lit depuis FILE, sinon utilise data
|
||||
|
@ -600,9 +593,8 @@ def view_apo_csv_store(
|
|||
if not semset_id:
|
||||
raise ValueError("invalid null semset_id")
|
||||
semset = sco_semset.SemSet(semset_id=semset_id)
|
||||
|
||||
if csvfile:
|
||||
data = csvfile.read()
|
||||
data = csvfile.read() # bytes
|
||||
if autodetect:
|
||||
# check encoding (although documentation states that users SHOULD upload LATIN1)
|
||||
data, message = sco_apogee_csv.fix_data_encoding(data)
|
||||
|
@ -612,22 +604,29 @@ def view_apo_csv_store(
|
|||
log("view_apo_csv_store: autodetection of encoding disabled by user")
|
||||
if not data:
|
||||
raise ScoValueError("view_apo_csv_store: no data")
|
||||
|
||||
# data est du bytes, encodé en APO_INPUT_ENCODING
|
||||
data_str = data.decode(APO_INPUT_ENCODING)
|
||||
# check si etape maquette appartient bien au semset
|
||||
apo_data = sco_apogee_csv.ApoData(
|
||||
data, periode=semset["sem_id"]
|
||||
data_str, periode=semset["sem_id"]
|
||||
) # parse le fichier -> exceptions
|
||||
if apo_data.etape not in semset["etapes"]:
|
||||
raise ScoValueError(
|
||||
"Le code étape de ce fichier ne correspond pas à ceux de cet ensemble"
|
||||
)
|
||||
|
||||
sco_etape_apogee.apo_csv_store(data, semset["annee_scolaire"], semset["sem_id"])
|
||||
sco_etape_apogee.apo_csv_store(data_str, semset["annee_scolaire"], semset["sem_id"])
|
||||
|
||||
return flask.redirect("apo_semset_maq_status?semset_id=" + semset_id)
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.apo_semset_maq_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
semset_id=semset_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def view_apo_csv_download_and_store(etape_apo="", semset_id="", REQUEST=None):
|
||||
def view_apo_csv_download_and_store(etape_apo="", semset_id=""):
|
||||
"""Download maquette and store it"""
|
||||
if not semset_id:
|
||||
raise ValueError("invalid null semset_id")
|
||||
|
@ -636,20 +635,18 @@ def view_apo_csv_download_and_store(etape_apo="", semset_id="", REQUEST=None):
|
|||
data = sco_portal_apogee.get_maquette_apogee(
|
||||
etape=etape_apo, annee_scolaire=semset["annee_scolaire"]
|
||||
)
|
||||
# here, data is utf8
|
||||
# here, data is str
|
||||
# but we store and generate latin1 files, to ease further import in Apogée
|
||||
data = data.decode(APO_PORTAL_ENCODING).encode(APO_INPUT_ENCODING) # XXX #py3
|
||||
return view_apo_csv_store(semset_id, data=data, autodetect=False, REQUEST=REQUEST)
|
||||
data = data.encode(APO_OUTPUT_ENCODING)
|
||||
return view_apo_csv_store(semset_id, data=data, autodetect=False)
|
||||
|
||||
|
||||
def view_apo_csv_delete(
|
||||
etape_apo="", semset_id="", dialog_confirmed=False, REQUEST=None
|
||||
):
|
||||
def view_apo_csv_delete(etape_apo="", semset_id="", dialog_confirmed=False):
|
||||
"""Delete CSV file"""
|
||||
if not semset_id:
|
||||
raise ValueError("invalid null semset_id")
|
||||
semset = sco_semset.SemSet(semset_id=semset_id)
|
||||
dest_url = "apo_semset_maq_status?semset_id=" + semset_id
|
||||
dest_url = f"apo_semset_maq_status?semset_id={semset_id}"
|
||||
if not dialog_confirmed:
|
||||
return scu.confirm_dialog(
|
||||
"""<h2>Confirmer la suppression du fichier étape <tt>%s</tt>?</h2>
|
||||
|
@ -667,7 +664,7 @@ def view_apo_csv_delete(
|
|||
return flask.redirect(dest_url + "&head_message=Archive%20supprimée")
|
||||
|
||||
|
||||
def view_apo_csv(etape_apo="", semset_id="", format="html", REQUEST=None):
|
||||
def view_apo_csv(etape_apo="", semset_id="", format="html"):
|
||||
"""Visualise une maquette stockée
|
||||
Si format="raw", renvoie le fichier maquette tel quel
|
||||
"""
|
||||
|
@ -678,7 +675,8 @@ def view_apo_csv(etape_apo="", semset_id="", format="html", REQUEST=None):
|
|||
sem_id = semset["sem_id"]
|
||||
csv_data = sco_etape_apogee.apo_csv_get(etape_apo, annee_scolaire, sem_id)
|
||||
if format == "raw":
|
||||
return scu.sendCSVFile(REQUEST, csv_data, etape_apo + ".txt")
|
||||
return scu.send_file(csv_data, etape_apo, suffix=".txt", mime=scu.CSV_MIMETYPE)
|
||||
|
||||
apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
|
||||
|
||||
(
|
||||
|
@ -746,14 +744,15 @@ def view_apo_csv(etape_apo="", semset_id="", format="html", REQUEST=None):
|
|||
rows=etuds,
|
||||
html_sortable=True,
|
||||
html_class="table_leftalign apo_maq_table",
|
||||
base_url="%s?etape_apo=%s&semset_id=%s" % (REQUEST.URL0, etape_apo, semset_id),
|
||||
base_url="%s?etape_apo=%s&semset_id=%s"
|
||||
% (request.base_url, etape_apo, semset_id),
|
||||
filename="students_" + etape_apo,
|
||||
caption="Etudiants Apogée en " + etape_apo,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
)
|
||||
|
||||
if format != "html":
|
||||
return tab.make_page(format=format, REQUEST=REQUEST)
|
||||
return tab.make_page(format=format)
|
||||
|
||||
H += [
|
||||
tab.html(),
|
||||
|
@ -768,7 +767,7 @@ def view_apo_csv(etape_apo="", semset_id="", format="html", REQUEST=None):
|
|||
return "\n".join(H)
|
||||
|
||||
|
||||
# called from Web
|
||||
# called from Web (GET)
|
||||
def apo_csv_export_results(
|
||||
semset_id,
|
||||
block_export_res_etape=False,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -31,57 +31,21 @@
|
|||
# Ancien module "scolars"
|
||||
import os
|
||||
import time
|
||||
|
||||
from flask import url_for, g, request
|
||||
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.header import Header
|
||||
from email.mime.base import MIMEBase
|
||||
from operator import itemgetter
|
||||
|
||||
from flask import url_for, g, request
|
||||
from flask_mail import Message
|
||||
|
||||
from app import email
|
||||
from app import log
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import SCO_ENCODING
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
|
||||
|
||||
from app import log
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
from app.scodoc import safehtml
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.scolog import logdb
|
||||
from flask_mail import Message
|
||||
from app import mail
|
||||
|
||||
MONTH_NAMES_ABBREV = [
|
||||
"Jan ",
|
||||
"Fév ",
|
||||
"Mars",
|
||||
"Avr ",
|
||||
"Mai ",
|
||||
"Juin",
|
||||
"Jul ",
|
||||
"Août",
|
||||
"Sept",
|
||||
"Oct ",
|
||||
"Nov ",
|
||||
"Déc ",
|
||||
]
|
||||
|
||||
MONTH_NAMES = [
|
||||
"janvier",
|
||||
"février",
|
||||
"mars",
|
||||
"avril",
|
||||
"mai",
|
||||
"juin",
|
||||
"juillet",
|
||||
"août",
|
||||
"septembre",
|
||||
"octobre",
|
||||
"novembre",
|
||||
"décembre",
|
||||
]
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
|
||||
|
||||
def format_etud_ident(etud):
|
||||
|
@ -158,7 +122,7 @@ def format_nom(s, uppercase=True):
|
|||
def input_civilite(s):
|
||||
"""Converts external representation of civilite to internal:
|
||||
'M', 'F', or 'X' (and nothing else).
|
||||
Raises valueError if conversion fails.
|
||||
Raises ScoValueError if conversion fails.
|
||||
"""
|
||||
s = s.upper().strip()
|
||||
if s in ("M", "M.", "MR", "H"):
|
||||
|
@ -167,12 +131,13 @@ def input_civilite(s):
|
|||
return "F"
|
||||
elif s == "X" or not s:
|
||||
return "X"
|
||||
raise ValueError("valeur invalide pour la civilité: %s" % s)
|
||||
raise ScoValueError("valeur invalide pour la civilité: %s" % s)
|
||||
|
||||
|
||||
def format_civilite(civilite):
|
||||
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
|
||||
personne ne souhaitant pas d'affichage)
|
||||
personne ne souhaitant pas d'affichage).
|
||||
Raises ScoValueError if conversion fails.
|
||||
"""
|
||||
try:
|
||||
return {
|
||||
|
@ -181,7 +146,7 @@ def format_civilite(civilite):
|
|||
"X": "",
|
||||
}[civilite]
|
||||
except KeyError:
|
||||
raise ValueError("valeur invalide pour la civilité: %s" % civilite)
|
||||
raise ScoValueError("valeur invalide pour la civilité: %s" % civilite)
|
||||
|
||||
|
||||
def format_lycee(nomlycee):
|
||||
|
@ -256,7 +221,6 @@ _identiteEditor = ndb.EditableTable(
|
|||
"photo_filename",
|
||||
"code_ine",
|
||||
"code_nip",
|
||||
"scodoc7_id",
|
||||
),
|
||||
filter_dept=True,
|
||||
sortkey="nom",
|
||||
|
@ -321,9 +285,7 @@ def check_nom_prenom(cnx, nom="", prenom="", etudid=None):
|
|||
return True, len(res)
|
||||
|
||||
|
||||
def _check_duplicate_code(
|
||||
cnx, args, code_name, disable_notify=False, edit=True, REQUEST=None
|
||||
):
|
||||
def _check_duplicate_code(cnx, args, code_name, disable_notify=False, edit=True):
|
||||
etudid = args.get("etudid", None)
|
||||
if args.get(code_name, None):
|
||||
etuds = identite_list(cnx, {code_name: str(args[code_name])})
|
||||
|
@ -345,31 +307,33 @@ def _check_duplicate_code(
|
|||
)
|
||||
if etudid:
|
||||
OK = "retour à la fiche étudiant"
|
||||
dest_url = "ficheEtud"
|
||||
dest_endpoint = "scolar.ficheEtud"
|
||||
parameters = {"etudid": etudid}
|
||||
else:
|
||||
if "tf_submitted" in args:
|
||||
del args["tf_submitted"]
|
||||
OK = "Continuer"
|
||||
dest_url = "etudident_create_form"
|
||||
dest_endpoint = "scolar.etudident_create_form"
|
||||
parameters = args
|
||||
else:
|
||||
OK = "Annuler"
|
||||
dest_url = ""
|
||||
dest_endpoint = "notes.index_html"
|
||||
parameters = {}
|
||||
if not disable_notify:
|
||||
err_page = scu.confirm_dialog(
|
||||
message="""<h3>Code étudiant (%s) dupliqué !</h3>""" % code_name,
|
||||
helpmsg="""Le %s %s est déjà utilisé: un seul étudiant peut avoir ce code. Vérifier votre valeur ou supprimer l'autre étudiant avec cette valeur.<p><ul><li>"""
|
||||
% (code_name, args[code_name])
|
||||
+ "</li><li>".join(listh)
|
||||
+ "</li></ul><p>",
|
||||
OK=OK,
|
||||
dest_url=dest_url,
|
||||
parameters=parameters,
|
||||
)
|
||||
err_page = f"""<h3><h3>Code étudiant ({code_name}) dupliqué !</h3>
|
||||
<p class="help">Le {code_name} {args[code_name]} est déjà utilisé: un seul étudiant peut avoir
|
||||
ce code. Vérifier votre valeur ou supprimer l'autre étudiant avec cette valeur.
|
||||
</p>
|
||||
<ul><li>
|
||||
{ '</li><li>'.join(listh) }
|
||||
</li></ul>
|
||||
<p>
|
||||
<a href="{ url_for(dest_endpoint, scodoc_dept=g.scodoc_dept, **parameters) }
|
||||
">{OK}</a>
|
||||
</p>
|
||||
"""
|
||||
else:
|
||||
err_page = """<h3>Code étudiant (%s) dupliqué !</h3>""" % code_name
|
||||
err_page = f"""<h3>Code étudiant ({code_name}) dupliqué !</h3>"""
|
||||
log("*** error: code %s duplique: %s" % (code_name, args[code_name]))
|
||||
raise ScoGenError(err_page)
|
||||
|
||||
|
@ -379,15 +343,15 @@ def _check_civilite(args):
|
|||
args["civilite"] = input_civilite(civilite) # TODO: A faire valider
|
||||
|
||||
|
||||
def identite_edit(cnx, args, disable_notify=False, REQUEST=None):
|
||||
def identite_edit(cnx, args, disable_notify=False):
|
||||
"""Modifie l'identite d'un étudiant.
|
||||
Si pref notification et difference, envoie message notification, sauf si disable_notify
|
||||
"""
|
||||
_check_duplicate_code(
|
||||
cnx, args, "code_nip", disable_notify=disable_notify, edit=True, REQUEST=REQUEST
|
||||
cnx, args, "code_nip", disable_notify=disable_notify, edit=True
|
||||
)
|
||||
_check_duplicate_code(
|
||||
cnx, args, "code_ine", disable_notify=disable_notify, edit=True, REQUEST=REQUEST
|
||||
cnx, args, "code_ine", disable_notify=disable_notify, edit=True
|
||||
)
|
||||
notify_to = None
|
||||
if not disable_notify:
|
||||
|
@ -415,10 +379,10 @@ def identite_edit(cnx, args, disable_notify=False, REQUEST=None):
|
|||
)
|
||||
|
||||
|
||||
def identite_create(cnx, args, REQUEST=None):
|
||||
def identite_create(cnx, args):
|
||||
"check unique etudid, then create"
|
||||
_check_duplicate_code(cnx, args, "code_nip", edit=False, REQUEST=REQUEST)
|
||||
_check_duplicate_code(cnx, args, "code_ine", edit=False, REQUEST=REQUEST)
|
||||
_check_duplicate_code(cnx, args, "code_nip", edit=False)
|
||||
_check_duplicate_code(cnx, args, "code_ine", edit=False)
|
||||
_check_civilite(args)
|
||||
|
||||
if "etudid" in args:
|
||||
|
@ -456,7 +420,7 @@ def notify_etud_change(email_addr, etud, before, after, subject):
|
|||
log("notify_etud_change: sending notification to %s" % email_addr)
|
||||
log("notify_etud_change: subject: %s" % subject)
|
||||
log(txt)
|
||||
mail.send_email(
|
||||
email.send_email(
|
||||
subject, sco_preferences.get_preference("email_from_addr"), [email_addr], txt
|
||||
)
|
||||
return txt
|
||||
|
@ -559,7 +523,6 @@ _admissionEditor = ndb.EditableTable(
|
|||
"villelycee",
|
||||
"codepostallycee",
|
||||
"codelycee",
|
||||
"debouche",
|
||||
"type_admission",
|
||||
"boursier_prec",
|
||||
),
|
||||
|
@ -583,8 +546,8 @@ admission_edit = _admissionEditor.edit
|
|||
|
||||
# Edition simultanee de identite et admission
|
||||
class EtudIdentEditor(object):
|
||||
def create(self, cnx, args, REQUEST=None):
|
||||
etudid = identite_create(cnx, args, REQUEST)
|
||||
def create(self, cnx, args):
|
||||
etudid = identite_create(cnx, args)
|
||||
args["etudid"] = etudid
|
||||
admission_create(cnx, args)
|
||||
return etudid
|
||||
|
@ -615,8 +578,8 @@ class EtudIdentEditor(object):
|
|||
res.sort(key=itemgetter("nom", "prenom"))
|
||||
return res
|
||||
|
||||
def edit(self, cnx, args, disable_notify=False, REQUEST=None):
|
||||
identite_edit(cnx, args, disable_notify=disable_notify, REQUEST=REQUEST)
|
||||
def edit(self, cnx, args, disable_notify=False):
|
||||
identite_edit(cnx, args, disable_notify=disable_notify)
|
||||
if "adm_id" in args: # safety net
|
||||
admission_edit(cnx, args)
|
||||
|
||||
|
@ -656,11 +619,17 @@ def make_etud_args(etudid=None, code_nip=None, use_request=True, raise_exc=True)
|
|||
return args
|
||||
|
||||
|
||||
def log_unknown_etud():
|
||||
"""Log request: cas ou getEtudInfo n'a pas ramene de resultat"""
|
||||
etud_args = make_etud_args(raise_exc=False)
|
||||
log(f"unknown student: args={etud_args}")
|
||||
|
||||
|
||||
def get_etud_info(etudid=False, code_nip=False, filled=False) -> list:
|
||||
"""infos sur un etudiant (API). If not foud, returns empty list.
|
||||
On peut specifier etudid ou code_nip
|
||||
ou bien cherche dans REQUEST.form: etudid, code_nip, code_ine
|
||||
(dans cet ordre).
|
||||
"""infos sur un etudiant (API). If not found, returns empty list.
|
||||
On peut spécifier etudid ou code_nip
|
||||
ou bien cherche dans les arguments de la requête courante:
|
||||
etudid, code_nip, code_ine (dans cet ordre).
|
||||
"""
|
||||
if etudid is None:
|
||||
return []
|
||||
|
@ -673,7 +642,20 @@ def get_etud_info(etudid=False, code_nip=False, filled=False) -> list:
|
|||
return etud
|
||||
|
||||
|
||||
def create_etud(cnx, args={}, REQUEST=None):
|
||||
# Optim par cache local, utilité non prouvée mais
|
||||
# on s'oriente vers un cahce persistent dans Redis ou bien l'utilisation de NT
|
||||
# def get_etud_info_filled_by_etudid(etudid, cnx=None) -> dict:
|
||||
# """Infos sur un étudiant, avec cache local à la requête"""
|
||||
# if etudid in g.stored_etud_info:
|
||||
# return g.stored_etud_info[etudid]
|
||||
# cnx = cnx or ndb.GetDBConnexion()
|
||||
# etud = etudident_list(cnx, args={"etudid": etudid})
|
||||
# fill_etuds_info(etud)
|
||||
# g.stored_etud_info[etudid] = etud[0]
|
||||
# return etud[0]
|
||||
|
||||
|
||||
def create_etud(cnx, args={}):
|
||||
"""Creation d'un étudiant. génère aussi évenement et "news".
|
||||
|
||||
Args:
|
||||
|
@ -685,7 +667,7 @@ def create_etud(cnx, args={}, REQUEST=None):
|
|||
from app.scodoc import sco_news
|
||||
|
||||
# creation d'un etudiant
|
||||
etudid = etudident_create(cnx, args, REQUEST=REQUEST)
|
||||
etudid = etudident_create(cnx, args)
|
||||
# crée une adresse vide (chaque etudiant doit etre dans la table "adresse" !)
|
||||
_ = adresse_create(
|
||||
cnx,
|
||||
|
@ -819,8 +801,8 @@ appreciations_edit = _appreciationsEditor.edit
|
|||
def read_etablissements():
|
||||
filename = os.path.join(scu.SCO_TOOLS_DIR, scu.CONFIG.ETABL_FILENAME)
|
||||
log("reading %s" % filename)
|
||||
f = open(filename)
|
||||
L = [x[:-1].split(";") for x in f]
|
||||
with open(filename) as f:
|
||||
L = [x[:-1].split(";") for x in f]
|
||||
E = {}
|
||||
for l in L[1:]:
|
||||
E[l[0]] = {
|
||||
|
|
|
@ -0,0 +1,254 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Vérification des abasneces à une évaluation
|
||||
"""
|
||||
from flask import url_for, g
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_moduleimpl
|
||||
|
||||
# matin et/ou après-midi ?
|
||||
def _eval_demijournee(E):
|
||||
"1 si matin, 0 si apres midi, 2 si toute la journee"
|
||||
am, pm = False, False
|
||||
if E["heure_debut"] < "13:00":
|
||||
am = True
|
||||
if E["heure_fin"] > "13:00":
|
||||
pm = True
|
||||
if am and pm:
|
||||
demijournee = 2
|
||||
elif am:
|
||||
demijournee = 1
|
||||
else:
|
||||
demijournee = 0
|
||||
pm = True
|
||||
return am, pm, demijournee
|
||||
|
||||
|
||||
def evaluation_check_absences(evaluation_id):
|
||||
"""Vérifie les absences au moment de cette évaluation.
|
||||
Cas incohérents que l'on peut rencontrer pour chaque étudiant:
|
||||
note et absent
|
||||
ABS et pas noté absent
|
||||
ABS et absent justifié
|
||||
EXC et pas noté absent
|
||||
EXC et pas justifie
|
||||
Ramene 3 listes d'etudid
|
||||
"""
|
||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
if not E["jour"]:
|
||||
return [], [], [], [], [] # evaluation sans date
|
||||
|
||||
am, pm, demijournee = _eval_demijournee(E)
|
||||
|
||||
# Liste les absences à ce moment:
|
||||
A = sco_abs.list_abs_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm)
|
||||
As = set([x["etudid"] for x in A]) # ensemble des etudiants absents
|
||||
NJ = sco_abs.list_abs_non_just_jour(ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm)
|
||||
NJs = set([x["etudid"] for x in NJ]) # ensemble des etudiants absents non justifies
|
||||
Just = sco_abs.list_abs_jour(
|
||||
ndb.DateDMYtoISO(E["jour"]), am=am, pm=pm, is_abs=None, is_just=True
|
||||
)
|
||||
Justs = set([x["etudid"] for x in Just]) # ensemble des etudiants avec justif
|
||||
|
||||
# Les notes:
|
||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
ValButAbs = [] # une note mais noté absent
|
||||
AbsNonSignalee = [] # note ABS mais pas noté absent
|
||||
ExcNonSignalee = [] # note EXC mais pas noté absent
|
||||
ExcNonJust = [] # note EXC mais absent non justifie
|
||||
AbsButExc = [] # note ABS mais justifié
|
||||
for etudid, _ in sco_groups.do_evaluation_listeetuds_groups(
|
||||
evaluation_id, getallstudents=True
|
||||
):
|
||||
if etudid in notes_db:
|
||||
val = notes_db[etudid]["value"]
|
||||
if (
|
||||
val != None and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_ATTENTE
|
||||
) and etudid in As:
|
||||
# note valide et absent
|
||||
ValButAbs.append(etudid)
|
||||
if val is None and not etudid in As:
|
||||
# absent mais pas signale comme tel
|
||||
AbsNonSignalee.append(etudid)
|
||||
if val == scu.NOTES_NEUTRALISE and not etudid in As:
|
||||
# Neutralisé mais pas signale absent
|
||||
ExcNonSignalee.append(etudid)
|
||||
if val == scu.NOTES_NEUTRALISE and etudid in NJs:
|
||||
# EXC mais pas justifié
|
||||
ExcNonJust.append(etudid)
|
||||
if val is None and etudid in Justs:
|
||||
# ABS mais justificatif
|
||||
AbsButExc.append(etudid)
|
||||
|
||||
return ValButAbs, AbsNonSignalee, ExcNonSignalee, ExcNonJust, AbsButExc
|
||||
|
||||
|
||||
def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True):
|
||||
"""Affiche état vérification absences d'une évaluation"""
|
||||
|
||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
am, pm, demijournee = _eval_demijournee(E)
|
||||
|
||||
(
|
||||
ValButAbs,
|
||||
AbsNonSignalee,
|
||||
ExcNonSignalee,
|
||||
ExcNonJust,
|
||||
AbsButExc,
|
||||
) = evaluation_check_absences(evaluation_id)
|
||||
|
||||
if with_header:
|
||||
H = [
|
||||
html_sco_header.html_sem_header("Vérification absences à l'évaluation"),
|
||||
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
|
||||
"""<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.</p>""",
|
||||
]
|
||||
else:
|
||||
# pas de header, mais un titre
|
||||
H = [
|
||||
"""<h2 class="eval_check_absences">%s du %s """
|
||||
% (E["description"], E["jour"])
|
||||
]
|
||||
if (
|
||||
not ValButAbs
|
||||
and not AbsNonSignalee
|
||||
and not ExcNonSignalee
|
||||
and not ExcNonJust
|
||||
):
|
||||
H.append(': <span class="eval_check_absences_ok">ok</span>')
|
||||
H.append("</h2>")
|
||||
|
||||
def etudlist(etudids, linkabs=False):
|
||||
H.append("<ul>")
|
||||
if not etudids and show_ok:
|
||||
H.append("<li>aucun</li>")
|
||||
for etudid in etudids:
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
H.append(
|
||||
'<li><a class="discretelink" href="%s">'
|
||||
% url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
|
||||
)
|
||||
+ "%(nomprenom)s</a>" % etud
|
||||
)
|
||||
if linkabs:
|
||||
H.append(
|
||||
f"""<a class="stdlink" href="{url_for(
|
||||
'absences.doSignaleAbsence',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud["etudid"],
|
||||
datedebut=E["jour"],
|
||||
datefin=E["jour"],
|
||||
demijournee=demijournee,
|
||||
moduleimpl_id=E["moduleimpl_id"],
|
||||
)
|
||||
}">signaler cette absence</a>"""
|
||||
)
|
||||
H.append("</li>")
|
||||
H.append("</ul>")
|
||||
|
||||
if ValButAbs or show_ok:
|
||||
H.append(
|
||||
"<h3>Etudiants ayant une note alors qu'ils sont signalés absents:</h3>"
|
||||
)
|
||||
etudlist(ValButAbs)
|
||||
|
||||
if AbsNonSignalee or show_ok:
|
||||
H.append(
|
||||
"""<h3>Etudiants avec note "ABS" alors qu'ils ne sont <em>pas</em> signalés absents:</h3>"""
|
||||
)
|
||||
etudlist(AbsNonSignalee, linkabs=True)
|
||||
|
||||
if ExcNonSignalee or show_ok:
|
||||
H.append(
|
||||
"""<h3>Etudiants avec note "EXC" alors qu'ils ne sont <em>pas</em> signalés absents:</h3>"""
|
||||
)
|
||||
etudlist(ExcNonSignalee)
|
||||
|
||||
if ExcNonJust or show_ok:
|
||||
H.append(
|
||||
"""<h3>Etudiants avec note "EXC" alors qu'ils sont absents <em>non justifiés</em>:</h3>"""
|
||||
)
|
||||
etudlist(ExcNonJust)
|
||||
|
||||
if AbsButExc or show_ok:
|
||||
H.append(
|
||||
"""<h3>Etudiants avec note "ABS" alors qu'ils ont une <em>justification</em>:</h3>"""
|
||||
)
|
||||
etudlist(AbsButExc)
|
||||
|
||||
if with_header:
|
||||
H.append(html_sco_header.sco_footer())
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def formsemestre_check_absences_html(formsemestre_id):
|
||||
"""Affiche etat verification absences pour toutes les evaluations du semestre !"""
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
H = [
|
||||
html_sco_header.html_sem_header(
|
||||
"Vérification absences aux évaluations de ce semestre",
|
||||
sem,
|
||||
),
|
||||
"""<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.
|
||||
Sont listés tous les modules avec des évaluations.<br/>Aucune action n'est effectuée:
|
||||
il vous appartient de corriger les erreurs détectées si vous le jugez nécessaire.
|
||||
</p>""",
|
||||
]
|
||||
# Modules, dans l'ordre
|
||||
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
|
||||
for M in Mlist:
|
||||
evals = sco_evaluation_db.do_evaluation_list(
|
||||
{"moduleimpl_id": M["moduleimpl_id"]}
|
||||
)
|
||||
if evals:
|
||||
H.append(
|
||||
'<div class="module_check_absences"><h2><a href="moduleimpl_status?moduleimpl_id=%s">%s: %s</a></h2>'
|
||||
% (M["moduleimpl_id"], M["module"]["code"], M["module"]["abbrev"])
|
||||
)
|
||||
for E in evals:
|
||||
H.append(
|
||||
evaluation_check_absences_html(
|
||||
E["evaluation_id"],
|
||||
with_header=False,
|
||||
show_ok=False,
|
||||
)
|
||||
)
|
||||
if evals:
|
||||
H.append("</div>")
|
||||
H.append(html_sco_header.sco_footer())
|
||||
return "\n".join(H)
|
|
@ -0,0 +1,482 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@gmail.com
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Gestion evaluations (ScoDoc7, sans SQlAlchemy)
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import pprint
|
||||
|
||||
import flask
|
||||
from flask import url_for, g
|
||||
from flask_login import current_user
|
||||
|
||||
from app import log
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_news
|
||||
from app.scodoc import sco_permissions_check
|
||||
|
||||
|
||||
_evaluationEditor = ndb.EditableTable(
|
||||
"notes_evaluation",
|
||||
"evaluation_id",
|
||||
(
|
||||
"evaluation_id",
|
||||
"moduleimpl_id",
|
||||
"jour",
|
||||
"heure_debut",
|
||||
"heure_fin",
|
||||
"description",
|
||||
"note_max",
|
||||
"coefficient",
|
||||
"visibulletin",
|
||||
"publish_incomplete",
|
||||
"evaluation_type",
|
||||
"numero",
|
||||
),
|
||||
sortkey="numero desc, jour desc, heure_debut desc", # plus recente d'abord
|
||||
output_formators={
|
||||
"jour": ndb.DateISOtoDMY,
|
||||
"numero": ndb.int_null_is_zero,
|
||||
},
|
||||
input_formators={
|
||||
"jour": ndb.DateDMYtoISO,
|
||||
"heure_debut": ndb.TimetoISO8601, # converti par evaluation_enrich_dict
|
||||
"heure_fin": ndb.TimetoISO8601, # converti par evaluation_enrich_dict
|
||||
"visibulletin": bool,
|
||||
"publish_incomplete": bool,
|
||||
"evaluation_type": int,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def evaluation_enrich_dict(e):
|
||||
"""add or convert some fileds in an evaluation dict"""
|
||||
# For ScoDoc7 compat
|
||||
heure_debut_dt = e["heure_debut"] or datetime.time(
|
||||
8, 00
|
||||
) # au cas ou pas d'heure (note externe?)
|
||||
heure_fin_dt = e["heure_fin"] or datetime.time(8, 00)
|
||||
e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"])
|
||||
e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"])
|
||||
e["jouriso"] = ndb.DateDMYtoISO(e["jour"])
|
||||
heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
|
||||
d = ndb.TimeDuration(heure_debut, heure_fin)
|
||||
if d is not None:
|
||||
m = d % 60
|
||||
e["duree"] = "%dh" % (d / 60)
|
||||
if m != 0:
|
||||
e["duree"] += "%02d" % m
|
||||
else:
|
||||
e["duree"] = ""
|
||||
if heure_debut and (not heure_fin or heure_fin == heure_debut):
|
||||
e["descrheure"] = " à " + heure_debut
|
||||
elif heure_debut and heure_fin:
|
||||
e["descrheure"] = " de %s à %s" % (heure_debut, heure_fin)
|
||||
else:
|
||||
e["descrheure"] = ""
|
||||
# matin, apresmidi: utile pour se referer aux absences:
|
||||
if heure_debut_dt < datetime.time(12, 00):
|
||||
e["matin"] = 1
|
||||
else:
|
||||
e["matin"] = 0
|
||||
if heure_fin_dt > datetime.time(12, 00):
|
||||
e["apresmidi"] = 1
|
||||
else:
|
||||
e["apresmidi"] = 0
|
||||
return e
|
||||
|
||||
|
||||
def do_evaluation_list(args, sortkey=None):
|
||||
"""List evaluations, sorted by numero (or most recent date first).
|
||||
|
||||
Ajoute les champs:
|
||||
'duree' : '2h30'
|
||||
'matin' : 1 (commence avant 12:00) ou 0
|
||||
'apresmidi' : 1 (termine après 12:00) ou 0
|
||||
'descrheure' : ' de 15h00 à 16h30'
|
||||
"""
|
||||
# Attention: transformation fonction ScoDc7 en SQLAlchemy
|
||||
cnx = ndb.GetDBConnexion()
|
||||
evals = _evaluationEditor.list(cnx, args, sortkey=sortkey)
|
||||
# calcule duree (chaine de car.) de chaque evaluation et ajoute jouriso, matin, apresmidi
|
||||
for e in evals:
|
||||
evaluation_enrich_dict(e)
|
||||
|
||||
return evals
|
||||
|
||||
|
||||
def do_evaluation_list_in_formsemestre(formsemestre_id):
|
||||
"list evaluations in this formsemestre"
|
||||
mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
|
||||
evals = []
|
||||
for modimpl in mods:
|
||||
evals += do_evaluation_list(args={"moduleimpl_id": modimpl["moduleimpl_id"]})
|
||||
return evals
|
||||
|
||||
|
||||
def _check_evaluation_args(args):
|
||||
"Check coefficient, dates and duration, raises exception if invalid"
|
||||
moduleimpl_id = args["moduleimpl_id"]
|
||||
# check bareme
|
||||
note_max = args.get("note_max", None)
|
||||
if note_max is None:
|
||||
raise ScoValueError("missing note_max")
|
||||
try:
|
||||
note_max = float(note_max)
|
||||
except ValueError:
|
||||
raise ScoValueError("Invalid note_max value")
|
||||
if note_max < 0:
|
||||
raise ScoValueError("Invalid note_max value (must be positive or null)")
|
||||
# check coefficient
|
||||
coef = args.get("coefficient", None)
|
||||
if coef is None:
|
||||
raise ScoValueError("missing coefficient")
|
||||
try:
|
||||
coef = float(coef)
|
||||
except ValueError:
|
||||
raise ScoValueError("Invalid coefficient value")
|
||||
if coef < 0:
|
||||
raise ScoValueError("Invalid coefficient value (must be positive or null)")
|
||||
# check date
|
||||
jour = args.get("jour", None)
|
||||
args["jour"] = jour
|
||||
if jour:
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
|
||||
d, m, y = [int(x) for x in sem["date_debut"].split("/")]
|
||||
date_debut = datetime.date(y, m, d)
|
||||
d, m, y = [int(x) for x in sem["date_fin"].split("/")]
|
||||
date_fin = datetime.date(y, m, d)
|
||||
# passe par ndb.DateDMYtoISO pour avoir date pivot
|
||||
y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")]
|
||||
jour = datetime.date(y, m, d)
|
||||
if (jour > date_fin) or (jour < date_debut):
|
||||
raise ScoValueError(
|
||||
"La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !"
|
||||
% (d, m, y)
|
||||
)
|
||||
heure_debut = args.get("heure_debut", None)
|
||||
args["heure_debut"] = heure_debut
|
||||
heure_fin = args.get("heure_fin", None)
|
||||
args["heure_fin"] = heure_fin
|
||||
if jour and ((not heure_debut) or (not heure_fin)):
|
||||
raise ScoValueError("Les heures doivent être précisées")
|
||||
d = ndb.TimeDuration(heure_debut, heure_fin)
|
||||
if d and ((d < 0) or (d > 60 * 12)):
|
||||
raise ScoValueError("Heures de l'évaluation incohérentes !")
|
||||
|
||||
|
||||
def do_evaluation_create(
|
||||
moduleimpl_id=None,
|
||||
jour=None,
|
||||
heure_debut=None,
|
||||
heure_fin=None,
|
||||
description=None,
|
||||
note_max=None,
|
||||
coefficient=None,
|
||||
visibulletin=None,
|
||||
publish_incomplete=None,
|
||||
evaluation_type=None,
|
||||
numero=None,
|
||||
**kw, # ceci pour absorber les arguments excedentaires de tf #sco8
|
||||
):
|
||||
"""Create an evaluation"""
|
||||
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
|
||||
raise AccessDenied(
|
||||
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
|
||||
)
|
||||
args = locals()
|
||||
log("do_evaluation_create: args=" + str(args))
|
||||
_check_evaluation_args(args)
|
||||
# Check numeros
|
||||
module_evaluation_renumber(moduleimpl_id, only_if_unumbered=True)
|
||||
if not "numero" in args or args["numero"] is None:
|
||||
n = None
|
||||
# determine le numero avec la date
|
||||
# Liste des eval existantes triees par date, la plus ancienne en tete
|
||||
mod_evals = do_evaluation_list(
|
||||
args={"moduleimpl_id": moduleimpl_id},
|
||||
sortkey="jour asc, heure_debut asc",
|
||||
)
|
||||
if args["jour"]:
|
||||
next_eval = None
|
||||
t = (
|
||||
ndb.DateDMYtoISO(args["jour"], null_is_empty=True),
|
||||
ndb.TimetoISO8601(args["heure_debut"], null_is_empty=True),
|
||||
)
|
||||
for e in mod_evals:
|
||||
if (
|
||||
ndb.DateDMYtoISO(e["jour"], null_is_empty=True),
|
||||
ndb.TimetoISO8601(e["heure_debut"], null_is_empty=True),
|
||||
) > t:
|
||||
next_eval = e
|
||||
break
|
||||
if next_eval:
|
||||
n = module_evaluation_insert_before(mod_evals, next_eval)
|
||||
else:
|
||||
n = None # a placer en fin
|
||||
if n is None: # pas de date ou en fin:
|
||||
if mod_evals:
|
||||
log(pprint.pformat(mod_evals[-1]))
|
||||
n = mod_evals[-1]["numero"] + 1
|
||||
else:
|
||||
n = 0 # the only one
|
||||
# log("creating with numero n=%d" % n)
|
||||
args["numero"] = n
|
||||
|
||||
#
|
||||
cnx = ndb.GetDBConnexion()
|
||||
r = _evaluationEditor.create(cnx, args)
|
||||
|
||||
# news
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
mod["moduleimpl_id"] = M["moduleimpl_id"]
|
||||
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_NOTE,
|
||||
object=moduleimpl_id,
|
||||
text='Création d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
|
||||
url=mod["url"],
|
||||
)
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def do_evaluation_edit(args):
|
||||
"edit an evaluation"
|
||||
evaluation_id = args["evaluation_id"]
|
||||
the_evals = do_evaluation_list({"evaluation_id": evaluation_id})
|
||||
if not the_evals:
|
||||
raise ValueError("evaluation inexistante !")
|
||||
moduleimpl_id = the_evals[0]["moduleimpl_id"]
|
||||
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
|
||||
raise AccessDenied(
|
||||
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
|
||||
)
|
||||
args["moduleimpl_id"] = moduleimpl_id
|
||||
_check_evaluation_args(args)
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_evaluationEditor.edit(cnx, args)
|
||||
# inval cache pour ce semestre
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"])
|
||||
|
||||
|
||||
def do_evaluation_delete(evaluation_id):
|
||||
"delete evaluation"
|
||||
the_evals = do_evaluation_list({"evaluation_id": evaluation_id})
|
||||
if not the_evals:
|
||||
raise ValueError("evaluation inexistante !")
|
||||
moduleimpl_id = the_evals[0]["moduleimpl_id"]
|
||||
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
|
||||
raise AccessDenied(
|
||||
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
|
||||
)
|
||||
notes_db = do_evaluation_get_all_notes(evaluation_id) # { etudid : value }
|
||||
notes = [x["value"] for x in notes_db.values()]
|
||||
if notes:
|
||||
raise ScoValueError(
|
||||
"Impossible de supprimer cette évaluation: il reste des notes"
|
||||
)
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
|
||||
_evaluationEditor.delete(cnx, evaluation_id)
|
||||
# inval cache pour ce semestre
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"])
|
||||
# news
|
||||
|
||||
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
mod["moduleimpl_id"] = M["moduleimpl_id"]
|
||||
mod["url"] = (
|
||||
scu.NotesURL() + "/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
|
||||
)
|
||||
sco_news.add(
|
||||
typ=sco_news.NEWS_NOTE,
|
||||
object=moduleimpl_id,
|
||||
text='Suppression d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
|
||||
url=mod["url"],
|
||||
)
|
||||
|
||||
|
||||
# ancien _notes_getall
|
||||
def do_evaluation_get_all_notes(
|
||||
evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None
|
||||
):
|
||||
"""Toutes les notes pour une evaluation: { etudid : { 'value' : value, 'date' : date ... }}
|
||||
Attention: inclut aussi les notes des étudiants qui ne sont plus inscrits au module.
|
||||
"""
|
||||
do_cache = (
|
||||
filter_suppressed and table == "notes_notes" and (by_uid is None)
|
||||
) # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant
|
||||
if do_cache:
|
||||
r = sco_cache.EvaluationCache.get(evaluation_id)
|
||||
if r != None:
|
||||
return r
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
cond = " where evaluation_id=%(evaluation_id)s"
|
||||
if by_uid:
|
||||
cond += " and uid=%(by_uid)s"
|
||||
|
||||
cursor.execute(
|
||||
"select * from " + table + cond,
|
||||
{"evaluation_id": evaluation_id, "by_uid": by_uid},
|
||||
)
|
||||
res = cursor.dictfetchall()
|
||||
d = {}
|
||||
if filter_suppressed:
|
||||
for x in res:
|
||||
if x["value"] != scu.NOTES_SUPPRESS:
|
||||
d[x["etudid"]] = x
|
||||
else:
|
||||
for x in res:
|
||||
d[x["etudid"]] = x
|
||||
if do_cache:
|
||||
status = sco_cache.EvaluationCache.set(evaluation_id, d)
|
||||
if not status:
|
||||
log(f"Warning: EvaluationCache.set: {evaluation_id}\t{status}")
|
||||
return d
|
||||
|
||||
|
||||
def module_evaluation_renumber(moduleimpl_id, only_if_unumbered=False, redirect=0):
|
||||
"""Renumber evaluations in this module, according to their date. (numero=0: oldest one)
|
||||
Needed because previous versions of ScoDoc did not have eval numeros
|
||||
Note: existing numeros are ignored
|
||||
"""
|
||||
redirect = int(redirect)
|
||||
# log('module_evaluation_renumber( moduleimpl_id=%s )' % moduleimpl_id )
|
||||
# List sorted according to date/heure, ignoring numeros:
|
||||
# (note that we place evaluations with NULL date at the end)
|
||||
mod_evals = do_evaluation_list(
|
||||
args={"moduleimpl_id": moduleimpl_id},
|
||||
sortkey="jour asc, heure_debut asc",
|
||||
)
|
||||
|
||||
all_numbered = False not in [x["numero"] > 0 for x in mod_evals]
|
||||
if all_numbered and only_if_unumbered:
|
||||
return # all ok
|
||||
|
||||
# Reset all numeros:
|
||||
i = 1
|
||||
for e in mod_evals:
|
||||
e["numero"] = i
|
||||
do_evaluation_edit(e)
|
||||
i += 1
|
||||
|
||||
# If requested, redirect to moduleimpl page:
|
||||
if redirect:
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def module_evaluation_insert_before(mod_evals, next_eval):
|
||||
"""Renumber evals such that an evaluation with can be inserted before next_eval
|
||||
Returns numero suitable for the inserted evaluation
|
||||
"""
|
||||
if next_eval:
|
||||
n = next_eval["numero"]
|
||||
if not n:
|
||||
log("renumbering old evals")
|
||||
module_evaluation_renumber(next_eval["moduleimpl_id"])
|
||||
next_eval = do_evaluation_list(
|
||||
args={"evaluation_id": next_eval["evaluation_id"]}
|
||||
)[0]
|
||||
n = next_eval["numero"]
|
||||
else:
|
||||
n = 1
|
||||
# log('inserting at position numero %s' % n )
|
||||
# all numeros >= n are incremented
|
||||
for e in mod_evals:
|
||||
if e["numero"] >= n:
|
||||
e["numero"] += 1
|
||||
# log('incrementing %s to %s' % (e['evaluation_id'], e['numero']))
|
||||
do_evaluation_edit(e)
|
||||
|
||||
return n
|
||||
|
||||
|
||||
def module_evaluation_move(evaluation_id, after=0, redirect=1):
|
||||
"""Move before/after previous one (decrement/increment numero)
|
||||
(published)
|
||||
"""
|
||||
e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
|
||||
redirect = int(redirect)
|
||||
# access: can change eval ?
|
||||
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=e["moduleimpl_id"]):
|
||||
raise AccessDenied(
|
||||
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
|
||||
)
|
||||
|
||||
module_evaluation_renumber(e["moduleimpl_id"], only_if_unumbered=True)
|
||||
e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
|
||||
|
||||
after = int(after) # 0: deplace avant, 1 deplace apres
|
||||
if after not in (0, 1):
|
||||
raise ValueError('invalid value for "after"')
|
||||
mod_evals = do_evaluation_list({"moduleimpl_id": e["moduleimpl_id"]})
|
||||
if len(mod_evals) > 1:
|
||||
idx = [p["evaluation_id"] for p in mod_evals].index(evaluation_id)
|
||||
neigh = None # object to swap with
|
||||
if after == 0 and idx > 0:
|
||||
neigh = mod_evals[idx - 1]
|
||||
elif after == 1 and idx < len(mod_evals) - 1:
|
||||
neigh = mod_evals[idx + 1]
|
||||
if neigh: #
|
||||
if neigh["numero"] == e["numero"]:
|
||||
log("Warning: module_evaluation_move: forcing renumber")
|
||||
module_evaluation_renumber(e["moduleimpl_id"], only_if_unumbered=False)
|
||||
else:
|
||||
# swap numero with neighbor
|
||||
e["numero"], neigh["numero"] = neigh["numero"], e["numero"]
|
||||
do_evaluation_edit(e)
|
||||
do_evaluation_edit(neigh)
|
||||
# redirect to moduleimpl page:
|
||||
if redirect:
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=e["moduleimpl_id"],
|
||||
)
|
||||
)
|
|
@ -0,0 +1,339 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@gmail.com
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Formulaire ajout/édition d'une évaluation
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import flask
|
||||
from flask import url_for, render_template
|
||||
from flask import g
|
||||
from flask_login import current_user
|
||||
from flask import request
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app import models
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_permissions_check
|
||||
|
||||
|
||||
def evaluation_create_form(
|
||||
moduleimpl_id=None,
|
||||
evaluation_id=None,
|
||||
edit=False,
|
||||
page_title="Évaluation",
|
||||
):
|
||||
"Formulaire création/édition d'une évaluation (pas de ses notes)"
|
||||
if evaluation_id is not None:
|
||||
evaluation = models.Evaluation.query.get(evaluation_id)
|
||||
moduleimpl_id = evaluation.moduleimpl_id
|
||||
#
|
||||
modimpl_o = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[
|
||||
0
|
||||
]
|
||||
mod = modimpl_o["module"]
|
||||
formsemestre_id = modimpl_o["formsemestre_id"]
|
||||
sem = FormSemestre.query.get(formsemestre_id)
|
||||
sem_ues = sem.query_ues(with_sport=False).all()
|
||||
is_malus = mod["module_type"] == ModuleType.MALUS
|
||||
is_apc = mod["module_type"] in (ModuleType.RESSOURCE, ModuleType.SAE)
|
||||
|
||||
min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible
|
||||
#
|
||||
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
|
||||
return (
|
||||
html_sco_header.sco_header()
|
||||
+ "<h2>Opération non autorisée</h2><p>"
|
||||
+ "Modification évaluation impossible pour %s"
|
||||
% current_user.get_nomplogin()
|
||||
+ "</p>"
|
||||
+ '<p><a href="moduleimpl_status?moduleimpl_id=%s">Revenir</a></p>'
|
||||
% (moduleimpl_id,)
|
||||
+ html_sco_header.sco_footer()
|
||||
)
|
||||
if not edit:
|
||||
# création nouvel
|
||||
if moduleimpl_id is None:
|
||||
raise ValueError("missing moduleimpl_id parameter")
|
||||
initvalues = {
|
||||
"note_max": 20,
|
||||
"jour": time.strftime("%d/%m/%Y", time.localtime()),
|
||||
"publish_incomplete": is_malus,
|
||||
}
|
||||
submitlabel = "Créer cette évaluation"
|
||||
action = "Création d'une évaluation"
|
||||
link = ""
|
||||
else:
|
||||
# édition données existantes
|
||||
# setup form init values
|
||||
if evaluation_id is None:
|
||||
raise ValueError("missing evaluation_id parameter")
|
||||
initvalues = evaluation.to_dict()
|
||||
moduleimpl_id = initvalues["moduleimpl_id"]
|
||||
submitlabel = "Modifier les données"
|
||||
action = "Modification d'une évaluation"
|
||||
link = ""
|
||||
# Note maximale actuelle dans cette éval ?
|
||||
etat = sco_evaluations.do_evaluation_etat(evaluation_id)
|
||||
if etat["maxi_num"] is not None:
|
||||
min_note_max = max(scu.NOTES_PRECISION, etat["maxi_num"])
|
||||
else:
|
||||
min_note_max = scu.NOTES_PRECISION
|
||||
#
|
||||
if min_note_max > scu.NOTES_PRECISION:
|
||||
min_note_max_str = scu.fmt_note(min_note_max)
|
||||
else:
|
||||
min_note_max_str = "0"
|
||||
#
|
||||
mod_descr = '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> %s' % (
|
||||
moduleimpl_id,
|
||||
mod["code"],
|
||||
mod["titre"],
|
||||
link,
|
||||
)
|
||||
H = [
|
||||
f"""<h3>{action} en
|
||||
{scu.MODULE_TYPE_NAMES[mod["module_type"]]} {mod_descr}</h3>
|
||||
"""
|
||||
]
|
||||
|
||||
heures = ["%02dh%02d" % (h, m) for h in range(8, 19) for m in (0, 30)]
|
||||
#
|
||||
initvalues["visibulletin"] = initvalues.get("visibulletin", True)
|
||||
if initvalues["visibulletin"]:
|
||||
initvalues["visibulletinlist"] = ["X"]
|
||||
else:
|
||||
initvalues["visibulletinlist"] = []
|
||||
vals = scu.get_request_args()
|
||||
if vals.get("tf_submitted", False) and "visibulletinlist" not in vals:
|
||||
vals["visibulletinlist"] = []
|
||||
#
|
||||
if is_apc: # BUT: poids vers les UE
|
||||
ue_coef_dict = ModuleImpl.query.get(moduleimpl_id).module.get_ue_coef_dict()
|
||||
for ue in sem_ues:
|
||||
if edit:
|
||||
existing_poids = models.EvaluationUEPoids.query.filter_by(
|
||||
ue=ue, evaluation=evaluation
|
||||
).first()
|
||||
else:
|
||||
existing_poids = None
|
||||
if existing_poids:
|
||||
poids = existing_poids.poids
|
||||
else:
|
||||
coef_ue = ue_coef_dict.get(ue.id, 0.0) or 0.0
|
||||
if coef_ue > 0:
|
||||
poids = 1.0 # par defaut au départ
|
||||
else:
|
||||
poids = 0.0
|
||||
initvalues[f"poids_{ue.id}"] = poids
|
||||
#
|
||||
form = [
|
||||
("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
|
||||
("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}),
|
||||
("moduleimpl_id", {"default": moduleimpl_id, "input_type": "hidden"}),
|
||||
# ('jour', { 'title' : 'Date (j/m/a)', 'size' : 12, 'explanation' : 'date de l\'examen, devoir ou contrôle' }),
|
||||
(
|
||||
"jour",
|
||||
{
|
||||
"input_type": "datedmy",
|
||||
"title": "Date",
|
||||
"size": 12,
|
||||
"explanation": "date de l'examen, devoir ou contrôle",
|
||||
},
|
||||
),
|
||||
(
|
||||
"heure_debut",
|
||||
{
|
||||
"title": "Heure de début",
|
||||
"explanation": "heure du début de l'épreuve",
|
||||
"input_type": "menu",
|
||||
"allowed_values": heures,
|
||||
"labels": heures,
|
||||
},
|
||||
),
|
||||
(
|
||||
"heure_fin",
|
||||
{
|
||||
"title": "Heure de fin",
|
||||
"explanation": "heure de fin de l'épreuve",
|
||||
"input_type": "menu",
|
||||
"allowed_values": heures,
|
||||
"labels": heures,
|
||||
},
|
||||
),
|
||||
]
|
||||
if is_malus: # pas de coefficient
|
||||
form.append(("coefficient", {"input_type": "hidden", "default": "1."}))
|
||||
elif not is_apc: # modules standard hors BUT
|
||||
form.append(
|
||||
(
|
||||
"coefficient",
|
||||
{
|
||||
"size": 6,
|
||||
"type": "float",
|
||||
"explanation": "coef. dans le module (choisi librement par l'enseignant)",
|
||||
"allow_null": False,
|
||||
},
|
||||
)
|
||||
)
|
||||
form += [
|
||||
(
|
||||
"note_max",
|
||||
{
|
||||
"size": 4,
|
||||
"type": "float",
|
||||
"title": "Notes de 0 à",
|
||||
"explanation": "barème (note max actuelle: %s)" % min_note_max_str,
|
||||
"allow_null": False,
|
||||
"max_value": scu.NOTES_MAX,
|
||||
"min_value": min_note_max,
|
||||
},
|
||||
),
|
||||
(
|
||||
"description",
|
||||
{
|
||||
"size": 36,
|
||||
"type": "text",
|
||||
"explanation": 'type d\'évaluation, apparait sur le bulletins longs. Exemples: "contrôle court", "examen de TP", "examen final".',
|
||||
},
|
||||
),
|
||||
(
|
||||
"visibulletinlist",
|
||||
{
|
||||
"input_type": "checkbox",
|
||||
"allowed_values": ["X"],
|
||||
"labels": [""],
|
||||
"title": "Visible sur bulletins",
|
||||
"explanation": "(pour les bulletins en version intermédiaire)",
|
||||
},
|
||||
),
|
||||
(
|
||||
"publish_incomplete",
|
||||
{
|
||||
"input_type": "boolcheckbox",
|
||||
"title": "Prise en compte immédiate",
|
||||
"explanation": "notes utilisées même si incomplètes",
|
||||
},
|
||||
),
|
||||
(
|
||||
"evaluation_type",
|
||||
{
|
||||
"input_type": "menu",
|
||||
"title": "Modalité",
|
||||
"allowed_values": (
|
||||
scu.EVALUATION_NORMALE,
|
||||
scu.EVALUATION_RATTRAPAGE,
|
||||
scu.EVALUATION_SESSION2,
|
||||
),
|
||||
"type": "int",
|
||||
"labels": (
|
||||
"Normale",
|
||||
"Rattrapage (remplace si meilleure note)",
|
||||
"Deuxième session (remplace toujours)",
|
||||
),
|
||||
},
|
||||
),
|
||||
]
|
||||
if is_apc: # ressources et SAÉs
|
||||
form += [
|
||||
(
|
||||
"coefficient",
|
||||
{
|
||||
"size": 6,
|
||||
"type": "float",
|
||||
"explanation": "importance de l'évaluation (multiplie les poids ci-dessous)",
|
||||
"allow_null": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
# Liste des UE utilisées dans des modules de ce semestre:
|
||||
for ue in sem_ues:
|
||||
form.append(
|
||||
(
|
||||
f"poids_{ue.id}",
|
||||
{
|
||||
"title": f"Poids {ue.acronyme}",
|
||||
"size": 2,
|
||||
"type": "float",
|
||||
"explanation": f"{ue.titre}",
|
||||
"allow_null": False,
|
||||
},
|
||||
),
|
||||
)
|
||||
tf = TrivialFormulator(
|
||||
request.base_url,
|
||||
vals,
|
||||
form,
|
||||
cancelbutton="Annuler",
|
||||
submitlabel=submitlabel,
|
||||
initvalues=initvalues,
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
dest_url = "moduleimpl_status?moduleimpl_id=%s" % modimpl_o["moduleimpl_id"]
|
||||
if tf[0] == 0:
|
||||
head = html_sco_header.sco_header(page_title=page_title)
|
||||
return (
|
||||
head
|
||||
+ "\n".join(H)
|
||||
+ "\n"
|
||||
+ tf[1]
|
||||
+ render_template("scodoc/help/evaluations.html", is_apc=is_apc)
|
||||
+ html_sco_header.sco_footer()
|
||||
)
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(dest_url)
|
||||
else:
|
||||
# form submission
|
||||
if tf[2]["visibulletinlist"]:
|
||||
tf[2]["visibulletin"] = True
|
||||
else:
|
||||
tf[2]["visibulletin"] = False
|
||||
if edit:
|
||||
sco_evaluation_db.do_evaluation_edit(tf[2])
|
||||
else:
|
||||
# creation d'une evaluation
|
||||
evaluation_id = sco_evaluation_db.do_evaluation_create(**tf[2])
|
||||
if is_apc:
|
||||
# Set poids
|
||||
evaluation = models.Evaluation.query.get(evaluation_id)
|
||||
for ue in sem_ues:
|
||||
evaluation.set_ue_poids(ue, tf[2][f"poids_{ue.id}"])
|
||||
db.session.add(evaluation)
|
||||
db.session.commit()
|
||||
return flask.redirect(dest_url)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue