forked from ScoDoc/ScoDoc
Compare commits
531 Commits
master
...
modif_fron
Author | SHA1 | Date |
---|---|---|
iziram | db5fe4f640 | |
iziram | 2770b22d51 | |
iziram | 410cdf3a75 | |
iziram | c637976036 | |
iziram | 2d10b979d7 | |
iziram | bf84be32aa | |
iziram | 97b1d3e076 | |
iziram | eee7184ef3 | |
iziram | 41ba13ea84 | |
iziram | 598ce814ed | |
iziram | 5aa9a6afc4 | |
iziram | 4d3d72d6b5 | |
iziram | 0ed67e5d9d | |
iziram | d940ed4419 | |
Emmanuel Viennet | 0a66c69fa1 | |
Emmanuel Viennet | 48caf1c047 | |
Emmanuel Viennet | c46f64f4c5 | |
Emmanuel Viennet | 3715b552f5 | |
Emmanuel Viennet | 9629d9cc05 | |
Emmanuel Viennet | b87a9b7b71 | |
Emmanuel Viennet | 2f1f70f163 | |
Emmanuel Viennet | 5824bf385c | |
Emmanuel Viennet | 14a79f5de0 | |
Emmanuel Viennet | fee7ad381c | |
Emmanuel Viennet | 3396bb1cb8 | |
Emmanuel Viennet | 7debba1e9a | |
Emmanuel Viennet | e5bca2a34c | |
Emmanuel Viennet | 86b34fc130 | |
Emmanuel Viennet | d5934a78ad | |
Emmanuel Viennet | 48dc86b094 | |
Emmanuel Viennet | b7ae8acc09 | |
Emmanuel Viennet | 4fc09b32ee | |
Emmanuel Viennet | c66c8cf02a | |
Emmanuel Viennet | a2d62b296b | |
Emmanuel Viennet | 30588d83d7 | |
Emmanuel Viennet | 636a328f79 | |
Emmanuel Viennet | ffa9ca5c0b | |
Emmanuel Viennet | b5d480afd0 | |
Emmanuel Viennet | 7b93ae0870 | |
Emmanuel Viennet | 7044ab42c5 | |
Jean-Marie Place | 976ebcd919 | |
Emmanuel Viennet | b626dd7356 | |
Emmanuel Viennet | 6ea623fc3e | |
Emmanuel Viennet | 9d6b3e3784 | |
Emmanuel Viennet | e73bb7314c | |
Emmanuel Viennet | 35c737b88b | |
Emmanuel Viennet | 59c7cd76e6 | |
Emmanuel Viennet | 5caf4e925b | |
Emmanuel Viennet | 64772feabe | |
Emmanuel Viennet | ec5b66ebc9 | |
Emmanuel Viennet | ed34d0b2b6 | |
Emmanuel Viennet | 08e73fcdb3 | |
Emmanuel Viennet | f3bbd599fe | |
Emmanuel Viennet | 3b9e6c07bf | |
Emmanuel Viennet | cf6027d44f | |
Emmanuel Viennet | 866a52e73e | |
Emmanuel Viennet | efc5455a53 | |
Emmanuel Viennet | 24f6b7b196 | |
Emmanuel Viennet | 1a9a1be4af | |
Emmanuel Viennet | 37be7cb858 | |
Emmanuel Viennet | 94ea3ea6dd | |
Jean-Marie Place | 0a9e6127a7 | |
Emmanuel Viennet | 9e267d8099 | |
Emmanuel Viennet | 8a5d515b78 | |
Emmanuel Viennet | aa3b47aeef | |
Emmanuel Viennet | 5c212b0bea | |
Emmanuel Viennet | bb76dbb3b0 | |
Emmanuel Viennet | d50d3f033a | |
Emmanuel Viennet | 692b304c6c | |
Emmanuel Viennet | 05cc5f76f8 | |
Emmanuel Viennet | a3148f9796 | |
Emmanuel Viennet | ceda567fea | |
Emmanuel Viennet | 56c082078d | |
Emmanuel Viennet | 58b089a629 | |
Emmanuel Viennet | 07927950c0 | |
Emmanuel Viennet | 42878b12ad | |
Emmanuel Viennet | 33fc0ba93e | |
Emmanuel Viennet | a33f842471 | |
Emmanuel Viennet | d4a232905a | |
Emmanuel Viennet | 4e2c63e9ed | |
Emmanuel Viennet | 35449ecc3b | |
Emmanuel Viennet | 3db866b126 | |
Emmanuel Viennet | b42ae33108 | |
Emmanuel Viennet | 1fce48694b | |
Emmanuel Viennet | f3b927949d | |
Emmanuel Viennet | f26a5950de | |
Emmanuel Viennet | 62d894773b | |
Emmanuel Viennet | 61b9e1ee02 | |
Emmanuel Viennet | 36c08d6321 | |
Emmanuel Viennet | 6350d8d869 | |
Emmanuel Viennet | ac24b636af | |
Emmanuel Viennet | bc9750e814 | |
Emmanuel Viennet | 87a063f251 | |
Emmanuel Viennet | 4d03a315a6 | |
Emmanuel Viennet | 66209437fc | |
Emmanuel Viennet | 82eee8e07f | |
Emmanuel Viennet | 041b2df8a7 | |
Emmanuel Viennet | 0eb98ddd26 | |
Emmanuel Viennet | 53919e0d25 | |
Emmanuel Viennet | 6527e1c3c6 | |
Emmanuel Viennet | 616907ad09 | |
Emmanuel Viennet | 100e8e51f0 | |
Emmanuel Viennet | e51d13f6d2 | |
Emmanuel Viennet | 726dadafa9 | |
Emmanuel Viennet | e545890a5d | |
Emmanuel Viennet | d8f0f061f8 | |
Emmanuel Viennet | 9e96b4d583 | |
Emmanuel Viennet | 9dee0b7cb5 | |
Emmanuel Viennet | 007efb0e39 | |
Emmanuel Viennet | 67cec0c2a7 | |
iziram | ebaf0e7746 | |
iziram | 94b66ccc25 | |
iziram | 5ba3d5855b | |
Emmanuel Viennet | c73a24f212 | |
Emmanuel Viennet | c5bcce2017 | |
Emmanuel Viennet | 7cd6697d25 | |
Emmanuel Viennet | 26c1373ed8 | |
Emmanuel Viennet | b168cbfcb2 | |
Emmanuel Viennet | 6064a23b1d | |
Emmanuel Viennet | fcbdb281db | |
Emmanuel Viennet | 9beac4b4f1 | |
Emmanuel Viennet | f10b5f2be8 | |
Emmanuel Viennet | d0d8f0b7d9 | |
Emmanuel Viennet | 7d715ec77e | |
Emmanuel Viennet | d2a969856a | |
Emmanuel Viennet | b9b2195351 | |
Emmanuel Viennet | 20b9736ded | |
Emmanuel Viennet | d39daf2a0c | |
Emmanuel Viennet | c29c3f467f | |
Emmanuel Viennet | 6f10c92fe3 | |
Emmanuel Viennet | f8ed29ef69 | |
Emmanuel Viennet | 3aa1953e97 | |
Emmanuel Viennet | a8be49129b | |
Emmanuel Viennet | 0c88833d3b | |
Emmanuel Viennet | d2fe25c7f5 | |
Emmanuel Viennet | 8d4df3061e | |
Emmanuel Viennet | 0342d167e0 | |
Jean-Marie Place | 8cff25b8ce | |
Jean-Marie Place | a1bd33a185 | |
Emmanuel Viennet | f063103065 | |
Emmanuel Viennet | d077a3bf2d | |
Emmanuel Viennet | 492f527436 | |
Emmanuel Viennet | 4197546601 | |
Emmanuel Viennet | dae0d6d8fa | |
Emmanuel Viennet | 3d086b1dcd | |
Emmanuel Viennet | cd32a54724 | |
Emmanuel Viennet | 3a791f79a7 | |
Jean-Marie Place | 3adc0aa24f | |
Emmanuel Viennet | 5250bee9b0 | |
Emmanuel Viennet | d47451e973 | |
Emmanuel Viennet | 4c4064841a | |
Emmanuel Viennet | d27d0bb475 | |
Emmanuel Viennet | 2f9289c916 | |
Sébastien Lehmann | 22211e68e2 | |
Emmanuel Viennet | cc42b93d32 | |
Emmanuel Viennet | 93342da435 | |
Emmanuel Viennet | 2921b55f0f | |
Emmanuel Viennet | 6c56740cdd | |
Emmanuel Viennet | d113f24c65 | |
Emmanuel Viennet | 3f0b7d00bf | |
Emmanuel Viennet | 26a0fa24a2 | |
Emmanuel Viennet | 42b59d0087 | |
Emmanuel Viennet | cde33019be | |
Emmanuel Viennet | 535271bff8 | |
Emmanuel Viennet | 8c44e4cfb7 | |
Emmanuel Viennet | 844fb5149a | |
iziram | 8b5baa3110 | |
iziram | 522c953f4a | |
iziram | 21c10b681f | |
iziram | 741d58e2e7 | |
iziram | 0f47ba15b2 | |
iziram | 7e76ba2fdd | |
iziram | 345a842f59 | |
iziram | 5aeffcbf1d | |
Emmanuel Viennet | 84ac334b13 | |
Emmanuel Viennet | 84a333c6d9 | |
Emmanuel Viennet | f78653c184 | |
Emmanuel Viennet | ca5ea5148d | |
Emmanuel Viennet | 7c25e14dda | |
Emmanuel Viennet | 06ad4f39a9 | |
Emmanuel Viennet | 41a4610b7a | |
Emmanuel Viennet | 12ce1d16f2 | |
Sébastien Lehmann | 462db3f9ef | |
Emmanuel Viennet | 107341efab | |
Emmanuel Viennet | 7739530383 | |
Emmanuel Viennet | 7018a41425 | |
Emmanuel Viennet | a992d80982 | |
Emmanuel Viennet | a8f97bedde | |
Emmanuel Viennet | 73a242663f | |
Emmanuel Viennet | eae49810dd | |
Emmanuel Viennet | 6bafcdbcac | |
Emmanuel Viennet | 323522a27c | |
Emmanuel Viennet | 9a7c98e906 | |
Emmanuel Viennet | c5e7012237 | |
Emmanuel Viennet | 83d0200e57 | |
Emmanuel Viennet | 18d5dc5fbd | |
Emmanuel Viennet | 3fb157e316 | |
Emmanuel Viennet | 613837ac37 | |
Emmanuel Viennet | bd1c2f1cb3 | |
Emmanuel Viennet | 8ecdfd4e62 | |
Emmanuel Viennet | db58c57a78 | |
Emmanuel Viennet | c49bc44700 | |
Emmanuel Viennet | ecb3748c85 | |
Emmanuel Viennet | 40919063f8 | |
Emmanuel Viennet | 6a985f8558 | |
Emmanuel Viennet | 91176d282c | |
Emmanuel Viennet | c45a229e95 | |
Emmanuel Viennet | b8f5cf712f | |
Emmanuel Viennet | 705fcc31cd | |
Emmanuel Viennet | b7b12d20ad | |
Emmanuel Viennet | 5144f9b8e1 | |
Emmanuel Viennet | 0bd873ba65 | |
Emmanuel Viennet | e84e55467a | |
Emmanuel Viennet | 87ff6b793c | |
Emmanuel Viennet | 95a3a74ce8 | |
Emmanuel Viennet | 48f54fa232 | |
Emmanuel Viennet | bf6d718e47 | |
Emmanuel Viennet | d77f745437 | |
iziram | 0f3e1ea95e | |
iziram | aa956f4530 | |
iziram | 21f57aab8f | |
iziram | 53c9658ce1 | |
iziram | e18990d804 | |
iziram | c11599b64f | |
iziram | 095eb6ce20 | |
iziram | 61d4186ad3 | |
iziram | 4d72fec42d | |
Emmanuel Viennet | a63e14ce06 | |
Emmanuel Viennet | ba909d72f0 | |
Emmanuel Viennet | 44a72c1ab9 | |
iziram | 2fd1b039f4 | |
Emmanuel Viennet | 0f8998c891 | |
Emmanuel Viennet | be367de2a1 | |
Emmanuel Viennet | e81ad610b6 | |
Emmanuel Viennet | 0f45101000 | |
Emmanuel Viennet | 1224b46846 | |
Emmanuel Viennet | 6b2ea5c5bc | |
iziram | a7b856b1ec | |
iziram | 547040bb93 | |
iziram | 8bc780f2cf | |
Emmanuel Viennet | 86f5751e79 | |
Emmanuel Viennet | b160f64e4f | |
Emmanuel Viennet | ee2ac9d986 | |
Emmanuel Viennet | fc78484186 | |
Emmanuel Viennet | 21b5474a6f | |
Emmanuel Viennet | 3c1acc9c00 | |
Emmanuel Viennet | 2548a97515 | |
Emmanuel Viennet | ce1cb7516b | |
iziram | cf3258f5f9 | |
Emmanuel Viennet | 3998b5a366 | |
Emmanuel Viennet | 728010bf69 | |
Emmanuel Viennet | e9f23d8b3e | |
Emmanuel Viennet | f6d442beb4 | |
Emmanuel Viennet | b19c94a1f4 | |
Emmanuel Viennet | 9fb70aef5d | |
Emmanuel Viennet | c8c05ecd77 | |
iziram | 02ec55ca18 | |
Emmanuel Viennet | b30d5eb996 | |
Emmanuel Viennet | e33bc1e303 | |
Emmanuel Viennet | d2362c1080 | |
Emmanuel Viennet | 6e4bf424e5 | |
Emmanuel Viennet | 2170b06e33 | |
Emmanuel Viennet | 696f4a5410 | |
Emmanuel Viennet | 9880645c01 | |
Emmanuel Viennet | 4d0ea06559 | |
Emmanuel Viennet | f46e3f6db5 | |
Emmanuel Viennet | 4d66fb13ee | |
Emmanuel Viennet | 138f9597f5 | |
Emmanuel Viennet | 91e8c9185b | |
Emmanuel Viennet | f3b2c6d4fe | |
Emmanuel Viennet | 7277c9f999 | |
Emmanuel Viennet | 9a19919bae | |
Emmanuel Viennet | d97c0c08aa | |
Emmanuel Viennet | 325978a175 | |
Emmanuel Viennet | 135ca9fc1c | |
Emmanuel Viennet | a4072efe4c | |
Emmanuel Viennet | 4430eb9a61 | |
Emmanuel Viennet | 073c3c7c44 | |
Emmanuel Viennet | 75b87b24de | |
Emmanuel Viennet | e0f6b022b1 | |
Emmanuel Viennet | 98c6761f6a | |
Emmanuel Viennet | 53514ef919 | |
Emmanuel Viennet | 294ce1d708 | |
Emmanuel Viennet | cf63e1c038 | |
Emmanuel Viennet | 584a7af2a1 | |
Emmanuel Viennet | 635320fd62 | |
Emmanuel Viennet | 6867974957 | |
Emmanuel Viennet | 6ad415dfca | |
Emmanuel Viennet | 2919ff517c | |
Emmanuel Viennet | 89948db135 | |
Emmanuel Viennet | bdf90dfd69 | |
Emmanuel Viennet | 0b9c9be222 | |
Emmanuel Viennet | b5cf210112 | |
Emmanuel Viennet | 6833a28274 | |
Emmanuel Viennet | 5753ac92f4 | |
Emmanuel Viennet | 16cc35f63c | |
Emmanuel Viennet | 5e0922a4bf | |
Emmanuel Viennet | 10148bc7c0 | |
Emmanuel Viennet | 556d8e7cbf | |
Emmanuel Viennet | c6b2af5635 | |
Emmanuel Viennet | cc0c544519 | |
Emmanuel Viennet | 71116e6b39 | |
Emmanuel Viennet | 3121a6d54c | |
pascal.bouron | 83afc1d6a0 | |
Emmanuel Viennet | 5fc08b9716 | |
Emmanuel Viennet | e7559b7a78 | |
Emmanuel Viennet | d8a98b6e5b | |
Emmanuel Viennet | 1287aecc4b | |
Sébastien Lehmann | 549323e781 | |
Emmanuel Viennet | 0ff5fa46d9 | |
Emmanuel Viennet | 8d124eca3e | |
Emmanuel Viennet | 6a7638d7ff | |
Emmanuel Viennet | 452bbf2885 | |
Emmanuel Viennet | 4915852d66 | |
Emmanuel Viennet | 85f00c7cb6 | |
Emmanuel Viennet | b8b3fbb324 | |
Emmanuel Viennet | dd93d952d7 | |
Emmanuel Viennet | 00c09b1eb8 | |
Emmanuel Viennet | 0e628273cf | |
pascal.bouron | 664d5483fc | |
Emmanuel Viennet | b4eab5fcbc | |
Emmanuel Viennet | c551634417 | |
pascal.bouron | efe8673e8a | |
Sébastien Lehmann | f17b10da3b | |
Emmanuel Viennet | cf18520e9c | |
Emmanuel Viennet | cda20c27b2 | |
Sébastien Lehmann | b7983a8d59 | |
Emmanuel Viennet | 47b3eec14b | |
Emmanuel Viennet | 9f6068caa2 | |
Emmanuel Viennet | 04277d1f57 | |
Emmanuel Viennet | f9d15da553 | |
Emmanuel Viennet | a7126990f0 | |
Emmanuel Viennet | 42d92cb998 | |
Emmanuel Viennet | 2d3d7d49fc | |
Emmanuel Viennet | 277e87add9 | |
Emmanuel Viennet | fffb07d612 | |
Emmanuel Viennet | afe9ae69a9 | |
Emmanuel Viennet | 16f953caf6 | |
iziram | f3b1b8a3cb | |
Emmanuel Viennet | 7adc7d824b | |
Emmanuel Viennet | cf900d2027 | |
Emmanuel Viennet | 1fa8375b11 | |
Emmanuel Viennet | bdefa111a7 | |
Emmanuel Viennet | c5b2df379e | |
Emmanuel Viennet | 3460b217dd | |
Emmanuel Viennet | 18aed44644 | |
Emmanuel Viennet | ec632dd43c | |
Emmanuel Viennet | acc1ecf906 | |
Emmanuel Viennet | 9566551e7e | |
Emmanuel Viennet | 7e1b0177f0 | |
Emmanuel Viennet | 8e6dc37a87 | |
Emmanuel Viennet | a4840f494b | |
Emmanuel Viennet | a28f58a443 | |
Emmanuel Viennet | 2a41cf972c | |
iziram | f96571f520 | |
iziram | 4df1bdda8e | |
Emmanuel Viennet | 6c88dfa722 | |
iziram | b9f3db91d4 | |
Emmanuel Viennet | 3e2631b94d | |
Emmanuel Viennet | 9251810814 | |
Emmanuel Viennet | 4c83c69f7c | |
Emmanuel Viennet | b09dc63fe3 | |
Emmanuel Viennet | 075d864de3 | |
Emmanuel Viennet | 6440ca4a1f | |
Emmanuel Viennet | 7d2d19f3a8 | |
Emmanuel Viennet | 882d131837 | |
Emmanuel Viennet | 3012fc465d | |
Emmanuel Viennet | 7069fb6e31 | |
Emmanuel Viennet | be2d7926bf | |
Emmanuel Viennet | bc6d9d5442 | |
Emmanuel Viennet | a0a6dbea00 | |
Emmanuel Viennet | 872e741d9f | |
Emmanuel Viennet | 5258a570a6 | |
Emmanuel Viennet | f0da8434a9 | |
Emmanuel Viennet | e995228ca7 | |
Emmanuel Viennet | e59fce5f6b | |
Emmanuel Viennet | 0d9338dc0a | |
Emmanuel Viennet | f1fd4d98d7 | |
Emmanuel Viennet | c6e35dd4cd | |
Emmanuel Viennet | cb8d313dc7 | |
Emmanuel Viennet | 3e3b09134d | |
Emmanuel Viennet | 7b3c50620b | |
Emmanuel Viennet | 63fb09348d | |
Emmanuel Viennet | 7a9dc11af3 | |
Sébastien Lehmann | d178c636bf | |
Emmanuel Viennet | c6a06266fa | |
Emmanuel Viennet | e1504adc03 | |
iziram | a9615bc077 | |
iziram | 3ff4abd19c | |
Emmanuel Viennet | 0a1a847044 | |
Emmanuel Viennet | f5442b924f | |
Emmanuel Viennet | bec4cd7978 | |
Emmanuel Viennet | f63fa43862 | |
Emmanuel Viennet | ca20c303f0 | |
Emmanuel Viennet | 014886c288 | |
Emmanuel Viennet | e2110f4abb | |
Emmanuel Viennet | ff12f4312e | |
Emmanuel Viennet | 930a96b984 | |
Emmanuel Viennet | 60fa12df81 | |
Emmanuel Viennet | 0324771aa2 | |
Emmanuel Viennet | 688fc5401f | |
Emmanuel Viennet | c1cbd6bce0 | |
Emmanuel Viennet | f2ffd69fe6 | |
Emmanuel Viennet | ba5b5cdb6f | |
Emmanuel Viennet | 51b0ca088c | |
Emmanuel Viennet | 9c618692d1 | |
Emmanuel Viennet | a0c33b3c19 | |
Emmanuel Viennet | ef1b28fe27 | |
Emmanuel Viennet | f246d9e82c | |
Emmanuel Viennet | cd36737460 | |
Emmanuel Viennet | acb8e6aab2 | |
Emmanuel Viennet | 8ef19b14c7 | |
Emmanuel Viennet | 7af381becc | |
Emmanuel Viennet | c9b4058717 | |
Emmanuel Viennet | 42b03dbdfa | |
Emmanuel Viennet | a87dbd9927 | |
Emmanuel Viennet | ae9aad0619 | |
Emmanuel Viennet | e38d4bde81 | |
Sébastien Lehmann | 25d1132a06 | |
Emmanuel Viennet | 4c730a6302 | |
Emmanuel Viennet | 287e4df74e | |
Emmanuel Viennet | 77348c2cdf | |
Emmanuel Viennet | f67a11519e | |
Emmanuel Viennet | f9a9c2088d | |
Emmanuel Viennet | afbb1fb0e2 | |
Emmanuel Viennet | 346701d91e | |
Emmanuel Viennet | f647ff1139 | |
Emmanuel Viennet | f5988b9e34 | |
Emmanuel Viennet | f318f35c1b | |
Emmanuel Viennet | 4626cb9a3e | |
Emmanuel Viennet | 0a58437fa9 | |
Emmanuel Viennet | c906cd7f16 | |
Emmanuel Viennet | ee86fba3d3 | |
Emmanuel Viennet | 5018298d12 | |
Emmanuel Viennet | 9d64caa749 | |
Emmanuel Viennet | 6f257dc80d | |
Emmanuel Viennet | 49d176c603 | |
Emmanuel Viennet | 559b66de8b | |
Emmanuel Viennet | d7f1114a42 | |
Emmanuel Viennet | a2ea7d7a02 | |
Emmanuel Viennet | f6d8de5a20 | |
Emmanuel Viennet | 2bf678ac50 | |
Jean-Marie PLACE | 63c0667694 | |
Jean-Marie PLACE | 3f7f4172b5 | |
Emmanuel Viennet | 528d5c8863 | |
Jean-Marie Place | 7b28e0ba6b | |
Emmanuel Viennet | 588f2f26eb | |
Emmanuel Viennet | 4940decf57 | |
Emmanuel Viennet | d055c17c6b | |
Emmanuel Viennet | ea0a49d837 | |
Emmanuel Viennet | 59a6ee3b3e | |
Emmanuel Viennet | 111634db99 | |
Emmanuel Viennet | 26dcc31ffb | |
Emmanuel Viennet | 18f4b9cd42 | |
Emmanuel Viennet | dbc9aab7c3 | |
Jean-Marie PLACE | 9c50d03dd8 | |
Emmanuel Viennet | 2d76cc0ad1 | |
iziram | b7fb8879df | |
Emmanuel Viennet | 3e0f43d5ea | |
Emmanuel Viennet | dcdd83d2e8 | |
Emmanuel Viennet | 4d453d5d14 | |
Emmanuel Viennet | 20b13b05cf | |
Emmanuel Viennet | a730bf759b | |
Jean-Marie PLACE | a63349382e | |
Emmanuel Viennet | dab6bad08f | |
Emmanuel Viennet | e435dd10db | |
Sébastien Lehmann | 95100ed429 | |
Emmanuel Viennet | 155a093635 | |
Jean-Marie PLACE | c85a51a8c5 | |
Emmanuel Viennet | d50107079b | |
Emmanuel Viennet | 9535ff1e91 | |
Emmanuel Viennet | f4d8f4dded | |
Emmanuel Viennet | ba003d7c02 | |
Emmanuel Viennet | 7ca3290357 | |
Emmanuel Viennet | 7cb98e3f31 | |
Emmanuel Viennet | 365e54f7e1 | |
Emmanuel Viennet | 9da5506361 | |
Emmanuel Viennet | 979359257b | |
Emmanuel Viennet | cc674b4e65 | |
Emmanuel Viennet | c103111aa1 | |
Emmanuel Viennet | b9d6688250 | |
Emmanuel Viennet | 93e54982b6 | |
Emmanuel Viennet | eefdd5458e | |
Emmanuel Viennet | 072d839b75 | |
Emmanuel Viennet | 0de35b5400 | |
Emmanuel Viennet | cfcb100ab7 | |
iziram | 1c48758940 | |
Emmanuel Viennet | 3bae99c5cd | |
Emmanuel Viennet | ae2f8a97fd | |
Emmanuel Viennet | 1598537f24 | |
Emmanuel Viennet | 557a0c3f6d | |
Emmanuel Viennet | fa84f9ed89 | |
Emmanuel Viennet | dac46b8366 | |
Emmanuel Viennet | a92d2a6edf | |
Emmanuel Viennet | caf88c5909 | |
Emmanuel Viennet | 4da21bf4d3 | |
Emmanuel Viennet | bdaf416ccb | |
Emmanuel Viennet | 066e03dae8 | |
Emmanuel Viennet | 1847250bab | |
Emmanuel Viennet | 1ce4ffecad | |
Emmanuel Viennet | 91e77dd2dc | |
Emmanuel Viennet | 7dcebf4b83 | |
Emmanuel Viennet | 10caea92ae | |
Emmanuel Viennet | 75c5256ba9 | |
Emmanuel Viennet | 678959c76a | |
Emmanuel Viennet | 77c0294c83 | |
Emmanuel Viennet | 8d72229e8b | |
Emmanuel Viennet | 84b02edd48 | |
Emmanuel Viennet | ec4aa0e26f | |
Emmanuel Viennet | 50e8f2b4fe | |
Emmanuel Viennet | 318f6f8a65 | |
Emmanuel Viennet | 91e508bf9f | |
Emmanuel Viennet | 036ce650c6 | |
Emmanuel Viennet | 37a8b3bb0b | |
Emmanuel Viennet | ad46a190ab | |
Emmanuel Viennet | f69ce75b1f | |
Emmanuel Viennet | 1813e3c7ce | |
Emmanuel Viennet | 7bbdff67a0 | |
Emmanuel Viennet | dcf0f73c1b | |
Emmanuel Viennet | 06cbd65365 | |
Emmanuel Viennet | 268b75d441 | |
Emmanuel Viennet | 0c5e338970 | |
Emmanuel Viennet | 2731a4728b | |
Emmanuel Viennet | 4f87f22586 | |
Emmanuel Viennet | a3e4c34745 | |
Emmanuel Viennet | 78bb9a706e | |
Emmanuel Viennet | d6be0e131f | |
Emmanuel Viennet | 9527240ea8 | |
Emmanuel Viennet | c6c7187c34 | |
Emmanuel Viennet | 453f11084b | |
Emmanuel Viennet | 6b29a205b6 |
|
@ -1,4 +1,8 @@
|
|||
[[MESSAGES CONTROL]
|
||||
|
||||
[MASTER]
|
||||
load-plugins=pylint_flask_sqlalchemy,pylint_flask
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
# pylint and black disagree...
|
||||
disable=bad-continuation
|
||||
|
||||
|
|
65
README.md
65
README.md
|
@ -1,5 +1,4 @@
|
|||
i
|
||||
# ScoDoc - Gestion de la scolarité - Version ScoDoc 9
|
||||
# ScoDoc - Gestion de la scolarité - Version ScoDoc 9
|
||||
|
||||
(c) Emmanuel Viennet 1999 - 2022 (voir LICENCE.txt).
|
||||
|
||||
|
@ -9,39 +8,34 @@ Documentation utilisateur: <https://scodoc.org>
|
|||
|
||||
## Version ScoDoc 9
|
||||
|
||||
La version ScoDoc 9 est parue en septembre 2021.
|
||||
Elle représente une évolution majeure du projet, maintenant basé sur
|
||||
Flask (au lieu de Zope) et sur **python 3.9+**.
|
||||
La version ScoDoc 9 est parue en septembre 2021. Elle représente une évolution
|
||||
majeure du projet, maintenant basé sur Flask (au lieu de Zope) et sur **python
|
||||
3.9+**.
|
||||
|
||||
La version 9.0 s'efforce de reproduire presque à l'identique le fonctionnement
|
||||
La version 9.0 s'efforce de reproduire presque à l'identique le fonctionnement
|
||||
de ScoDoc7, avec des composants logiciels différents (Debian 11, Python 3,
|
||||
Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
|
||||
|
||||
### État actuel (dec 22)
|
||||
|
||||
- 9.4.x est en production
|
||||
- le prochain jalon est 9.5. Voir branches sur gitea.
|
||||
|
||||
### État actuel (26 jan 22)
|
||||
|
||||
- 9.1.5x (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf:
|
||||
- ancien module "Entreprises" (obsolète) et ajoute la gestion du BUT.
|
||||
|
||||
- 9.2 (branche dev92) est la version de développement.
|
||||
|
||||
|
||||
### Lignes de commandes
|
||||
|
||||
Voir [https://scodoc.org/GuideConfig](le guide de configuration).
|
||||
|
||||
|
||||
## Organisation des fichiers
|
||||
|
||||
L'installation comporte les fichiers de l'application, sous `/opt/scodoc/`, et
|
||||
les fichiers locaux (archives, photos, configurations, logs) sous
|
||||
`/opt/scodoc-data`. Par ailleurs, il y a évidemment les bases de données
|
||||
postgresql et la configuration du système Linux.
|
||||
postgresql et la configuration du système Linux.
|
||||
|
||||
### Fichiers locaux
|
||||
Sous `/opt/scodoc-data`, fichiers et répertoires appartenant à l'utilisateur `scodoc`.
|
||||
Ils ne doivent pas être modifiés à la main, sauf certains fichiers de configuration sous
|
||||
|
||||
Sous `/opt/scodoc-data`, fichiers et répertoires appartenant à l'utilisateur `scodoc`.
|
||||
Ils ne doivent pas être modifiés à la main, sauf certains fichiers de configuration sous
|
||||
`/opt/scodoc-data/config`.
|
||||
|
||||
Le répertoire `/opt/scodoc-data` doit être régulièrement sauvegardé.
|
||||
|
@ -62,7 +56,7 @@ Principaux contenus:
|
|||
|
||||
Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian11)).
|
||||
|
||||
Puis remplacer `/opt/scodoc` par un clone du git.
|
||||
Puis remplacer `/opt/scodoc` par un clone du git.
|
||||
|
||||
sudo su
|
||||
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
|
||||
|
@ -76,7 +70,7 @@ Puis remplacer `/opt/scodoc` par un clone du git.
|
|||
|
||||
# Et donner ce répertoire à l'utilisateur scodoc:
|
||||
chown -R scodoc.scodoc /opt/scodoc
|
||||
|
||||
|
||||
Il faut ensuite installer l'environnement et le fichier de configuration:
|
||||
|
||||
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
|
||||
|
@ -100,14 +94,14 @@ Avant le premier lancement, créer cette base ainsi:
|
|||
flask db upgrade
|
||||
|
||||
Cette commande n'est nécessaire que la première fois (le contenu de la base
|
||||
est effacé au début de chaque test, mais son schéma reste) et aussi si des
|
||||
est effacé au début de chaque test, mais son schéma reste) et aussi si des
|
||||
migrations (changements de schéma) ont eu lieu dans le code.
|
||||
|
||||
Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les
|
||||
Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les
|
||||
scripts de tests:
|
||||
Lancer au préalable:
|
||||
|
||||
flask delete-dept TEST00 && flask create-dept TEST00
|
||||
flask delete-dept -fy TEST00 && flask create-dept TEST00
|
||||
|
||||
Puis dérouler les tests unitaires:
|
||||
|
||||
|
@ -117,24 +111,24 @@ Ou avec couverture (`pip install pytest-cov`)
|
|||
|
||||
pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/*
|
||||
|
||||
|
||||
#### Utilisation des tests unitaires pour initialiser la base de dev
|
||||
On peut aussi utiliser les tests unitaires pour mettre la base
|
||||
de données de développement dans un état connu, par exemple pour éviter de
|
||||
recréer à la main étudiants et semestres quand on développe.
|
||||
|
||||
Il suffit de positionner une variable d'environnement indiquant la BD
|
||||
utilisée par les tests:
|
||||
On peut aussi utiliser les tests unitaires pour mettre la base de données de
|
||||
développement dans un état connu, par exemple pour éviter de recréer à la main
|
||||
étudiants et semestres quand on développe.
|
||||
|
||||
Il suffit de positionner une variable d'environnement indiquant la BD utilisée
|
||||
par les tests:
|
||||
|
||||
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
|
||||
|
||||
(si elle n'existe pas, voir plus loin pour la créer) puis de les lancer
|
||||
normalement, par exemple:
|
||||
normalement, par exemple:
|
||||
|
||||
pytest tests/unit/test_sco_basic.py
|
||||
|
||||
Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins)
|
||||
un utilisateur:
|
||||
Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins) un
|
||||
utilisateur:
|
||||
|
||||
flask user-password admin
|
||||
|
||||
|
@ -178,12 +172,10 @@ Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bie
|
|||
|
||||
pip install snakeviz
|
||||
|
||||
puis
|
||||
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
|
||||
|
@ -191,5 +183,4 @@ important est `postinst`qui se charge de configurer le système (install ou
|
|||
upgrade de scodoc9).
|
||||
|
||||
La préparation d'une release se fait à l'aide du script
|
||||
`tools/build_release.sh`.
|
||||
|
||||
`tools/build_release.sh`.
|
||||
|
|
|
@ -26,11 +26,13 @@ from flask_mail import Mail
|
|||
from flask_bootstrap import Bootstrap
|
||||
from flask_moment import Moment
|
||||
from flask_caching import Cache
|
||||
from jinja2 import select_autoescape
|
||||
import sqlalchemy
|
||||
|
||||
from app.scodoc.sco_exceptions import (
|
||||
AccessDenied,
|
||||
ScoBugCatcher,
|
||||
ScoException,
|
||||
ScoGenError,
|
||||
ScoValueError,
|
||||
APIInvalidParams,
|
||||
|
@ -60,11 +62,11 @@ cache = Cache(
|
|||
|
||||
|
||||
def handle_sco_value_error(exc):
|
||||
return render_template("sco_value_error.html", exc=exc), 404
|
||||
return render_template("sco_value_error.j2", exc=exc), 404
|
||||
|
||||
|
||||
def handle_access_denied(exc):
|
||||
return render_template("error_access_denied.html", exc=exc), 403
|
||||
return render_template("error_access_denied.j2", exc=exc), 403
|
||||
|
||||
|
||||
def internal_server_error(exc):
|
||||
|
@ -74,7 +76,7 @@ def internal_server_error(exc):
|
|||
|
||||
return (
|
||||
render_template(
|
||||
"error_500.html",
|
||||
"error_500.j2",
|
||||
SCOVERSION=sco_version.SCOVERSION,
|
||||
date=datetime.datetime.now().isoformat(),
|
||||
exc=exc,
|
||||
|
@ -92,9 +94,12 @@ def handle_sco_bug(exc):
|
|||
"""Un bug, en général rare, sur lequel les dev cherchent des
|
||||
informations pour le corriger.
|
||||
"""
|
||||
Thread(
|
||||
target=_async_dump, args=(current_app._get_current_object(), request.url)
|
||||
).start()
|
||||
if current_app.config["TESTING"] or current_app.config["DEBUG"]:
|
||||
raise ScoException # for development servers only
|
||||
else:
|
||||
Thread(
|
||||
target=_async_dump, args=(current_app._get_current_object(), request.url)
|
||||
).start()
|
||||
|
||||
return internal_server_error(exc)
|
||||
|
||||
|
@ -119,7 +124,7 @@ def handle_invalid_usage(error):
|
|||
# JSON ENCODING
|
||||
class ScoDocJSONEncoder(JSONEncoder):
|
||||
def default(self, o):
|
||||
if isinstance(o, datetime.datetime):
|
||||
if isinstance(o, (datetime.datetime, datetime.date)):
|
||||
return o.isoformat()
|
||||
|
||||
return super().default(o)
|
||||
|
@ -142,7 +147,7 @@ def render_raw_html(template_filename: str, **args) -> str:
|
|||
|
||||
def postgresql_server_error(e):
|
||||
"""Erreur de connection au serveur postgresql (voir notesdb.open_db_connection)"""
|
||||
return render_raw_html("error_503.html", SCOVERSION=sco_version.SCOVERSION), 503
|
||||
return render_raw_html("error_503.j2", SCOVERSION=sco_version.SCOVERSION), 503
|
||||
|
||||
|
||||
class LogRequestFormatter(logging.Formatter):
|
||||
|
@ -268,9 +273,13 @@ def create_app(config_class=DevConfig):
|
|||
from app.views import notes_bp
|
||||
from app.views import users_bp
|
||||
from app.views import absences_bp
|
||||
from app.views import assiduites_bp
|
||||
from app.api import api_bp
|
||||
from app.api import api_web_bp
|
||||
|
||||
# Enable autoescaping of all templates, including .j2
|
||||
app.jinja_env.autoescape = select_autoescape(default_for_string=True, default=True)
|
||||
|
||||
# https://scodoc.fr/ScoDoc
|
||||
app.register_blueprint(scodoc_bp)
|
||||
# https://scodoc.fr/ScoDoc/RT/Scolarite/...
|
||||
|
@ -283,6 +292,9 @@ def create_app(config_class=DevConfig):
|
|||
app.register_blueprint(
|
||||
absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences"
|
||||
)
|
||||
app.register_blueprint(
|
||||
assiduites_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Assiduites"
|
||||
)
|
||||
app.register_blueprint(api_bp, url_prefix="/ScoDoc/api")
|
||||
app.register_blueprint(api_web_bp, url_prefix="/ScoDoc/<scodoc_dept>/api")
|
||||
|
||||
|
@ -435,8 +447,6 @@ def initialize_scodoc_database(erase=False, create_all=False):
|
|||
SQL tables and functions.
|
||||
If erase is True, _erase_ all database content.
|
||||
"""
|
||||
from app import models
|
||||
|
||||
# - ERASE (the truncation sql function has been defined above)
|
||||
if erase:
|
||||
truncate_database()
|
||||
|
@ -463,6 +473,26 @@ def truncate_database():
|
|||
except:
|
||||
db.session.rollback()
|
||||
raise
|
||||
# Remet les compteurs (séquences sql) à zéro
|
||||
db.session.execute(
|
||||
"""
|
||||
CREATE OR REPLACE FUNCTION reset_sequences(username IN VARCHAR) RETURNS void AS $$
|
||||
DECLARE
|
||||
statements CURSOR FOR
|
||||
SELECT sequence_name
|
||||
FROM information_schema.sequences
|
||||
ORDER BY sequence_name ;
|
||||
BEGIN
|
||||
FOR stmt IN statements LOOP
|
||||
EXECUTE 'ALTER SEQUENCE ' || quote_ident(stmt.sequence_name) || ' RESTART;';
|
||||
END LOOP;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
SELECT reset_sequences('scodoc');
|
||||
"""
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def clear_scodoc_cache():
|
||||
|
@ -480,12 +510,10 @@ def clear_scodoc_cache():
|
|||
|
||||
|
||||
# --------- Logging
|
||||
def log(msg: str, silent_test=True):
|
||||
def log(msg: str):
|
||||
"""log a message.
|
||||
If Flask app, use configured logger, else stderr.
|
||||
"""
|
||||
if silent_test and current_app and current_app.config["TESTING"]:
|
||||
return
|
||||
try:
|
||||
dept = getattr(g, "scodoc_dept", "")
|
||||
msg = f" ({dept}) {msg}"
|
||||
|
@ -530,3 +558,22 @@ def scodoc_flash_status_messages():
|
|||
f"Mode test: mails redirigés vers {email_test_mode_address}",
|
||||
category="warning",
|
||||
)
|
||||
|
||||
|
||||
def critical_error(msg):
|
||||
"""Handle a critical error: flush all caches, display message to the user"""
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
log(f"\n*** CRITICAL ERROR: {msg}")
|
||||
send_scodoc_alarm(f"CRITICAL ERROR: {msg}", msg)
|
||||
clear_scodoc_cache()
|
||||
raise ScoValueError(
|
||||
f"""
|
||||
Une erreur est survenue.
|
||||
|
||||
Si le problème persiste, merci de contacter le support ScoDoc via
|
||||
{scu.SCO_DISCORD_ASSISTANCE}
|
||||
|
||||
{msg}
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -2,13 +2,17 @@
|
|||
"""
|
||||
|
||||
from flask import Blueprint
|
||||
from flask import request
|
||||
from flask import request, g, jsonify
|
||||
from app import db
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoException
|
||||
|
||||
api_bp = Blueprint("api", __name__)
|
||||
api_web_bp = Blueprint("apiweb", __name__)
|
||||
|
||||
# HTTP ERROR STATUS
|
||||
API_CLIENT_ERROR = 400 # erreur dans les paramètres fournis par le client
|
||||
|
||||
|
||||
@api_bp.errorhandler(ScoException)
|
||||
@api_bp.errorhandler(404)
|
||||
|
@ -31,9 +35,26 @@ def requested_format(default_format="json", allowed_formats=None):
|
|||
return None
|
||||
|
||||
|
||||
def get_model_api_object(model_cls: db.Model, model_id: int, join_cls: db.Model = None):
|
||||
"""
|
||||
Retourne une réponse contenant la représentation api de l'objet "Model[model_id]"
|
||||
|
||||
Filtrage du département en fonction d'une classe de jointure (eg: Identite, Formsemstre) -> join_cls
|
||||
|
||||
exemple d'utilisation : fonction "justificatif()" -> app/api/justificatifs.py
|
||||
"""
|
||||
query = model_cls.query.filter_by(id=model_id)
|
||||
if g.scodoc_dept and join_cls is not None:
|
||||
query = query.join(join_cls).filter_by(dept_id=g.scodoc_dept_id)
|
||||
unique: model_cls = query.first_or_404()
|
||||
|
||||
return jsonify(unique.to_dict(format_api=True))
|
||||
|
||||
|
||||
from app.api import tokens
|
||||
from app.api import (
|
||||
absences,
|
||||
assiduites,
|
||||
billets_absences,
|
||||
departements,
|
||||
etudiants,
|
||||
|
@ -41,6 +62,7 @@ from app.api import (
|
|||
formations,
|
||||
formsemestres,
|
||||
jury,
|
||||
justificatifs,
|
||||
logos,
|
||||
partitions,
|
||||
users,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : Absences
|
||||
|
@ -8,7 +8,7 @@
|
|||
|
||||
from flask import jsonify
|
||||
|
||||
from app.api import api_bp as bp
|
||||
from app.api import api_bp as bp, API_CLIENT_ERROR
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import Identite
|
||||
|
|
|
@ -0,0 +1,868 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : Assiduités
|
||||
"""
|
||||
from datetime import datetime
|
||||
from flask import g, jsonify, request
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import db
|
||||
from app.api import api_bp as bp
|
||||
from app.api import api_web_bp
|
||||
from app.api import get_model_api_object
|
||||
from app.decorators import permission_required, scodoc
|
||||
from app.models import Assiduite, FormSemestre, Identite, ModuleImpl
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error
|
||||
|
||||
|
||||
@bp.route("/assiduite/<int:assiduite_id>")
|
||||
@api_web_bp.route("/assiduite/<int:assiduite_id>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduite(assiduite_id: int = None):
|
||||
"""Retourne un objet assiduité à partir de son id
|
||||
|
||||
Exemple de résultat:
|
||||
{
|
||||
"assiduite_id": 1,
|
||||
"etudid": 2,
|
||||
"moduleimpl_id": 3,
|
||||
"date_debut": "2022-10-31T08:00+01:00",
|
||||
"date_fin": "2022-10-31T10:00+01:00",
|
||||
"etat": "retard",
|
||||
"desc": "une description",
|
||||
"user_id: 1 or null,
|
||||
"est_just": False or True,
|
||||
}
|
||||
"""
|
||||
|
||||
return get_model_api_object(Assiduite, assiduite_id, Identite)
|
||||
|
||||
|
||||
@bp.route("/assiduites/<int:etudid>/count", defaults={"with_query": False})
|
||||
@bp.route("/assiduites/<int:etudid>/count/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/assiduites/<int:etudid>/count", defaults={"with_query": False})
|
||||
@api_web_bp.route("/assiduites/<int:etudid>/count/query", defaults={"with_query": True})
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def count_assiduites(etudid: int = None, with_query: bool = False):
|
||||
"""
|
||||
Retourne le nombre d'assiduités d'un étudiant
|
||||
chemin : /assiduites/<int:etudid>/count
|
||||
|
||||
Un filtrage peut être donné avec une query
|
||||
chemin : /assiduites/<int:etudid>/count/query?
|
||||
|
||||
Les différents filtres :
|
||||
Type (type de comptage -> journee, demi, heure, nombre d'assiduite):
|
||||
query?type=(journee, demi, heure) -> une seule valeur parmis les trois
|
||||
ex: .../query?type=heure
|
||||
Comportement par défaut : compte le nombre d'assiduité enregistrée
|
||||
|
||||
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||
query?etat=[- liste des états séparé par une virgule -]
|
||||
ex: .../query?etat=present,retard
|
||||
Date debut
|
||||
(date de début de l'assiduité, sont affichés les assiduités
|
||||
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||
query?date_debut=[- date au format iso -]
|
||||
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||
Date fin
|
||||
(date de fin de l'assiduité, sont affichés les assiduités
|
||||
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||
query?date_fin=[- date au format iso -]
|
||||
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||
query?moduleimpl_id=[- int ou vide -]
|
||||
ex: query?moduleimpl_id=1234
|
||||
query?moduleimpl_od=
|
||||
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||
query?formsemestre_id=[int]
|
||||
ex query?formsemestre_id=3
|
||||
user_id (l'id de l'auteur de l'assiduité)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||
query?est_just=[bool]
|
||||
query?est_just=f
|
||||
query?est_just=t
|
||||
|
||||
|
||||
|
||||
"""
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
|
||||
etud: Identite = query.first_or_404(etudid)
|
||||
filtered: dict[str, object] = {}
|
||||
metric: str = "all"
|
||||
|
||||
if with_query:
|
||||
metric, filtered = _count_manager(request)
|
||||
|
||||
return jsonify(
|
||||
scass.get_assiduites_stats(
|
||||
assiduites=etud.assiduites, metric=metric, filtered=filtered
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/assiduites/<int:etudid>", defaults={"with_query": False})
|
||||
@bp.route("/assiduites/<int:etudid>/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/assiduites/<int:etudid>", defaults={"with_query": False})
|
||||
@api_web_bp.route("/assiduites/<int:etudid>/query", defaults={"with_query": True})
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites(etudid: int = None, with_query: bool = False):
|
||||
"""
|
||||
Retourne toutes les assiduités d'un étudiant
|
||||
chemin : /assiduites/<int:etudid>
|
||||
|
||||
Un filtrage peut être donné avec une query
|
||||
chemin : /assiduites/<int:etudid>/query?
|
||||
|
||||
Les différents filtres :
|
||||
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||
query?etat=[- liste des états séparé par une virgule -]
|
||||
ex: .../query?etat=present,retard
|
||||
Date debut
|
||||
(date de début de l'assiduité, sont affichés les assiduités
|
||||
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||
query?date_debut=[- date au format iso -]
|
||||
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||
Date fin
|
||||
(date de fin de l'assiduité, sont affichés les assiduités
|
||||
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||
query?date_fin=[- date au format iso -]
|
||||
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||
query?moduleimpl_id=[- int ou vide -]
|
||||
ex: query?moduleimpl_id=1234
|
||||
query?moduleimpl_od=
|
||||
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||
query?formsemstre_id=[int]
|
||||
ex query?formsemestre_id=3
|
||||
user_id (l'id de l'auteur de l'assiduité)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||
query?est_just=[bool]
|
||||
query?est_just=f
|
||||
query?est_just=t
|
||||
|
||||
|
||||
"""
|
||||
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
|
||||
etud: Identite = query.first_or_404(etudid)
|
||||
assiduites_query = etud.assiduites
|
||||
|
||||
if with_query:
|
||||
assiduites_query = _filter_manager(request, assiduites_query)
|
||||
|
||||
data_set: list[dict] = []
|
||||
for ass in assiduites_query.all():
|
||||
data = ass.to_dict(format_api=True)
|
||||
data_set.append(data)
|
||||
|
||||
return jsonify(data_set)
|
||||
|
||||
|
||||
@bp.route("/assiduites/group/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/assiduites/group/query", defaults={"with_query": True})
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites_group(with_query: bool = False):
|
||||
"""
|
||||
Retourne toutes les assiduités d'un groupe d'étudiants
|
||||
chemin : /assiduites/group/query?etudids=1,2,3
|
||||
|
||||
Un filtrage peut être donné avec une query
|
||||
chemin : /assiduites/group/query?etudids=1,2,3
|
||||
|
||||
Les différents filtres :
|
||||
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||
query?etat=[- liste des états séparé par une virgule -]
|
||||
ex: .../query?etat=present,retard
|
||||
Date debut
|
||||
(date de début de l'assiduité, sont affichés les assiduités
|
||||
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||
query?date_debut=[- date au format iso -]
|
||||
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||
Date fin
|
||||
(date de fin de l'assiduité, sont affichés les assiduités
|
||||
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||
query?date_fin=[- date au format iso -]
|
||||
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||
query?moduleimpl_id=[- int ou vide -]
|
||||
ex: query?moduleimpl_id=1234
|
||||
query?moduleimpl_od=
|
||||
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||
query?formsemstre_id=[int]
|
||||
ex query?formsemestre_id=3
|
||||
user_id (l'id de l'auteur de l'assiduité)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||
query?est_just=[bool]
|
||||
query?est_just=f
|
||||
query?est_just=t
|
||||
|
||||
|
||||
"""
|
||||
|
||||
etuds = request.args.get("etudids", "")
|
||||
etuds = etuds.split(",")
|
||||
try:
|
||||
etuds = [int(etu) for etu in etuds]
|
||||
except ValueError:
|
||||
return json_error(404, "Le champs etudids n'est pas correctement formé")
|
||||
|
||||
query = Identite.query.filter(Identite.id.in_(etuds))
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
|
||||
if len(etuds) != query.count() or len(etuds) == 0:
|
||||
return json_error(
|
||||
404,
|
||||
"Tous les étudiants ne sont pas dans le même département et/ou n'existe pas.",
|
||||
)
|
||||
assiduites_query = Assiduite.query.filter(Assiduite.etudid.in_(etuds))
|
||||
|
||||
if with_query:
|
||||
assiduites_query = _filter_manager(request, assiduites_query)
|
||||
|
||||
data_set: dict[list[dict]] = {key: [] for key in etuds}
|
||||
for ass in assiduites_query.all():
|
||||
data = ass.to_dict(format_api=True)
|
||||
data_set.get(data["etudid"]).append(data)
|
||||
|
||||
return jsonify(data_set)
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/assiduites/formsemestre/<int:formsemestre_id>", defaults={"with_query": False}
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/assiduites/formsemestre/<int:formsemestre_id>", defaults={"with_query": False}
|
||||
)
|
||||
@bp.route(
|
||||
"/assiduites/formsemestre/<int:formsemestre_id>/query",
|
||||
defaults={"with_query": True},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/assiduites/formsemestre/<int:formsemestre_id>/query",
|
||||
defaults={"with_query": True},
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||
"""Retourne toutes les assiduités du formsemestre"""
|
||||
formsemestre: FormSemestre = None
|
||||
formsemestre_id = int(formsemestre_id)
|
||||
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||
|
||||
if formsemestre is None:
|
||||
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
|
||||
|
||||
assiduites_query = scass.filter_by_formsemestre(Assiduite.query, formsemestre)
|
||||
|
||||
if with_query:
|
||||
assiduites_query = _filter_manager(request, assiduites_query)
|
||||
|
||||
data_set: list[dict] = []
|
||||
for ass in assiduites_query.all():
|
||||
data = ass.to_dict(format_api=True)
|
||||
data_set.append(data)
|
||||
|
||||
return jsonify(data_set)
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/assiduites/formsemestre/<int:formsemestre_id>/count",
|
||||
defaults={"with_query": False},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/assiduites/formsemestre/<int:formsemestre_id>/count",
|
||||
defaults={"with_query": False},
|
||||
)
|
||||
@bp.route(
|
||||
"/assiduites/formsemestre/<int:formsemestre_id>/count/query",
|
||||
defaults={"with_query": True},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/assiduites/formsemestre/<int:formsemestre_id>/count/query",
|
||||
defaults={"with_query": True},
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def count_assiduites_formsemestre(
|
||||
formsemestre_id: int = None, with_query: bool = False
|
||||
):
|
||||
"""Comptage des assiduités du formsemestre"""
|
||||
formsemestre: FormSemestre = None
|
||||
formsemestre_id = int(formsemestre_id)
|
||||
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||
|
||||
if formsemestre is None:
|
||||
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
|
||||
|
||||
etuds = formsemestre.etuds.all()
|
||||
etuds_id = [etud.id for etud in etuds]
|
||||
|
||||
assiduites_query = Assiduite.query.filter(Assiduite.etudid.in_(etuds_id))
|
||||
assiduites_query = scass.filter_by_formsemestre(assiduites_query, formsemestre)
|
||||
metric: str = "all"
|
||||
filtered: dict = {}
|
||||
if with_query:
|
||||
metric, filtered = _count_manager(request)
|
||||
|
||||
return jsonify(scass.get_assiduites_stats(assiduites_query, metric, filtered))
|
||||
|
||||
|
||||
@bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
|
||||
@api_web_bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def assiduite_create(etudid: int = None):
|
||||
"""
|
||||
Création d'une assiduité pour l'étudiant (etudid)
|
||||
La requête doit avoir un content type "application/json":
|
||||
[
|
||||
{
|
||||
"date_debut": str,
|
||||
"date_fin": str,
|
||||
"etat": str,
|
||||
},
|
||||
{
|
||||
"date_debut": str,
|
||||
"date_fin": str,
|
||||
"etat": str,
|
||||
"moduleimpl_id": int,
|
||||
"desc":str,
|
||||
}
|
||||
...
|
||||
]
|
||||
|
||||
"""
|
||||
etud: Identite = Identite.query.filter_by(id=etudid).first_or_404()
|
||||
|
||||
create_list: list[object] = request.get_json(force=True)
|
||||
|
||||
if not isinstance(create_list, list):
|
||||
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||
|
||||
errors: dict[int, str] = {}
|
||||
success: dict[int, object] = {}
|
||||
for i, data in enumerate(create_list):
|
||||
code, obj = _create_singular(data, etud)
|
||||
if code == 404:
|
||||
errors[i] = obj
|
||||
else:
|
||||
success[i] = obj
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"errors": errors, "success": success})
|
||||
|
||||
|
||||
@bp.route("/assiduites/create", methods=["POST"])
|
||||
@api_web_bp.route("/assiduites/create", methods=["POST"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def assiduites_create():
|
||||
"""
|
||||
Création d'une assiduité ou plusieurs assiduites
|
||||
La requête doit avoir un content type "application/json":
|
||||
[
|
||||
{
|
||||
"date_debut": str,
|
||||
"date_fin": str,
|
||||
"etat": str,
|
||||
"etudid":int,
|
||||
},
|
||||
{
|
||||
"date_debut": str,
|
||||
"date_fin": str,
|
||||
"etat": str,
|
||||
"etudid":int,
|
||||
|
||||
"moduleimpl_id": int,
|
||||
"desc":str,
|
||||
}
|
||||
...
|
||||
]
|
||||
|
||||
"""
|
||||
|
||||
create_list: list[object] = request.get_json(force=True)
|
||||
|
||||
if not isinstance(create_list, list):
|
||||
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||
|
||||
errors: dict[int, str] = {}
|
||||
success: dict[int, object] = {}
|
||||
for i, data in enumerate(create_list):
|
||||
etud: Identite = Identite.query.filter_by(id=data["etudid"]).first()
|
||||
if etud is None:
|
||||
errors[i] = "Cet étudiant n'existe pas."
|
||||
continue
|
||||
|
||||
code, obj = _create_singular(data, etud)
|
||||
if code == 404:
|
||||
errors[i] = obj
|
||||
else:
|
||||
success[i] = obj
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"errors": errors, "success": success})
|
||||
|
||||
|
||||
def _create_singular(
|
||||
data: dict,
|
||||
etud: Identite,
|
||||
) -> tuple[int, object]:
|
||||
errors: list[str] = []
|
||||
|
||||
# -- vérifications de l'objet json --
|
||||
# cas 1 : ETAT
|
||||
etat = data.get("etat", None)
|
||||
if etat is None:
|
||||
errors.append("param 'etat': manquant")
|
||||
elif not scu.EtatAssiduite.contains(etat):
|
||||
errors.append("param 'etat': invalide")
|
||||
|
||||
etat = scu.EtatAssiduite.get(etat)
|
||||
|
||||
# cas 2 : date_debut
|
||||
date_debut = data.get("date_debut", None)
|
||||
if date_debut is None:
|
||||
errors.append("param 'date_debut': manquant")
|
||||
deb = scu.is_iso_formated(date_debut, convert=True)
|
||||
if deb is None:
|
||||
errors.append("param 'date_debut': format invalide")
|
||||
|
||||
# cas 3 : date_fin
|
||||
date_fin = data.get("date_fin", None)
|
||||
if date_fin is None:
|
||||
errors.append("param 'date_fin': manquant")
|
||||
fin = scu.is_iso_formated(date_fin, convert=True)
|
||||
if fin is None:
|
||||
errors.append("param 'date_fin': format invalide")
|
||||
|
||||
# cas 4 : moduleimpl_id
|
||||
|
||||
moduleimpl_id = data.get("moduleimpl_id", False)
|
||||
moduleimpl: ModuleImpl = None
|
||||
|
||||
if moduleimpl_id is not False:
|
||||
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
|
||||
if moduleimpl is None:
|
||||
errors.append("param 'moduleimpl_id': invalide")
|
||||
|
||||
# cas 5 : desc
|
||||
|
||||
desc: str = data.get("desc", None)
|
||||
|
||||
if errors:
|
||||
err: str = ", ".join(errors)
|
||||
return (404, err)
|
||||
|
||||
# TOUT EST OK
|
||||
try:
|
||||
nouv_assiduite: Assiduite = Assiduite.create_assiduite(
|
||||
date_debut=deb,
|
||||
date_fin=fin,
|
||||
etat=etat,
|
||||
etud=etud,
|
||||
moduleimpl=moduleimpl,
|
||||
description=desc,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
|
||||
db.session.add(nouv_assiduite)
|
||||
return (200, {"assiduite_id": nouv_assiduite.id})
|
||||
except ScoValueError as excp:
|
||||
return (
|
||||
404,
|
||||
excp.args[0],
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/assiduite/delete", methods=["POST"])
|
||||
@api_web_bp.route("/assiduite/delete", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def assiduite_delete():
|
||||
"""
|
||||
Suppression d'une assiduité à partir de son id
|
||||
|
||||
Forme des données envoyées :
|
||||
|
||||
[
|
||||
<assiduite_id:int>,
|
||||
...
|
||||
]
|
||||
|
||||
|
||||
"""
|
||||
assiduites_list: list[int] = request.get_json(force=True)
|
||||
if not isinstance(assiduites_list, list):
|
||||
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||
|
||||
output = {"errors": {}, "success": {}}
|
||||
|
||||
for i, ass in enumerate(assiduites_list):
|
||||
code, msg = _delete_singular(ass, db)
|
||||
if code == 404:
|
||||
output["errors"][f"{i}"] = msg
|
||||
else:
|
||||
output["success"][f"{i}"] = {"OK": True}
|
||||
db.session.commit()
|
||||
return jsonify(output)
|
||||
|
||||
|
||||
def _delete_singular(assiduite_id: int, database):
|
||||
assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first()
|
||||
if assiduite_unique is None:
|
||||
return (404, "Assiduite non existante")
|
||||
database.session.delete(assiduite_unique)
|
||||
return (200, "OK")
|
||||
|
||||
|
||||
@bp.route("/assiduite/<int:assiduite_id>/edit", methods=["POST"])
|
||||
@api_web_bp.route("/assiduite/<int:assiduite_id>/edit", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def assiduite_edit(assiduite_id: int):
|
||||
"""
|
||||
Edition d'une assiduité à partir de son id
|
||||
La requête doit avoir un content type "application/json":
|
||||
{
|
||||
"etat"?: str,
|
||||
"moduleimpl_id"?: int
|
||||
"desc"?: str
|
||||
"est_just"?: bool
|
||||
}
|
||||
"""
|
||||
assiduite_unique: Assiduite = Assiduite.query.filter_by(
|
||||
id=assiduite_id
|
||||
).first_or_404()
|
||||
errors: list[str] = []
|
||||
data = request.get_json(force=True)
|
||||
|
||||
# Vérifications de data
|
||||
|
||||
# Cas 1 : Etat
|
||||
if data.get("etat") is not None:
|
||||
etat = scu.EtatAssiduite.get(data.get("etat"))
|
||||
if etat is None:
|
||||
errors.append("param 'etat': invalide")
|
||||
else:
|
||||
assiduite_unique.etat = etat
|
||||
|
||||
# Cas 2 : Moduleimpl_id
|
||||
moduleimpl_id = data.get("moduleimpl_id", False)
|
||||
moduleimpl: ModuleImpl = None
|
||||
|
||||
if moduleimpl_id is not False:
|
||||
if moduleimpl_id is not None:
|
||||
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
|
||||
if moduleimpl is None:
|
||||
errors.append("param 'moduleimpl_id': invalide")
|
||||
else:
|
||||
if not moduleimpl.est_inscrit(
|
||||
Identite.query.filter_by(id=assiduite_unique.etudid).first()
|
||||
):
|
||||
errors.append("param 'moduleimpl_id': etud non inscrit")
|
||||
else:
|
||||
assiduite_unique.moduleimpl_id = moduleimpl_id
|
||||
else:
|
||||
assiduite_unique.moduleimpl_id = moduleimpl_id
|
||||
|
||||
# Cas 3 : desc
|
||||
desc = data.get("desc", False)
|
||||
if desc is not False:
|
||||
assiduite_unique.desc = desc
|
||||
|
||||
# Cas 4 : est_just
|
||||
est_just = data.get("est_just")
|
||||
if est_just is not None:
|
||||
if not isinstance(est_just, bool):
|
||||
errors.append("param 'est_just' : booléen non reconnu")
|
||||
else:
|
||||
assiduite_unique.est_just = est_just
|
||||
|
||||
if errors:
|
||||
err: str = ", ".join(errors)
|
||||
return json_error(404, err)
|
||||
|
||||
db.session.add(assiduite_unique)
|
||||
db.session.commit()
|
||||
return jsonify({"OK": True})
|
||||
|
||||
|
||||
@bp.route("/assiduites/edit", methods=["POST"])
|
||||
@api_web_bp.route("/assiduites/edit", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def assiduites_edit():
|
||||
"""
|
||||
Edition d'une assiduité à partir de son id
|
||||
La requête doit avoir un content type "application/json":
|
||||
{
|
||||
"etat"?: str,
|
||||
"moduleimpl_id"?: int
|
||||
"desc"?: str
|
||||
"est_just"?: bool
|
||||
}
|
||||
"""
|
||||
edit_list: list[object] = request.get_json(force=True)
|
||||
|
||||
if not isinstance(edit_list, list):
|
||||
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||
|
||||
errors: dict[int, str] = {}
|
||||
success: dict[int, object] = {}
|
||||
for i, data in enumerate(edit_list):
|
||||
assi: Identite = Assiduite.query.filter_by(id=data["assiduite_id"]).first()
|
||||
if assi is None:
|
||||
errors[i] = "Cet assiduité n'existe pas."
|
||||
continue
|
||||
|
||||
code, obj = _edit_singular(assi, data)
|
||||
if code == 404:
|
||||
errors[i] = obj
|
||||
else:
|
||||
success[i] = obj
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"errors": errors, "success": success})
|
||||
|
||||
|
||||
def _edit_singular(assiduite_unique, data):
|
||||
errors: list[str] = []
|
||||
|
||||
# Vérifications de data
|
||||
|
||||
# Cas 1 : Etat
|
||||
if data.get("etat") is not None:
|
||||
etat = scu.EtatAssiduite.get(data.get("etat"))
|
||||
if etat is None:
|
||||
errors.append("param 'etat': invalide")
|
||||
else:
|
||||
assiduite_unique.etat = etat
|
||||
|
||||
# Cas 2 : Moduleimpl_id
|
||||
moduleimpl_id = data.get("moduleimpl_id", False)
|
||||
moduleimpl: ModuleImpl = None
|
||||
|
||||
if moduleimpl_id is not False:
|
||||
if moduleimpl_id is not None:
|
||||
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
|
||||
if moduleimpl is None:
|
||||
errors.append("param 'moduleimpl_id': invalide")
|
||||
else:
|
||||
if not moduleimpl.est_inscrit(
|
||||
Identite.query.filter_by(id=assiduite_unique.etudid).first()
|
||||
):
|
||||
errors.append("param 'moduleimpl_id': etud non inscrit")
|
||||
else:
|
||||
assiduite_unique.moduleimpl_id = moduleimpl_id
|
||||
else:
|
||||
assiduite_unique.moduleimpl_id = moduleimpl_id
|
||||
|
||||
# Cas 3 : desc
|
||||
desc = data.get("desc", False)
|
||||
if desc is not False:
|
||||
assiduite_unique.desc = desc
|
||||
|
||||
# Cas 4 : est_just
|
||||
est_just = data.get("est_just")
|
||||
if est_just is not None:
|
||||
if not isinstance(est_just, bool):
|
||||
errors.append("param 'est_just' : booléen non reconnu")
|
||||
else:
|
||||
assiduite_unique.est_just = est_just
|
||||
|
||||
if errors:
|
||||
err: str = ", ".join(errors)
|
||||
return (404, err)
|
||||
|
||||
db.session.add(assiduite_unique)
|
||||
|
||||
return (200, "OK")
|
||||
|
||||
|
||||
# -- Utils --
|
||||
|
||||
|
||||
def _count_manager(requested) -> tuple[str, dict]:
|
||||
"""
|
||||
Retourne la/les métriques à utiliser ainsi que le filtre donnés en query de la requête
|
||||
"""
|
||||
filtered: dict = {}
|
||||
# cas 1 : etat assiduite
|
||||
etat = requested.args.get("etat")
|
||||
if etat is not None:
|
||||
filtered["etat"] = etat
|
||||
|
||||
# cas 2 : date de début
|
||||
deb = requested.args.get("date_debut", "").replace(" ", "+")
|
||||
deb: datetime = scu.is_iso_formated(deb, True)
|
||||
if deb is not None:
|
||||
filtered["date_debut"] = deb
|
||||
|
||||
# cas 3 : date de fin
|
||||
fin = requested.args.get("date_fin", "").replace(" ", "+")
|
||||
fin = scu.is_iso_formated(fin, True)
|
||||
|
||||
if fin is not None:
|
||||
filtered["date_fin"] = fin
|
||||
|
||||
# cas 4 : moduleimpl_id
|
||||
module = requested.args.get("moduleimpl_id", False)
|
||||
try:
|
||||
if module is False:
|
||||
raise ValueError
|
||||
if module != "":
|
||||
module = int(module)
|
||||
else:
|
||||
module = None
|
||||
except ValueError:
|
||||
module = False
|
||||
|
||||
if module is not False:
|
||||
filtered["moduleimpl_id"] = module
|
||||
|
||||
# cas 5 : formsemestre_id
|
||||
formsemestre_id = requested.args.get("formsemestre_id")
|
||||
|
||||
if formsemestre_id is not None:
|
||||
formsemestre: FormSemestre = None
|
||||
formsemestre_id = int(formsemestre_id)
|
||||
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||
filtered["formsemestre"] = formsemestre
|
||||
|
||||
# cas 6 : type
|
||||
metric = requested.args.get("metric", "all")
|
||||
|
||||
# cas 7 : est_just
|
||||
|
||||
est_just: str = requested.args.get("est_just")
|
||||
if est_just is not None:
|
||||
trues: tuple[str] = ("v", "t", "vrai", "true")
|
||||
falses: tuple[str] = ("f", "faux", "false")
|
||||
|
||||
if est_just.lower() in trues:
|
||||
filtered["est_just"] = True
|
||||
elif est_just.lower() in falses:
|
||||
filtered["est_just"] = False
|
||||
|
||||
# cas 8 : user_id
|
||||
|
||||
user_id = requested.args.get("user_id", False)
|
||||
if user_id is not False:
|
||||
filtered["user_id"] = user_id
|
||||
|
||||
return (metric, filtered)
|
||||
|
||||
|
||||
def _filter_manager(requested, assiduites_query: Assiduite):
|
||||
"""
|
||||
Retourne les assiduites entrées filtrées en fonction de la request
|
||||
"""
|
||||
# cas 1 : etat assiduite
|
||||
etat = requested.args.get("etat")
|
||||
if etat is not None:
|
||||
assiduites_query = scass.filter_assiduites_by_etat(assiduites_query, etat)
|
||||
|
||||
# cas 2 : date de début
|
||||
deb = requested.args.get("date_debut", "").replace(" ", "+")
|
||||
deb: datetime = scu.is_iso_formated(deb, True)
|
||||
|
||||
# cas 3 : date de fin
|
||||
fin = requested.args.get("date_fin", "").replace(" ", "+")
|
||||
fin = scu.is_iso_formated(fin, True)
|
||||
|
||||
if (deb, fin) != (None, None):
|
||||
assiduites_query: Assiduite = scass.filter_by_date(
|
||||
assiduites_query, Assiduite, deb, fin
|
||||
)
|
||||
|
||||
# cas 4 : moduleimpl_id
|
||||
module = requested.args.get("moduleimpl_id", False)
|
||||
try:
|
||||
if module is False:
|
||||
raise ValueError
|
||||
if module != "":
|
||||
module = int(module)
|
||||
else:
|
||||
module = None
|
||||
except ValueError:
|
||||
module = False
|
||||
|
||||
if module is not False:
|
||||
assiduites_query = scass.filter_by_module_impl(assiduites_query, module)
|
||||
|
||||
# cas 5 : formsemestre_id
|
||||
formsemestre_id = requested.args.get("formsemestre_id")
|
||||
|
||||
if formsemestre_id is not None:
|
||||
formsemestre: FormSemestre = None
|
||||
formsemestre_id = int(formsemestre_id)
|
||||
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||
assiduites_query = scass.filter_by_formsemestre(assiduites_query, formsemestre)
|
||||
|
||||
# cas 6 : est_just
|
||||
|
||||
est_just: str = requested.args.get("est_just")
|
||||
if est_just is not None:
|
||||
trues: tuple[str] = ("v", "t", "vrai", "true")
|
||||
falses: tuple[str] = ("f", "faux", "false")
|
||||
|
||||
if est_just.lower() in trues:
|
||||
assiduites_query: Assiduite = scass.filter_assiduites_by_est_just(
|
||||
assiduites_query, True
|
||||
)
|
||||
elif est_just.lower() in falses:
|
||||
assiduites_query: Assiduite = scass.filter_assiduites_by_est_just(
|
||||
assiduites_query, False
|
||||
)
|
||||
|
||||
# cas 8 : user_id
|
||||
|
||||
user_id = requested.args.get("user_id", False)
|
||||
if user_id is not False:
|
||||
assiduites_query: Assiduite = scass.filter_by_user_id(assiduites_query, user_id)
|
||||
|
||||
return assiduites_query
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -11,7 +11,6 @@
|
|||
from flask import g, jsonify, request
|
||||
from flask_login import login_required
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.decorators import scodoc, permission_required
|
||||
|
@ -48,7 +47,7 @@ def billets_absence_create():
|
|||
justified = data.get("justified", False)
|
||||
if None in (etudid, abs_begin, abs_end):
|
||||
return json_error(
|
||||
404, message="Paramètre manquant: etudid, abs_bein, abs_end requis"
|
||||
404, message="Paramètre manquant: etudid, abs_begin, abs_end requis"
|
||||
)
|
||||
query = Identite.query.filter_by(etudid=etudid)
|
||||
if g.scodoc_dept:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -10,13 +10,14 @@
|
|||
Note: les routes /departement[s] sont publiées sur l'API (/ScoDoc/api/),
|
||||
mais évidemment pas sur l'API web (/ScoDoc/<dept>/api).
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from flask import jsonify, request
|
||||
from flask_login import login_required
|
||||
|
||||
import app
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp
|
||||
from app import db
|
||||
from app.api import api_bp as bp, API_CLIENT_ERROR
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import Departement, FormSemestre
|
||||
|
@ -42,7 +43,7 @@ def get_departement(dept_ident: str) -> Departement:
|
|||
@permission_required(Permission.ScoView)
|
||||
def departements_list():
|
||||
"""Liste les départements"""
|
||||
return jsonify([dept.to_dict() for dept in Departement.query])
|
||||
return jsonify([dept.to_dict(with_dept_name=True) for dept in Departement.query])
|
||||
|
||||
|
||||
@bp.route("/departements_ids")
|
||||
|
@ -66,13 +67,14 @@ def departement(acronym: str):
|
|||
{
|
||||
"id": 1,
|
||||
"acronym": "TAPI",
|
||||
"dept_name" : "TEST",
|
||||
"description": null,
|
||||
"visible": true,
|
||||
"date_creation": "Fri, 15 Apr 2022 12:19:28 GMT"
|
||||
}
|
||||
"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
return jsonify(dept.to_dict())
|
||||
return jsonify(dept.to_dict(with_dept_name=True))
|
||||
|
||||
|
||||
@bp.route("/departement/id/<int:dept_id>")
|
||||
|
@ -103,12 +105,12 @@ def departement_create():
|
|||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
acronym = str(data.get("acronym", ""))
|
||||
if not acronym:
|
||||
return json_error(404, "missing acronym")
|
||||
return json_error(API_CLIENT_ERROR, "missing acronym")
|
||||
visible = bool(data.get("visible", True))
|
||||
try:
|
||||
dept = departements.create_dept(acronym, visible=visible)
|
||||
except ScoValueError as exc:
|
||||
return json_error(404, exc.args[0] if exc.args else "")
|
||||
return json_error(500, exc.args[0] if exc.args else "")
|
||||
return jsonify(dept.to_dict())
|
||||
|
||||
|
||||
|
@ -128,7 +130,7 @@ def departement_edit(acronym):
|
|||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
visible = bool(data.get("visible", None))
|
||||
if visible is None:
|
||||
return json_error(404, "missing argument: visible")
|
||||
return json_error(API_CLIENT_ERROR, "missing argument: visible")
|
||||
visible = bool(visible)
|
||||
dept.visible = visible
|
||||
db.session.add(dept)
|
||||
|
@ -256,15 +258,18 @@ def dept_formsemestres_courants(acronym: str):
|
|||
]
|
||||
"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
|
||||
date_courante = request.args.get("date_courante")
|
||||
if date_courante:
|
||||
test_date = datetime.fromisoformat(date_courante)
|
||||
else:
|
||||
test_date = app.db.func.now()
|
||||
# Les semestres en cours de ce département
|
||||
formsemestres = FormSemestre.query.filter(
|
||||
FormSemestre.dept_id == dept.id,
|
||||
FormSemestre.date_debut <= app.db.func.now(),
|
||||
FormSemestre.date_fin >= app.db.func.now(),
|
||||
FormSemestre.date_debut <= test_date,
|
||||
FormSemestre.date_fin >= test_date,
|
||||
)
|
||||
|
||||
return jsonify([d.to_dict(convert_objects=True) for d in formsemestres])
|
||||
return jsonify([d.to_dict_api() for d in formsemestres])
|
||||
|
||||
|
||||
@bp.route("/departement/id/<int:dept_id>/formsemestres_courants")
|
||||
|
@ -277,12 +282,16 @@ def dept_formsemestres_courants_by_id(dept_id: int):
|
|||
"""
|
||||
# Le département, spécifié par un id ou un acronyme
|
||||
dept = Departement.query.get_or_404(dept_id)
|
||||
|
||||
date_courante = request.args.get("date_courante")
|
||||
if date_courante:
|
||||
test_date = datetime.fromisoformat(date_courante)
|
||||
else:
|
||||
test_date = app.db.func.now()
|
||||
# Les semestres en cours de ce département
|
||||
formsemestres = FormSemestre.query.filter(
|
||||
FormSemestre.dept_id == dept.id,
|
||||
FormSemestre.date_debut <= app.db.func.now(),
|
||||
FormSemestre.date_fin >= app.db.func.now(),
|
||||
FormSemestre.date_debut <= test_date,
|
||||
FormSemestre.date_fin >= test_date,
|
||||
)
|
||||
|
||||
return jsonify([d.to_dict(convert_objects=True) for d in formsemestres])
|
||||
return jsonify([d.to_dict_api() for d in formsemestres])
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
API : accès aux étudiants
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from flask import g, jsonify
|
||||
from flask import abort, g, jsonify, request
|
||||
from flask_login import current_user
|
||||
from flask_login import login_required
|
||||
from sqlalchemy import desc, or_
|
||||
|
@ -29,6 +30,7 @@ from app.scodoc import sco_bulletins
|
|||
from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
import app.scodoc.sco_photos as sco_photos
|
||||
|
||||
# Un exemple:
|
||||
# @bp.route("/api_function/<int:arg>")
|
||||
|
@ -75,11 +77,16 @@ def etudiants_courants(long=False):
|
|||
|
||||
"""
|
||||
allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
|
||||
date_courante = request.args.get("date_courante")
|
||||
if date_courante:
|
||||
test_date = datetime.fromisoformat(date_courante)
|
||||
else:
|
||||
test_date = app.db.func.now()
|
||||
etuds = Identite.query.filter(
|
||||
Identite.id == FormSemestreInscription.etudid,
|
||||
FormSemestreInscription.formsemestre_id == FormSemestre.id,
|
||||
FormSemestre.date_debut <= app.db.func.now(),
|
||||
FormSemestre.date_fin >= app.db.func.now(),
|
||||
FormSemestre.date_debut <= test_date,
|
||||
FormSemestre.date_fin >= test_date,
|
||||
)
|
||||
if not None in allowed_depts:
|
||||
# restreint aux départements autorisés:
|
||||
|
@ -124,6 +131,42 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
|||
return jsonify(etud.to_dict_api())
|
||||
|
||||
|
||||
@api_web_bp.route("/etudiant/etudid/<int:etudid>/photo")
|
||||
@api_web_bp.route("/etudiant/nip/<string:nip>/photo")
|
||||
@api_web_bp.route("/etudiant/ine/<string:ine>/photo")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def get_photo_image(etudid: int = None, nip: str = None, ine: str = None):
|
||||
"""
|
||||
Retourne la photo de l'étudiant
|
||||
correspondant ou un placeholder si non existant.
|
||||
|
||||
etudid : l'etudid de l'étudiant
|
||||
nip : le code nip de l'étudiant
|
||||
ine : le code ine de l'étudiant
|
||||
|
||||
Attention : Ne peut être qu'utilisée en tant que route de département
|
||||
"""
|
||||
|
||||
etud = tools.get_etud(etudid, nip, ine)
|
||||
|
||||
if etud is None:
|
||||
return json_error(
|
||||
404,
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
if not etudid:
|
||||
filename = sco_photos.UNKNOWN_IMAGE_PATH
|
||||
|
||||
size = request.args.get("size", "orig")
|
||||
filename = sco_photos.photo_pathname(etud.photo_filename, size=size)
|
||||
if not filename:
|
||||
filename = sco_photos.UNKNOWN_IMAGE_PATH
|
||||
res = sco_photos.build_image_response(filename)
|
||||
return res
|
||||
|
||||
|
||||
@bp.route("/etudiants/etudid/<int:etudid>", methods=["GET"])
|
||||
@bp.route("/etudiants/nip/<string:nip>", methods=["GET"])
|
||||
@bp.route("/etudiants/ine/<string:ine>", methods=["GET"])
|
||||
|
@ -204,160 +247,88 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None)
|
|||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": False},
|
||||
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": False},
|
||||
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>",
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": False},
|
||||
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>/pdf",
|
||||
defaults={"pdf": True},
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": True},
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/short",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": False},
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin/short",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": False},
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin/short",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": False},
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": True},
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": True},
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": True},
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": True},
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": True},
|
||||
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>/pdf/nosig",
|
||||
defaults={"pdf": True, "with_img_signatures_pdf": False},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": False},
|
||||
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": False},
|
||||
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>",
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin",
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": False},
|
||||
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>/pdf",
|
||||
defaults={"pdf": True},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": True},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/short",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": False},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin/short",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": False},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin/short",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": False},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": True},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": True},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": True},
|
||||
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>/pdf/nosig",
|
||||
defaults={"pdf": True, "with_img_signatures_pdf": False},
|
||||
)
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def etudiant_bulletin_semestre(
|
||||
formsemestre_id,
|
||||
etudid: int = None,
|
||||
nip: str = None,
|
||||
ine: str = None,
|
||||
version="long",
|
||||
def bulletin(
|
||||
code_type: str = "etudid",
|
||||
code: str = None,
|
||||
formsemestre_id: int = None,
|
||||
version: str = "long",
|
||||
pdf: bool = False,
|
||||
with_img_signatures_pdf: bool = True,
|
||||
):
|
||||
"""
|
||||
Retourne le bulletin d'un étudiant en fonction de son id et d'un semestre donné
|
||||
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
etudid : l'etudid d'un étudiant
|
||||
nip : le code nip d'un étudiant
|
||||
ine : le code ine d'un étudiant
|
||||
Exemple de résultat : voir https://scodoc.org/ScoDoc9API/#bulletin
|
||||
code_type : "etudid", "nip" ou "ine"
|
||||
code : valeur du code INE, NIP ou etudid, selon code_type.
|
||||
version : type de bulletin (par défaut, "long"): short, long, long_mat
|
||||
pdf : si spécifié, bulletin au format PDF (et non JSON).
|
||||
|
||||
Exemple de résultat : voir https://scodoc.org/ScoDoc9API/#bulletin
|
||||
"""
|
||||
if version == "pdf":
|
||||
version = "long"
|
||||
pdf = True
|
||||
# return f"{code_type}={code}, version={version}, pdf={pdf}"
|
||||
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
|
||||
dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
|
||||
if g.scodoc_dept and dept.acronym != g.scodoc_dept:
|
||||
return json_error(404, "formsemestre non trouve")
|
||||
if etudid is not None:
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
elif nip is not None:
|
||||
query = Identite.query.filter_by(code_nip=nip, dept_id=dept.id)
|
||||
elif ine is not None:
|
||||
query = Identite.query.filter_by(code_ine=ine, dept_id=dept.id)
|
||||
else:
|
||||
return json_error(404, message="parametre manquant")
|
||||
return json_error(404, "formsemestre inexistant")
|
||||
app.set_sco_dept(dept.acronym)
|
||||
|
||||
if code_type == "nip":
|
||||
query = Identite.query.filter_by(code_nip=code, dept_id=dept.id)
|
||||
elif code_type == "etudid":
|
||||
try:
|
||||
etudid = int(code)
|
||||
except ValueError:
|
||||
return json_error(404, "invalid etudid type")
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
elif code_type == "ine":
|
||||
query = Identite.query.filter_by(code_ine=code, dept_id=dept.id)
|
||||
else:
|
||||
return json_error(404, "invalid code_type")
|
||||
etud = query.first()
|
||||
if etud is None:
|
||||
return json_error(404, message="etudiant inexistant")
|
||||
|
||||
app.set_sco_dept(dept.acronym)
|
||||
|
||||
if pdf:
|
||||
pdf_response, _ = do_formsemestre_bulletinetud(
|
||||
formsemestre, etud.id, version=version, format="pdf"
|
||||
formsemestre,
|
||||
etud.id,
|
||||
version=version,
|
||||
format="pdf",
|
||||
with_img_signatures_pdf=with_img_signatures_pdf,
|
||||
)
|
||||
return pdf_response
|
||||
|
||||
return sco_bulletins.get_formsemestre_bulletin_etud_json(
|
||||
formsemestre, etud, version=version
|
||||
)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -15,13 +15,50 @@ import app
|
|||
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.models import Evaluation, ModuleImpl, FormSemestre
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
@bp.route("/evaluation/<int:evaluation_id>")
|
||||
@api_web_bp.route("/evaluation/<int:evaluation_id>")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def evaluation(evaluation_id: int):
|
||||
"""Description d'une évaluation.
|
||||
|
||||
{
|
||||
'coefficient': 1.0,
|
||||
'date_debut': '2016-01-04T08:30:00',
|
||||
'date_fin': '2016-01-04T12:30:00',
|
||||
'description': 'TP NI9219 Température',
|
||||
'evaluation_type': 0,
|
||||
'id': 15797,
|
||||
'moduleimpl_id': 1234,
|
||||
'note_max': 20.0,
|
||||
'numero': 3,
|
||||
'poids': {
|
||||
'UE1.1': 1.0,
|
||||
'UE1.2': 1.0,
|
||||
'UE1.3': 1.0
|
||||
},
|
||||
'publish_incomplete': False,
|
||||
'visi_bulletin': True
|
||||
}
|
||||
"""
|
||||
query = Evaluation.query.filter_by(id=evaluation_id)
|
||||
if g.scodoc_dept:
|
||||
query = (
|
||||
query.join(ModuleImpl)
|
||||
.join(FormSemestre)
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
e = query.first_or_404()
|
||||
return jsonify(e.to_dict_api())
|
||||
|
||||
|
||||
@bp.route("/moduleimpl/<int:moduleimpl_id>/evaluations")
|
||||
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>/evaluations")
|
||||
@login_required
|
||||
|
@ -33,39 +70,16 @@ def evaluations(moduleimpl_id: int):
|
|||
|
||||
moduleimpl_id : l'id d'un moduleimpl
|
||||
|
||||
Exemple de résultat :
|
||||
[
|
||||
{
|
||||
"moduleimpl_id": 1,
|
||||
"jour": "20/04/2022",
|
||||
"heure_debut": "08h00",
|
||||
"description": "eval1",
|
||||
"coefficient": 1.0,
|
||||
"publish_incomplete": false,
|
||||
"numero": 0,
|
||||
"id": 1,
|
||||
"heure_fin": "09h00",
|
||||
"note_max": 20.0,
|
||||
"visibulletin": true,
|
||||
"evaluation_type": 0,
|
||||
"evaluation_id": 1,
|
||||
"jouriso": "2022-04-20",
|
||||
"duree": "1h",
|
||||
"descrheure": " de 08h00 à 09h00",
|
||||
"matin": 1,
|
||||
"apresmidi": 0
|
||||
},
|
||||
...
|
||||
]
|
||||
Exemple de résultat : voir /evaluation
|
||||
"""
|
||||
query = Evaluation.query.filter_by(id=moduleimpl_id)
|
||||
query = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id)
|
||||
if g.scodoc_dept:
|
||||
query = (
|
||||
query.join(ModuleImpl)
|
||||
.join(FormSemestre)
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
return jsonify([d.to_dict() for d in query])
|
||||
return jsonify([e.to_dict_api() for e in query])
|
||||
|
||||
|
||||
@bp.route("/evaluation/<int:evaluation_id>/notes")
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -11,7 +11,7 @@ from flask import g, jsonify, request
|
|||
from flask_login import login_required
|
||||
|
||||
import app
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.comp import res_sem
|
||||
|
@ -22,6 +22,8 @@ from app.models import (
|
|||
Evaluation,
|
||||
FormSemestre,
|
||||
FormSemestreEtape,
|
||||
FormSemestreInscription,
|
||||
Identite,
|
||||
ModuleImpl,
|
||||
NotesNotes,
|
||||
)
|
||||
|
@ -30,6 +32,7 @@ from app.scodoc import sco_groups
|
|||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.tables.recap import TableRecap
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>")
|
||||
|
@ -95,11 +98,14 @@ def formsemestres_query():
|
|||
annee_scolaire : année de début de l'année scolaire
|
||||
dept_acronym : acronyme du département (eg "RT")
|
||||
dept_id : id du département
|
||||
ine ou nip: code d'un étudiant: ramène alors tous les semestres auxquels il est inscrit.
|
||||
"""
|
||||
etape_apo = request.args.get("etape_apo")
|
||||
annee_scolaire = request.args.get("annee_scolaire")
|
||||
dept_acronym = request.args.get("dept_acronym")
|
||||
dept_id = request.args.get("dept_id")
|
||||
nip = request.args.get("nip")
|
||||
ine = request.args.get("ine")
|
||||
formsemestres = FormSemestre.query
|
||||
if g.scodoc_dept:
|
||||
formsemestres = formsemestres.filter_by(dept_id=g.scodoc_dept_id)
|
||||
|
@ -107,7 +113,7 @@ def formsemestres_query():
|
|||
try:
|
||||
annee_scolaire_int = int(annee_scolaire)
|
||||
except ValueError:
|
||||
return json_error(404, "invalid annee_scolaire: not int")
|
||||
return json_error(API_CLIENT_ERROR, "invalid annee_scolaire: not int")
|
||||
debut_annee = scu.date_debut_anne_scolaire(annee_scolaire_int)
|
||||
fin_annee = scu.date_fin_anne_scolaire(annee_scolaire_int)
|
||||
formsemestres = formsemestres.filter(
|
||||
|
@ -119,22 +125,36 @@ def formsemestres_query():
|
|||
try:
|
||||
dept_id = int(dept_id)
|
||||
except ValueError:
|
||||
return json_error(404, "invalid dept_id: not int")
|
||||
return json_error(404, "invalid dept_id: integer expected")
|
||||
formsemestres = formsemestres.filter_by(dept_id=dept_id)
|
||||
if etape_apo is not None:
|
||||
formsemestres = formsemestres.join(FormSemestreEtape).filter(
|
||||
FormSemestreEtape.etape_apo == etape_apo
|
||||
)
|
||||
inscr_joined = False
|
||||
if nip is not None:
|
||||
formsemestres = (
|
||||
formsemestres.join(FormSemestreInscription)
|
||||
.join(Identite)
|
||||
.filter_by(code_nip=nip)
|
||||
)
|
||||
inscr_joined = True
|
||||
if ine is not None:
|
||||
if not inscr_joined:
|
||||
formsemestres = formsemestres.join(FormSemestreInscription).join(Identite)
|
||||
formsemestres = formsemestres.filter_by(code_ine=ine)
|
||||
|
||||
return jsonify([formsemestre.to_dict_api() for formsemestre in formsemestres])
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def bulletins(formsemestre_id: int):
|
||||
def bulletins(formsemestre_id: int, version: str = "long"):
|
||||
"""
|
||||
Retourne les bulletins d'un formsemestre donné
|
||||
|
||||
|
@ -145,12 +165,16 @@ def bulletins(formsemestre_id: int):
|
|||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||
formsemestre: FormSemestre = query.first()
|
||||
if formsemestre is None:
|
||||
return json_error(404, "formsemestre non trouve")
|
||||
app.set_sco_dept(formsemestre.departement.acronym)
|
||||
|
||||
data = []
|
||||
for etu in formsemestre.etuds:
|
||||
bul_etu = get_formsemestre_bulletin_etud_json(formsemestre, etu)
|
||||
bul_etu = get_formsemestre_bulletin_etud_json(
|
||||
formsemestre, etu, version=version
|
||||
)
|
||||
data.append(bul_etu.json)
|
||||
|
||||
return jsonify(data)
|
||||
|
@ -381,7 +405,7 @@ def etat_evals(formsemestre_id: int):
|
|||
for evaluation_id in modimpl_results.evaluations_etat:
|
||||
eval_etat = modimpl_results.evaluations_etat[evaluation_id]
|
||||
evaluation = Evaluation.query.get_or_404(evaluation_id)
|
||||
eval_dict = evaluation.to_dict()
|
||||
eval_dict = evaluation.to_dict_api()
|
||||
eval_dict["etat"] = eval_etat.to_dict()
|
||||
|
||||
eval_dict["nb_inscrits"] = modimpl_results.nb_inscrits_module
|
||||
|
@ -444,7 +468,7 @@ def formsemestre_resultat(formsemestre_id: int):
|
|||
"""
|
||||
format_spec = request.args.get("format", None)
|
||||
if format_spec is not None and format_spec != "raw":
|
||||
return json_error(404, "invalid format specification")
|
||||
return json_error(API_CLIENT_ERROR, "invalid format specification")
|
||||
convert_values = format_spec != "raw"
|
||||
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
|
@ -453,16 +477,14 @@ def formsemestre_resultat(formsemestre_id: int):
|
|||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||
app.set_sco_dept(formsemestre.departement.acronym)
|
||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
rows, footer_rows, titles, column_ids = res.get_table_recap(
|
||||
convert_values=convert_values,
|
||||
include_evaluations=False,
|
||||
mode_jury=False,
|
||||
allow_html=False,
|
||||
table = TableRecap(
|
||||
res, convert_values=convert_values, include_evaluations=False, mode_jury=False
|
||||
)
|
||||
# Supprime les champs inutiles (mise en forme)
|
||||
table = [{k: row[k] for k in row if not k[0] == "_"} for row in rows]
|
||||
# Ajoute les groupes
|
||||
rows = table.to_list()
|
||||
# Ajoute le groupe de chaque partition:
|
||||
etud_groups = sco_groups.get_formsemestre_etuds_groups(formsemestre_id)
|
||||
for row in table:
|
||||
for row in rows:
|
||||
row["partitions"] = etud_groups.get(row["etudid"], {})
|
||||
return jsonify(table)
|
||||
|
||||
return jsonify(rows)
|
||||
|
|
|
@ -1,24 +1,22 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : jury
|
||||
ScoDoc 9 API : jury WIP
|
||||
"""
|
||||
|
||||
from flask import g, jsonify, request
|
||||
from flask import jsonify
|
||||
from flask_login import login_required
|
||||
|
||||
import app
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_exceptions import ScoException
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.but import jury_but_recap
|
||||
from app.models import FormSemestre, FormSemestreInscription, Identite
|
||||
from app.but import jury_but_results
|
||||
from app.models import FormSemestre
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
|
@ -33,7 +31,7 @@ def decisions_jury(formsemestre_id: int):
|
|||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
if formsemestre.formation.is_apc():
|
||||
app.set_sco_dept(formsemestre.departement.acronym)
|
||||
rows = jury_but_recap.get_jury_but_results(formsemestre)
|
||||
rows = jury_but_results.get_jury_but_results(formsemestre)
|
||||
return jsonify(rows)
|
||||
else:
|
||||
raise ScoException("non implemente")
|
||||
|
|
|
@ -0,0 +1,591 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : Assiduités
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from flask import g, jsonify, request
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import db
|
||||
from app.api import api_bp as bp
|
||||
from app.api import api_web_bp
|
||||
from app.api import get_model_api_object
|
||||
from app.decorators import permission_required, scodoc
|
||||
from app.models import Identite, Justificatif
|
||||
from app.models.assiduites import compute_assiduites_justified
|
||||
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error
|
||||
|
||||
|
||||
# Partie Modèle
|
||||
@bp.route("/justificatif/<int:justif_id>")
|
||||
@api_web_bp.route("/justificatif/<int:justif_id>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def justificatif(justif_id: int = None):
|
||||
"""Retourne un objet justificatif à partir de son id
|
||||
|
||||
Exemple de résultat:
|
||||
{
|
||||
"justif_id": 1,
|
||||
"etudid": 2,
|
||||
"date_debut": "2022-10-31T08:00+01:00",
|
||||
"date_fin": "2022-10-31T10:00+01:00",
|
||||
"etat": "valide",
|
||||
"fichier": "archive_id",
|
||||
"raison": "une raison",
|
||||
"entry_date": "2022-10-31T08:00+01:00",
|
||||
"user_id": 1 or null,
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
return get_model_api_object(Justificatif, justif_id, Identite)
|
||||
|
||||
|
||||
@bp.route("/justificatifs/<int:etudid>", defaults={"with_query": False})
|
||||
@bp.route("/justificatifs/<int:etudid>/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/justificatifs/<int:etudid>", defaults={"with_query": False})
|
||||
@api_web_bp.route("/justificatifs/<int:etudid>/query", defaults={"with_query": True})
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def justificatifs(etudid: int = None, with_query: bool = False):
|
||||
"""
|
||||
Retourne toutes les assiduités d'un étudiant
|
||||
chemin : /justificatifs/<int:etudid>
|
||||
|
||||
Un filtrage peut être donné avec une query
|
||||
chemin : /justificatifs/<int:etudid>/query?
|
||||
|
||||
Les différents filtres :
|
||||
Etat (etat du justificatif -> validé, non validé, modifé, en attente):
|
||||
query?etat=[- liste des états séparé par une virgule -]
|
||||
ex: .../query?etat=validé,modifié
|
||||
Date debut
|
||||
(date de début du justificatif, sont affichés les justificatifs
|
||||
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||
query?date_debut=[- date au format iso -]
|
||||
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||
Date fin
|
||||
(date de fin du justificatif, sont affichés les justificatifs
|
||||
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||
query?date_fin=[- date au format iso -]
|
||||
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||
user_id (l'id de l'auteur du justificatif)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
"""
|
||||
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
|
||||
etud: Identite = query.first_or_404(etudid)
|
||||
justificatifs_query = etud.justificatifs
|
||||
|
||||
if with_query:
|
||||
justificatifs_query = _filter_manager(request, justificatifs_query)
|
||||
|
||||
data_set: list[dict] = []
|
||||
for just in justificatifs_query.all():
|
||||
data = just.to_dict(format_api=True)
|
||||
data_set.append(data)
|
||||
|
||||
return jsonify(data_set)
|
||||
|
||||
|
||||
@bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
|
||||
@api_web_bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_create(etudid: int = None):
|
||||
"""
|
||||
Création d'un justificatif pour l'étudiant (etudid)
|
||||
La requête doit avoir un content type "application/json":
|
||||
[
|
||||
{
|
||||
"date_debut": str,
|
||||
"date_fin": str,
|
||||
"etat": str,
|
||||
},
|
||||
{
|
||||
"date_debut": str,
|
||||
"date_fin": str,
|
||||
"etat": str,
|
||||
"raison":str,
|
||||
}
|
||||
...
|
||||
]
|
||||
|
||||
"""
|
||||
etud: Identite = Identite.query.filter_by(id=etudid).first_or_404()
|
||||
|
||||
create_list: list[object] = request.get_json(force=True)
|
||||
|
||||
if not isinstance(create_list, list):
|
||||
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||
|
||||
errors: dict[int, str] = {}
|
||||
success: dict[int, object] = {}
|
||||
for i, data in enumerate(create_list):
|
||||
code, obj = _create_singular(data, etud)
|
||||
if code == 404:
|
||||
errors[i] = obj
|
||||
else:
|
||||
success[i] = obj
|
||||
compute_assiduites_justified(Justificatif.query.filter_by(etudid=etudid), True)
|
||||
return jsonify({"errors": errors, "success": success})
|
||||
|
||||
|
||||
def _create_singular(
|
||||
data: dict,
|
||||
etud: Identite,
|
||||
) -> tuple[int, object]:
|
||||
errors: list[str] = []
|
||||
|
||||
# -- vérifications de l'objet json --
|
||||
# cas 1 : ETAT
|
||||
etat = data.get("etat", None)
|
||||
if etat is None:
|
||||
errors.append("param 'etat': manquant")
|
||||
elif not scu.EtatJustificatif.contains(etat):
|
||||
errors.append("param 'etat': invalide")
|
||||
|
||||
etat = scu.EtatJustificatif.get(etat)
|
||||
|
||||
# cas 2 : date_debut
|
||||
date_debut = data.get("date_debut", None)
|
||||
if date_debut is None:
|
||||
errors.append("param 'date_debut': manquant")
|
||||
deb = scu.is_iso_formated(date_debut, convert=True)
|
||||
if deb is None:
|
||||
errors.append("param 'date_debut': format invalide")
|
||||
|
||||
# cas 3 : date_fin
|
||||
date_fin = data.get("date_fin", None)
|
||||
if date_fin is None:
|
||||
errors.append("param 'date_fin': manquant")
|
||||
fin = scu.is_iso_formated(date_fin, convert=True)
|
||||
if fin is None:
|
||||
errors.append("param 'date_fin': format invalide")
|
||||
|
||||
# cas 4 : raison
|
||||
|
||||
raison: str = data.get("raison", None)
|
||||
|
||||
if errors:
|
||||
err: str = ", ".join(errors)
|
||||
return (404, err)
|
||||
|
||||
# TOUT EST OK
|
||||
|
||||
try:
|
||||
nouv_justificatif: Justificatif = Justificatif.create_justificatif(
|
||||
date_debut=deb,
|
||||
date_fin=fin,
|
||||
etat=etat,
|
||||
etud=etud,
|
||||
raison=raison,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
|
||||
db.session.add(nouv_justificatif)
|
||||
db.session.commit()
|
||||
|
||||
return (
|
||||
200,
|
||||
{
|
||||
"justif_id": nouv_justificatif.id,
|
||||
"couverture": scass.justifies(nouv_justificatif),
|
||||
},
|
||||
)
|
||||
except ScoValueError as excp:
|
||||
return (
|
||||
404,
|
||||
excp.args[0],
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/justificatif/<int:justif_id>/edit", methods=["POST"])
|
||||
@api_web_bp.route("/justificatif/<int:justif_id>/edit", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_edit(justif_id: int):
|
||||
"""
|
||||
Edition d'un justificatif à partir de son id
|
||||
La requête doit avoir un content type "application/json":
|
||||
|
||||
{
|
||||
"etat"?: str,
|
||||
"raison"?: str
|
||||
"date_debut"?: str
|
||||
"date_fin"?: str
|
||||
}
|
||||
"""
|
||||
justificatif_unique: Justificatif = Justificatif.query.filter_by(
|
||||
id=justif_id
|
||||
).first_or_404()
|
||||
|
||||
errors: list[str] = []
|
||||
data = request.get_json(force=True)
|
||||
avant_ids: list[int] = scass.justifies(justificatif_unique)
|
||||
# Vérifications de data
|
||||
|
||||
# Cas 1 : Etat
|
||||
if data.get("etat") is not None:
|
||||
etat = scu.EtatJustificatif.get(data.get("etat"))
|
||||
if etat is None:
|
||||
errors.append("param 'etat': invalide")
|
||||
else:
|
||||
justificatif_unique.etat = etat
|
||||
|
||||
# Cas 2 : raison
|
||||
raison = data.get("raison", False)
|
||||
if raison is not False:
|
||||
justificatif_unique.raison = raison
|
||||
|
||||
deb, fin = None, None
|
||||
|
||||
# cas 3 : date_debut
|
||||
date_debut = data.get("date_debut", False)
|
||||
if date_debut is not False:
|
||||
if date_debut is None:
|
||||
errors.append("param 'date_debut': manquant")
|
||||
deb = scu.is_iso_formated(date_debut.replace(" ", "+"), convert=True)
|
||||
if deb is None:
|
||||
errors.append("param 'date_debut': format invalide")
|
||||
|
||||
if justificatif_unique.date_fin >= deb:
|
||||
errors.append("param 'date_debut': date de début située après date de fin ")
|
||||
|
||||
# cas 4 : date_fin
|
||||
date_fin = data.get("date_fin", False)
|
||||
if date_fin is not False:
|
||||
if date_fin is None:
|
||||
errors.append("param 'date_fin': manquant")
|
||||
fin = scu.is_iso_formated(date_fin.replace(" ", "+"), convert=True)
|
||||
if fin is None:
|
||||
errors.append("param 'date_fin': format invalide")
|
||||
if justificatif_unique.date_debut <= fin:
|
||||
errors.append("param 'date_fin': date de fin située avant date de début ")
|
||||
|
||||
# Mise à jour des dates
|
||||
deb = deb if deb is not None else justificatif_unique.date_debut
|
||||
fin = fin if fin is not None else justificatif_unique.date_fin
|
||||
|
||||
justificatif_unique.date_debut = deb
|
||||
justificatif_unique.date_fin = fin
|
||||
|
||||
if errors:
|
||||
err: str = ", ".join(errors)
|
||||
return json_error(404, err)
|
||||
|
||||
db.session.add(justificatif_unique)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"couverture": {
|
||||
"avant": avant_ids,
|
||||
"après": compute_assiduites_justified(
|
||||
Justificatif.query.filter_by(etudid=justificatif_unique.etudid),
|
||||
True,
|
||||
),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/justificatif/delete", methods=["POST"])
|
||||
@api_web_bp.route("/justificatif/delete", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_delete():
|
||||
"""
|
||||
Suppression d'un justificatif à partir de son id
|
||||
|
||||
Forme des données envoyées :
|
||||
|
||||
[
|
||||
<justif_id:int>,
|
||||
...
|
||||
]
|
||||
|
||||
|
||||
"""
|
||||
justificatifs_list: list[int] = request.get_json(force=True)
|
||||
if not isinstance(justificatifs_list, list):
|
||||
return json_error(404, "Le contenu envoyé n'est pas une liste")
|
||||
|
||||
output = {"errors": {}, "success": {}}
|
||||
|
||||
for i, ass in enumerate(justificatifs_list):
|
||||
code, msg = _delete_singular(ass, db)
|
||||
if code == 404:
|
||||
output["errors"][f"{i}"] = msg
|
||||
else:
|
||||
output["success"][f"{i}"] = {"OK": True}
|
||||
db.session.commit()
|
||||
return jsonify(output)
|
||||
|
||||
|
||||
def _delete_singular(justif_id: int, database):
|
||||
justificatif_unique: Justificatif = Justificatif.query.filter_by(
|
||||
id=justif_id
|
||||
).first()
|
||||
if justificatif_unique is None:
|
||||
return (404, "Justificatif non existant")
|
||||
|
||||
archive_name: str = justificatif_unique.fichier
|
||||
|
||||
if archive_name is not None:
|
||||
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||
archiver.delete_justificatif(justificatif_unique.etudid, archive_name)
|
||||
|
||||
database.session.delete(justificatif_unique)
|
||||
return (200, "OK")
|
||||
|
||||
|
||||
# Partie archivage
|
||||
@bp.route("/justificatif/<int:justif_id>/import", methods=["POST"])
|
||||
@api_web_bp.route("/justificatif/<int:justif_id>/import", methods=["POST"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_import(justif_id: int = None):
|
||||
"""
|
||||
Importation d'un fichier (création d'archive)
|
||||
"""
|
||||
if len(request.files) == 0:
|
||||
return json_error(404, "Il n'y a pas de fichier joint")
|
||||
|
||||
file = list(request.files.values())[0]
|
||||
if file.filename == "":
|
||||
return json_error(404, "Il n'y a pas de fichier joint")
|
||||
|
||||
query = Justificatif.query.filter_by(id=justif_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||
|
||||
justificatif_unique: Justificatif = query.first_or_404()
|
||||
|
||||
archive_name: str = justificatif_unique.fichier
|
||||
|
||||
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||
try:
|
||||
fname: str
|
||||
archive_name, fname = archiver.save_justificatif(
|
||||
etudid=justificatif_unique.etudid,
|
||||
filename=file.filename,
|
||||
data=file.stream.read(),
|
||||
archive_name=archive_name,
|
||||
)
|
||||
|
||||
justificatif_unique.fichier = archive_name
|
||||
|
||||
db.session.add(justificatif_unique)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"filename": fname})
|
||||
except ScoValueError as err:
|
||||
return json_error(404, err.args[0])
|
||||
|
||||
|
||||
@bp.route("/justificatif/<int:justif_id>/export/<filename>", methods=["POST"])
|
||||
@api_web_bp.route("/justificatif/<int:justif_id>/export/<filename>", methods=["POST"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_export(justif_id: int = None, filename: str = None):
|
||||
"""
|
||||
Retourne un fichier d'une archive d'un justificatif
|
||||
"""
|
||||
|
||||
query = Justificatif.query.filter_by(id=justif_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||
|
||||
justificatif_unique: Justificatif = query.first_or_404()
|
||||
|
||||
archive_name: str = justificatif_unique.fichier
|
||||
if archive_name is None:
|
||||
return json_error(404, "le justificatif ne possède pas de fichier")
|
||||
|
||||
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||
|
||||
try:
|
||||
return archiver.get_justificatif_file(
|
||||
archive_name, justificatif_unique.etudid, filename
|
||||
)
|
||||
except ScoValueError as err:
|
||||
return json_error(404, err.args[0])
|
||||
|
||||
|
||||
@bp.route("/justificatif/<int:justif_id>/remove", methods=["POST"])
|
||||
@api_web_bp.route("/justificatif/<int:justif_id>/remove", methods=["POST"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_remove(justif_id: int = None):
|
||||
"""
|
||||
Supression d'un fichier ou d'une archive
|
||||
# TOTALK: Doc, expliquer les noms coté server
|
||||
{
|
||||
"remove": <"all"/"list">
|
||||
|
||||
"filenames"?: [
|
||||
<filename:str>,
|
||||
...
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
data: dict = request.get_json(force=True)
|
||||
|
||||
query = Justificatif.query.filter_by(id=justif_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||
|
||||
justificatif_unique: Justificatif = query.first_or_404()
|
||||
|
||||
archive_name: str = justificatif_unique.fichier
|
||||
if archive_name is None:
|
||||
return json_error(404, "le justificatif ne possède pas de fichier")
|
||||
|
||||
remove: str = data.get("remove")
|
||||
if remove is None or remove not in ("all", "list"):
|
||||
return json_error(404, "param 'remove': Valeur invalide")
|
||||
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||
etudid: int = justificatif_unique.etudid
|
||||
try:
|
||||
if remove == "all":
|
||||
archiver.delete_justificatif(etudid=etudid, archive_name=archive_name)
|
||||
justificatif_unique.fichier = None
|
||||
db.session.add(justificatif_unique)
|
||||
db.session.commit()
|
||||
|
||||
else:
|
||||
for fname in data.get("filenames", []):
|
||||
archiver.delete_justificatif(
|
||||
etudid=etudid,
|
||||
archive_name=archive_name,
|
||||
filename=fname,
|
||||
)
|
||||
|
||||
if len(archiver.list_justificatifs(archive_name, etudid)) == 0:
|
||||
archiver.delete_justificatif(etudid, archive_name)
|
||||
justificatif_unique.fichier = None
|
||||
db.session.add(justificatif_unique)
|
||||
db.session.commit()
|
||||
|
||||
except ScoValueError as err:
|
||||
return json_error(404, err.args[0])
|
||||
|
||||
return jsonify({"response": "removed"})
|
||||
|
||||
|
||||
@bp.route("/justificatif/<int:justif_id>/list", methods=["GET"])
|
||||
@api_web_bp.route("/justificatif/<int:justif_id>/list", methods=["GET"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_list(justif_id: int = None):
|
||||
"""
|
||||
Liste les fichiers du justificatif
|
||||
"""
|
||||
|
||||
query = Justificatif.query.filter_by(id=justif_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||
|
||||
justificatif_unique: Justificatif = query.first_or_404()
|
||||
|
||||
archive_name: str = justificatif_unique.fichier
|
||||
|
||||
filenames: list[str] = []
|
||||
|
||||
archiver: JustificatifArchiver = JustificatifArchiver()
|
||||
if archive_name is not None:
|
||||
filenames = archiver.list_justificatifs(
|
||||
archive_name, justificatif_unique.etudid
|
||||
)
|
||||
|
||||
return jsonify(filenames)
|
||||
|
||||
|
||||
# Partie justification
|
||||
@bp.route("/justificatif/<int:justif_id>/justifies", methods=["GET"])
|
||||
@api_web_bp.route("/justificatif/<int:justif_id>/justifies", methods=["GET"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def justif_justifies(justif_id: int = None):
|
||||
"""
|
||||
Liste assiduite_id justifiées par le justificatif
|
||||
"""
|
||||
|
||||
query = Justificatif.query.filter_by(id=justif_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||
|
||||
justificatif_unique: Justificatif = query.first_or_404()
|
||||
|
||||
assiduites_list: list[int] = scass.justifies(justificatif_unique)
|
||||
|
||||
return jsonify(assiduites_list)
|
||||
|
||||
|
||||
# -- Utils --
|
||||
|
||||
|
||||
def _filter_manager(requested, justificatifs_query):
|
||||
"""
|
||||
Retourne les justificatifs entrés filtrés en fonction de la request
|
||||
"""
|
||||
# cas 1 : etat justificatif
|
||||
etat = requested.args.get("etat")
|
||||
if etat is not None:
|
||||
justificatifs_query = scass.filter_justificatifs_by_etat(
|
||||
justificatifs_query, etat
|
||||
)
|
||||
|
||||
# cas 2 : date de début
|
||||
deb = requested.args.get("date_debut", "").replace(" ", "+")
|
||||
deb: datetime = scu.is_iso_formated(deb, True)
|
||||
|
||||
# cas 3 : date de fin
|
||||
fin = requested.args.get("date_fin", "").replace(" ", "+")
|
||||
fin = scu.is_iso_formated(fin, True)
|
||||
|
||||
if (deb, fin) != (None, None):
|
||||
justificatifs_query: Justificatif = scass.filter_by_date(
|
||||
justificatifs_query, Justificatif, deb, fin
|
||||
)
|
||||
|
||||
user_id = requested.args.get("user_id", False)
|
||||
if user_id is not False:
|
||||
justificatif_query: Justificatif = scass.filter_by_user_id(
|
||||
justificatif_query, user_id
|
||||
)
|
||||
|
||||
return justificatifs_query
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -48,6 +48,7 @@ from app.scodoc.sco_permissions import Permission
|
|||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
def api_get_glob_logos():
|
||||
"""Liste tous les logos"""
|
||||
logos = list_logos()[None]
|
||||
return jsonify(list(logos.keys()))
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -12,13 +12,14 @@ from flask_login import login_required
|
|||
|
||||
import app
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.models import FormSemestre, FormSemestreInscription, Identite
|
||||
from app.models import GroupDescr, Partition
|
||||
from app.models import GroupDescr, Partition, Scolog
|
||||
from app.models.groups import group_membership
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
@ -137,7 +138,7 @@ def etud_in_group_query(group_id: int):
|
|||
"""Étudiants du groupe, filtrés par état"""
|
||||
etat = request.args.get("etat")
|
||||
if etat not in {None, scu.INSCRIT, scu.DEMISSION, scu.DEF}:
|
||||
return json_error(404, "etat: valeur invalide")
|
||||
return json_error(API_CLIENT_ERROR, "etat: valeur invalide")
|
||||
query = GroupDescr.query.filter_by(id=group_id)
|
||||
if g.scodoc_dept:
|
||||
query = (
|
||||
|
@ -170,24 +171,15 @@ def set_etud_group(etudid: int, group_id: int):
|
|||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
group = query.first_or_404()
|
||||
if not group.partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if etud.id not in {e.id for e in group.partition.formsemestre.etuds}:
|
||||
return json_error(404, "etud non inscrit au formsemestre du groupe")
|
||||
groups = (
|
||||
GroupDescr.query.filter_by(partition_id=group.partition.id)
|
||||
.join(group_membership)
|
||||
.filter_by(etudid=etudid)
|
||||
|
||||
sco_groups.change_etud_group_in_partition(
|
||||
etudid, group_id, group.partition.to_dict()
|
||||
)
|
||||
ok = False
|
||||
for other_group in groups:
|
||||
if other_group.id == group_id:
|
||||
ok = True
|
||||
else:
|
||||
other_group.etuds.remove(etud)
|
||||
if not ok:
|
||||
group.etuds.append(etud)
|
||||
log(f"set_etud_group({etud}, {group})")
|
||||
db.session.commit()
|
||||
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
|
||||
|
||||
return jsonify({"group_id": group_id, "etudid": etudid})
|
||||
|
||||
|
||||
|
@ -207,9 +199,19 @@ def group_remove_etud(group_id: int, etudid: int):
|
|||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
group = query.first_or_404()
|
||||
if not group.partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if etud in group.etuds:
|
||||
group.etuds.remove(etud)
|
||||
db.session.commit()
|
||||
Scolog.logdb(
|
||||
method="group_remove_etud",
|
||||
etudid=etud.id,
|
||||
msg=f"Retrait du groupe {group.group_name} de {group.partition.partition_name}",
|
||||
commit=True,
|
||||
)
|
||||
# Update parcours
|
||||
group.partition.formsemestre.update_inscriptions_parcours_from_groups()
|
||||
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
|
||||
return jsonify({"group_id": group_id, "etudid": etudid})
|
||||
|
||||
|
@ -232,6 +234,8 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
|||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
partition = query.first_or_404()
|
||||
if not partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
groups = (
|
||||
GroupDescr.query.filter_by(partition_id=partition_id)
|
||||
.join(group_membership)
|
||||
|
@ -239,7 +243,15 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
|||
)
|
||||
for group in groups:
|
||||
group.etuds.remove(etud)
|
||||
Scolog.logdb(
|
||||
method="partition_remove_etud",
|
||||
etudid=etud.id,
|
||||
msg=f"Retrait du groupe {group.group_name} de {group.partition.partition_name}",
|
||||
commit=True,
|
||||
)
|
||||
db.session.commit()
|
||||
# Update parcours
|
||||
group.partition.formsemestre.update_inscriptions_parcours_from_groups()
|
||||
app.set_sco_dept(partition.formsemestre.departement.acronym)
|
||||
sco_cache.invalidate_formsemestre(partition.formsemestre_id)
|
||||
return jsonify({"partition_id": partition_id, "etudid": etudid})
|
||||
|
@ -262,14 +274,16 @@ def group_create(partition_id: int):
|
|||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
partition: Partition = query.first_or_404()
|
||||
if not partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if not partition.groups_editable:
|
||||
return json_error(404, "partition non editable")
|
||||
return json_error(403, "partition non editable")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
group_name = data.get("group_name")
|
||||
if group_name is None:
|
||||
return json_error(404, "missing group name or invalid data format")
|
||||
return json_error(API_CLIENT_ERROR, "missing group name or invalid data format")
|
||||
if not GroupDescr.check_name(partition, group_name):
|
||||
return json_error(404, "invalid group_name")
|
||||
return json_error(API_CLIENT_ERROR, "invalid group_name")
|
||||
group_name = group_name.strip()
|
||||
|
||||
group = GroupDescr(group_name=group_name, partition_id=partition_id)
|
||||
|
@ -294,8 +308,10 @@ def group_delete(group_id: int):
|
|||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
group: GroupDescr = query.first_or_404()
|
||||
if not group.partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if not group.partition.groups_editable:
|
||||
return json_error(404, "partition non editable")
|
||||
return json_error(403, "partition non editable")
|
||||
formsemestre_id = group.partition.formsemestre_id
|
||||
log(f"deleting {group}")
|
||||
db.session.delete(group)
|
||||
|
@ -318,14 +334,16 @@ def group_edit(group_id: int):
|
|||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
group: GroupDescr = query.first_or_404()
|
||||
if not group.partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if not group.partition.groups_editable:
|
||||
return json_error(404, "partition non editable")
|
||||
return json_error(403, "partition non editable")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
group_name = data.get("group_name")
|
||||
if group_name is not None:
|
||||
group_name = group_name.strip()
|
||||
if not GroupDescr.check_name(group.partition, group_name, existing=True):
|
||||
return json_error(404, "invalid group_name")
|
||||
return json_error(API_CLIENT_ERROR, "invalid group_name")
|
||||
group.group_name = group_name
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
|
@ -358,17 +376,23 @@ def partition_create(formsemestre_id: int):
|
|||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||
if not formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
partition_name = data.get("partition_name")
|
||||
if partition_name is None:
|
||||
return json_error(404, "missing partition_name or invalid data format")
|
||||
return json_error(
|
||||
API_CLIENT_ERROR, "missing partition_name or invalid data format"
|
||||
)
|
||||
if partition_name == scu.PARTITION_PARCOURS:
|
||||
return json_error(404, f"invalid partition_name {scu.PARTITION_PARCOURS}")
|
||||
return json_error(
|
||||
API_CLIENT_ERROR, f"invalid partition_name {scu.PARTITION_PARCOURS}"
|
||||
)
|
||||
if not Partition.check_name(formsemestre, partition_name):
|
||||
return json_error(404, "invalid partition_name")
|
||||
return json_error(API_CLIENT_ERROR, "invalid partition_name")
|
||||
numero = data.get("numero", 0)
|
||||
if not isinstance(numero, int):
|
||||
return json_error(404, "invalid type for numero")
|
||||
return json_error(API_CLIENT_ERROR, "invalid type for numero")
|
||||
args = {
|
||||
"formsemestre_id": formsemestre_id,
|
||||
"partition_name": partition_name.strip(),
|
||||
|
@ -379,7 +403,7 @@ def partition_create(formsemestre_id: int):
|
|||
boolean_field, False if boolean_field != "groups_editable" else True
|
||||
)
|
||||
if not isinstance(value, bool):
|
||||
return json_error(404, f"invalid type for {boolean_field}")
|
||||
return json_error(API_CLIENT_ERROR, f"invalid type for {boolean_field}")
|
||||
args[boolean_field] = value
|
||||
|
||||
partition = Partition(**args)
|
||||
|
@ -406,12 +430,14 @@ def formsemestre_order_partitions(formsemestre_id: int):
|
|||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||
if not formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
partition_ids = request.get_json(force=True) # may raise 400 Bad Request
|
||||
if not isinstance(partition_ids, int) and not all(
|
||||
isinstance(x, int) for x in partition_ids
|
||||
):
|
||||
return json_error(
|
||||
404,
|
||||
API_CLIENT_ERROR,
|
||||
message="paramètre liste des partitions invalide",
|
||||
)
|
||||
for p_id, numero in zip(partition_ids, range(len(partition_ids))):
|
||||
|
@ -443,12 +469,14 @@ def partition_order_groups(partition_id: int):
|
|||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
partition: Partition = query.first_or_404()
|
||||
if not partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
group_ids = request.get_json(force=True) # may raise 400 Bad Request
|
||||
if not isinstance(group_ids, int) and not all(
|
||||
isinstance(x, int) for x in group_ids
|
||||
):
|
||||
return json_error(
|
||||
404,
|
||||
API_CLIENT_ERROR,
|
||||
message="paramètre liste de groupe invalide",
|
||||
)
|
||||
for group_id, numero in zip(group_ids, range(len(group_ids))):
|
||||
|
@ -484,24 +512,28 @@ def partition_edit(partition_id: int):
|
|||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
partition: Partition = query.first_or_404()
|
||||
if not partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
modified = False
|
||||
partition_name = data.get("partition_name")
|
||||
#
|
||||
if partition_name is not None and partition_name != partition.partition_name:
|
||||
if partition.is_parcours():
|
||||
return json_error(404, f"can't rename {scu.PARTITION_PARCOURS}")
|
||||
return json_error(
|
||||
API_CLIENT_ERROR, f"can't rename {scu.PARTITION_PARCOURS}"
|
||||
)
|
||||
if not Partition.check_name(
|
||||
partition.formsemestre, partition_name, existing=True
|
||||
):
|
||||
return json_error(404, "invalid partition_name")
|
||||
return json_error(API_CLIENT_ERROR, "invalid partition_name")
|
||||
partition.partition_name = partition_name.strip()
|
||||
modified = True
|
||||
|
||||
numero = data.get("numero")
|
||||
if numero is not None and numero != partition.numero:
|
||||
if not isinstance(numero, int):
|
||||
return json_error(404, "invalid type for numero")
|
||||
return json_error(API_CLIENT_ERROR, "invalid type for numero")
|
||||
partition.numero = numero
|
||||
modified = True
|
||||
|
||||
|
@ -509,9 +541,11 @@ def partition_edit(partition_id: int):
|
|||
value = data.get(boolean_field)
|
||||
if value is not None and value != getattr(partition, boolean_field):
|
||||
if not isinstance(value, bool):
|
||||
return json_error(404, f"invalid type for {boolean_field}")
|
||||
return json_error(API_CLIENT_ERROR, f"invalid type for {boolean_field}")
|
||||
if boolean_field == "groups_editable" and partition.is_parcours():
|
||||
return json_error(404, f"can't change {scu.PARTITION_PARCOURS}")
|
||||
return json_error(
|
||||
API_CLIENT_ERROR, f"can't change {scu.PARTITION_PARCOURS}"
|
||||
)
|
||||
setattr(partition, boolean_field, value)
|
||||
modified = True
|
||||
|
||||
|
@ -542,8 +576,12 @@ def partition_delete(partition_id: int):
|
|||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
partition: Partition = query.first_or_404()
|
||||
if not partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if not partition.partition_name:
|
||||
return json_error(404, "ne peut pas supprimer la partition par défaut")
|
||||
return json_error(
|
||||
API_CLIENT_ERROR, "ne peut pas supprimer la partition par défaut"
|
||||
)
|
||||
is_parcours = partition.is_parcours()
|
||||
formsemestre: FormSemestre = partition.formsemestre
|
||||
log(f"deleting partition {partition}")
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : outils
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -12,8 +12,8 @@
|
|||
from flask import g, jsonify, request
|
||||
from flask_login import current_user, login_required
|
||||
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app import db
|
||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.auth.models import User, Role, UserRole
|
||||
from app.auth.models import is_valid_password
|
||||
|
@ -187,7 +187,7 @@ def user_password(uid: int):
|
|||
if not password:
|
||||
return json_error(404, "user_password: missing password")
|
||||
if not is_valid_password(password):
|
||||
return json_error(400, "user_password: invalid password")
|
||||
return json_error(API_CLIENT_ERROR, "user_password: invalid password")
|
||||
allowed_depts = current_user.get_depts_with_permission(Permission.ScoUsersAdmin)
|
||||
if (None not in allowed_depts) and ((user.dept not in allowed_depts)):
|
||||
return json_error(403, "user_password: departement non autorise")
|
||||
|
|
|
@ -0,0 +1,251 @@
|
|||
# -*- coding: UTF-8 -*
|
||||
"""
|
||||
auth.cas.py
|
||||
"""
|
||||
import datetime
|
||||
|
||||
import flask
|
||||
from flask import current_app, flash, url_for
|
||||
from flask_login import current_user, login_user
|
||||
|
||||
from app import db
|
||||
from app.auth import bp
|
||||
from app.auth.models import User
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.scodoc import sco_excel
|
||||
from app.scodoc.sco_exceptions import ScoValueError, AccessDenied
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
# after_cas_login/after_cas_logout : routes appelées par redirect depuis le serveur CAS.
|
||||
|
||||
|
||||
@bp.route("/after_cas_login")
|
||||
def after_cas_login():
|
||||
"Called by CAS after CAS authentication"
|
||||
# Ici on a les infos dans flask.session["CAS_ATTRIBUTES"]
|
||||
if ScoDocSiteConfig.is_cas_enabled() and ("CAS_ATTRIBUTES" in flask.session):
|
||||
# Lookup user:
|
||||
cas_id = flask.session["CAS_ATTRIBUTES"].get(
|
||||
"cas:" + ScoDocSiteConfig.get("cas_attribute_id"),
|
||||
flask.session.get("CAS_USERNAME"),
|
||||
)
|
||||
if cas_id is not None:
|
||||
user: User = User.query.filter_by(cas_id=cas_id).first()
|
||||
if user and user.active:
|
||||
if user.cas_allow_login:
|
||||
current_app.logger.info(f"CAS: login {user.user_name}")
|
||||
if login_user(user):
|
||||
flask.session[
|
||||
"scodoc_cas_login_date"
|
||||
] = datetime.datetime.now().isoformat()
|
||||
user.cas_last_login = datetime.datetime.utcnow()
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return flask.redirect(url_for("scodoc.index"))
|
||||
else:
|
||||
current_app.logger.info(
|
||||
f"CAS login denied for {user.user_name} (not allowed to use CAS)"
|
||||
)
|
||||
else:
|
||||
current_app.logger.info(
|
||||
f"""CAS login denied for {
|
||||
user.user_name if user else ""
|
||||
} cas_id={cas_id} (unknown or inactive)"""
|
||||
)
|
||||
else:
|
||||
current_app.logger.info(
|
||||
f"""CAS attribute '{ScoDocSiteConfig.get("cas_attribute_id")}' not found !
|
||||
(check your ScoDoc config)"""
|
||||
)
|
||||
|
||||
# Echec:
|
||||
flash("échec de l'authentification")
|
||||
return flask.redirect(url_for("auth.login"))
|
||||
|
||||
|
||||
@bp.route("/after_cas_logout")
|
||||
def after_cas_logout():
|
||||
"Called by CAS after CAS logout"
|
||||
flash("Vous êtes déconnecté")
|
||||
current_app.logger.info("after_cas_logout")
|
||||
return flask.redirect(url_for("scodoc.index"))
|
||||
|
||||
|
||||
def cas_error_callback(message):
|
||||
"Called by CAS when an error occurs, with a message"
|
||||
raise ScoValueError(f"Erreur authentification CAS: {message}")
|
||||
|
||||
|
||||
def set_cas_configuration(app: flask.app.Flask = None):
|
||||
"""Force la configuration du module flask_cas à partir des paramètres de
|
||||
la config de ScoDoc.
|
||||
Appelé au démarrage et à chaque modif des paramètres.
|
||||
"""
|
||||
app = app or current_app
|
||||
if ScoDocSiteConfig.is_cas_enabled():
|
||||
current_app.logger.debug("CAS: set_cas_configuration")
|
||||
app.config["CAS_SERVER"] = ScoDocSiteConfig.get("cas_server")
|
||||
app.config["CAS_LOGIN_ROUTE"] = ScoDocSiteConfig.get("cas_login_route", "/cas")
|
||||
app.config["CAS_LOGOUT_ROUTE"] = ScoDocSiteConfig.get(
|
||||
"cas_logout_route", "/cas/logout"
|
||||
)
|
||||
app.config["CAS_VALIDATE_ROUTE"] = ScoDocSiteConfig.get(
|
||||
"cas_validate_route", "/cas/serviceValidate"
|
||||
)
|
||||
app.config["CAS_AFTER_LOGIN"] = "auth.after_cas_login"
|
||||
app.config["CAS_AFTER_LOGOUT"] = "auth.after_cas_logout"
|
||||
app.config["CAS_ERROR_CALLBACK"] = cas_error_callback
|
||||
app.config["CAS_SSL_VERIFY"] = ScoDocSiteConfig.get("cas_ssl_verify")
|
||||
app.config["CAS_SSL_CERTIFICATE"] = ScoDocSiteConfig.get("cas_ssl_certificate")
|
||||
else:
|
||||
app.config.pop("CAS_SERVER", None)
|
||||
app.config.pop("CAS_AFTER_LOGIN", None)
|
||||
app.config.pop("CAS_AFTER_LOGOUT", None)
|
||||
app.config.pop("CAS_SSL_VERIFY", None)
|
||||
app.config.pop("CAS_SSL_CERTIFICATE", None)
|
||||
|
||||
|
||||
CAS_USER_INFO_IDS = (
|
||||
"user_name",
|
||||
"nom",
|
||||
"prenom",
|
||||
"email",
|
||||
"roles_string",
|
||||
"active",
|
||||
"dept",
|
||||
"cas_id",
|
||||
"cas_allow_login",
|
||||
"cas_allow_scodoc_login",
|
||||
"email_institutionnel",
|
||||
)
|
||||
CAS_USER_INFO_COMMENTS = (
|
||||
"""user_name:
|
||||
L'identifiant (login).
|
||||
""",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Pour info: 0 si compte inactif",
|
||||
"""Pour info: roles:
|
||||
chaînes séparées par _:
|
||||
1. Le rôle (Ens, Secr ou Admin)
|
||||
2. Le département (en majuscule)
|
||||
""",
|
||||
"""dept:
|
||||
Le département d'appartenance de l'utilisateur. Vide si l'utilisateur
|
||||
intervient dans plusieurs départements.
|
||||
""",
|
||||
"""cas_id:
|
||||
identifiant de l'utilisateur sur CAS (requis pour CAS).
|
||||
""",
|
||||
"""cas_allow_login:
|
||||
autorise la connexion via CAS (optionnel, faux par défaut)
|
||||
""",
|
||||
"""cas_allow_scodoc_login
|
||||
autorise connexion via ScoDoc même si CAS obligatoire (optionnel, faux par défaut)
|
||||
""",
|
||||
"""email_institutionnel
|
||||
optionnel, le mail officiel de l'utilisateur.
|
||||
Maximum 120 caractères.""",
|
||||
)
|
||||
|
||||
|
||||
def cas_users_generate_excel_sample() -> bytes:
|
||||
"""generate an excel document suitable to import users CAS information"""
|
||||
style = sco_excel.excel_make_style(bold=True)
|
||||
titles = CAS_USER_INFO_IDS
|
||||
titles_styles = [style] * len(titles)
|
||||
# Extrait tous les utilisateurs (tous dept et statuts)
|
||||
rows = []
|
||||
for user in User.query.order_by(User.user_name):
|
||||
u_dict = user.to_dict()
|
||||
rows.append([u_dict.get(k) for k in CAS_USER_INFO_IDS])
|
||||
return sco_excel.excel_simple_table(
|
||||
lines=rows,
|
||||
titles=titles,
|
||||
titles_styles=titles_styles,
|
||||
sheet_name="Utilisateurs ScoDoc",
|
||||
comments=CAS_USER_INFO_COMMENTS,
|
||||
)
|
||||
|
||||
|
||||
def cas_users_import_excel_file(datafile) -> int:
|
||||
"""
|
||||
Import users CAS configuration from Excel file.
|
||||
May change cas_id, cas_allow_login, cas_allow_scodoc_login
|
||||
and active.
|
||||
:param datafile: stream to be imported
|
||||
:return: nb de comptes utilisateurs modifiés
|
||||
"""
|
||||
from app.scodoc import sco_import_users
|
||||
|
||||
if not current_user.is_administrator():
|
||||
raise AccessDenied(f"invalid user ({current_user}) must be SuperAdmin")
|
||||
current_app.logger.info("cas_users_import_excel_file by {current_user}")
|
||||
|
||||
users_infos = sco_import_users.read_users_excel_file(
|
||||
datafile, titles=CAS_USER_INFO_IDS
|
||||
)
|
||||
|
||||
return cas_users_import_data(users_infos=users_infos)
|
||||
|
||||
|
||||
def cas_users_import_data(users_infos: list[dict]) -> int:
|
||||
"""Import informations configuration CAS
|
||||
users est une liste de dict, on utilise seulement les champs:
|
||||
- user_name : la clé, l'utilisateur DOIT déjà exister
|
||||
- cas_id : l'ID CAS a enregistrer.
|
||||
- cas_allow_login
|
||||
- cas_allow_scodoc_login
|
||||
Les éventuels autres champs sont ignorés.
|
||||
|
||||
Return: nb de comptes modifiés.
|
||||
"""
|
||||
nb_modif = 0
|
||||
users = []
|
||||
for info in users_infos:
|
||||
user: User = User.query.filter_by(user_name=info["user_name"]).first()
|
||||
if not user:
|
||||
db.session.rollback() # au cas où auto-flush
|
||||
raise ScoValueError(f"""Utilisateur '{info["user_name"]}' inexistant""")
|
||||
modif = False
|
||||
new_cas_id = info["cas_id"].strip()
|
||||
if new_cas_id != (user.cas_id or ""):
|
||||
# check unicity
|
||||
other = User.query.filter_by(cas_id=new_cas_id).first()
|
||||
if other and other.id != user.id:
|
||||
db.session.rollback() # au cas où auto-flush
|
||||
raise ScoValueError(f"cas_id {new_cas_id} dupliqué")
|
||||
user.cas_id = info["cas_id"].strip() or None
|
||||
modif = True
|
||||
val = scu.to_bool(info["cas_allow_login"])
|
||||
if val != user.cas_allow_login:
|
||||
user.cas_allow_login = val
|
||||
modif = True
|
||||
val = scu.to_bool(info["cas_allow_scodoc_login"])
|
||||
if val != user.cas_allow_scodoc_login:
|
||||
user.cas_allow_scodoc_login = val
|
||||
modif = True
|
||||
val = scu.to_bool(info["active"])
|
||||
if val != (user.active or False):
|
||||
user.active = val
|
||||
modif = True
|
||||
if modif:
|
||||
nb_modif += 1
|
||||
# Record modifications
|
||||
for user in users:
|
||||
try:
|
||||
db.session.add(user)
|
||||
except Exception as exc:
|
||||
db.session.rollback()
|
||||
raise ScoValueError(
|
||||
"Erreur (1) durant l'importation des modifications"
|
||||
) from exc
|
||||
try:
|
||||
db.session.commit()
|
||||
except Exception as exc:
|
||||
db.session.rollback()
|
||||
raise ScoValueError(
|
||||
"Erreur (2) durant l'importation des modifications"
|
||||
) from exc
|
||||
return nb_modif
|
|
@ -11,5 +11,5 @@ def send_password_reset_email(user):
|
|||
sender=current_app.config["SCODOC_MAIL_FROM"],
|
||||
recipients=[user.email],
|
||||
text_body=render_template("email/reset_password.txt", user=user, token=token),
|
||||
html_body=render_template("email/reset_password.html", user=user, token=token),
|
||||
html_body=render_template("email/reset_password.j2", user=user, token=token),
|
||||
)
|
||||
|
|
|
@ -42,7 +42,7 @@ def login():
|
|||
return form.redirect("scodoc.index")
|
||||
message = request.args.get("message", "")
|
||||
return render_template(
|
||||
"auth/login.html", title=_("Sign In"), form=form, message=message
|
||||
"auth/login.j2", title=_("Sign In"), form=form, message=message
|
||||
)
|
||||
|
||||
|
||||
|
@ -63,11 +63,9 @@ def create_user():
|
|||
user.set_password(form.password.data)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
flash("User {} created".format(user.user_name))
|
||||
flash(f"Utilisateur {user.user_name} créé")
|
||||
return redirect(url_for("scodoc.index"))
|
||||
return render_template(
|
||||
"auth/register.html", title="Création utilisateur", form=form
|
||||
)
|
||||
return render_template("auth/register.j2", title="Création utilisateur", form=form)
|
||||
|
||||
|
||||
@bp.route("/reset_password_request", methods=["GET", "POST"])
|
||||
|
@ -98,13 +96,13 @@ def reset_password_request():
|
|||
)
|
||||
return redirect(url_for("auth.login"))
|
||||
return render_template(
|
||||
"auth/reset_password_request.html", title=_("Reset Password"), form=form
|
||||
"auth/reset_password_request.j2", title=_("Reset Password"), form=form
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/reset_password/<token>", methods=["GET", "POST"])
|
||||
def reset_password(token):
|
||||
"Reset passord après demande par mail"
|
||||
"Reset password après demande par mail"
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for("scodoc.index"))
|
||||
user: User = User.verify_reset_password_token(token)
|
||||
|
@ -116,7 +114,7 @@ def reset_password(token):
|
|||
db.session.commit()
|
||||
flash(_("Votre mot de passe a été changé."))
|
||||
return redirect(url_for("auth.login"))
|
||||
return render_template("auth/reset_password.html", form=form, user=user)
|
||||
return render_template("auth/reset_password.j2", form=form, user=user)
|
||||
|
||||
|
||||
@bp.route("/reset_standard_roles_permissions", methods=["GET", "POST"])
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -8,15 +8,15 @@
|
|||
Edition associations UE <-> Ref. Compétence
|
||||
"""
|
||||
from flask import g, url_for
|
||||
from app import db, log
|
||||
from app.models import Formation, UniteEns
|
||||
from app.models.but_refcomp import ApcNiveau
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.models import ApcReferentielCompetences, Formation, UniteEns
|
||||
from app.scodoc import codes_cursus
|
||||
|
||||
|
||||
def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
|
||||
"""Form. HTML pour associer une UE à un niveau de compétence"""
|
||||
if ue.type != sco_codes_parcours.UE_STANDARD:
|
||||
def form_ue_choix_niveau(ue: UniteEns) -> str:
|
||||
"""Form. HTML pour associer une UE à un niveau de compétence.
|
||||
Le menu select lui meême est vide et rempli en JS par appel à get_ue_niveaux_options_html
|
||||
"""
|
||||
if ue.type != codes_cursus.UE_STANDARD:
|
||||
return ""
|
||||
ref_comp = ue.formation.referentiel_competence
|
||||
if ref_comp is None:
|
||||
|
@ -27,11 +27,70 @@ def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
|
|||
}">associer un référentiel de compétence</a>
|
||||
</div>
|
||||
</div>"""
|
||||
annee = 1 if ue.semestre_idx is None else (ue.semestre_idx + 1) // 2 # 1, 2, 3
|
||||
niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(annee)
|
||||
# Les parcours:
|
||||
parcours_options = []
|
||||
for parcour in ref_comp.parcours:
|
||||
parcours_options.append(
|
||||
f"""<option value="{parcour.id}" {
|
||||
'selected' if ue.parcour == parcour else ''}
|
||||
>{parcour.libelle} ({parcour.code})
|
||||
</option>"""
|
||||
)
|
||||
|
||||
newline = "\n"
|
||||
return f"""
|
||||
<div class="ue_choix_niveau">
|
||||
<form class="form_ue_choix_niveau">
|
||||
<div class="cont_ue_choix_niveau">
|
||||
<div>
|
||||
<b>Parcours :</b>
|
||||
<select class="select_parcour"
|
||||
onchange="set_ue_parcour(this);"
|
||||
data-ue_id="{ue.id}"
|
||||
data-setter="{
|
||||
url_for( "notes.set_ue_parcours", scodoc_dept=g.scodoc_dept)
|
||||
}">
|
||||
<option value="" {
|
||||
'selected' if ue.parcour is None else ''
|
||||
}>Tous</option>
|
||||
{newline.join(parcours_options)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<b>Niveau de compétence :</b>
|
||||
<select class="select_niveau_ue"
|
||||
onchange="set_ue_niveau_competence(this);"
|
||||
data-ue_id="{ue.id}"
|
||||
data-setter="{
|
||||
url_for( "notes.set_ue_niveau_competence", scodoc_dept=g.scodoc_dept)
|
||||
}">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def get_ue_niveaux_options_html(ue: UniteEns) -> str:
|
||||
"""fragment html avec les options du menu de sélection du
|
||||
niveau de compétences associé à une UE.
|
||||
|
||||
Si l'UE n'a pas de parcours associé: présente les niveaux
|
||||
de tous les parcours.
|
||||
Si l'UE a un parcours: seulement les niveaux de ce parcours.
|
||||
"""
|
||||
ref_comp: ApcReferentielCompetences = ue.formation.referentiel_competence
|
||||
if ref_comp is None:
|
||||
return ""
|
||||
# Les niveaux:
|
||||
annee = ue.annee() # 1, 2, 3
|
||||
parcours, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(
|
||||
annee, parcour=ue.parcour
|
||||
)
|
||||
|
||||
# Les niveaux déjà associés à d'autres UE du même semestre
|
||||
autres_ues = formation.ues.filter_by(semestre_idx=ue.semestre_idx)
|
||||
autres_ues = ue.formation.ues.filter_by(semestre_idx=ue.semestre_idx)
|
||||
niveaux_autres_ues = {
|
||||
oue.niveau_competence_id for oue in autres_ues if oue.id != ue.id
|
||||
}
|
||||
|
@ -39,18 +98,14 @@ def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
|
|||
if niveaux_by_parcours["TC"]: # TC pour Tronc Commun
|
||||
options.append("""<optgroup label="Tronc commun">""")
|
||||
for n in niveaux_by_parcours["TC"]:
|
||||
if n.id in niveaux_autres_ues:
|
||||
disabled = "disabled"
|
||||
else:
|
||||
disabled = ""
|
||||
options.append(
|
||||
f"""<option value="{n.id}" {'selected'
|
||||
if ue.niveau_competence == n else ''}
|
||||
{disabled}>{n.annee} {n.competence.titre_long}
|
||||
f"""<option value="{n.id}" {
|
||||
'selected' if ue.niveau_competence == n else ''}
|
||||
>{n.annee} {n.competence.titre_long}
|
||||
niveau {n.ordre}</option>"""
|
||||
)
|
||||
options.append("""</optgroup>""")
|
||||
for parcour in ref_comp.parcours:
|
||||
for parcour in parcours:
|
||||
if len(niveaux_by_parcours[parcour.id]):
|
||||
options.append(f"""<optgroup label="Parcours {parcour.libelle}">""")
|
||||
for n in niveaux_by_parcours[parcour.id]:
|
||||
|
@ -65,46 +120,7 @@ def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
|
|||
niveau {n.ordre}</option>"""
|
||||
)
|
||||
options.append("""</optgroup>""")
|
||||
options_str = "\n".join(options)
|
||||
return f"""
|
||||
<div class="ue_choix_niveau">
|
||||
<form class="form_ue_choix_niveau">
|
||||
<b>Niveau de compétence associé:</b>
|
||||
<select onchange="set_ue_niveau_competence(this);"
|
||||
data-ue_id="{ue.id}"
|
||||
data-setter="{
|
||||
url_for( "notes.set_ue_niveau_competence", scodoc_dept=g.scodoc_dept)
|
||||
}">
|
||||
<option value="" {'selected' if ue.niveau_competence is None else ''}>aucun</option>
|
||||
{options_str}
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def set_ue_niveau_competence(ue_id: int, niveau_id: int):
|
||||
"""Associe le niveau et l'UE"""
|
||||
ue = UniteEns.query.get_or_404(ue_id)
|
||||
|
||||
autres_ues = ue.formation.ues.filter_by(semestre_idx=ue.semestre_idx)
|
||||
niveaux_autres_ues = {
|
||||
oue.niveau_competence_id for oue in autres_ues if oue.id != ue.id
|
||||
}
|
||||
if niveau_id in niveaux_autres_ues:
|
||||
log(
|
||||
f"set_ue_niveau_competence: denying association of {ue} to already associated {niveau_id}"
|
||||
)
|
||||
return "", 409 # conflict
|
||||
if niveau_id == "":
|
||||
niveau = ""
|
||||
# suppression de l'association
|
||||
ue.niveau_competence = None
|
||||
else:
|
||||
niveau = ApcNiveau.query.get_or_404(niveau_id)
|
||||
ue.niveau_competence = niveau
|
||||
db.session.add(ue)
|
||||
db.session.commit()
|
||||
log(f"set_ue_niveau_competence( {ue}, {niveau} )")
|
||||
|
||||
return "", 204
|
||||
return (
|
||||
f"""<option value="" {'selected' if ue.niveau_competence is None else ''}>aucun</option>"""
|
||||
+ "\n".join(options)
|
||||
)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -19,10 +19,10 @@ from app.models.ues import UniteEns
|
|||
from app.scodoc import sco_bulletins, sco_utils as scu
|
||||
from app.scodoc import sco_bulletins_json
|
||||
from app.scodoc import sco_bulletins_pdf
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
|
||||
from app.scodoc.codes_cursus import UE_SPORT, DEF
|
||||
from app.scodoc.sco_utils import fmt_note
|
||||
|
||||
|
||||
|
@ -80,6 +80,9 @@ class BulletinBUT:
|
|||
"""
|
||||
res = self.res
|
||||
|
||||
if (etud.id, ue.id) in self.res.dispense_ues:
|
||||
return {}
|
||||
|
||||
if ue.type == UE_SPORT:
|
||||
modimpls_spo = [
|
||||
modimpl
|
||||
|
@ -154,7 +157,7 @@ class BulletinBUT:
|
|||
for _, ue_capitalisee in self.res.validations.ue_capitalisees.loc[
|
||||
[etud.id]
|
||||
].iterrows():
|
||||
if sco_codes_parcours.code_ue_validant(ue_capitalisee.code):
|
||||
if codes_cursus.code_ue_validant(ue_capitalisee.code):
|
||||
ue = UniteEns.query.get(ue_capitalisee.ue_id) # XXX cacher ?
|
||||
# déjà capitalisé ? montre la meilleure
|
||||
if ue.acronyme in d:
|
||||
|
@ -239,6 +242,7 @@ class BulletinBUT:
|
|||
self.etud_eval_results(etud, e)
|
||||
for e in modimpl.evaluations
|
||||
if (e.visibulletin or version == "long")
|
||||
and (e.id in modimpl_results.evaluations_etat)
|
||||
and (
|
||||
modimpl_results.evaluations_etat[e.id].is_complete
|
||||
or self.prefs["bul_show_all_evals"]
|
||||
|
@ -256,10 +260,11 @@ class BulletinBUT:
|
|||
notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
|
||||
modimpls_evals_poids = self.res.modimpls_evals_poids[e.moduleimpl_id]
|
||||
try:
|
||||
etud_ues_ids = self.res.etud_ues_ids(etud.id)
|
||||
poids = {
|
||||
ue.acronyme: modimpls_evals_poids[ue.id][e.id]
|
||||
for ue in self.res.ues
|
||||
if ue.type != UE_SPORT
|
||||
if (ue.type != UE_SPORT) and (ue.id in etud_ues_ids)
|
||||
}
|
||||
except KeyError:
|
||||
poids = collections.defaultdict(lambda: 0.0)
|
||||
|
@ -356,7 +361,7 @@ class BulletinBUT:
|
|||
"formsemestre_id": formsemestre.id,
|
||||
"etat_inscription": etat_inscription,
|
||||
"options": sco_preferences.bulletin_option_affichage(
|
||||
formsemestre.id, self.prefs
|
||||
formsemestre, self.prefs
|
||||
),
|
||||
}
|
||||
if not published:
|
||||
|
@ -380,7 +385,7 @@ class BulletinBUT:
|
|||
"injustifie": nbabs - nbabsjust,
|
||||
"total": nbabs,
|
||||
}
|
||||
decisions_ues = self.res.get_etud_decision_ues(etud.id) or {}
|
||||
decisions_ues = self.res.get_etud_decisions_ue(etud.id) or {}
|
||||
if self.prefs["bul_show_ects"]:
|
||||
ects_tot = res.etud_ects_tot_sem(etud.id)
|
||||
ects_acquis = res.get_etud_ects_valides(etud.id, decisions_ues)
|
||||
|
@ -460,6 +465,7 @@ class BulletinBUT:
|
|||
"ressources": {},
|
||||
"saes": {},
|
||||
"ues": {},
|
||||
"ues_capitalisees": {},
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -12,7 +12,7 @@ from reportlab.platypus import Paragraph, Spacer
|
|||
|
||||
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
|
||||
from app.scodoc import gen_tables
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -43,13 +43,13 @@ from app.but import bulletin_but
|
|||
from app.models import FormSemestre, Identite
|
||||
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 codes_cursus
|
||||
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
|
||||
from app.scodoc.sco_xml import quote_xml_attr
|
||||
|
||||
|
||||
def bulletin_but_xml_compat(
|
||||
|
@ -108,13 +108,13 @@ def bulletin_but_xml_compat(
|
|||
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 ""),
|
||||
nom=quote_xml_attr(etud.nom),
|
||||
prenom=quote_xml_attr(etud.prenom),
|
||||
civilite=quote_xml_attr(etud.civilite_str),
|
||||
sexe=quote_xml_attr(etud.civilite_str), # compat
|
||||
photo_url=quote_xml_attr(sco_photos.get_etud_photo_url(etud.id)),
|
||||
email=quote_xml_attr(etud.get_first_email() or ""),
|
||||
emailperso=quote_xml_attr(etud.get_first_email("emailperso") or ""),
|
||||
)
|
||||
)
|
||||
# Disponible pour publication ?
|
||||
|
@ -153,13 +153,13 @@ def bulletin_but_xml_compat(
|
|||
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 ""),
|
||||
numero=quote_xml_attr(ue.numero),
|
||||
acronyme=quote_xml_attr(ue.acronyme or ""),
|
||||
titre=quote_xml_attr(ue.titre or ""),
|
||||
code_apogee=quote_xml_attr(ue.code_apogee or ""),
|
||||
)
|
||||
doc.append(x_ue)
|
||||
if ue.type != sco_codes_parcours.UE_SPORT:
|
||||
if ue.type != codes_cursus.UE_SPORT:
|
||||
v = results.etud_moy_ue[ue.id][etud.id]
|
||||
vmin = results.etud_moy_ue[ue.id].min()
|
||||
vmax = results.etud_moy_ue[ue.id].max()
|
||||
|
@ -192,11 +192,9 @@ def bulletin_but_xml_compat(
|
|||
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 ""
|
||||
),
|
||||
titre=quote_xml_attr(modimpl.module.titre or ""),
|
||||
abbrev=quote_xml_attr(modimpl.module.abbrev or ""),
|
||||
code_apogee=quote_xml_attr(modimpl.module.code_apogee or ""),
|
||||
)
|
||||
# XXX TODO rangs et effectifs
|
||||
# --- notes de chaque eval:
|
||||
|
@ -215,7 +213,7 @@ def bulletin_but_xml_compat(
|
|||
coefficient=str(e.coefficient),
|
||||
# pas les poids en XML compat
|
||||
evaluation_type=str(e.evaluation_type),
|
||||
description=scu.quote_xml_attr(e.description),
|
||||
description=quote_xml_attr(e.description),
|
||||
# notes envoyées sur 20, ceci juste pour garder trace:
|
||||
note_max_origin=str(e.note_max),
|
||||
)
|
||||
|
@ -262,7 +260,7 @@ def bulletin_but_xml_compat(
|
|||
),
|
||||
)
|
||||
x_situation = Element("situation")
|
||||
x_situation.text = scu.quote_xml_attr(infos["situation"])
|
||||
x_situation.text = quote_xml_attr(infos["situation"])
|
||||
doc.append(x_situation)
|
||||
if dpv:
|
||||
decision = dpv["decisions"][0]
|
||||
|
@ -297,9 +295,9 @@ def bulletin_but_xml_compat(
|
|||
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"]),
|
||||
numero=quote_xml_attr(ue["numero"]),
|
||||
acronyme=quote_xml_attr(ue["acronyme"]),
|
||||
titre=quote_xml_attr(ue["titre"]),
|
||||
code=decision["decisions_ue"][ue_id]["code"],
|
||||
)
|
||||
)
|
||||
|
@ -322,7 +320,7 @@ def bulletin_but_xml_compat(
|
|||
"appreciation",
|
||||
date=ndb.DateDMYtoISO(appr["date"]),
|
||||
)
|
||||
x_appr.text = scu.quote_xml_attr(appr["comment"])
|
||||
x_appr.text = quote_xml_attr(appr["comment"])
|
||||
doc.append(x_appr)
|
||||
|
||||
if is_appending:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -13,7 +13,7 @@ Classe raccordant avec ScoDoc 7:
|
|||
avec la même interface.
|
||||
|
||||
"""
|
||||
|
||||
import collections
|
||||
from typing import Union
|
||||
|
||||
from flask import g, url_for
|
||||
|
@ -44,15 +44,17 @@ from app.models.formations import Formation
|
|||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.validations import ScolarFormSemestreValidation
|
||||
from app.scodoc import sco_codes_parcours as sco_codes
|
||||
from app.scodoc.sco_codes_parcours import RED, UE_STANDARD
|
||||
from app.scodoc import codes_cursus as sco_codes
|
||||
from app.scodoc.codes_cursus import RED, UE_STANDARD
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoException, ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||
|
||||
from app.scodoc import sco_cursus_dut
|
||||
|
||||
|
||||
class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
||||
"""Pour compat ScoDoc 7: à revoir pour le BUT"""
|
||||
|
||||
def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT):
|
||||
super().__init__(etud, formsemestre_id, res)
|
||||
# Ajustements pour le BUT
|
||||
|
@ -65,3 +67,140 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
|||
def parcours_validated(self):
|
||||
"True si le parcours est validé"
|
||||
return False # XXX TODO
|
||||
|
||||
|
||||
class EtudCursusBUT:
|
||||
"""L'état de l'étudiant dans son cursus BUT
|
||||
Liste des niveaux validés/à valider
|
||||
"""
|
||||
|
||||
def __init__(self, etud: Identite, formation: Formation):
|
||||
"""formation indique la spécialité préparée"""
|
||||
# Vérifie que l'étudiant est bien inscrit à un sem. de cette formation
|
||||
if formation.id not in (
|
||||
ins.formsemestre.formation.id for ins in etud.formsemestre_inscriptions
|
||||
):
|
||||
raise ScoValueError(
|
||||
f"{etud.nomprenom} non inscrit dans {formation.titre} v{formation.version}"
|
||||
)
|
||||
if not formation.referentiel_competence:
|
||||
raise ScoNoReferentielCompetences(formation=formation)
|
||||
#
|
||||
self.etud = etud
|
||||
self.formation = formation
|
||||
self.inscriptions = sorted(
|
||||
[
|
||||
ins
|
||||
for ins in etud.formsemestre_inscriptions
|
||||
if ins.formsemestre.formation.referentiel_competence
|
||||
and (
|
||||
ins.formsemestre.formation.referentiel_competence.id
|
||||
== formation.referentiel_competence.id
|
||||
)
|
||||
],
|
||||
key=lambda s: (s.formsemestre.semestre_id, s.formsemestre.date_debut),
|
||||
)
|
||||
"Liste des inscriptions aux sem. de la formation, triées par indice et chronologie"
|
||||
self.parcour: ApcParcours = self.inscriptions[-1].parcour
|
||||
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
|
||||
self.niveaux_by_annee = {}
|
||||
"{ annee : liste des niveaux à valider }"
|
||||
self.niveaux: dict[int, ApcNiveau] = {}
|
||||
"cache les niveaux"
|
||||
for annee in (1, 2, 3):
|
||||
niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
|
||||
annee, self.parcour
|
||||
)[1]
|
||||
# groupe les niveaux de tronc commun et ceux spécifiques au parcour
|
||||
self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
|
||||
niveaux_d[self.parcour.id] if self.parcour else []
|
||||
)
|
||||
self.niveaux.update(
|
||||
{niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
|
||||
)
|
||||
# Probablement inutile:
|
||||
# # Cherche les validations de jury enregistrées pour chaque niveau
|
||||
# self.validations_by_niveau = collections.defaultdict(lambda: [])
|
||||
# " { niveau_id : [ ApcValidationRCUE ] }"
|
||||
# for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
# self.validations_by_niveau[validation_rcue.niveau().id].append(
|
||||
# validation_rcue
|
||||
# )
|
||||
# self.validation_by_niveau = {
|
||||
# niveau_id: sorted(
|
||||
# validations, key=lambda v: sco_codes.BUT_CODES_ORDERED[v.code]
|
||||
# )[0]
|
||||
# for niveau_id, validations in self.validations_by_niveau.items()
|
||||
# }
|
||||
# "{ niveau_id : meilleure validation pour ce niveau }"
|
||||
|
||||
self.validation_par_competence_et_annee = {}
|
||||
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
niveau = validation_rcue.niveau()
|
||||
if not niveau.competence.id in self.validation_par_competence_et_annee:
|
||||
self.validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||
previous_validation = self.validation_par_competence_et_annee.get(
|
||||
niveau.competence.id
|
||||
).get(validation_rcue.annee())
|
||||
# prend la "meilleure" validation
|
||||
if (not previous_validation) or (
|
||||
sco_codes.BUT_CODES_ORDERED[validation_rcue.code]
|
||||
> sco_codes.BUT_CODES_ORDERED[previous_validation["code"]]
|
||||
):
|
||||
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
niveau.annee
|
||||
] = validation_rcue
|
||||
|
||||
self.competences = {
|
||||
competence.id: competence
|
||||
for competence in (
|
||||
self.parcour.query_competences()
|
||||
if self.parcour
|
||||
else self.formation.referentiel_competence.get_competences_tronc_commun()
|
||||
)
|
||||
}
|
||||
"cache { competence_id : competence }"
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
{
|
||||
competence_id : {
|
||||
annee : meilleure_validation
|
||||
}
|
||||
}
|
||||
"""
|
||||
return {
|
||||
competence.id: {
|
||||
annee: self.validation_par_competence_et_annee.get(
|
||||
competence.id, {}
|
||||
).get(annee)
|
||||
for annee in ("BUT1", "BUT2", "BUT3")
|
||||
}
|
||||
for competence in self.competences.values()
|
||||
}
|
||||
|
||||
# XXX TODO OPTIMISATION ACCESS TABLE JURY
|
||||
def to_dict_codes(self) -> dict[int, dict[str, int]]:
|
||||
"""
|
||||
{
|
||||
competence_id : {
|
||||
annee : { validation}
|
||||
}
|
||||
}
|
||||
où validation est un petit dict avec niveau_id, etc.
|
||||
"""
|
||||
d = {}
|
||||
for competence in self.competences.values():
|
||||
d[competence.id] = {}
|
||||
for annee in ("BUT1", "BUT2", "BUT3"):
|
||||
validation_rcue: ApcValidationRCUE = (
|
||||
self.validation_par_competence_et_annee.get(competence.id, {}).get(
|
||||
annee
|
||||
)
|
||||
)
|
||||
|
||||
d[competence.id][annee] = (
|
||||
validation_rcue.to_dict_codes() if validation_rcue else None
|
||||
)
|
||||
return d
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
|||
"""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileField, FileAllowed, FileRequired
|
||||
from flask_wtf.file import FileField, FileAllowed
|
||||
from wtforms import SelectField, SubmitField
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
from xml.etree import ElementTree
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -38,66 +38,18 @@ def _descr_cursus_but(etud: Identite) -> str:
|
|||
return ", ".join(f"S{indice}" for indice in indices)
|
||||
|
||||
|
||||
def pvjury_table_but(formsemestre_id: int, format="html"):
|
||||
def pvjury_page_but(formsemestre_id: int, fmt="html"):
|
||||
"""Page récapitulant les décisions de jury BUT
|
||||
formsemestre peut être pair ou impair
|
||||
"""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
assert formsemestre.formation.is_apc()
|
||||
title = "Procès-verbal de jury BUT annuel"
|
||||
|
||||
if format == "html":
|
||||
line_sep = "<br/>"
|
||||
title = "Procès-verbal de jury BUT"
|
||||
if fmt == "html":
|
||||
line_sep = "<br>"
|
||||
else:
|
||||
line_sep = "\n"
|
||||
# remplace pour le BUT la fonction sco_pvjury.pvjury_table
|
||||
annee_but = (formsemestre.semestre_id + 1) // 2
|
||||
titles = {
|
||||
"nom": "Nom",
|
||||
"cursus": "Cursus",
|
||||
"ues": "UE validées",
|
||||
"niveaux": "Niveaux de compétences validés",
|
||||
"decision_but": f"Décision BUT{annee_but}",
|
||||
"diplome": "Résultat au diplôme",
|
||||
"devenir": "Devenir",
|
||||
"observations": "Observations",
|
||||
}
|
||||
rows = []
|
||||
for etudid in formsemestre.etuds_inscriptions:
|
||||
etud: Identite = Identite.query.get(etudid)
|
||||
try:
|
||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||
if deca.annee_but != annee_but: # wtf ?
|
||||
log(
|
||||
f"pvjury_table_but: inconsistent annee_but {deca.annee_but} != {annee_but}"
|
||||
)
|
||||
continue
|
||||
except ScoValueError:
|
||||
deca = None
|
||||
row = {
|
||||
"nom": etud.etat_civil_pv(line_sep=line_sep),
|
||||
"_nom_order": etud.sort_key,
|
||||
"_nom_target_attrs": f'class="etudinfo" id="{etud.id}"',
|
||||
"_nom_td_attrs": f'id="{etud.id}" class="etudinfo"',
|
||||
"_nom_target": url_for(
|
||||
"scolar.ficheEtud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud.id,
|
||||
),
|
||||
"cursus": _descr_cursus_but(etud),
|
||||
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
|
||||
"niveaux": deca.descr_niveaux_validation(line_sep=line_sep)
|
||||
if deca
|
||||
else "-",
|
||||
"decision_but": deca.code_valide if deca else "",
|
||||
"devenir": ", ".join([f"S{i}" for i in deca.get_autorisations_passage()])
|
||||
if deca
|
||||
else "",
|
||||
}
|
||||
|
||||
rows.append(row)
|
||||
|
||||
rows.sort(key=lambda x: x["_nom_order"])
|
||||
rows, titles = pvjury_table_but(formsemestre, line_sep=line_sep)
|
||||
|
||||
# Style excel... passages à la ligne sur \n
|
||||
xls_style_base = sco_excel.excel_make_style()
|
||||
|
@ -111,8 +63,8 @@ def pvjury_table_but(formsemestre_id: int, format="html"):
|
|||
html_class="pvjury_table_but table_leftalign",
|
||||
html_title=f"""<div style="margin-bottom: 8px;"><span style="font-size: 120%; font-weight: bold;">{title}</span>
|
||||
<span style="padding-left: 20px;">
|
||||
<a href="{url_for("notes.pvjury_table_but",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, format="xlsx")}"
|
||||
<a href="{url_for("notes.pvjury_page_but",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, fmt="xlsx")}"
|
||||
class="stdlink">version excel</a></span></div>
|
||||
|
||||
""",
|
||||
|
@ -136,4 +88,78 @@ def pvjury_table_but(formsemestre_id: int, format="html"):
|
|||
},
|
||||
xls_style_base=xls_style_base,
|
||||
)
|
||||
return tab.make_page(format=format, javascripts=["js/etud_info.js"], init_qtip=True)
|
||||
return tab.make_page(format=fmt, javascripts=["js/etud_info.js"], init_qtip=True)
|
||||
|
||||
|
||||
def pvjury_table_but(
|
||||
formsemestre: FormSemestre,
|
||||
etudids: list[int] = None,
|
||||
line_sep: str = "\n",
|
||||
only_diplome=False,
|
||||
anonymous=False,
|
||||
with_paragraph_nom=False,
|
||||
) -> tuple[list[dict], dict]:
|
||||
"""Table avec résultats jury BUT pour PV.
|
||||
Si etudids est None, prend tous les étudiants inscrits.
|
||||
"""
|
||||
# remplace pour le BUT la fonction sco_pv_forms.pvjury_table
|
||||
annee_but = (formsemestre.semestre_id + 1) // 2
|
||||
titles = {
|
||||
"nom": "Code" if anonymous else "Nom",
|
||||
"cursus": "Cursus",
|
||||
"ects": "ECTS",
|
||||
"ues": "UE validées",
|
||||
"niveaux": "Niveaux de compétences validés",
|
||||
"decision_but": f"Décision BUT{annee_but}",
|
||||
"diplome": "Résultat au diplôme",
|
||||
"devenir": "Devenir",
|
||||
"observations": "Observations",
|
||||
}
|
||||
rows = []
|
||||
formsemestre_etudids = formsemestre.etuds_inscriptions.keys()
|
||||
if etudids is None:
|
||||
etudids = formsemestre_etudids
|
||||
for etudid in etudids:
|
||||
if not etudid in formsemestre_etudids:
|
||||
continue # garde fou
|
||||
etud: Identite = Identite.query.get(etudid)
|
||||
try:
|
||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||
if deca.annee_but != annee_but: # wtf ?
|
||||
log(
|
||||
f"pvjury_table_but: inconsistent annee_but {deca.annee_but} != {annee_but}"
|
||||
)
|
||||
continue
|
||||
except ScoValueError:
|
||||
deca = None
|
||||
|
||||
row = {
|
||||
"nom": etud.code_ine or etud.code_nip or etud.id
|
||||
if anonymous # Mode anonyme: affiche INE ou sinon NIP, ou id
|
||||
else etud.etat_civil_pv(
|
||||
line_sep=line_sep, with_paragraph=with_paragraph_nom
|
||||
),
|
||||
"_nom_order": etud.sort_key,
|
||||
"_nom_target_attrs": f'class="etudinfo" id="{etud.id}"',
|
||||
"_nom_td_attrs": f'id="{etud.id}" class="etudinfo"',
|
||||
"_nom_target": url_for(
|
||||
"scolar.ficheEtud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud.id,
|
||||
),
|
||||
"cursus": _descr_cursus_but(etud),
|
||||
"ects": f"{deca.formsemestre_ects():g}",
|
||||
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
|
||||
"niveaux": deca.descr_niveaux_validation(line_sep=line_sep)
|
||||
if deca
|
||||
else "-",
|
||||
"decision_but": deca.code_valide if deca else "",
|
||||
"devenir": ", ".join([f"S{i}" for i in deca.get_autorisations_passage()])
|
||||
if deca
|
||||
else "",
|
||||
}
|
||||
if deca.valide_diplome() or not only_diplome:
|
||||
rows.append(row)
|
||||
|
||||
rows.sort(key=lambda x: x["_nom_order"])
|
||||
return rows, titles
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Jury BUT: table recap annuelle et liens saisie
|
||||
"""
|
||||
|
||||
import collections
|
||||
import time
|
||||
import numpy as np
|
||||
from flask import g, url_for
|
||||
|
@ -20,6 +21,7 @@ from app.but.jury_but import (
|
|||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.comp import res_sem
|
||||
from app.models.etudiants import Identite
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc.sco_codes_parcours import (
|
||||
|
@ -31,7 +33,79 @@ from app.scodoc.sco_codes_parcours import (
|
|||
from app.scodoc import sco_formsemestre_status
|
||||
from app.scodoc import sco_pvjury
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import table_builder as tb
|
||||
|
||||
|
||||
class TableJury(tb.Table):
|
||||
pass
|
||||
|
||||
|
||||
class RowJury(tb.Row):
|
||||
"Ligne de la table saisie jury"
|
||||
|
||||
def add_nb_rcues_cell(self, deca: DecisionsProposeesAnnee):
|
||||
"cell avec nb niveaux validables / total"
|
||||
classes = ["col_rcue", "col_rcues_validables"]
|
||||
if deca.nb_rcues_under_8 > 0:
|
||||
classes.append("moy_ue_warning")
|
||||
elif deca.nb_validables < deca.nb_competences:
|
||||
classes.append("moy_ue_inf")
|
||||
else:
|
||||
classes.append("moy_ue_valid")
|
||||
|
||||
if len(deca.rcues_annee) > 0:
|
||||
# permet un tri par nb de niveaux validables + moyenne gen indicative S_pair
|
||||
if deca.res_pair and deca.etud.id in deca.res_pair.etud_moy_gen:
|
||||
moy = deca.res_pair.etud_moy_gen[deca.etud.id]
|
||||
if np.isnan(moy):
|
||||
moy_gen_d = "x"
|
||||
else:
|
||||
moy_gen_d = f"{int(moy*1000):05}"
|
||||
else:
|
||||
moy_gen_d = "x"
|
||||
order = f"{deca.nb_validables:04d}-{moy_gen_d}"
|
||||
else:
|
||||
# étudiants sans RCUE: pas de semestre impair, ...
|
||||
# les classe à la fin
|
||||
order = f"{deca.nb_validables:04d}-00000-{deca.etud.sort_key}"
|
||||
|
||||
self.add_cell(
|
||||
"rcues_validables",
|
||||
"RCUEs",
|
||||
f"""{deca.nb_validables}/{deca.nb_competences}"""
|
||||
+ ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""),
|
||||
group="rcues_validables",
|
||||
classes=classes,
|
||||
data={"order": order},
|
||||
)
|
||||
|
||||
def add_ue_cells(self, dec_ue: DecisionsProposeesUE):
|
||||
"cell de moyenne d'UE"
|
||||
col_id = f"moy_ue_{dec_ue.ue.id}"
|
||||
note_class = ""
|
||||
val = dec_ue.moy_ue
|
||||
if isinstance(val, float):
|
||||
if val < BUT_BARRE_UE:
|
||||
note_class = "moy_inf"
|
||||
elif val >= BUT_BARRE_UE:
|
||||
note_class = "moy_ue_valid"
|
||||
if val < BUT_BARRE_UE8:
|
||||
note_class = "moy_ue_warning" # notes très basses
|
||||
self.add_cell(
|
||||
col_id,
|
||||
dec_ue.ue.acronyme,
|
||||
self.fmt_note(val),
|
||||
group="col_ue",
|
||||
"col_ue" + note_class,
|
||||
column_class="col_ue",
|
||||
)
|
||||
self.add_cell(
|
||||
col_id + "_code",
|
||||
dec_ue.ue.acronyme,
|
||||
dec_ue.code_valide or "",
|
||||
"col_ue_code recorded_code",
|
||||
column_class="col_ue",
|
||||
)
|
||||
|
||||
|
||||
def formsemestre_saisie_jury_but(
|
||||
|
@ -58,20 +132,11 @@ def formsemestre_saisie_jury_but(
|
|||
# DecisionsProposeesAnnee(etud, formsemestre2)
|
||||
# Pour le 1er etud, faire un check_ues_ready_jury(self) -> page d'erreur
|
||||
# -> rcue .ue_1, .ue_2 -> stroe moy ues, rcue.moy_rcue, etc
|
||||
if formsemestre2.semestre_id % 2 != 0:
|
||||
raise ScoValueError("Cette page ne fonctionne que sur les semestres pairs")
|
||||
|
||||
if formsemestre2.formation.referentiel_competence is None:
|
||||
raise ScoValueError(
|
||||
"""
|
||||
<p>Pas de référentiel de compétences associé à la formation !</p>
|
||||
<p>Pour associer un référentiel, passer par le menu <b>Semestre /
|
||||
Voir la formation... </b> et suivre le lien <em>"associer à un référentiel
|
||||
de compétences"</em>
|
||||
"""
|
||||
)
|
||||
raise ScoNoReferentielCompetences(formation=formsemestre2.formation)
|
||||
|
||||
rows, titles, column_ids = get_jury_but_table(
|
||||
rows, titles, column_ids, jury_stats = get_jury_but_table(
|
||||
formsemestre2, read_only=read_only, mode=mode
|
||||
)
|
||||
if not rows:
|
||||
|
@ -153,6 +218,28 @@ def formsemestre_saisie_jury_but(
|
|||
f"""
|
||||
</div>
|
||||
|
||||
<div class="jury_stats">
|
||||
<div>Nb d'étudiants avec décision annuelle:
|
||||
{sum(jury_stats["codes_annuels"].values())} / {jury_stats["nb_etuds"]}
|
||||
</div>
|
||||
<div><b>Codes annuels octroyés:</b></div>
|
||||
<table class="jury_stats_codes">
|
||||
"""
|
||||
)
|
||||
for code in sorted(jury_stats["codes_annuels"].keys()):
|
||||
H.append(
|
||||
f"""<tr>
|
||||
<td>{code}</td>
|
||||
<td style="text-align:right">{jury_stats["codes_annuels"][code]}</td>
|
||||
<td style="text-align:right">{
|
||||
(100*jury_stats["codes_annuels"][code] / jury_stats["nb_etuds"]):2.1f}%
|
||||
</td>
|
||||
</tr>"""
|
||||
)
|
||||
H.append(
|
||||
f"""
|
||||
</table>
|
||||
</div>
|
||||
{html_sco_header.sco_footer()}
|
||||
"""
|
||||
)
|
||||
|
@ -255,58 +342,37 @@ class RowCollector:
|
|||
self.column_classes[col_id] = column_class
|
||||
self.idx += 1
|
||||
|
||||
def add_etud_cells(
|
||||
self, etud: Identite, formsemestre: FormSemestre, with_links=True
|
||||
):
|
||||
"Les cells code, nom, prénom etc."
|
||||
# --- Codes (seront cachés, mais exportés en excel)
|
||||
self.add_cell("etudid", "etudid", etud.id, "codes")
|
||||
self.add_cell("code_nip", "code_nip", etud.code_nip or "", "codes")
|
||||
# --- Identité étudiant (adapté de res_comon/get_table_recap, à factoriser XXX TODO)
|
||||
self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail")
|
||||
self.add_cell("nom_disp", "Nom", etud.nom_disp(), "identite_detail")
|
||||
self["_nom_disp_order"] = etud.sort_key
|
||||
self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
|
||||
self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court")
|
||||
if with_links:
|
||||
self["_nom_short_order"] = etud.sort_key
|
||||
self["_nom_short_target"] = url_for(
|
||||
"notes.formsemestre_bulletinetud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
etudid=etud.id,
|
||||
)
|
||||
self["_nom_short_target_attrs"] = f'class="etudinfo" id="{etud.id}"'
|
||||
self["_nom_disp_target"] = self["_nom_short_target"]
|
||||
self["_nom_disp_target_attrs"] = self["_nom_short_target_attrs"]
|
||||
self.last_etud_cell_idx = self.idx
|
||||
# def add_etud_cells(
|
||||
# self, etud: Identite, formsemestre: FormSemestre, with_links=True
|
||||
# ):
|
||||
# "Les cells code, nom, prénom etc."
|
||||
# # --- Codes (seront cachés, mais exportés en excel)
|
||||
# self.add_cell("etudid", "etudid", etud.id, "codes")
|
||||
# self.add_cell("code_nip", "code_nip", etud.code_nip or "", "codes")
|
||||
# # --- Identité étudiant (adapté de res_common/get_table_recap, à factoriser XXX TODO)
|
||||
# self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail")
|
||||
# self.add_cell("nom_disp", "Nom", etud.nom_disp(), "identite_detail")
|
||||
# self["_nom_disp_order"] = etud.sort_key
|
||||
# self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
|
||||
# self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court")
|
||||
# self["_nom_short_data"] = {
|
||||
# "etudid": etud.id,
|
||||
# "nomprenom": etud.nomprenom,
|
||||
# }
|
||||
# if with_links:
|
||||
# self["_nom_short_order"] = etud.sort_key
|
||||
# self["_nom_short_target"] = url_for(
|
||||
# "notes.formsemestre_bulletinetud",
|
||||
# scodoc_dept=g.scodoc_dept,
|
||||
# formsemestre_id=formsemestre.id,
|
||||
# etudid=etud.id,
|
||||
# )
|
||||
# self["_nom_short_target_attrs"] = f'class="etudinfo" id="{etud.id}"'
|
||||
# self["_nom_disp_target"] = self["_nom_short_target"]
|
||||
# self["_nom_disp_target_attrs"] = self["_nom_short_target_attrs"]
|
||||
# self.last_etud_cell_idx = self.idx
|
||||
|
||||
def add_ue_cells(self, dec_ue: DecisionsProposeesUE):
|
||||
"cell de moyenne d'UE"
|
||||
col_id = f"moy_ue_{dec_ue.ue.id}"
|
||||
note_class = ""
|
||||
val = dec_ue.moy_ue
|
||||
if isinstance(val, float):
|
||||
if val < BUT_BARRE_UE:
|
||||
note_class = " moy_inf"
|
||||
elif val >= BUT_BARRE_UE:
|
||||
note_class = " moy_ue_valid"
|
||||
if val < BUT_BARRE_UE8:
|
||||
note_class = " moy_ue_warning" # notes très basses
|
||||
self.add_cell(
|
||||
col_id,
|
||||
dec_ue.ue.acronyme,
|
||||
self.fmt_note(val),
|
||||
"col_ue" + note_class,
|
||||
column_class="col_ue",
|
||||
)
|
||||
self.add_cell(
|
||||
col_id + "_code",
|
||||
dec_ue.ue.acronyme,
|
||||
dec_ue.code_valide or "",
|
||||
"col_ue_code recorded_code",
|
||||
column_class="col_ue",
|
||||
)
|
||||
|
||||
|
||||
def add_rcue_cells(self, dec_rcue: DecisionsProposeesRCUE):
|
||||
"2 cells: moyenne du RCUE, code enregistré"
|
||||
|
@ -336,59 +402,28 @@ class RowCollector:
|
|||
column_class="col_rcue",
|
||||
)
|
||||
|
||||
def add_nb_rcues_cell(self, deca: DecisionsProposeesAnnee):
|
||||
"cell avec nb niveaux validables / total"
|
||||
klass = " "
|
||||
if deca.nb_rcues_under_8 > 0:
|
||||
klass += "moy_ue_warning"
|
||||
elif deca.nb_validables < deca.nb_competences:
|
||||
klass += "moy_ue_inf"
|
||||
else:
|
||||
klass += "moy_ue_valid"
|
||||
self.add_cell(
|
||||
"rcues_validables",
|
||||
"RCUEs",
|
||||
f"""{deca.nb_validables}/{deca.nb_competences}"""
|
||||
+ ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""),
|
||||
"col_rcue col_rcues_validables" + klass,
|
||||
)
|
||||
self["_rcues_validables_data"] = {
|
||||
"etudid": deca.etud.id,
|
||||
"nomprenom": deca.etud.nomprenom,
|
||||
}
|
||||
if len(deca.rcues_annee) > 0:
|
||||
# permet un tri par nb de niveaux validables + moyenne gen indicative S_pair
|
||||
if deca.res_pair and deca.etud.id in deca.res_pair.etud_moy_gen:
|
||||
moy = deca.res_pair.etud_moy_gen[deca.etud.id]
|
||||
if np.isnan(moy):
|
||||
moy_gen_d = "x"
|
||||
else:
|
||||
moy_gen_d = f"{int(moy*1000):05}"
|
||||
else:
|
||||
moy_gen_d = "x"
|
||||
self["_rcues_validables_order"] = f"{deca.nb_validables:04d}-{moy_gen_d}"
|
||||
else:
|
||||
# etudiants sans RCUE: pas de semestre impair, ...
|
||||
# les classe à la fin
|
||||
self[
|
||||
"_rcues_validables_order"
|
||||
] = f"{deca.nb_validables:04d}-00000-{deca.etud.sort_key}"
|
||||
|
||||
|
||||
def get_jury_but_table(
|
||||
formsemestre2: FormSemestre, read_only: bool = False, mode="jury", with_links=True
|
||||
) -> tuple[list[dict], list[str], list[str]]:
|
||||
"""Construit la table des résultats annuels pour le jury BUT"""
|
||||
) -> tuple[list[dict], list[str], list[str], dict]:
|
||||
"""Construit la table des résultats annuels pour le jury BUT
|
||||
=> rows_dict, titles, column_ids, jury_stats
|
||||
où jury_stats est un dict donnant des comptages sur le jury.
|
||||
"""
|
||||
res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2)
|
||||
titles = {} # column_id : title
|
||||
column_classes = {}
|
||||
rows = []
|
||||
jury_stats = {
|
||||
"nb_etuds": len(formsemestre2.etuds_inscriptions),
|
||||
"codes_annuels": collections.Counter(),
|
||||
}
|
||||
table = TableJury(res2, mode_jury=True)
|
||||
for etudid in formsemestre2.etuds_inscriptions:
|
||||
etud: Identite = Identite.query.get(etudid)
|
||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre2)
|
||||
row = RowCollector(titles=titles, column_classes=column_classes)
|
||||
row.add_etud_cells(etud, formsemestre2, with_links=with_links)
|
||||
row.idx = 100 # laisse place pour les colonnes de groupes
|
||||
# XXX row = RowCollector(titles=titles, column_classes=column_classes)
|
||||
row = RowJury(table, etudid)
|
||||
table.add_row(row)
|
||||
row.add_etud(etud)
|
||||
# --- Nombre de niveaux
|
||||
row.add_nb_rcues_cell(deca)
|
||||
# --- Les RCUEs
|
||||
|
@ -417,6 +452,8 @@ def get_jury_but_table(
|
|||
f"""{deca.code_valide or ''}""",
|
||||
"col_code_annee",
|
||||
)
|
||||
if deca.code_valide:
|
||||
jury_stats["codes_annuels"][deca.code_valide] += 1
|
||||
# --- Le lien de saisie
|
||||
if mode != "recap" and with_links:
|
||||
row.add_cell(
|
||||
|
@ -439,11 +476,14 @@ def get_jury_but_table(
|
|||
rows.append(row)
|
||||
rows_dict = [row.get_row_dict() for row in rows]
|
||||
if len(rows_dict) > 0:
|
||||
res2.recap_add_partitions(rows_dict, titles, col_idx=row.last_etud_cell_idx + 1)
|
||||
col_idx = res2.recap_add_partitions(
|
||||
rows_dict, titles, col_idx=row.last_etud_cell_idx + 1
|
||||
)
|
||||
res2.recap_add_cursus(rows_dict, titles, col_idx=col_idx + 1)
|
||||
column_ids = [title for title in titles if not title.startswith("_")]
|
||||
column_ids.sort(key=lambda col_id: titles.get("_" + col_id + "_col_order", 1000))
|
||||
rows_dict.sort(key=lambda row: row["_nom_disp_order"])
|
||||
return rows_dict, titles, column_ids
|
||||
return rows_dict, titles, column_ids, jury_stats
|
||||
|
||||
|
||||
def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]:
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Jury BUT et classiques: récupération des résults pour API
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from app.but import jury_but
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc import sco_pv_dict
|
||||
|
||||
|
||||
def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]:
|
||||
"""Liste des résultats jury BUT sous forme de dict, pour API"""
|
||||
if formsemestre.formation.referentiel_competence is None:
|
||||
# pas de ref. comp., donc pas de decisions de jury (ne lance pas d'exception)
|
||||
return []
|
||||
dpv = sco_pv_dict.dict_pvjury(formsemestre.id)
|
||||
rows = []
|
||||
for etudid in formsemestre.etuds_inscriptions:
|
||||
rows.append(_get_jury_but_etud_result(formsemestre, dpv, etudid))
|
||||
return rows
|
||||
|
||||
|
||||
def _get_jury_but_etud_result(
|
||||
formsemestre: FormSemestre, dpv: dict, etudid: int
|
||||
) -> dict:
|
||||
"""Résultats de jury d'un étudiant sur un semestre pair de BUT"""
|
||||
etud: Identite = Identite.query.get(etudid)
|
||||
dec_etud = dpv["decisions_dict"][etudid]
|
||||
if formsemestre.formation.is_apc():
|
||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||
else:
|
||||
deca = None
|
||||
row = {
|
||||
"etudid": etud.id,
|
||||
"code_nip": etud.code_nip,
|
||||
"code_ine": etud.code_ine,
|
||||
"is_apc": dpv["is_apc"], # BUT ou classic ?
|
||||
"etat": dec_etud["etat"], # I ou D ou DEF
|
||||
"nb_competences": deca.nb_competences if deca else 0,
|
||||
}
|
||||
# --- Les RCUEs
|
||||
rcue_list = []
|
||||
if deca:
|
||||
for rcue in deca.rcues_annee:
|
||||
dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id)
|
||||
if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau
|
||||
dec_ue1 = deca.decisions_ues[rcue.ue_1.id]
|
||||
dec_ue2 = deca.decisions_ues[rcue.ue_2.id]
|
||||
rcue_dict = {
|
||||
"ue_1": {
|
||||
"ue_id": rcue.ue_1.id,
|
||||
"moy": None
|
||||
if (dec_ue1.moy_ue is None or np.isnan(dec_ue1.moy_ue))
|
||||
else dec_ue1.moy_ue,
|
||||
"code": dec_ue1.code_valide,
|
||||
},
|
||||
"ue_2": {
|
||||
"ue_id": rcue.ue_2.id,
|
||||
"moy": None
|
||||
if (dec_ue2.moy_ue is None or np.isnan(dec_ue2.moy_ue))
|
||||
else dec_ue2.moy_ue,
|
||||
"code": dec_ue2.code_valide,
|
||||
},
|
||||
"moy": rcue.moy_rcue,
|
||||
"code": dec_rcue.code_valide,
|
||||
}
|
||||
rcue_list.append(rcue_dict)
|
||||
row["rcues"] = rcue_list
|
||||
# --- Les UEs
|
||||
ue_list = []
|
||||
if dec_etud["decisions_ue"]:
|
||||
for ue_id, ue_dec in dec_etud["decisions_ue"].items():
|
||||
ue_dict = {
|
||||
"ue_id": ue_id,
|
||||
"code": ue_dec["code"],
|
||||
"ects": ue_dec["ects"],
|
||||
}
|
||||
ue_list.append(ue_dict)
|
||||
row["ues"] = ue_list
|
||||
# --- Le semestre (pour les formations classiques)
|
||||
if dec_etud["decision_sem"]:
|
||||
row["semestre"] = {"code": dec_etud["decision_sem"].get("code")}
|
||||
else:
|
||||
row["semestre"] = {} # APC, ...
|
||||
# --- Autorisations
|
||||
row["autorisations"] = dec_etud["autorisations"]
|
||||
return row
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -15,20 +15,32 @@ from app.scodoc import sco_cache
|
|||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
def formsemestre_validation_auto_but(formsemestre: FormSemestre) -> int:
|
||||
"""Calcul automatique des décisions de jury sur une année BUT.
|
||||
Returns: nombre d'étudiants "admis"
|
||||
def formsemestre_validation_auto_but(
|
||||
formsemestre: FormSemestre, only_adm: bool = True, no_overwrite: bool = True
|
||||
) -> int:
|
||||
"""Calcul automatique des décisions de jury sur une "année" BUT.
|
||||
|
||||
- N'enregistre jamais de décisions de l'année scolaire précédente, même
|
||||
si on a des RCUE "à cheval".
|
||||
- Ne modifie jamais de décisions déjà enregistrées (sauf si no_overwrite est faux,
|
||||
ce qui est utilisé pour certains tests unitaires).
|
||||
- Normalement, only_adm est True et on n'enregistre que les décisions validantes
|
||||
de droit: ADM ou CMP.
|
||||
En revanche, si only_adm est faux, on enregistre la première décision proposée par ScoDoc
|
||||
(mode à n'utiliser que pour les tests unitaires vérifiant la saisie des jurys)
|
||||
|
||||
Returns: nombre d'étudiants pour lesquels on a enregistré au moins un code.
|
||||
"""
|
||||
if not formsemestre.formation.is_apc():
|
||||
raise ScoValueError("fonction réservée aux formations BUT")
|
||||
nb_admis = 0
|
||||
nb_etud_modif = 0
|
||||
with sco_cache.DeferredSemCacheManager():
|
||||
for etudid in formsemestre.etuds_inscriptions:
|
||||
etud: Identite = Identite.query.get(etudid)
|
||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||
if deca.admis: # année réussie
|
||||
deca.record_all()
|
||||
nb_admis += 1
|
||||
nb_etud_modif += deca.record_all(
|
||||
no_overwrite=no_overwrite, only_validantes=only_adm
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
return nb_admis
|
||||
return nb_etud_modif
|
||||
|
|
|
@ -1,18 +1,42 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Jury BUT: affichage/formulaire
|
||||
"""
|
||||
from flask import g, url_for
|
||||
from app.models.etudiants import Identite
|
||||
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.but.jury_but import DecisionsProposeesAnnee, DecisionsProposeesUE
|
||||
from app.models import FormSemestre, FormSemestreInscription, UniteEns
|
||||
import re
|
||||
import numpy as np
|
||||
|
||||
import flask
|
||||
from flask import flash, render_template, url_for
|
||||
from flask import g, request
|
||||
|
||||
from app import db
|
||||
from app.but import jury_but
|
||||
from app.but.jury_but import (
|
||||
DecisionsProposeesAnnee,
|
||||
DecisionsProposeesRCUE,
|
||||
DecisionsProposeesUE,
|
||||
)
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import (
|
||||
ApcNiveau,
|
||||
FormSemestre,
|
||||
FormSemestreInscription,
|
||||
Identite,
|
||||
UniteEns,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
)
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
||||
|
@ -20,35 +44,50 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
|||
Si pas read_only, menus sélection codes jury.
|
||||
"""
|
||||
H = []
|
||||
if deca.code_valide and not read_only:
|
||||
erase_span = f"""<a href="{
|
||||
url_for("notes.formsemestre_jury_but_erase",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=deca.formsemestre_id,
|
||||
etudid=deca.etud.id)}" class="stdlink">effacer décisions</a>"""
|
||||
else:
|
||||
erase_span = ""
|
||||
|
||||
H.append(
|
||||
f"""<div class="but_section_annee">
|
||||
<div>
|
||||
<b>Décision de jury pour l'année :</b> {
|
||||
_gen_but_select("code_annee", deca.codes, deca.code_valide,
|
||||
disabled=True, klass="manual")
|
||||
}
|
||||
<span>({'non ' if deca.code_valide is None else ''}enregistrée)</span>
|
||||
<span>{erase_span}</span>
|
||||
if deca.jury_annuel:
|
||||
H.append(
|
||||
f"""
|
||||
<div class="but_section_annee">
|
||||
<div>
|
||||
<b>Décision de jury pour l'année :</b> {
|
||||
_gen_but_select("code_annee", deca.codes, deca.code_valide,
|
||||
disabled=True, klass="manual")
|
||||
}
|
||||
<span>({deca.code_valide or 'non'} enregistrée)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="but_explanation">{deca.explanation}</div>
|
||||
</div>"""
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
formsemestre_1 = deca.formsemestre_impair
|
||||
formsemestre_2 = deca.formsemestre_pair
|
||||
# Ordonne selon les dates des 2 semestres considérés (pour les redoublants à cheval):
|
||||
reverse_semestre = (
|
||||
deca.formsemestre_pair
|
||||
and deca.formsemestre_impair
|
||||
and deca.formsemestre_pair.date_debut < deca.formsemestre_impair.date_debut
|
||||
)
|
||||
if reverse_semestre:
|
||||
formsemestre_1, formsemestre_2 = formsemestre_2, formsemestre_1
|
||||
H.append(
|
||||
f"""
|
||||
<div><b>Niveaux de compétences et unités d'enseignement :</b></div>
|
||||
<div class="titre_niveaux">
|
||||
<b>Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}</b>
|
||||
</div>
|
||||
<div class="but_explanation">{deca.explanation}</div>
|
||||
<div class="but_annee">
|
||||
<div class="titre"></div>
|
||||
<div class="titre">S{1}</div>
|
||||
<div class="titre">S{2}</div>
|
||||
<div class="titre">{"S" +str(formsemestre_1.semestre_id)
|
||||
if formsemestre_1 else "-"}
|
||||
<span class="avertissement_redoublement">{formsemestre_1.annee_scolaire_str()
|
||||
if formsemestre_1 else ""}</span>
|
||||
</div>
|
||||
<div class="titre">{"S"+str(formsemestre_2.semestre_id)
|
||||
if formsemestre_2 else "-"}
|
||||
<span class="avertissement_redoublement">{formsemestre_2.annee_scolaire_str()
|
||||
if formsemestre_2 else ""}</span>
|
||||
</div>
|
||||
<div class="titre">RCUE</div>
|
||||
"""
|
||||
)
|
||||
|
@ -58,42 +97,52 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
|||
<div title="{niveau.competence.titre_long}">{niveau.competence.titre}</div>
|
||||
</div>"""
|
||||
)
|
||||
dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id)
|
||||
if dec_rcue is None:
|
||||
break
|
||||
# Semestre impair
|
||||
H.append(
|
||||
_gen_but_niveau_ue(
|
||||
dec_rcue.rcue.ue_1,
|
||||
dec_rcue.rcue.moy_ue_1,
|
||||
deca.decisions_ues[dec_rcue.rcue.ue_1.id],
|
||||
disabled=read_only,
|
||||
)
|
||||
)
|
||||
# Semestre pair
|
||||
H.append(
|
||||
_gen_but_niveau_ue(
|
||||
dec_rcue.rcue.ue_2,
|
||||
dec_rcue.rcue.moy_ue_2,
|
||||
deca.decisions_ues[dec_rcue.rcue.ue_2.id],
|
||||
disabled=read_only,
|
||||
)
|
||||
)
|
||||
# RCUE
|
||||
H.append(
|
||||
f"""<div class="but_niveau_rcue
|
||||
{'recorded' if dec_rcue.code_valide is not None else ''}
|
||||
">
|
||||
<div class="but_note">{scu.fmt_note(dec_rcue.rcue.moy_rcue)}</div>
|
||||
<div class="but_code">{
|
||||
_gen_but_select("code_rcue_"+str(niveau.id),
|
||||
dec_rcue.codes,
|
||||
dec_rcue.code_valide,
|
||||
disabled=True, klass="manual"
|
||||
dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id) # peut être None
|
||||
ues = [
|
||||
ue
|
||||
for ue in deca.ues_impair
|
||||
if ue.niveau_competence and ue.niveau_competence.id == niveau.id
|
||||
]
|
||||
ue_impair = ues[0] if ues else None
|
||||
ues = [
|
||||
ue
|
||||
for ue in deca.ues_pair
|
||||
if ue.niveau_competence and ue.niveau_competence.id == niveau.id
|
||||
]
|
||||
ue_pair = ues[0] if ues else None
|
||||
# Les UEs à afficher,
|
||||
# qui seront toujours en readonly sur le formsemestre de l'année précédente du redoublant
|
||||
ues_ro = [
|
||||
(
|
||||
ue_impair,
|
||||
(deca.a_cheval and deca.formsemestre_id != deca.formsemestre_impair.id),
|
||||
),
|
||||
(
|
||||
ue_pair,
|
||||
deca.a_cheval and deca.formsemestre_id != deca.formsemestre_pair.id,
|
||||
),
|
||||
]
|
||||
# Ordonne selon les dates des 2 semestres considérés:
|
||||
if reverse_semestre:
|
||||
ues_ro[0], ues_ro[1] = ues_ro[1], ues_ro[0]
|
||||
# Colonnes d'UE:
|
||||
for ue, ue_read_only in ues_ro:
|
||||
if ue:
|
||||
H.append(
|
||||
_gen_but_niveau_ue(
|
||||
ue,
|
||||
deca.decisions_ues[ue.id],
|
||||
disabled=read_only or ue_read_only,
|
||||
annee_prec=ue_read_only,
|
||||
niveau_id=ue.niveau_competence.id,
|
||||
)
|
||||
)
|
||||
}</div>
|
||||
</div>"""
|
||||
)
|
||||
else:
|
||||
H.append("""<div class="niveau_vide"></div>""")
|
||||
|
||||
# Colonne RCUE
|
||||
H.append(_gen_but_rcue(dec_rcue, niveau))
|
||||
|
||||
H.append("</div>") # but_annee
|
||||
return "\n".join(H)
|
||||
|
||||
|
@ -104,43 +153,368 @@ def _gen_but_select(
|
|||
code_valide: str,
|
||||
disabled: bool = False,
|
||||
klass: str = "",
|
||||
data: dict = {},
|
||||
) -> str:
|
||||
"Le menu html select avec les codes"
|
||||
h = "\n".join(
|
||||
# if disabled: # mauvaise idée car le disabled est traité en JS
|
||||
# return f"""<div class="but_code {klass}">{code_valide}</div>"""
|
||||
options_htm = "\n".join(
|
||||
[
|
||||
f"""<option value="{code}"
|
||||
f"""<option value="{code}"
|
||||
{'selected' if code == code_valide else ''}
|
||||
class="{'recorded' if code == code_valide else ''}"
|
||||
>{code}</option>"""
|
||||
for code in codes
|
||||
]
|
||||
)
|
||||
return f"""<select required name="{name}"
|
||||
return f"""<select required name="{name}"
|
||||
class="but_code {klass}"
|
||||
data-orig_code="{code_valide or (codes[0] if codes else '')}"
|
||||
data-orig_recorded="{code_valide or ''}"
|
||||
onchange="change_menu_code(this);"
|
||||
{"disabled" if disabled else ""}
|
||||
>{h}</select>
|
||||
{" ".join( f'data-{k}="{v}"' for (k,v) in data.items() )}
|
||||
>{options_htm}</select>
|
||||
"""
|
||||
|
||||
|
||||
def _gen_but_niveau_ue(
|
||||
ue: UniteEns, moy_ue: float, dec_ue: DecisionsProposeesUE, disabled=False
|
||||
):
|
||||
ue: UniteEns,
|
||||
dec_ue: DecisionsProposeesUE,
|
||||
disabled: bool = False,
|
||||
annee_prec: bool = False,
|
||||
niveau_id: int = None,
|
||||
) -> str:
|
||||
if dec_ue.ue_status and dec_ue.ue_status["is_capitalized"]:
|
||||
moy_ue_str = f"""<span class="ue_cap">{
|
||||
scu.fmt_note(dec_ue.moy_ue_with_cap)}</span>"""
|
||||
scoplement = f"""<div class="scoplement">
|
||||
<div>
|
||||
<b>UE {ue.acronyme} capitalisée </b>
|
||||
<span>le {dec_ue.ue_status["event_date"].strftime("%d/%m/%Y")}
|
||||
</span>
|
||||
</div>
|
||||
<div>UE en cours
|
||||
{ "sans notes" if np.isnan(dec_ue.moy_ue)
|
||||
else
|
||||
("avec moyenne <b>" + scu.fmt_note(dec_ue.moy_ue) + "</b>")
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.moy_ue)}</span>"""
|
||||
if dec_ue.code_valide:
|
||||
scoplement = f"""<div class="scoplement">
|
||||
<div>Code {dec_ue.code_valide} enregistré le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
|
||||
à {dec_ue.validation.event_date.strftime("%Hh%M")}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
scoplement = ""
|
||||
|
||||
return f"""<div class="but_niveau_ue {
|
||||
'recorded' if dec_ue.code_valide is not None else ''}
|
||||
{'annee_prec' if annee_prec else ''}
|
||||
">
|
||||
<div title="{ue.titre}">{ue.acronyme}</div>
|
||||
<div class="but_note">{scu.fmt_note(moy_ue)}</div>
|
||||
<div class="but_note with_scoplement">
|
||||
<div>{moy_ue_str}</div>
|
||||
{scoplement}
|
||||
</div>
|
||||
<div class="but_code">{
|
||||
_gen_but_select("code_ue_"+str(ue.id),
|
||||
dec_ue.codes,
|
||||
dec_ue.code_valide, disabled=disabled
|
||||
dec_ue.codes,
|
||||
dec_ue.code_valide,
|
||||
disabled=disabled,
|
||||
klass=f"code_ue ue_rcue_{niveau_id}" if not disabled else ""
|
||||
)
|
||||
}</div>
|
||||
|
||||
</div>"""
|
||||
|
||||
|
||||
#
|
||||
def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
|
||||
if dec_rcue is None:
|
||||
return """
|
||||
<div class="but_niveau_rcue niveau_vide with_scoplement">
|
||||
<div></div>
|
||||
<div class="scoplement">Pas de RCUE (UE non capitalisée ?)</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
scoplement = (
|
||||
f"""<div class="scoplement">{
|
||||
dec_rcue.validation.to_html()
|
||||
}</div>"""
|
||||
if dec_rcue.validation
|
||||
else ""
|
||||
)
|
||||
|
||||
# Déjà enregistré ?
|
||||
niveau_rcue_class = ""
|
||||
if dec_rcue.code_valide is not None and dec_rcue.codes:
|
||||
if dec_rcue.code_valide == dec_rcue.codes[0]:
|
||||
niveau_rcue_class = "recorded"
|
||||
else:
|
||||
niveau_rcue_class = "recorded_different"
|
||||
|
||||
return f"""
|
||||
<div class="but_niveau_rcue {niveau_rcue_class}
|
||||
">
|
||||
<div class="but_note with_scoplement">
|
||||
<div>{scu.fmt_note(dec_rcue.rcue.moy_rcue)}</div>
|
||||
{scoplement}
|
||||
</div>
|
||||
<div class="but_code">
|
||||
{_gen_but_select("code_rcue_"+str(niveau.id),
|
||||
dec_rcue.codes,
|
||||
dec_rcue.code_valide,
|
||||
disabled=True,
|
||||
klass="manual code_rcue",
|
||||
data = { "niveau_id" : str(niveau.id)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def jury_but_semestriel(
|
||||
formsemestre: FormSemestre,
|
||||
etud: Identite,
|
||||
read_only: bool,
|
||||
navigation_div: str = "",
|
||||
) -> str:
|
||||
"""Page: formulaire saisie décision d'UE d'un semestre BUT isolé (pas jury annuel)."""
|
||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
||||
parcour, ues = jury_but.list_ue_parcour_etud(formsemestre, etud, res)
|
||||
inscription_etat = etud.inscription_etat(formsemestre.id)
|
||||
semestre_terminal = (
|
||||
formsemestre.semestre_id >= formsemestre.formation.get_cursus().NB_SEM
|
||||
)
|
||||
autorisations_passage = ScolarAutorisationInscription.query.filter_by(
|
||||
etudid=etud.id,
|
||||
origin_formsemestre_id=formsemestre.id,
|
||||
).all()
|
||||
# Par défaut: autorisé à passer dans le semestre suivant si sem. impair,
|
||||
# ou si décision déjà enregistrée:
|
||||
est_autorise_a_passer = (formsemestre.semestre_id % 2) or (
|
||||
formsemestre.semestre_id + 1
|
||||
) in (a.semestre_id for a in autorisations_passage)
|
||||
decisions_ues = {
|
||||
ue.id: DecisionsProposeesUE(etud, formsemestre, ue, inscription_etat)
|
||||
for ue in ues
|
||||
}
|
||||
for dec_ue in decisions_ues.values():
|
||||
dec_ue.compute_codes()
|
||||
|
||||
if request.method == "POST":
|
||||
if not read_only:
|
||||
for key in request.form:
|
||||
code = request.form[key]
|
||||
# Codes d'UE
|
||||
code_match = re.match(r"^code_ue_(\d+)$", key)
|
||||
if code_match:
|
||||
ue_id = int(code_match.group(1))
|
||||
dec_ue = decisions_ues.get(ue_id)
|
||||
if not dec_ue:
|
||||
raise ScoValueError(f"UE invalide ue_id={ue_id}")
|
||||
dec_ue.record(code)
|
||||
db.session.commit()
|
||||
flash("codes enregistrés")
|
||||
if not semestre_terminal:
|
||||
if request.form.get("autorisation_passage"):
|
||||
if not formsemestre.semestre_id + 1 in (
|
||||
a.semestre_id for a in autorisations_passage
|
||||
):
|
||||
ScolarAutorisationInscription.delete_autorisation_etud(
|
||||
etud.id, formsemestre.id
|
||||
)
|
||||
ScolarAutorisationInscription.autorise_etud(
|
||||
etud.id,
|
||||
formsemestre.formation.formation_code,
|
||||
formsemestre.id,
|
||||
formsemestre.semestre_id + 1,
|
||||
)
|
||||
db.session.commit()
|
||||
flash(
|
||||
f"""autorisation de passage en S{formsemestre.semestre_id + 1
|
||||
} enregistrée"""
|
||||
)
|
||||
else:
|
||||
if est_autorise_a_passer:
|
||||
ScolarAutorisationInscription.delete_autorisation_etud(
|
||||
etud.id, formsemestre.id
|
||||
)
|
||||
db.session.commit()
|
||||
flash(
|
||||
f"autorisation de passage en S{formsemestre.semestre_id + 1} annulée"
|
||||
)
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.formsemestre_validation_but",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
etudid=etud.id,
|
||||
)
|
||||
)
|
||||
# GET
|
||||
if formsemestre.semestre_id % 2 == 0:
|
||||
warning = f"""<div class="warning">
|
||||
Cet étudiant de S{formsemestre.semestre_id} ne peut pas passer
|
||||
en jury BUT annuel car il lui manque le semestre précédent.
|
||||
</div>"""
|
||||
else:
|
||||
warning = ""
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title=f"Validation BUT S{formsemestre.semestre_id}",
|
||||
formsemestre_id=formsemestre.id,
|
||||
etudid=etud.id,
|
||||
cssstyles=("css/jury_but.css",),
|
||||
javascripts=("js/jury_but.js",),
|
||||
),
|
||||
f"""
|
||||
<div class="jury_but">
|
||||
<div>
|
||||
<div class="bull_head">
|
||||
<div>
|
||||
<div class="titre_parcours">Jury BUT S{formsemestre.id}
|
||||
- Parcours {(parcour.libelle if parcour else False) or "non spécifié"}
|
||||
</div>
|
||||
<div class="nom_etud">{etud.nomprenom}</div>
|
||||
</div>
|
||||
<div class="bull_photo"><a href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Jury sur un semestre BUT isolé (ne concerne que les UEs)</h3>
|
||||
{warning}
|
||||
</div>
|
||||
|
||||
<form method="post" id="jury_but">
|
||||
""",
|
||||
]
|
||||
|
||||
erase_span = ""
|
||||
if not read_only:
|
||||
# Requête toutes les validations (pas seulement celles du deca courant),
|
||||
# au cas où: changement d'architecture, saisie en mode classique, ...
|
||||
validations = ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=etud.id, formsemestre_id=formsemestre.id
|
||||
).all()
|
||||
if validations:
|
||||
erase_span = f"""<a href="{
|
||||
url_for("notes.formsemestre_jury_but_erase",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id,
|
||||
etudid=etud.id, only_one_sem=1)
|
||||
}" class="stdlink">effacer les décisions enregistrées</a>"""
|
||||
else:
|
||||
erase_span = (
|
||||
"Cet étudiant n'a aucune décision enregistrée pour ce semestre."
|
||||
)
|
||||
|
||||
H.append(
|
||||
f"""
|
||||
<div class="but_section_annee">
|
||||
</div>
|
||||
<div><b>Unités d'enseignement de S{formsemestre.semestre_id}:</b></div>
|
||||
"""
|
||||
)
|
||||
if not ues:
|
||||
H.append(
|
||||
"""<div class="warning">Aucune UE ! Vérifiez votre programme de
|
||||
formation, et l'association UEs / Niveaux de compétences</div>"""
|
||||
)
|
||||
else:
|
||||
H.append(
|
||||
"""
|
||||
<div class="but_annee">
|
||||
<div class="titre"></div>
|
||||
<div class="titre"></div>
|
||||
<div class="titre"></div>
|
||||
<div class="titre"></div>
|
||||
"""
|
||||
)
|
||||
for ue in ues:
|
||||
dec_ue = decisions_ues[ue.id]
|
||||
H.append("""<div class="but_niveau_titre"><div></div></div>""")
|
||||
H.append(
|
||||
_gen_but_niveau_ue(
|
||||
ue,
|
||||
dec_ue,
|
||||
disabled=read_only,
|
||||
)
|
||||
)
|
||||
H.append(
|
||||
"""<div style=""></div>
|
||||
<div class=""></div>"""
|
||||
)
|
||||
H.append("</div>") # but_annee
|
||||
|
||||
div_autorisations_passage = (
|
||||
f"""
|
||||
<div class="but_autorisations_passage">
|
||||
<span>Autorisé à passer en :</span>
|
||||
{ ", ".join( ["S" + str(a.semestre_id or '') for a in autorisations_passage ] )}
|
||||
</div>
|
||||
"""
|
||||
if autorisations_passage
|
||||
else """<div class="but_autorisations_passage but_explanation">pas d'autorisations de passage enregistrées.</div>"""
|
||||
)
|
||||
H.append(div_autorisations_passage)
|
||||
|
||||
if read_only:
|
||||
H.append(
|
||||
f"""<div class="but_explanation">
|
||||
{"Vous n'avez pas la permission de modifier ces décisions."
|
||||
if formsemestre.etat
|
||||
else "Semestre verrouillé."}
|
||||
Les champs entourés en vert sont enregistrés.
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
else:
|
||||
if formsemestre.semestre_id < formsemestre.formation.get_cursus().NB_SEM:
|
||||
H.append(
|
||||
f"""
|
||||
<div class="but_settings">
|
||||
<input type="checkbox" name="autorisation_passage" value="1" {
|
||||
"checked" if est_autorise_a_passer else ""}>
|
||||
<em>autoriser à passer dans le semestre S{formsemestre.semestre_id+1}</em>
|
||||
</input>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
else:
|
||||
H.append("""<div class="help">dernier semestre de la formation.</div>""")
|
||||
H.append(
|
||||
f"""
|
||||
<div class="but_buttons">
|
||||
<span><input type="submit" value="Enregistrer ces décisions"></span>
|
||||
<span>{erase_span}</span>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
H.append(navigation_div)
|
||||
H.append("</div>")
|
||||
H.append(
|
||||
render_template(
|
||||
"but/documentation_codes_jury.j2",
|
||||
nom_univ=f"""Export {sco_preferences.get_preference("InstituteName")
|
||||
or sco_preferences.get_preference("UnivName")
|
||||
or "Apogée"}""",
|
||||
codes=ScoDocSiteConfig.get_codes_apo_dict(),
|
||||
)
|
||||
)
|
||||
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
# -------------
|
||||
def infos_fiche_etud_html(etudid: int) -> str:
|
||||
"""Section html pour fiche etudiant
|
||||
provisoire pour BUT 2022
|
||||
|
@ -162,11 +536,10 @@ def infos_fiche_etud_html(etudid: int) -> str:
|
|||
# temporaire quick & dirty: affiche le dernier
|
||||
try:
|
||||
deca = DecisionsProposeesAnnee(etud, formsemestres_but[-1])
|
||||
if len(deca.rcues_annee) > 0:
|
||||
return f"""<div class="infos_but">
|
||||
return f"""<div class="infos_but">
|
||||
{show_etud(deca, read_only=True)}
|
||||
</div>
|
||||
"""
|
||||
"""
|
||||
except ScoValueError:
|
||||
pass
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -18,21 +18,11 @@ import pandas as pd
|
|||
|
||||
from flask import g
|
||||
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
from app.scodoc.sco_codes_parcours import ParcoursDUT, ParcoursDUTMono
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.codes_cursus import CursusDUT, CursusDUTMono
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
def get_bonus_sport_class_from_name(dept_id):
|
||||
"""La classe de bonus sport pour le département indiqué.
|
||||
Note: en ScoDoc 9, le bonus sport est défini gloabelement et
|
||||
ne dépend donc pas du département.
|
||||
Résultat: une sous-classe de BonusSport
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class BonusSport:
|
||||
"""Calcul du bonus sport.
|
||||
|
||||
|
@ -65,7 +55,7 @@ class BonusSport:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
formsemestre: FormSemestre,
|
||||
formsemestre: "FormSemestre",
|
||||
sem_modimpl_moys: np.array,
|
||||
ues: list,
|
||||
modimpl_inscr_df: pd.DataFrame,
|
||||
|
@ -362,18 +352,37 @@ class BonusAisneStQuentin(BonusSportAdditif):
|
|||
|
||||
|
||||
class BonusAmiens(BonusSportAdditif):
|
||||
"""Bonus IUT Amiens pour les modules optionnels (sport, culture, ...).
|
||||
"""Bonus IUT Amiens pour les modules optionnels (sport, culture, ...)
|
||||
|
||||
Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,1 point
|
||||
<p><b>À partir d'août 2022:</b></p>
|
||||
<p>
|
||||
Deux activités optionnelles sont possibles chaque semestre, et peuvent donner lieu à une bonification de 0,1 chacune sur la moyenne de chaque UE.
|
||||
</p><p>
|
||||
La note saisie peut valoir 0 (pas de bonus), 1 (bonus de 0,1 points) ou 2 (bonus de 0,2 points).
|
||||
</p>
|
||||
|
||||
<p><b>Avant juillet 2022:</b></p>
|
||||
<p>Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,1 point
|
||||
sur toutes les moyennes d'UE.
|
||||
</p>
|
||||
"""
|
||||
|
||||
name = "bonus_amiens"
|
||||
displayed_name = "IUT d'Amiens"
|
||||
seuil_moy_gen = 0.0 # tous les points sont comptés
|
||||
proportion_point = 1e10
|
||||
bonus_max = 0.1
|
||||
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
||||
seuil_moy_gen = 0.0 # tous les points sont comptés
|
||||
|
||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||
"""calcul du bonus, avec réglage différent suivant la date"""
|
||||
|
||||
if self.formsemestre.date_debut > datetime.date(2022, 8, 1):
|
||||
self.proportion_point = 0.1
|
||||
self.bonus_max = 0.2
|
||||
else: # anciens semestres
|
||||
self.proportion_point = 1e10
|
||||
self.bonus_max = 0.1
|
||||
|
||||
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
||||
|
||||
|
||||
# Finalement ils n'en veulent pas.
|
||||
|
@ -421,6 +430,22 @@ class BonusAmiens(BonusSportAdditif):
|
|||
# )
|
||||
|
||||
|
||||
class BonusBesanconVesoul(BonusSportAdditif):
|
||||
"""Bonus IUT Besançon - Vesoul pour les UE libres
|
||||
|
||||
<p>Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,2 point
|
||||
sur toutes les moyennes d'UE.
|
||||
</p>
|
||||
"""
|
||||
|
||||
name = "bonus_besancon_vesoul"
|
||||
displayed_name = "IUT de Besançon - Vesoul"
|
||||
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
||||
seuil_moy_gen = 0.0 # tous les points sont comptés
|
||||
proportion_point = 1e10 # infini
|
||||
bonus_max = 0.2
|
||||
|
||||
|
||||
class BonusBethune(BonusSportMultiplicatif):
|
||||
"""
|
||||
Calcul bonus modules optionnels (sport, culture), règle IUT de Béthune.
|
||||
|
@ -638,7 +663,10 @@ class BonusCalais(BonusSportAdditif):
|
|||
dans la limite de 10 points. 6% de ces points cumulés s'ajoutent :
|
||||
<ul>
|
||||
<li><b>en DUT</b> à la moyenne générale du semestre déjà obtenue par l'étudiant.
|
||||
<li><b>en BUT et LP</b> à la moyenne des UE dont l'acronyme fini par <b>BS</b> (ex : UE2.1BS, UE32BS)
|
||||
</li>
|
||||
<li><b>en BUT et LP</b> à la moyenne des UE dont l'acronyme fini par <b>BS</b>
|
||||
(ex : UE2.1BS, UE32BS)
|
||||
</li>
|
||||
</ul>
|
||||
"""
|
||||
|
||||
|
@ -649,11 +677,11 @@ class BonusCalais(BonusSportAdditif):
|
|||
proportion_point = 0.06 # 6%
|
||||
|
||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||
parcours = self.formsemestre.formation.get_parcours()
|
||||
parcours = self.formsemestre.formation.get_cursus()
|
||||
# Variantes de DUT ?
|
||||
if (
|
||||
isinstance(parcours, ParcoursDUT)
|
||||
or parcours.TYPE_PARCOURS == ParcoursDUTMono.TYPE_PARCOURS
|
||||
isinstance(parcours, CursusDUT)
|
||||
or parcours.TYPE_CURSUS == CursusDUTMono.TYPE_CURSUS
|
||||
): # DUT
|
||||
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
||||
else:
|
||||
|
@ -808,7 +836,7 @@ class BonusLaRochelle(BonusSportAdditif):
|
|||
"""Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle.
|
||||
|
||||
<ul>
|
||||
<li>Si la note de sport est comprise entre 0 et 10 : pas d’ajout de point.</li>
|
||||
<li>Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point.</li>
|
||||
<li>Si la note de sport est comprise entre 10 et 20 :
|
||||
<ul>
|
||||
<li>Pour le BUT, application pour chaque UE du semestre :
|
||||
|
@ -868,15 +896,15 @@ class BonusLeHavre(BonusSportAdditif):
|
|||
<p>
|
||||
Les enseignements optionnels de langue, préprofessionnalisation,
|
||||
PIX (compétences numériques), l'entrepreneuriat étudiant, l'engagement
|
||||
bénévole au sein d’association dès lors qu’une grille d’évaluation des
|
||||
bénévole au sein d'association dès lors qu'une grille d'évaluation des
|
||||
compétences existe ainsi que les activités sportives et culturelles
|
||||
seront traités au niveau semestriel.
|
||||
</p><p>
|
||||
Le maximum de bonification qu’un étudiant peut obtenir sur sa moyenne
|
||||
Le maximum de bonification qu'un étudiant peut obtenir sur sa moyenne
|
||||
est plafonné à 0.5 point.
|
||||
</p><p>
|
||||
Lorsqu’un étudiant suit plus de deux matières qui donnent droit à
|
||||
bonification, l’étudiant choisit les deux notes à retenir.
|
||||
Lorsqu'un étudiant suit plus de deux matières qui donnent droit à
|
||||
bonification, l'étudiant choisit les deux notes à retenir.
|
||||
</p><p>
|
||||
Les points bonus ne sont acquis que pour une note supérieure à 10/20.
|
||||
</p><p>
|
||||
|
@ -885,7 +913,7 @@ class BonusLeHavre(BonusSportAdditif):
|
|||
Pour chaque matière (max. 2) donnant lieu à bonification :<br>
|
||||
|
||||
Bonification = (N-10) x 0,05,
|
||||
N étant la note de l’activité sur 20.
|
||||
N étant la note de l'activité sur 20.
|
||||
</p>
|
||||
"""
|
||||
|
||||
|
@ -1097,13 +1125,13 @@ class BonusOrleans(BonusSportAdditif):
|
|||
"""Calcul bonus modules optionnels (sport, culture), règle IUT d'Orléans
|
||||
<p><b>Cadre général :</b>
|
||||
En reconnaissance de l'engagement des étudiants dans la vie associative,
|
||||
sociale ou professionnelle, l’IUT d’Orléans accorde, sous conditions,
|
||||
sociale ou professionnelle, l'IUT d'Orléans accorde, sous conditions,
|
||||
une bonification aux étudiants inscrits qui en font la demande en début
|
||||
d’année universitaire.
|
||||
d'année universitaire.
|
||||
</p>
|
||||
<p>Cet engagement doit être régulier et correspondre à une activité réelle
|
||||
et sérieuse qui bénéficie à toute la communauté étudiante de l’IUT,
|
||||
de l’Université ou à l’ensemble de la collectivité.</p>
|
||||
et sérieuse qui bénéficie à toute la communauté étudiante de l'IUT,
|
||||
de l'Université ou à l'ensemble de la collectivité.</p>
|
||||
<p><b>Bonification :</b>
|
||||
Pour les DUT et LP, cette bonification interviendra sur la moyenne générale
|
||||
des semestres pairs :
|
||||
|
@ -1199,7 +1227,7 @@ class BonusStDenis(BonusSportAdditif):
|
|||
bonus_max = 0.5
|
||||
|
||||
|
||||
class BonusStNazaire(BonusSportMultiplicatif):
|
||||
class BonusStNazaire(BonusSport):
|
||||
"""IUT de Saint-Nazaire
|
||||
|
||||
Trois bonifications sont possibles : sport, culture et engagement citoyen
|
||||
|
@ -1221,9 +1249,37 @@ class BonusStNazaire(BonusSportMultiplicatif):
|
|||
name = "bonus_iutSN"
|
||||
displayed_name = "IUT de Saint-Nazaire"
|
||||
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
||||
seuil_moy_gen = 0.0 # tous les points comptent
|
||||
amplitude = 0.01 / 4 # 4pt => 1%
|
||||
factor_max = 0.1 # 10% max
|
||||
# Modifié 2022-11-29: calculer chaque bonus
|
||||
# (de 1 à 3 modules) séparément.
|
||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||
"""Calcul du bonus St Nazaire 2022
|
||||
sem_modimpl_moys_inscrits: les notes de sport
|
||||
En APC: ndarray (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
|
||||
En classic: ndarray (nb_etuds, nb_mod_sport)
|
||||
"""
|
||||
if 0 in sem_modimpl_moys_inscrits.shape:
|
||||
# pas d'étudiants ou pas d'UE ou pas de module...
|
||||
return
|
||||
# Prend les 3 premiers bonus trouvés
|
||||
# ignore les coefficients
|
||||
bonus_mod_moys = sem_modimpl_moys_inscrits[:, :3]
|
||||
bonus_mod_moys = np.nan_to_num(bonus_mod_moys, copy=False)
|
||||
factor = bonus_mod_moys * self.amplitude
|
||||
# somme les bonus:
|
||||
factor = factor.sum(axis=1)
|
||||
# et limite à 10%:
|
||||
factor.clip(0.0, self.factor_max, out=factor)
|
||||
|
||||
# Applique aux moyennes d'UE
|
||||
if len(factor.shape) == 1: # classic
|
||||
factor = factor[:, np.newaxis]
|
||||
bonus = self.etud_moy_ue * factor
|
||||
self.bonus_ues = bonus # DataFrame
|
||||
|
||||
# Les bonus multiplicatifs ne s'appliquent pas à la moyenne générale
|
||||
self.bonus_moy_gen = None
|
||||
|
||||
|
||||
class BonusTarbes(BonusIUTRennes1):
|
||||
|
@ -1302,7 +1358,45 @@ class BonusIUTvannes(BonusSportAdditif):
|
|||
classic_use_bonus_ues = False # seulement sur moy gen.
|
||||
|
||||
|
||||
class BonusVilleAvray(BonusSport):
|
||||
class BonusValenciennes(BonusDirect):
|
||||
"""Article 7 des RCC de l'IUT de Valenciennes
|
||||
|
||||
<p>
|
||||
Une bonification maximale de 0.25 point (1/4 de point) peut être ajoutée
|
||||
à la moyenne de chaque Unité d'Enseignement pour :
|
||||
</p>
|
||||
<ul>
|
||||
<li>l'engagement citoyen ;</li>
|
||||
<li>la participation à un module de sport.</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Une bonification accordée par la commission des sports de l'UPHF peut être attribuée
|
||||
aux sportifs de haut niveau. Cette bonification est appliquée à l'ensemble des
|
||||
Unités d'Enseignement. Ce bonus est :
|
||||
</p>
|
||||
<ul>
|
||||
<li> 0.5 pour la catégorie <em>or</em> (sportif inscrit sur liste ministérielle
|
||||
jeunesse et sport) ;
|
||||
</li>
|
||||
<li> 0.45 pour la catégorie <em>argent</em> (sportif en club professionnel) ;
|
||||
</li>
|
||||
<li> 0.40 pour le <em>bronze</em> (sportif de niveau départemental, régional ou national).
|
||||
</li>
|
||||
</ul>
|
||||
<p>Le cumul de bonifications est possible mais ne peut excéder 0.5 point (un demi-point).
|
||||
</p>
|
||||
<p><em>Dans ScoDoc, saisir directement la valeur désirée du bonus
|
||||
dans une évaluation notée sur 20.</em>
|
||||
</p>
|
||||
"""
|
||||
|
||||
name = "bonus_valenciennes"
|
||||
displayed_name = "IUT de Valenciennes"
|
||||
bonus_max = 0.5
|
||||
|
||||
|
||||
class BonusVilleAvray(BonusSportAdditif):
|
||||
"""Bonus modules optionnels (sport, culture), règle IUT Ville d'Avray.
|
||||
|
||||
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
|
||||
|
@ -1351,7 +1445,7 @@ class BonusIUTV(BonusSportAdditif):
|
|||
|
||||
name = "bonus_iutv"
|
||||
displayed_name = "IUT de Villetaneuse"
|
||||
pass # oui, c'est le bonus par défaut
|
||||
# c'est le bonus par défaut: aucune méthode à surcharger
|
||||
|
||||
|
||||
def get_bonus_class_dict(start=BonusSport, d=None):
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -9,10 +9,10 @@
|
|||
import pandas as pd
|
||||
|
||||
from app import db
|
||||
from app.models import FormSemestre, ScolarFormSemestreValidation, UniteEns
|
||||
from app.models import FormSemestre, Identite, ScolarFormSemestreValidation, UniteEns
|
||||
from app.comp.res_cache import ResultatsCache
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import codes_cursus
|
||||
|
||||
|
||||
class ValidationsSemestre(ResultatsCache):
|
||||
|
@ -53,7 +53,7 @@ class ValidationsSemestre(ResultatsCache):
|
|||
self.comp_decisions_jury()
|
||||
|
||||
def comp_decisions_jury(self):
|
||||
"""Cherche les decisions du jury pour le semestre (pas les UE).
|
||||
"""Cherche les decisions du jury pour le semestre (pas les RCUE).
|
||||
Calcule les attributs:
|
||||
decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}
|
||||
decision_jury_ues={ etudid :
|
||||
|
@ -89,7 +89,7 @@ class ValidationsSemestre(ResultatsCache):
|
|||
if decision.etudid not in decisions_jury_ues:
|
||||
decisions_jury_ues[decision.etudid] = {}
|
||||
# Calcul des ECTS associés à cette UE:
|
||||
if sco_codes_parcours.code_ue_validant(decision.code) and decision.ue:
|
||||
if codes_cursus.code_ue_validant(decision.code) and decision.ue:
|
||||
ects = decision.ue.ects or 0.0 # 0 if None
|
||||
else:
|
||||
ects = 0.0
|
||||
|
@ -102,6 +102,12 @@ class ValidationsSemestre(ResultatsCache):
|
|||
|
||||
self.decisions_jury_ues = decisions_jury_ues
|
||||
|
||||
def has_decision(self, etud: Identite) -> bool:
|
||||
"""Vrai si etud a au moins une décision enregistrée depuis
|
||||
ce semestre (quelle qu'elle soit)
|
||||
"""
|
||||
return (etud.id in self.decisions_jury_ues) or (etud.id in self.decisions_jury)
|
||||
|
||||
|
||||
def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame:
|
||||
"""Liste des UE capitalisées (ADM) utilisables dans ce formsemestre
|
||||
|
@ -122,6 +128,10 @@ def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame
|
|||
event_date :
|
||||
} ]
|
||||
"""
|
||||
|
||||
# Note: pour récupérer aussi les UE validées en CMp ou ADJ, changer une ligne
|
||||
# and ( SFV.code = 'ADM' or SFV.code = 'ADJ' or SFV.code = 'CMP' )
|
||||
|
||||
query = """
|
||||
SELECT DISTINCT SFV.*, ue.ue_code
|
||||
FROM
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -14,7 +14,7 @@ import pandas as pd
|
|||
from app.comp import moy_ue
|
||||
from app.models.formsemestre import FormSemestre
|
||||
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -35,14 +35,16 @@ moyenne générale d'une UE.
|
|||
"""
|
||||
import dataclasses
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app.models import ModuleImpl, Evaluation, EvaluationUEPoids
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
from app.models import Evaluation, EvaluationUEPoids, ModuleImpl
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.sco_exceptions import ScoBugCatcher
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
@ -83,6 +85,8 @@ class ModuleImplResults:
|
|||
"{ evaluation.id : bool } indique si à prendre en compte ou non."
|
||||
self.evaluations_etat = {}
|
||||
"{ evaluation_id: EvaluationEtat }"
|
||||
self.etudids_attente = set()
|
||||
"etudids avec au moins une note ATT dans ce module"
|
||||
self.en_attente = False
|
||||
"Vrai si au moins une évaluation a une note en attente"
|
||||
#
|
||||
|
@ -143,7 +147,6 @@ class ModuleImplResults:
|
|||
evals_notes = pd.DataFrame(index=self.etudids, dtype=float)
|
||||
self.evaluations_completes = []
|
||||
self.evaluations_completes_dict = {}
|
||||
self.en_attente = False
|
||||
for evaluation in moduleimpl.evaluations:
|
||||
eval_df = self._load_evaluation_notes(evaluation)
|
||||
# is_complete ssi tous les inscrits (non dem) au semestre ont une note
|
||||
|
@ -170,15 +173,20 @@ class ModuleImplResults:
|
|||
eval_df, how="left", left_index=True, right_index=True
|
||||
)
|
||||
# Notes en attente: (ne prend en compte que les inscrits, non démissionnaires)
|
||||
nb_att = sum(
|
||||
evals_notes[str(evaluation.id)][list(inscrits_module)]
|
||||
== scu.NOTES_ATTENTE
|
||||
eval_notes_inscr = evals_notes[str(evaluation.id)][list(inscrits_module)]
|
||||
eval_etudids_attente = set(
|
||||
eval_notes_inscr.iloc[
|
||||
(eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy()
|
||||
].index
|
||||
)
|
||||
self.etudids_attente |= eval_etudids_attente
|
||||
self.evaluations_etat[evaluation.id] = EvaluationEtat(
|
||||
evaluation_id=evaluation.id, nb_attente=nb_att, is_complete=is_complete
|
||||
evaluation_id=evaluation.id,
|
||||
nb_attente=len(eval_etudids_attente),
|
||||
is_complete=is_complete,
|
||||
)
|
||||
if nb_att > 0:
|
||||
self.en_attente = True
|
||||
# au moins une note en ATT dans ce modimpl:
|
||||
self.en_attente = bool(self.etudids_attente)
|
||||
|
||||
# Force columns names to integers (evaluation ids)
|
||||
evals_notes.columns = pd.Index([int(x) for x in evals_notes.columns], dtype=int)
|
||||
|
@ -217,12 +225,19 @@ class ModuleImplResults:
|
|||
]
|
||||
|
||||
def get_evaluations_coefs(self, moduleimpl: ModuleImpl) -> np.array:
|
||||
"""Coefficients des évaluations, met à zéro ceux des évals incomplètes.
|
||||
"""Coefficients des évaluations.
|
||||
Les coefs des évals incomplètes et non "normales" (session 2, rattrapage)
|
||||
sont zéro.
|
||||
Résultat: 2d-array of floats, shape (nb_evals, 1)
|
||||
"""
|
||||
return (
|
||||
np.array(
|
||||
[e.coefficient for e in moduleimpl.evaluations],
|
||||
[
|
||||
e.coefficient
|
||||
if e.evaluation_type == scu.EVALUATION_NORMALE
|
||||
else 0.0
|
||||
for e in moduleimpl.evaluations
|
||||
],
|
||||
dtype=float,
|
||||
)
|
||||
* self.evaluations_completes
|
||||
|
@ -236,8 +251,8 @@ class ModuleImplResults:
|
|||
]
|
||||
|
||||
def get_eval_notes_sur_20(self, moduleimpl: ModuleImpl) -> np.array:
|
||||
"""Les notes des évaluations,
|
||||
remplace les ATT, EXC, ABS, NaN par zéro et mets les notes sur 20.
|
||||
"""Les notes de toutes les évaluations du module, complètes ou non.
|
||||
Remplace les ATT, EXC, ABS, NaN par zéro et mets les notes sur 20.
|
||||
Résultat: 2d array of floats, shape nb_etuds x nb_evaluations
|
||||
"""
|
||||
return np.where(
|
||||
|
@ -368,7 +383,7 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||
etuds_moy_module = np.where(
|
||||
etuds_use_rattrapage, notes_rat_ues, etuds_moy_module
|
||||
)
|
||||
# Serie indiquant que l'étudiant utilise une note de rattarage sur l'une des UE:
|
||||
# Serie indiquant que l'étudiant utilise une note de rattrapage sur l'une des UE:
|
||||
self.etuds_use_rattrapage = pd.Series(
|
||||
etuds_use_rattrapage.any(axis=1), index=self.evals_notes.index
|
||||
)
|
||||
|
@ -429,7 +444,7 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
|||
|
||||
|
||||
def moduleimpl_is_conforme(
|
||||
moduleimpl, evals_poids: pd.DataFrame, modules_coefficients: pd.DataFrame
|
||||
moduleimpl, evals_poids: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
|
||||
) -> bool:
|
||||
"""Vérifie que les évaluations de ce moduleimpl sont bien conformes
|
||||
au PN.
|
||||
|
@ -438,7 +453,7 @@ def moduleimpl_is_conforme(
|
|||
|
||||
Arguments:
|
||||
evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
|
||||
modules_coefficients: DataFrame, cols module_id, lignes UEs
|
||||
modimpl_coefs_df: DataFrame, cols: modimpl_id, lignes: UEs du formsemestre
|
||||
NB: les UEs dans evals_poids sont sans le bonus sport
|
||||
"""
|
||||
nb_evals, nb_ues = evals_poids.shape
|
||||
|
@ -446,18 +461,18 @@ def moduleimpl_is_conforme(
|
|||
return True # modules vides conformes
|
||||
if nb_ues == 0:
|
||||
return False # situation absurde (pas d'UE)
|
||||
if len(modules_coefficients) != nb_ues:
|
||||
if len(modimpl_coefs_df) != nb_ues:
|
||||
# il arrive (#bug) que le cache ne soit pas à jour...
|
||||
sco_cache.invalidate_formsemestre()
|
||||
raise ScoBugCatcher("moduleimpl_is_conforme: nb ue incoherent")
|
||||
|
||||
if moduleimpl.module_id not in modules_coefficients:
|
||||
if moduleimpl.id not in modimpl_coefs_df:
|
||||
# soupçon de bug cache coef ?
|
||||
sco_cache.invalidate_formsemestre()
|
||||
raise ScoBugCatcher("Erreur 454 - merci de ré-essayer")
|
||||
|
||||
module_evals_poids = evals_poids.transpose().sum(axis=1) != 0
|
||||
return all((modules_coefficients[moduleimpl.module_id] != 0).eq(module_evals_poids))
|
||||
return all((modimpl_coefs_df[moduleimpl.id] != 0).eq(module_evals_poids))
|
||||
|
||||
|
||||
class ModuleImplResultsClassic(ModuleImplResults):
|
||||
|
@ -476,7 +491,8 @@ class ModuleImplResultsClassic(ModuleImplResults):
|
|||
if nb_etuds == 0:
|
||||
return pd.Series()
|
||||
evals_coefs = self.get_evaluations_coefs(modimpl).reshape(-1)
|
||||
assert evals_coefs.shape == (nb_evals,)
|
||||
if evals_coefs.shape != (nb_evals,):
|
||||
app.critical_error("compute_module_moy: vals_coefs.shape != nb_evals")
|
||||
evals_notes_20 = self.get_eval_notes_sur_20(modimpl)
|
||||
# Les coefs des évals pour chaque étudiant: là où il a des notes
|
||||
# non neutralisées
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -52,13 +52,16 @@ def compute_sem_moys_apc_using_coefs(
|
|||
|
||||
|
||||
def compute_sem_moys_apc_using_ects(
|
||||
etud_moy_ue_df: pd.DataFrame, ects: list, formation_id=None, skip_empty_ues=False
|
||||
etud_moy_ue_df: pd.DataFrame,
|
||||
ects_df: pd.DataFrame,
|
||||
formation_id=None,
|
||||
skip_empty_ues=False,
|
||||
) -> pd.Series:
|
||||
"""Calcule les moyennes générales indicatives de tous les étudiants
|
||||
= moyenne des moyennes d'UE, pondérée par leurs ECTS.
|
||||
|
||||
etud_moy_ue_df: DataFrame, colonnes ue_id, lignes etudid
|
||||
ects: liste de floats ou None, 1 par UE
|
||||
ects: DataFrame, col. ue_id, lignes etudid, valeur float ou None
|
||||
|
||||
Si skip_empty_ues: ne compte pas les UE non notées.
|
||||
Sinon (par défaut), une UE non notée compte comme zéro.
|
||||
|
@ -68,11 +71,11 @@ def compute_sem_moys_apc_using_ects(
|
|||
try:
|
||||
if skip_empty_ues:
|
||||
# annule les coefs des UE sans notes (NaN)
|
||||
ects = np.where(etud_moy_ue_df.isna(), 0.0, np.array(ects, dtype=float))
|
||||
# ects est devenu nb_etuds x nb_ues
|
||||
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / ects.sum(axis=1)
|
||||
ects = np.where(etud_moy_ue_df.isna(), 0.0, ects_df.to_numpy())
|
||||
else:
|
||||
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / sum(ects)
|
||||
ects = ects_df.to_numpy()
|
||||
# ects est maintenant un array nb_etuds x nb_ues
|
||||
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / ects.sum(axis=1)
|
||||
except TypeError:
|
||||
if None in ects:
|
||||
formation = Formation.query.get(formation_id)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -32,12 +32,17 @@ import pandas as pd
|
|||
|
||||
from app import db
|
||||
from app import models
|
||||
from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef
|
||||
from app.models import (
|
||||
FormSemestre,
|
||||
Module,
|
||||
ModuleImpl,
|
||||
ModuleUECoef,
|
||||
UniteEns,
|
||||
)
|
||||
from app.comp import moy_mod
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
|
@ -56,7 +61,7 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
|
|||
"""
|
||||
ues = (
|
||||
UniteEns.query.filter_by(formation_id=formation_id)
|
||||
.filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
|
||||
.filter(UniteEns.type != codes_cursus.UE_SPORT)
|
||||
.order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme)
|
||||
)
|
||||
modules = (
|
||||
|
@ -64,14 +69,9 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
|
|||
.filter(
|
||||
(Module.module_type == ModuleType.RESSOURCE)
|
||||
| (Module.module_type == ModuleType.SAE)
|
||||
| (
|
||||
(Module.ue_id == UniteEns.id)
|
||||
& (UniteEns.type == sco_codes_parcours.UE_SPORT)
|
||||
)
|
||||
)
|
||||
.order_by(
|
||||
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
|
||||
| ((Module.ue_id == UniteEns.id) & (UniteEns.type == codes_cursus.UE_SPORT))
|
||||
)
|
||||
.order_by(Module.semestre_id, Module.module_type, Module.numero, Module.code)
|
||||
)
|
||||
if semestre_idx is not None:
|
||||
ues = ues.filter_by(semestre_idx=semestre_idx)
|
||||
|
@ -140,7 +140,8 @@ def df_load_modimpl_coefs(
|
|||
mod_coef.ue_id
|
||||
] = mod_coef.coef
|
||||
except IndexError:
|
||||
# il peut y avoir en base des coefs sur des modules ou UE qui ont depuis été retirés de la formation
|
||||
# il peut y avoir en base des coefs sur des modules ou UE
|
||||
# qui ont depuis été retirés de la formation
|
||||
pass
|
||||
# Initialisation des poids non fixés:
|
||||
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
|
||||
|
@ -199,7 +200,7 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
|
|||
modimpls_results[modimpl.id] = mod_results
|
||||
modimpls_evals_poids[modimpl.id] = evals_poids
|
||||
modimpls_notes.append(etuds_moy_module)
|
||||
if len(modimpls_notes):
|
||||
if len(modimpls_notes) > 0:
|
||||
cube = notes_sem_assemble_cube(modimpls_notes)
|
||||
else:
|
||||
nb_etuds = formsemestre.etuds.count()
|
||||
|
@ -211,14 +212,40 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
|
|||
)
|
||||
|
||||
|
||||
def load_dispense_ues(
|
||||
formsemestre: FormSemestre, etudids: pd.Index, ues: list[UniteEns]
|
||||
) -> set[tuple[int, int]]:
|
||||
"""Construit l'ensemble des
|
||||
etudids = modimpl_inscr_df.index, # les etudids
|
||||
ue_ids : modimpl_coefs_df.index, # les UE du formsemestre sans les UE bonus sport
|
||||
|
||||
Résultat: set de (etudid, ue_id).
|
||||
"""
|
||||
dispense_ues = set()
|
||||
ue_sem_by_code = {ue.ue_code: ue for ue in ues}
|
||||
# Prend toutes les dispenses obtenues par des étudiants de ce formsemestre,
|
||||
# puis filtre sur inscrits et code d'UE UE
|
||||
for dispense_ue in DispenseUE.query.join(
|
||||
Identite, FormSemestreInscription
|
||||
).filter_by(formsemestre_id=formsemestre.id):
|
||||
if dispense_ue.etudid in etudids:
|
||||
# UE dans le semestre avec même code ?
|
||||
ue = ue_sem_by_code.get(dispense_ue.ue.ue_code)
|
||||
if ue is not None:
|
||||
dispense_ues.add((dispense_ue.etudid, ue.id))
|
||||
|
||||
return dispense_ues
|
||||
|
||||
|
||||
def compute_ue_moys_apc(
|
||||
sem_cube: np.array,
|
||||
etuds: list,
|
||||
modimpls: list,
|
||||
ues: list,
|
||||
modimpl_inscr_df: pd.DataFrame,
|
||||
modimpl_coefs_df: pd.DataFrame,
|
||||
modimpl_mask: np.array,
|
||||
dispense_ues: set[tuple[int, int]],
|
||||
block: bool = False,
|
||||
) -> pd.DataFrame:
|
||||
"""Calcul de la moyenne d'UE en mode APC (BUT).
|
||||
La moyenne d'UE est un nombre (note/20), ou NaN si pas de notes disponibles
|
||||
|
@ -229,18 +256,17 @@ def compute_ue_moys_apc(
|
|||
etuds : liste des étudiants (dim. 0 du cube)
|
||||
modimpls : liste des module_impl (dim. 1 du cube)
|
||||
ues : liste des UE (dim. 2 du cube)
|
||||
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
|
||||
modimpl_inscr_df: matrice d'inscription aux modules du semestre (etud x modimpl)
|
||||
modimpl_coefs_df: matrice coefficients (UE x modimpl), sans UEs bonus sport
|
||||
modimpl_mask: liste de booléens, indiquants le module doit être pris ou pas.
|
||||
(utilisé pour éliminer les bonus, et pourra servir à cacluler
|
||||
sur des sous-ensembles de modules)
|
||||
|
||||
block: si vrai, ne calcule rien et renvoie des NaNs
|
||||
Résultat: DataFrame columns UE (sans bonus), rows etudid
|
||||
"""
|
||||
nb_etuds, nb_modules, nb_ues_no_bonus = sem_cube.shape
|
||||
nb_ues_tot = len(ues)
|
||||
assert len(modimpls) == nb_modules
|
||||
if nb_modules == 0 or nb_etuds == 0 or nb_ues_no_bonus == 0:
|
||||
if block or nb_modules == 0 or nb_etuds == 0 or nb_ues_no_bonus == 0:
|
||||
return pd.DataFrame(
|
||||
index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
|
||||
)
|
||||
|
@ -277,11 +303,16 @@ def compute_ue_moys_apc(
|
|||
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_df = pd.DataFrame(
|
||||
etud_moy_ue,
|
||||
index=modimpl_inscr_df.index, # les etudids
|
||||
columns=modimpl_coefs_df.index, # les UE sans les UE bonus sport
|
||||
)
|
||||
# Les "dispenses" sont très peu nombreuses et traitées en python:
|
||||
for dispense_ue in dispense_ues:
|
||||
etud_moy_ue_df[dispense_ue[1]][dispense_ue[0]] = 0.0
|
||||
|
||||
return etud_moy_ue_df
|
||||
|
||||
|
||||
def compute_ue_moys_classic(
|
||||
|
@ -291,6 +322,7 @@ def compute_ue_moys_classic(
|
|||
modimpl_inscr_df: pd.DataFrame,
|
||||
modimpl_coefs: np.array,
|
||||
modimpl_mask: np.array,
|
||||
block: bool = False,
|
||||
) -> tuple[pd.Series, pd.DataFrame, pd.DataFrame]:
|
||||
"""Calcul de la moyenne d'UE et de la moy. générale en mode classique (DUT, LMD, ...).
|
||||
|
||||
|
@ -312,6 +344,7 @@ def compute_ue_moys_classic(
|
|||
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
|
||||
modimpl_coefs: vecteur des coefficients de modules
|
||||
modimpl_mask: masque des modimpls à prendre en compte
|
||||
block: si vrai, ne calcule rien et renvoie des NaNs
|
||||
|
||||
Résultat:
|
||||
- moyennes générales: pd.Series, index etudid
|
||||
|
@ -320,13 +353,14 @@ def compute_ue_moys_classic(
|
|||
les coefficients effectifs de chaque UE pour chaque étudiant
|
||||
(sommes de coefs de modules pris en compte)
|
||||
"""
|
||||
if (not len(modimpl_mask)) or (
|
||||
sem_matrix.shape[0] == 0
|
||||
if (
|
||||
block or (len(modimpl_mask) == 0) or (sem_matrix.shape[0] == 0)
|
||||
): # aucun module ou aucun étudiant
|
||||
# etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
|
||||
val = np.nan if block else 0.0
|
||||
return (
|
||||
pd.Series(
|
||||
[0.0] * len(modimpl_inscr_df.index), index=modimpl_inscr_df.index
|
||||
[val] * len(modimpl_inscr_df.index), index=modimpl_inscr_df.index
|
||||
),
|
||||
pd.DataFrame(columns=[ue.id for ue in ues], index=modimpl_inscr_df.index),
|
||||
pd.DataFrame(columns=[ue.id for ue in ues], index=modimpl_inscr_df.index),
|
||||
|
@ -431,7 +465,7 @@ def compute_mat_moys_classic(
|
|||
Résultat:
|
||||
- moyennes: pd.Series, index etudid
|
||||
"""
|
||||
if (not len(modimpl_mask)) or (
|
||||
if (0 == len(modimpl_mask)) or (
|
||||
sem_matrix.shape[0] == 0
|
||||
): # aucun module ou aucun étudiant
|
||||
# etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
|
||||
|
@ -462,9 +496,10 @@ def compute_mat_moys_classic(
|
|||
if modimpl_coefs_etuds_no_nan.dtype == object: # arrive sur des tableaux vides
|
||||
modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
|
||||
|
||||
etud_moy_mat = (modimpl_coefs_etuds_no_nan * sem_matrix_inscrits).sum(
|
||||
axis=1
|
||||
) / modimpl_coefs_etuds_no_nan.sum(axis=1)
|
||||
with np.errstate(invalid="ignore"): # il peut y avoir des NaN
|
||||
etud_moy_mat = (modimpl_coefs_etuds_no_nan * sem_matrix_inscrits).sum(
|
||||
axis=1
|
||||
) / modimpl_coefs_etuds_no_nan.sum(axis=1)
|
||||
|
||||
return pd.Series(etud_moy_mat, index=modimpl_inscr_df.index)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -16,9 +16,10 @@ from app.comp.res_compat import NotesTableCompat
|
|||
from app.comp.bonus_spo import BonusSport
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
from app.models.ues import UniteEns
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
from app.models.ues import DispenseUE, UniteEns
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
class ResultatsSemestreBUT(NotesTableCompat):
|
||||
|
@ -39,6 +40,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||
"""ndarray (etuds x modimpl x ue)"""
|
||||
self.etuds_parcour_id = None
|
||||
"""Parcours de chaque étudiant { etudid : parcour_id }"""
|
||||
|
||||
if not self.load_cached():
|
||||
t0 = time.time()
|
||||
self.compute()
|
||||
|
@ -71,15 +73,18 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||
modimpl.module.ue.type != UE_SPORT
|
||||
for modimpl in self.formsemestre.modimpls_sorted
|
||||
]
|
||||
|
||||
self.dispense_ues = DispenseUE.load_formsemestre_dispense_ues_set(
|
||||
self.formsemestre, self.modimpl_inscr_df.index, self.ues
|
||||
)
|
||||
self.etud_moy_ue = moy_ue.compute_ue_moys_apc(
|
||||
self.sem_cube,
|
||||
self.etuds,
|
||||
self.formsemestre.modimpls_sorted,
|
||||
self.ues,
|
||||
self.modimpl_inscr_df,
|
||||
self.modimpl_coefs_df,
|
||||
modimpls_mask,
|
||||
self.dispense_ues,
|
||||
block=self.formsemestre.block_moyennes,
|
||||
)
|
||||
# Les coefficients d'UE ne sont pas utilisés en APC
|
||||
self.etud_coef_ue_df = pd.DataFrame(
|
||||
|
@ -114,6 +119,10 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||
|
||||
# Nanifie les moyennes d'UE hors parcours pour chaque étudiant
|
||||
self.etud_moy_ue *= self.ues_inscr_parcours_df
|
||||
# Les ects (utilisés comme coefs) sont nuls pour les UE hors parcours:
|
||||
ects = self.ues_inscr_parcours_df.fillna(0.0) * [
|
||||
ue.ects for ue in self.ues if ue.type != UE_SPORT
|
||||
]
|
||||
|
||||
# Moyenne générale indicative:
|
||||
# (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte
|
||||
|
@ -121,14 +130,19 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||
# self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_coefs(
|
||||
# self.etud_moy_ue, self.modimpl_coefs_df
|
||||
# )
|
||||
self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_ects(
|
||||
self.etud_moy_ue,
|
||||
[ue.ects for ue in self.ues if ue.type != UE_SPORT],
|
||||
formation_id=self.formsemestre.formation_id,
|
||||
skip_empty_ues=sco_preferences.get_preference(
|
||||
"but_moy_skip_empty_ues", self.formsemestre.id
|
||||
),
|
||||
)
|
||||
if self.formsemestre.block_moyenne_generale or self.formsemestre.block_moyennes:
|
||||
self.etud_moy_gen = pd.Series(
|
||||
index=self.etud_moy_ue.index, dtype=float
|
||||
) # NaNs
|
||||
else:
|
||||
self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_ects(
|
||||
self.etud_moy_ue,
|
||||
ects,
|
||||
formation_id=self.formsemestre.formation_id,
|
||||
skip_empty_ues=sco_preferences.get_preference(
|
||||
"but_moy_skip_empty_ues", self.formsemestre.id
|
||||
),
|
||||
)
|
||||
# --- UE capitalisées
|
||||
self.apply_capitalisation()
|
||||
|
||||
|
@ -145,6 +159,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||
# moyenne sur les UE:
|
||||
if len(self.sem_cube[etud_idx, mod_idx]):
|
||||
return np.nanmean(self.sem_cube[etud_idx, mod_idx])
|
||||
# note: si toutes les valeurs sont nan, on va déclencher ici
|
||||
# un RuntimeWarning: Mean of empty slice
|
||||
return np.nan
|
||||
|
||||
def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float:
|
||||
|
@ -172,9 +188,15 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||
modimpls = [
|
||||
modimpl
|
||||
for modimpl in self.formsemestre.modimpls_sorted
|
||||
if modimpl.module.ue.type != UE_SPORT
|
||||
and (coefs[modimpl.id][ue.id] != 0)
|
||||
and self.modimpl_inscr_df[modimpl.id][etudid]
|
||||
if (
|
||||
modimpl.module.ue.type != UE_SPORT
|
||||
and (coefs[modimpl.id][ue.id] != 0)
|
||||
and self.modimpl_inscr_df[modimpl.id][etudid]
|
||||
)
|
||||
or (
|
||||
modimpl.module.module_type == ModuleType.MALUS
|
||||
and modimpl.module.ue_id == ue.id
|
||||
)
|
||||
]
|
||||
if not with_bonus:
|
||||
return [
|
||||
|
@ -204,27 +226,33 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||
}
|
||||
self.etuds_parcour_id = etuds_parcour_id
|
||||
ue_ids = [ue.id for ue in self.ues if ue.type != UE_SPORT]
|
||||
# matrice de 1, inscrits par défaut à toutes les UE:
|
||||
ues_inscr_parcours_df = pd.DataFrame(
|
||||
1.0, index=etuds_parcour_id.keys(), columns=ue_ids, dtype=float
|
||||
)
|
||||
if self.formsemestre.formation.referentiel_competence is None:
|
||||
return ues_inscr_parcours_df
|
||||
|
||||
if self.formsemestre.formation.referentiel_competence is None:
|
||||
return pd.DataFrame(
|
||||
1.0, index=etuds_parcour_id.keys(), columns=ue_ids, dtype=float
|
||||
)
|
||||
# matrice de NaN: inscrits par défaut à AUCUNE UE:
|
||||
ues_inscr_parcours_df = pd.DataFrame(
|
||||
np.nan, index=etuds_parcour_id.keys(), columns=ue_ids, dtype=float # XXX
|
||||
)
|
||||
# Construit pour chaque parcours du référentiel l'ensemble de ses UE
|
||||
# (considère aussi le cas des semestres sans parcours: None)
|
||||
ue_by_parcours = {} # parcours_id : {ue_id:0|1}
|
||||
for parcour in self.formsemestre.formation.referentiel_competence.parcours:
|
||||
ue_by_parcours[parcour.id] = {
|
||||
for (
|
||||
parcour
|
||||
) in self.formsemestre.formation.referentiel_competence.parcours.all() + [None]:
|
||||
ue_by_parcours[None if parcour is None else parcour.id] = {
|
||||
ue.id: 1.0
|
||||
for ue in self.formsemestre.formation.query_ues_parcour(
|
||||
parcour
|
||||
).filter_by(semestre_idx=self.formsemestre.semestre_id)
|
||||
}
|
||||
#
|
||||
for etudid in etuds_parcour_id:
|
||||
parcour = etuds_parcour_id[etudid]
|
||||
if parcour is not None:
|
||||
ues_inscr_parcours_df.loc[etudid] = ue_by_parcours[
|
||||
etuds_parcour_id[etudid]
|
||||
]
|
||||
parcour_id = etuds_parcour_id[etudid]
|
||||
if parcour_id in ue_by_parcours:
|
||||
if ue_by_parcours[parcour_id]:
|
||||
ues_inscr_parcours_df.loc[etudid] = ue_by_parcours[parcour_id]
|
||||
return ues_inscr_parcours_df
|
||||
|
||||
def etud_ues_ids(self, etudid: int) -> list[int]:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -22,7 +22,7 @@ from app.models import ScoDocSiteConfig
|
|||
from app.models.etudiants import Identite
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.ues import UniteEns
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
@ -90,6 +90,7 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
|||
self.modimpl_inscr_df,
|
||||
self.modimpl_coefs,
|
||||
modimpl_standards_mask,
|
||||
block=self.formsemestre.block_moyennes,
|
||||
)
|
||||
# --- Modules de MALUS sur les UEs et la moyenne générale
|
||||
self.malus = moy_ue.compute_malus(
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -14,11 +14,10 @@ from app import log
|
|||
from app.comp import moy_sem
|
||||
from app.comp.aux_stats import StatsMoyenne
|
||||
from app.comp.res_common import ResultatsSemestre
|
||||
from app.comp import res_sem
|
||||
from app.models import FormSemestre
|
||||
from app.models import Identite
|
||||
from app.models import ModuleImpl
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
|
||||
from app.scodoc.codes_cursus import UE_SPORT, DEF
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
# Pour raccorder le code des anciens codes qui attendent une NoteTable
|
||||
|
@ -26,7 +25,7 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
"""Implementation partielle de NotesTable
|
||||
|
||||
Les méthodes définies dans cette classe sont là
|
||||
pour conserver la compatibilité abvec les codes anciens et
|
||||
pour conserver la compatibilité avec les codes anciens et
|
||||
il n'est pas recommandé de les utiliser dans de nouveaux
|
||||
développements (API malcommode et peu efficace).
|
||||
"""
|
||||
|
@ -53,7 +52,7 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
self.moy_gen_rangs_by_group = {} # { group_id : (Series, Series) }
|
||||
self.ue_rangs_by_group = {} # { ue_id : {group_id : (Series, Series)}}
|
||||
self.expr_diagnostics = ""
|
||||
self.parcours = self.formsemestre.formation.get_parcours()
|
||||
self.parcours = self.formsemestre.formation.get_cursus()
|
||||
self._modimpls_dict_by_ue = {} # local cache
|
||||
|
||||
def get_inscrits(self, include_demdef=True, order_by=False) -> list[Identite]:
|
||||
|
@ -104,10 +103,9 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
"""Stats (moy/min/max) sur la moyenne générale"""
|
||||
return StatsMoyenne(self.etud_moy_gen)
|
||||
|
||||
def get_ues_stat_dict(
|
||||
self, filter_sport=False, check_apc_ects=True
|
||||
) -> list[dict]: # was get_ues()
|
||||
"""Liste des UEs, ordonnée par numero.
|
||||
def get_ues_stat_dict(self, filter_sport=False, check_apc_ects=True) -> list[dict]:
|
||||
"""Liste des UEs de toutes les UEs du semestre (tous parcours),
|
||||
ordonnée par numero.
|
||||
Si filter_sport, retire les UE de type SPORT.
|
||||
Résultat: liste de dicts { champs UE U stats moyenne UE }
|
||||
"""
|
||||
|
@ -273,9 +271,9 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
|
||||
def etud_has_decision(self, etudid):
|
||||
"""True s'il y a une décision de jury pour cet étudiant"""
|
||||
return self.get_etud_decision_ues(etudid) or self.get_etud_decision_sem(etudid)
|
||||
return self.get_etud_decisions_ue(etudid) or self.get_etud_decision_sem(etudid)
|
||||
|
||||
def get_etud_decision_ues(self, etudid: int) -> dict:
|
||||
def get_etud_decisions_ue(self, etudid: int) -> dict:
|
||||
"""Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu.
|
||||
Ne tient pas compte des UE capitalisées.
|
||||
{ ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : "d/m/y", 'ects' : x }
|
||||
|
@ -284,16 +282,16 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
if self.get_etud_etat(etudid) == DEF:
|
||||
return {}
|
||||
else:
|
||||
validations = self.load_validations()
|
||||
validations = self.get_formsemestre_validations()
|
||||
return validations.decisions_jury_ues.get(etudid, None)
|
||||
|
||||
def get_etud_ects_valides(self, etudid: int, decisions_ues: dict = False) -> 0:
|
||||
"""Le total des ECTS validés (et enregistrés) par l'étudiant dans ce semestre.
|
||||
NB: avant jury, rien d'enregistré, donc zéro ECTS.
|
||||
Optimisation: si decisions_ues est passé, l'utilise, sinon appelle get_etud_decision_ues()
|
||||
Optimisation: si decisions_ues est passé, l'utilise, sinon appelle get_etud_decisions_ue()
|
||||
"""
|
||||
if decisions_ues is False:
|
||||
decisions_ues = self.get_etud_decision_ues(etudid)
|
||||
decisions_ues = self.get_etud_decisions_ue(etudid)
|
||||
if not decisions_ues:
|
||||
return 0.0
|
||||
return sum([d.get("ects", 0.0) for d in decisions_ues.values()])
|
||||
|
@ -311,7 +309,7 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
"compense_formsemestre_id": None,
|
||||
}
|
||||
else:
|
||||
validations = self.load_validations()
|
||||
validations = self.get_formsemestre_validations()
|
||||
return validations.decisions_jury.get(etudid, None)
|
||||
|
||||
def get_etud_mat_moy(self, matiere_id: int, etudid: int) -> str:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import flask_login
|
|||
import app
|
||||
from app.auth.models import User
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
class ZUser(object):
|
||||
|
@ -180,19 +181,24 @@ def scodoc7func(func):
|
|||
else:
|
||||
arg_names = argspec.args
|
||||
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
|
||||
try:
|
||||
v = int(v)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
pos_arg_values.append(v)
|
||||
# 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
|
||||
try:
|
||||
v = int(v) if v else v
|
||||
except (ValueError, TypeError) as exc:
|
||||
if arg_name in {
|
||||
"etudid",
|
||||
"formation_id",
|
||||
"formsemestre_id",
|
||||
"module_id",
|
||||
"moduleimpl_id",
|
||||
"partition_id",
|
||||
"ue_id",
|
||||
}:
|
||||
raise ScoValueError("page introuvable (id invalide)") from exc
|
||||
pos_arg_values.append(v)
|
||||
# current_app.logger.info("pos_arg_values=%s" % pos_arg_values)
|
||||
# current_app.logger.info("req_args=%s" % req_args)
|
||||
# Add keyword arguments
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: UTF-8 -*
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -89,7 +89,7 @@ def index():
|
|||
visible=True, association=True, siret_provisoire=True
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/entreprises.html",
|
||||
"entreprises/entreprises.j2",
|
||||
title="Entreprises",
|
||||
entreprises=entreprises,
|
||||
logs=logs,
|
||||
|
@ -109,7 +109,7 @@ def logs():
|
|||
EntrepriseHistorique.date.desc()
|
||||
).paginate(page=page, per_page=20)
|
||||
return render_template(
|
||||
"entreprises/logs.html",
|
||||
"entreprises/logs.j2",
|
||||
title="Logs",
|
||||
logs=logs,
|
||||
)
|
||||
|
@ -134,7 +134,7 @@ def correspondants():
|
|||
.all()
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/correspondants.html",
|
||||
"entreprises/correspondants.j2",
|
||||
title="Correspondants",
|
||||
correspondants=correspondants,
|
||||
logs=logs,
|
||||
|
@ -149,7 +149,7 @@ def validation():
|
|||
"""
|
||||
entreprises = Entreprise.query.filter_by(visible=False).all()
|
||||
return render_template(
|
||||
"entreprises/entreprises_validation.html",
|
||||
"entreprises/entreprises_validation.j2",
|
||||
title="Validation entreprises",
|
||||
entreprises=entreprises,
|
||||
)
|
||||
|
@ -167,7 +167,7 @@ def fiche_entreprise_validation(entreprise_id):
|
|||
description=f"fiche entreprise (validation) {entreprise_id} inconnue"
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/fiche_entreprise_validation.html",
|
||||
"entreprises/fiche_entreprise_validation.j2",
|
||||
title="Validation fiche entreprise",
|
||||
entreprise=entreprise,
|
||||
)
|
||||
|
@ -205,7 +205,7 @@ def validate_entreprise(entreprise_id):
|
|||
flash("L'entreprise a été validé et ajouté à la liste.")
|
||||
return redirect(url_for("entreprises.validation"))
|
||||
return render_template(
|
||||
"entreprises/form_validate_confirmation.html",
|
||||
"entreprises/form_validate_confirmation.j2",
|
||||
title="Validation entreprise",
|
||||
form=form,
|
||||
)
|
||||
|
@ -242,7 +242,7 @@ def delete_validation_entreprise(entreprise_id):
|
|||
flash("L'entreprise a été supprimé de la liste des entreprise à valider.")
|
||||
return redirect(url_for("entreprises.validation"))
|
||||
return render_template(
|
||||
"entreprises/form_confirmation.html",
|
||||
"entreprises/form_confirmation.j2",
|
||||
title="Supression entreprise",
|
||||
form=form,
|
||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||
|
@ -282,7 +282,7 @@ def offres_recues():
|
|||
files.append(file)
|
||||
offres_recues_with_files.append([envoi_offre, offre, files, correspondant])
|
||||
return render_template(
|
||||
"entreprises/offres_recues.html",
|
||||
"entreprises/offres_recues.j2",
|
||||
title="Offres reçues",
|
||||
offres_recues=offres_recues_with_files,
|
||||
)
|
||||
|
@ -321,7 +321,7 @@ def preferences():
|
|||
form.mail_entreprise.data = EntreprisePreferences.get_email_notifications()
|
||||
form.check_siret.data = int(EntreprisePreferences.get_check_siret())
|
||||
return render_template(
|
||||
"entreprises/preferences.html",
|
||||
"entreprises/preferences.j2",
|
||||
title="Préférences",
|
||||
form=form,
|
||||
)
|
||||
|
@ -357,7 +357,7 @@ def add_entreprise():
|
|||
db.session.rollback()
|
||||
flash("Une erreur est survenue veuillez réessayer.")
|
||||
return render_template(
|
||||
"entreprises/form_ajout_entreprise.html",
|
||||
"entreprises/form_ajout_entreprise.j2",
|
||||
title="Ajout entreprise avec correspondant",
|
||||
form=form,
|
||||
)
|
||||
|
@ -408,7 +408,7 @@ def add_entreprise():
|
|||
flash("L'entreprise a été ajouté à la liste pour la validation.")
|
||||
return redirect(url_for("entreprises.index"))
|
||||
return render_template(
|
||||
"entreprises/form_ajout_entreprise.html",
|
||||
"entreprises/form_ajout_entreprise.j2",
|
||||
title="Ajout entreprise avec correspondant",
|
||||
form=form,
|
||||
)
|
||||
|
@ -446,7 +446,7 @@ def fiche_entreprise(entreprise_id):
|
|||
.all()
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/fiche_entreprise.html",
|
||||
"entreprises/fiche_entreprise.j2",
|
||||
title="Fiche entreprise",
|
||||
entreprise=entreprise,
|
||||
offres=offres_with_files,
|
||||
|
@ -472,7 +472,7 @@ def logs_entreprise(entreprise_id):
|
|||
.paginate(page=page, per_page=20)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/logs_entreprise.html",
|
||||
"entreprises/logs_entreprise.j2",
|
||||
title="Logs",
|
||||
logs=logs,
|
||||
entreprise=entreprise,
|
||||
|
@ -490,7 +490,7 @@ def offres_expirees(entreprise_id):
|
|||
).first_or_404(description=f"fiche entreprise {entreprise_id} inconnue")
|
||||
offres_with_files = are.get_offres_expirees_with_files(entreprise.offres)
|
||||
return render_template(
|
||||
"entreprises/offres_expirees.html",
|
||||
"entreprises/offres_expirees.j2",
|
||||
title="Offres expirées",
|
||||
entreprise=entreprise,
|
||||
offres_expirees=offres_with_files,
|
||||
|
@ -574,7 +574,7 @@ def edit_entreprise(entreprise_id):
|
|||
form.pays.data = entreprise.pays
|
||||
form.association.data = entreprise.association
|
||||
return render_template(
|
||||
"entreprises/form_modification_entreprise.html",
|
||||
"entreprises/form_modification_entreprise.j2",
|
||||
title="Modification entreprise",
|
||||
form=form,
|
||||
)
|
||||
|
@ -610,7 +610,7 @@ def fiche_entreprise_desactiver(entreprise_id):
|
|||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_confirmation.html",
|
||||
"entreprises/form_confirmation.j2",
|
||||
title="Désactiver entreprise",
|
||||
form=form,
|
||||
info_message="Cliquez sur le bouton Modifier pour confirmer la désactivation",
|
||||
|
@ -646,7 +646,7 @@ def fiche_entreprise_activer(entreprise_id):
|
|||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_confirmation.html",
|
||||
"entreprises/form_confirmation.j2",
|
||||
title="Activer entreprise",
|
||||
form=form,
|
||||
info_message="Cliquez sur le bouton Modifier pour confirmer l'activaction",
|
||||
|
@ -692,7 +692,7 @@ def add_taxe_apprentissage(entreprise_id):
|
|||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form.html",
|
||||
"entreprises/form.j2",
|
||||
title="Ajout taxe apprentissage",
|
||||
form=form,
|
||||
)
|
||||
|
@ -735,7 +735,7 @@ def edit_taxe_apprentissage(entreprise_id, taxe_id):
|
|||
form.montant.data = taxe.montant
|
||||
form.notes.data = taxe.notes
|
||||
return render_template(
|
||||
"entreprises/form.html",
|
||||
"entreprises/form.j2",
|
||||
title="Modification taxe apprentissage",
|
||||
form=form,
|
||||
)
|
||||
|
@ -775,7 +775,7 @@ def delete_taxe_apprentissage(entreprise_id, taxe_id):
|
|||
url_for("entreprises.fiche_entreprise", entreprise_id=taxe.entreprise_id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_confirmation.html",
|
||||
"entreprises/form_confirmation.j2",
|
||||
title="Supprimer taxe apprentissage",
|
||||
form=form,
|
||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||
|
@ -845,7 +845,7 @@ def add_offre(entreprise_id):
|
|||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form.html",
|
||||
"entreprises/form.j2",
|
||||
title="Ajout offre",
|
||||
form=form,
|
||||
)
|
||||
|
@ -921,7 +921,7 @@ def edit_offre(entreprise_id, offre_id):
|
|||
form.expiration_date.data = offre.expiration_date
|
||||
form.depts.data = offre_depts_list
|
||||
return render_template(
|
||||
"entreprises/form.html",
|
||||
"entreprises/form.j2",
|
||||
title="Modification offre",
|
||||
form=form,
|
||||
)
|
||||
|
@ -971,7 +971,7 @@ def delete_offre(entreprise_id, offre_id):
|
|||
url_for("entreprises.fiche_entreprise", entreprise_id=offre.entreprise_id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_confirmation.html",
|
||||
"entreprises/form_confirmation.j2",
|
||||
title="Supression offre",
|
||||
form=form,
|
||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||
|
@ -1047,7 +1047,7 @@ def add_site(entreprise_id):
|
|||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form.html",
|
||||
"entreprises/form.j2",
|
||||
title="Ajout site",
|
||||
form=form,
|
||||
)
|
||||
|
@ -1098,7 +1098,7 @@ def edit_site(entreprise_id, site_id):
|
|||
form.ville.data = site.ville
|
||||
form.pays.data = site.pays
|
||||
return render_template(
|
||||
"entreprises/form.html",
|
||||
"entreprises/form.j2",
|
||||
title="Modification site",
|
||||
form=form,
|
||||
)
|
||||
|
@ -1154,7 +1154,7 @@ def add_correspondant(entreprise_id, site_id):
|
|||
url_for("entreprises.fiche_entreprise", entreprise_id=site.entreprise_id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_ajout_correspondants.html",
|
||||
"entreprises/form_ajout_correspondants.j2",
|
||||
title="Ajout correspondant",
|
||||
form=form,
|
||||
)
|
||||
|
@ -1234,7 +1234,7 @@ def edit_correspondant(entreprise_id, site_id, correspondant_id):
|
|||
form.origine.data = correspondant.origine
|
||||
form.notes.data = correspondant.notes
|
||||
return render_template(
|
||||
"entreprises/form.html",
|
||||
"entreprises/form.j2",
|
||||
title="Modification correspondant",
|
||||
form=form,
|
||||
)
|
||||
|
@ -1290,7 +1290,7 @@ def delete_correspondant(entreprise_id, site_id, correspondant_id):
|
|||
)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_confirmation.html",
|
||||
"entreprises/form_confirmation.j2",
|
||||
title="Supression correspondant",
|
||||
form=form,
|
||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||
|
@ -1308,7 +1308,7 @@ def contacts(entreprise_id):
|
|||
).first_or_404(description=f"entreprise {entreprise_id} inconnue")
|
||||
contacts = EntrepriseContact.query.filter_by(entreprise=entreprise.id).all()
|
||||
return render_template(
|
||||
"entreprises/contacts.html",
|
||||
"entreprises/contacts.j2",
|
||||
title="Liste des contacts",
|
||||
contacts=contacts,
|
||||
entreprise=entreprise,
|
||||
|
@ -1365,7 +1365,7 @@ def add_contact(entreprise_id):
|
|||
db.session.commit()
|
||||
return redirect(url_for("entreprises.contacts", entreprise_id=entreprise.id))
|
||||
return render_template(
|
||||
"entreprises/form.html",
|
||||
"entreprises/form.j2",
|
||||
title="Ajout contact",
|
||||
form=form,
|
||||
)
|
||||
|
@ -1421,7 +1421,7 @@ def edit_contact(entreprise_id, contact_id):
|
|||
)
|
||||
form.notes.data = contact.notes
|
||||
return render_template(
|
||||
"entreprises/form.html",
|
||||
"entreprises/form.j2",
|
||||
title="Modification contact",
|
||||
form=form,
|
||||
)
|
||||
|
@ -1459,7 +1459,7 @@ def delete_contact(entreprise_id, contact_id):
|
|||
url_for("entreprises.contacts", entreprise_id=contact.entreprise)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_confirmation.html",
|
||||
"entreprises/form_confirmation.j2",
|
||||
title="Supression contact",
|
||||
form=form,
|
||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||
|
@ -1525,7 +1525,7 @@ def add_stage_apprentissage(entreprise_id):
|
|||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise.id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_ajout_stage_apprentissage.html",
|
||||
"entreprises/form_ajout_stage_apprentissage.j2",
|
||||
title="Ajout stage / apprentissage",
|
||||
form=form,
|
||||
)
|
||||
|
@ -1599,7 +1599,7 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
|||
form.date_fin.data = stage_apprentissage.date_fin
|
||||
form.notes.data = stage_apprentissage.notes
|
||||
return render_template(
|
||||
"entreprises/form_ajout_stage_apprentissage.html",
|
||||
"entreprises/form_ajout_stage_apprentissage.j2",
|
||||
title="Modification stage / apprentissage",
|
||||
form=form,
|
||||
)
|
||||
|
@ -1640,7 +1640,7 @@ def delete_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
|||
)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_confirmation.html",
|
||||
"entreprises/form_confirmation.j2",
|
||||
title="Supression stage/apprentissage",
|
||||
form=form,
|
||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||
|
@ -1690,7 +1690,7 @@ def envoyer_offre(entreprise_id, offre_id):
|
|||
url_for("entreprises.fiche_entreprise", entreprise_id=offre.entreprise_id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_envoi_offre.html",
|
||||
"entreprises/form_envoi_offre.j2",
|
||||
title="Envoyer une offre",
|
||||
form=form,
|
||||
)
|
||||
|
@ -1816,7 +1816,7 @@ def import_donnees():
|
|||
db.session.rollback()
|
||||
flash("Une erreur est survenue veuillez réessayer.")
|
||||
return render_template(
|
||||
"entreprises/import_donnees.html",
|
||||
"entreprises/import_donnees.j2",
|
||||
title="Importation données",
|
||||
form=form,
|
||||
)
|
||||
|
@ -1845,7 +1845,7 @@ def import_donnees():
|
|||
db.session.commit()
|
||||
flash(f"Importation réussie")
|
||||
return render_template(
|
||||
"entreprises/import_donnees.html",
|
||||
"entreprises/import_donnees.j2",
|
||||
title="Importation données",
|
||||
form=form,
|
||||
entreprises_import=entreprises_import,
|
||||
|
@ -1853,7 +1853,7 @@ def import_donnees():
|
|||
correspondants_import=correspondants,
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/import_donnees.html", title="Importation données", form=form
|
||||
"entreprises/import_donnees.j2", title="Importation données", form=form
|
||||
)
|
||||
|
||||
|
||||
|
@ -1927,7 +1927,7 @@ def add_offre_file(entreprise_id, offre_id):
|
|||
url_for("entreprises.fiche_entreprise", entreprise_id=offre.entreprise_id)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form.html",
|
||||
"entreprises/form.j2",
|
||||
title="Ajout fichier à une offre",
|
||||
form=form,
|
||||
)
|
||||
|
@ -1969,7 +1969,7 @@ def delete_offre_file(entreprise_id, offre_id, filedir):
|
|||
)
|
||||
)
|
||||
return render_template(
|
||||
"entreprises/form_confirmation.html",
|
||||
"entreprises/form_confirmation.j2",
|
||||
title="Suppression fichier d'une offre",
|
||||
form=form,
|
||||
info_message="Cliquez sur le bouton Supprimer pour confirmer votre supression",
|
||||
|
@ -1981,4 +1981,4 @@ def not_found_error_handler(e):
|
|||
"""
|
||||
Renvoie une page d'erreur pour l'erreur 404
|
||||
"""
|
||||
return render_template("entreprises/error.html", title="Erreur", e=e)
|
||||
return render_template("entreprises/error.j2", title="Erreur", e=e)
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Formulaire changement formation
|
||||
"""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import RadioField, SubmitField, validators
|
||||
|
||||
from app.models import Formation
|
||||
|
||||
|
||||
class FormSemestreChangeFormationForm(FlaskForm):
|
||||
"Formulaire changement formation d'un formsemestre"
|
||||
# consrtuit dynamiquement ci-dessous
|
||||
|
||||
|
||||
def gen_formsemestre_change_formation_form(
|
||||
formations: list[Formation],
|
||||
) -> FormSemestreChangeFormationForm:
|
||||
"Create our dynamical form"
|
||||
# see https://wtforms.readthedocs.io/en/2.3.x/specific_problems/#dynamic-form-composition
|
||||
class F(FormSemestreChangeFormationForm):
|
||||
pass
|
||||
|
||||
setattr(
|
||||
F,
|
||||
"radio_but",
|
||||
RadioField(
|
||||
"Label",
|
||||
choices=[
|
||||
(formation.id, formation.get_titre_version())
|
||||
for formation in formations
|
||||
],
|
||||
),
|
||||
)
|
||||
setattr(F, "submit", SubmitField("Changer la formation"))
|
||||
setattr(F, "cancel", SubmitField("Annuler"))
|
||||
return F()
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -35,14 +35,14 @@ from wtforms.fields.simple import StringField
|
|||
|
||||
from app.models import SHORT_STR_LEN
|
||||
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import codes_cursus
|
||||
|
||||
|
||||
def _build_code_field(code):
|
||||
return StringField(
|
||||
label=code,
|
||||
default=code,
|
||||
description=sco_codes_parcours.CODES_EXPL[code],
|
||||
description=codes_cursus.CODES_EXPL[code],
|
||||
validators=[
|
||||
validators.regexp(
|
||||
r"^[A-Z0-9_]*$",
|
||||
|
@ -63,6 +63,7 @@ class CodesDecisionsForm(FlaskForm):
|
|||
ABL = _build_code_field("ABL")
|
||||
ADC = _build_code_field("ADC")
|
||||
ADJ = _build_code_field("ADJ")
|
||||
ADJR = _build_code_field("ADJR")
|
||||
ADM = _build_code_field("ADM")
|
||||
AJ = _build_code_field("AJ")
|
||||
ATB = _build_code_field("ATB")
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Formulaire configuration CAS
|
||||
"""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import BooleanField, SubmitField
|
||||
from wtforms.fields.simple import FileField, StringField
|
||||
|
||||
|
||||
class ConfigCASForm(FlaskForm):
|
||||
"Formulaire paramétrage CAS"
|
||||
cas_enable = BooleanField("Activer le CAS")
|
||||
cas_force = BooleanField(
|
||||
"Forcer l'utilisation de CAS (tous les utilisateurs seront redirigés vers le CAS)"
|
||||
)
|
||||
|
||||
cas_server = StringField(
|
||||
label="URL du serveur CAS",
|
||||
description="""url complète. Commence en général par <tt>https://</tt>.""",
|
||||
)
|
||||
cas_login_route = StringField(
|
||||
label="Route du login CAS",
|
||||
description="""ajouté à l'URL du serveur: exemple <tt>/cas</tt> (si commence par <tt>/</tt>, part de la racine)""",
|
||||
default="/cas",
|
||||
)
|
||||
cas_logout_route = StringField(
|
||||
label="Route du logout CAS",
|
||||
description="""ajouté à l'URL du serveur: exemple <tt>/cas/logout</tt>""",
|
||||
default="/cas/logout",
|
||||
)
|
||||
cas_validate_route = StringField(
|
||||
label="Route de validation CAS",
|
||||
description="""ajouté à l'URL du serveur: exemple <tt>/cas/serviceValidate</tt>""",
|
||||
default="/cas/serviceValidate",
|
||||
)
|
||||
|
||||
cas_attribute_id = StringField(
|
||||
label="Attribut CAS utilisé comme id (laissez vide pour prendre l'id par défaut)",
|
||||
description="""Le champs CAS qui sera considéré comme l'id unique des
|
||||
comptes utilisateurs.""",
|
||||
)
|
||||
|
||||
cas_ssl_verify = BooleanField("Vérification du certificat SSL")
|
||||
cas_ssl_certificate_file = FileField(
|
||||
label="Certificat (PEM)",
|
||||
description="""Le contenu du certificat PEM
|
||||
(commence typiquement par <tt>-----BEGIN CERTIFICATE-----</tt>)""",
|
||||
)
|
||||
|
||||
submit = SubmitField("Valider")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -148,6 +148,9 @@ class AddLogoForm(FlaskForm):
|
|||
kwargs["meta"] = {"csrf": False}
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def id(self):
|
||||
return f"id=add_{self.dept_key.data}"
|
||||
|
||||
def validate_name(self, name):
|
||||
dept_id = dept_key_to_id(self.dept_key.data)
|
||||
if dept_id == GLOBAL:
|
||||
|
@ -171,7 +174,7 @@ class AddLogoForm(FlaskForm):
|
|||
|
||||
|
||||
class LogoForm(FlaskForm):
|
||||
"""Embed both presentation of a logo (cf. template file configuration.html)
|
||||
"""Embed both presentation of a logo (cf. template file configuration.j2)
|
||||
and all its data and UI action (change, delete)"""
|
||||
|
||||
dept_key = HiddenField()
|
||||
|
@ -227,6 +230,10 @@ class LogoForm(FlaskForm):
|
|||
self.description = "Se substitue au footer défini au niveau global"
|
||||
self.titre = "Logo pied de page"
|
||||
|
||||
def id(self):
|
||||
idstring = f"{self.dept_key.data}_{self.logo_id.data}"
|
||||
return f"id={idstring}"
|
||||
|
||||
def select_action(self):
|
||||
from app.scodoc.sco_config_actions import LogoRename
|
||||
from app.scodoc.sco_config_actions import LogoUpdate
|
||||
|
@ -258,6 +265,9 @@ class DeptForm(FlaskForm):
|
|||
kwargs["meta"] = {"csrf": False}
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def id(self):
|
||||
return f"id=DEPT_{self.dept_key.data}"
|
||||
|
||||
def is_local(self):
|
||||
if self.dept_key.data == GLOBAL:
|
||||
return None
|
||||
|
@ -434,7 +444,7 @@ def config_logos():
|
|||
scu.flash_errors(form)
|
||||
|
||||
return render_template(
|
||||
"config_logos.html",
|
||||
"config_logos.j2",
|
||||
scodoc_dept=None,
|
||||
title="Configuration ScoDoc",
|
||||
form=form,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -54,6 +54,22 @@ class BonusConfigurationForm(FlaskForm):
|
|||
class ScoDocConfigurationForm(FlaskForm):
|
||||
"Panneau de configuration avancée"
|
||||
enable_entreprises = BooleanField("activer le module <em>entreprises</em>")
|
||||
month_debut_annee_scolaire = SelectField(
|
||||
label="Mois de début des années scolaires",
|
||||
description="""Date pivot. En France métropolitaine, août.
|
||||
S'applique à tous les départements.""",
|
||||
choices=[
|
||||
(i, name.capitalize()) for (i, name) in enumerate(scu.MONTH_NAMES, start=1)
|
||||
],
|
||||
)
|
||||
month_debut_periode2 = SelectField(
|
||||
label="Mois de début deuxième période de l'année",
|
||||
description="""Date pivot. En France métropolitaine, décembre.
|
||||
S'applique à tous les départements.""",
|
||||
choices=[
|
||||
(i, name.capitalize()) for (i, name) in enumerate(scu.MONTH_NAMES, start=1)
|
||||
],
|
||||
)
|
||||
submit_scodoc = SubmitField("Valider")
|
||||
cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||
|
||||
|
@ -67,7 +83,11 @@ def configuration():
|
|||
}
|
||||
)
|
||||
form_scodoc = ScoDocConfigurationForm(
|
||||
data={"enable_entreprises": ScoDocSiteConfig.is_entreprises_enabled()}
|
||||
data={
|
||||
"enable_entreprises": ScoDocSiteConfig.is_entreprises_enabled(),
|
||||
"month_debut_annee_scolaire": ScoDocSiteConfig.get_month_debut_annee_scolaire(),
|
||||
"month_debut_periode2": ScoDocSiteConfig.get_month_debut_periode2(),
|
||||
}
|
||||
)
|
||||
if request.method == "POST" and (
|
||||
form_bonus.cancel_bonus.data or form_scodoc.cancel_scodoc.data
|
||||
|
@ -94,10 +114,26 @@ def configuration():
|
|||
"Module entreprise "
|
||||
+ ("activé" if form_scodoc.data["enable_entreprises"] else "désactivé")
|
||||
)
|
||||
if ScoDocSiteConfig.set_month_debut_annee_scolaire(
|
||||
int(form_scodoc.data["month_debut_annee_scolaire"])
|
||||
):
|
||||
flash(
|
||||
f"""Début des années scolaires fixé au mois de {
|
||||
scu.MONTH_NAMES[ScoDocSiteConfig.get_month_debut_annee_scolaire()-1]
|
||||
}"""
|
||||
)
|
||||
if ScoDocSiteConfig.set_month_debut_periode2(
|
||||
int(form_scodoc.data["month_debut_periode2"])
|
||||
):
|
||||
flash(
|
||||
f"""Début des années scolaires fixé au mois de {
|
||||
scu.MONTH_NAMES[ScoDocSiteConfig.get_month_debut_periode2()-1]
|
||||
}"""
|
||||
)
|
||||
return redirect(url_for("scodoc.index"))
|
||||
|
||||
return render_template(
|
||||
"configuration.html",
|
||||
"configuration.j2",
|
||||
form_bonus=form_bonus,
|
||||
form_scodoc=form_scodoc,
|
||||
scu=scu,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -36,7 +36,7 @@ from app.models.etudiants import (
|
|||
from app.models.events import Scolog, ScolarNews
|
||||
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.ues import DispenseUE, UniteEns
|
||||
from app.models.formsemestre import (
|
||||
FormSemestre,
|
||||
FormSemestreEtape,
|
||||
|
@ -72,12 +72,15 @@ from app.models.validations import (
|
|||
from app.models.preferences import ScoPreference
|
||||
|
||||
from app.models.but_refcomp import (
|
||||
ApcReferentielCompetences,
|
||||
ApcCompetence,
|
||||
ApcSituationPro,
|
||||
ApcAppCritique,
|
||||
ApcCompetence,
|
||||
ApcNiveau,
|
||||
ApcParcours,
|
||||
ApcReferentielCompetences,
|
||||
ApcSituationPro,
|
||||
)
|
||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
|
||||
from app.models.assiduites import Assiduite, Justificatif
|
||||
|
|
|
@ -15,8 +15,10 @@ class Absence(db.Model):
|
|||
db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
jour = db.Column(db.Date)
|
||||
# absent / justifié / absent+ justifié
|
||||
estabs = db.Column(db.Boolean())
|
||||
estjust = db.Column(db.Boolean())
|
||||
|
||||
matin = db.Column(db.Boolean())
|
||||
# motif de l'absence:
|
||||
description = db.Column(db.Text())
|
||||
|
|
|
@ -0,0 +1,343 @@
|
|||
# -*- coding: UTF-8 -*
|
||||
"""Gestion de l'assiduité (assiduités + justificatifs)
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from app import db
|
||||
from app.models import ModuleImpl
|
||||
from app.models.etudiants import Identite
|
||||
from app.scodoc.sco_utils import (
|
||||
EtatAssiduite,
|
||||
EtatJustificatif,
|
||||
localize_datetime,
|
||||
is_period_overlapping,
|
||||
)
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_utils import (
|
||||
EtatAssiduite,
|
||||
EtatJustificatif,
|
||||
localize_datetime,
|
||||
)
|
||||
|
||||
|
||||
class Assiduite(db.Model):
|
||||
"""
|
||||
Représente une assiduité:
|
||||
- une plage horaire lié à un état et un étudiant
|
||||
- un module si spécifiée
|
||||
- une description si spécifiée
|
||||
"""
|
||||
|
||||
__tablename__ = "assiduites"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, nullable=False)
|
||||
assiduite_id = db.synonym("id")
|
||||
|
||||
date_debut = db.Column(
|
||||
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||
)
|
||||
date_fin = db.Column(
|
||||
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||
)
|
||||
|
||||
moduleimpl_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_moduleimpl.id", ondelete="SET NULL"),
|
||||
)
|
||||
etudid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||
index=True,
|
||||
nullable=False,
|
||||
)
|
||||
etat = db.Column(db.Integer, nullable=False)
|
||||
|
||||
desc = db.Column(db.Text)
|
||||
|
||||
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
|
||||
user_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("user.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
)
|
||||
|
||||
est_just = db.Column(db.Boolean, server_default="false", nullable=False)
|
||||
|
||||
def to_dict(self, format_api=True) -> dict:
|
||||
"""Retourne la représentation json de l'assiduité"""
|
||||
etat = self.etat
|
||||
|
||||
if format_api:
|
||||
etat = EtatAssiduite.inverse().get(self.etat).name
|
||||
data = {
|
||||
"assiduite_id": self.id,
|
||||
"etudid": self.etudid,
|
||||
"moduleimpl_id": self.moduleimpl_id,
|
||||
"date_debut": self.date_debut,
|
||||
"date_fin": self.date_fin,
|
||||
"etat": etat,
|
||||
"desc": self.desc,
|
||||
"entry_date": self.entry_date,
|
||||
"user_id": self.user_id,
|
||||
"est_just": self.est_just,
|
||||
}
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def create_assiduite(
|
||||
cls,
|
||||
etud: Identite,
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etat: EtatAssiduite,
|
||||
moduleimpl: ModuleImpl = None,
|
||||
description: str = None,
|
||||
entry_date: datetime = None,
|
||||
user_id: int = None,
|
||||
est_just: bool = False,
|
||||
) -> object or int:
|
||||
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||
# Vérification de non duplication des périodes
|
||||
assiduites: list[Assiduite] = etud.assiduites
|
||||
if is_period_conflicting(date_debut, date_fin, assiduites, Assiduite):
|
||||
raise ScoValueError(
|
||||
"Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)"
|
||||
)
|
||||
if moduleimpl is not None:
|
||||
# Vérification de l'existence du module pour l'étudiant
|
||||
if moduleimpl.est_inscrit(etud):
|
||||
nouv_assiduite = Assiduite(
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
etat=etat,
|
||||
etudiant=etud,
|
||||
moduleimpl_id=moduleimpl.id,
|
||||
desc=description,
|
||||
entry_date=entry_date,
|
||||
user_id=user_id,
|
||||
est_just=est_just,
|
||||
)
|
||||
else:
|
||||
raise ScoValueError("L'étudiant n'est pas inscrit au moduleimpl")
|
||||
else:
|
||||
nouv_assiduite = Assiduite(
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
etat=etat,
|
||||
etudiant=etud,
|
||||
desc=description,
|
||||
entry_date=entry_date,
|
||||
user_id=user_id,
|
||||
est_just=est_just,
|
||||
)
|
||||
|
||||
return nouv_assiduite
|
||||
|
||||
@classmethod
|
||||
def fast_create_assiduite(
|
||||
cls,
|
||||
etudid: int,
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etat: EtatAssiduite,
|
||||
moduleimpl_id: int = None,
|
||||
description: str = None,
|
||||
entry_date: datetime = None,
|
||||
est_just: bool = False,
|
||||
) -> object or int:
|
||||
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||
# Vérification de non duplication des périodes
|
||||
|
||||
nouv_assiduite = Assiduite(
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
etat=etat,
|
||||
etudid=etudid,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
desc=description,
|
||||
entry_date=entry_date,
|
||||
est_just=est_just,
|
||||
)
|
||||
|
||||
return nouv_assiduite
|
||||
|
||||
|
||||
class Justificatif(db.Model):
|
||||
"""
|
||||
Représente un justificatif:
|
||||
- une plage horaire lié à un état et un étudiant
|
||||
- une raison si spécifiée
|
||||
- un fichier si spécifié
|
||||
"""
|
||||
|
||||
__tablename__ = "justificatifs"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
justif_id = db.synonym("id")
|
||||
|
||||
date_debut = db.Column(
|
||||
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||
)
|
||||
date_fin = db.Column(
|
||||
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||
)
|
||||
|
||||
etudid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||
index=True,
|
||||
nullable=False,
|
||||
)
|
||||
etat = db.Column(
|
||||
db.Integer,
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
|
||||
user_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("user.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
)
|
||||
|
||||
raison = db.Column(db.Text())
|
||||
|
||||
# Archive_id -> sco_archives_justificatifs.py
|
||||
fichier = db.Column(db.Text())
|
||||
|
||||
def to_dict(self, format_api: bool = False) -> dict:
|
||||
"""transformation de l'objet en dictionnaire sérialisable"""
|
||||
|
||||
etat = self.etat
|
||||
|
||||
if format_api:
|
||||
etat = EtatJustificatif.inverse().get(self.etat).name
|
||||
|
||||
data = {
|
||||
"justif_id": self.justif_id,
|
||||
"etudid": self.etudid,
|
||||
"date_debut": self.date_debut,
|
||||
"date_fin": self.date_fin,
|
||||
"etat": etat,
|
||||
"raison": self.raison,
|
||||
"fichier": self.fichier,
|
||||
"entry_date": self.entry_date,
|
||||
"user_id": self.user_id,
|
||||
}
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def create_justificatif(
|
||||
cls,
|
||||
etud: Identite,
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etat: EtatJustificatif,
|
||||
raison: str = None,
|
||||
entry_date: datetime = None,
|
||||
user_id: int = None,
|
||||
) -> object or int:
|
||||
"""Créer un nouveau justificatif pour l'étudiant"""
|
||||
nouv_justificatif = Justificatif(
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
etat=etat,
|
||||
etudiant=etud,
|
||||
raison=raison,
|
||||
entry_date=entry_date,
|
||||
user_id=user_id,
|
||||
)
|
||||
return nouv_justificatif
|
||||
|
||||
@classmethod
|
||||
def fast_create_justificatif(
|
||||
cls,
|
||||
etudid: int,
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etat: EtatJustificatif,
|
||||
raison: str = None,
|
||||
entry_date: datetime = None,
|
||||
) -> object or int:
|
||||
"""Créer un nouveau justificatif pour l'étudiant"""
|
||||
|
||||
nouv_justificatif = Justificatif(
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
etat=etat,
|
||||
etudid=etudid,
|
||||
raison=raison,
|
||||
entry_date=entry_date,
|
||||
)
|
||||
|
||||
return nouv_justificatif
|
||||
|
||||
|
||||
def is_period_conflicting(
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
collection: list[Assiduite or Justificatif],
|
||||
collection_cls: Assiduite or Justificatif,
|
||||
) -> bool:
|
||||
"""
|
||||
Vérifie si une date n'entre pas en collision
|
||||
avec les justificatifs ou assiduites déjà présentes
|
||||
"""
|
||||
|
||||
date_debut = localize_datetime(date_debut)
|
||||
date_fin = localize_datetime(date_fin)
|
||||
|
||||
if (
|
||||
collection.filter_by(date_debut=date_debut, date_fin=date_fin).first()
|
||||
is not None
|
||||
):
|
||||
return True
|
||||
|
||||
count: int = collection.filter(
|
||||
collection_cls.date_debut < date_fin, collection_cls.date_fin > date_debut
|
||||
).count()
|
||||
|
||||
return count > 0
|
||||
|
||||
|
||||
def compute_assiduites_justified(
|
||||
justificatifs: Justificatif = Justificatif, reset: bool = False
|
||||
) -> list[int]:
|
||||
"""Calcule et modifie les champs "est_just" de chaque assiduité lié à l'étud
|
||||
retourne la liste des assiduite_id justifiées
|
||||
|
||||
Si reset alors : met à false toutes les assiduités non justifiées par les justificatifs donnés
|
||||
"""
|
||||
|
||||
list_assiduites_id: set[int] = set()
|
||||
for justi in justificatifs:
|
||||
assiduites: Assiduite = (
|
||||
Assiduite.query.join(Justificatif, Justificatif.etudid == Assiduite.etudid)
|
||||
.filter(Assiduite.etat != EtatAssiduite.PRESENT)
|
||||
.filter(
|
||||
Assiduite.date_debut <= justi.date_fin,
|
||||
Assiduite.date_fin >= justi.date_debut,
|
||||
)
|
||||
)
|
||||
|
||||
for assi in assiduites:
|
||||
assi.est_just = True
|
||||
list_assiduites_id.add(assi.id)
|
||||
db.session.add(assi)
|
||||
|
||||
if reset:
|
||||
un_justified: Assiduite = (
|
||||
Assiduite.query.filter(Assiduite.id.not_in(list_assiduites_id))
|
||||
.filter(Assiduite.etat != EtatAssiduite.PRESENT)
|
||||
.join(Justificatif, Justificatif.etudid == Assiduite.etudid)
|
||||
)
|
||||
|
||||
for assi in un_justified:
|
||||
assi.est_just = False
|
||||
db.session.add(assi)
|
||||
|
||||
db.session.commit()
|
||||
return list(list_assiduites_id)
|
|
@ -1,6 +1,6 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 models : Référentiel Compétence BUT 2021
|
||||
|
@ -14,7 +14,7 @@ import sqlalchemy
|
|||
from app import db
|
||||
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
|
||||
|
||||
|
||||
# from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns
|
||||
|
@ -53,14 +53,18 @@ class XMLModel:
|
|||
class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
"Référentiel de compétence d'une spécialité"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||
annexe = db.Column(db.Text())
|
||||
specialite = db.Column(db.Text())
|
||||
specialite_long = db.Column(db.Text())
|
||||
type_titre = db.Column(db.Text())
|
||||
type_structure = db.Column(db.Text())
|
||||
dept_id = db.Column(
|
||||
db.Integer, db.ForeignKey("departement.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
annexe = db.Column(db.Text()) # '1', '22', ...
|
||||
specialite = db.Column(db.Text()) # 'CJ', 'RT', 'INFO', ...
|
||||
specialite_long = db.Column(
|
||||
db.Text()
|
||||
) # 'Carrière Juridique', 'Réseaux et télécommunications', ...
|
||||
type_titre = db.Column(db.Text()) # 'B.U.T.'
|
||||
type_structure = db.Column(db.Text()) # 'type1', 'type2', ...
|
||||
type_departement = db.Column(db.Text()) # "secondaire", "tertiaire"
|
||||
version_orebut = db.Column(db.Text())
|
||||
version_orebut = db.Column(db.Text()) # '2021-12-11 00:00:00'
|
||||
_xml_attribs = { # Orébut xml attrib : attribute
|
||||
"type": "type_titre",
|
||||
"version": "version_orebut",
|
||||
|
@ -86,9 +90,16 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||
def __repr__(self):
|
||||
return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>"
|
||||
|
||||
def to_dict(self):
|
||||
def get_version(self) -> str:
|
||||
"La version, normalement sous forme de date iso yyy-mm-dd"
|
||||
if not self.version_orebut:
|
||||
return ""
|
||||
return self.version_orebut.split()[0]
|
||||
|
||||
def to_dict(self, parcours: list["ApcParcours"] = None, with_app_critiques=True):
|
||||
"""Représentation complète du ref. de comp.
|
||||
comme un dict.
|
||||
Si parcours est une liste de parcours, restreint l'export aux parcours listés.
|
||||
"""
|
||||
return {
|
||||
"dept_id": self.dept_id,
|
||||
|
@ -103,29 +114,45 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||
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},
|
||||
"competences": {
|
||||
x.titre: x.to_dict(with_app_critiques=with_app_critiques)
|
||||
for x in self.competences
|
||||
},
|
||||
"parcours": {
|
||||
x.code: x.to_dict()
|
||||
for x in (self.parcours if parcours is None else parcours)
|
||||
},
|
||||
}
|
||||
|
||||
def get_niveaux_by_parcours(self, annee) -> dict:
|
||||
def get_niveaux_by_parcours(
|
||||
self, annee: int, parcour: "ApcParcours" = None
|
||||
) -> tuple[list["ApcParcours"], dict]:
|
||||
"""
|
||||
Construit la liste des niveaux de compétences pour chaque parcours
|
||||
de ce référentiel.
|
||||
de ce référentiel, ou seulement pour le parcours donné.
|
||||
|
||||
Les niveaux sont groupés par parcours, en isolant les niveaux de tronc commun.
|
||||
|
||||
Le tronc commun n'est pas identifié comme tel dans les référentiels Orébut:
|
||||
on cherche les niveaux qui sont présents dans tous les parcours et les range sous
|
||||
la clé "TC" (toujours présente mais éventuellement liste vide si pas de tronc commun).
|
||||
|
||||
résultat:
|
||||
{
|
||||
"TC" : [ ApcNiveau ],
|
||||
parcour.id : [ ApcNiveau ]
|
||||
}
|
||||
Résultat: couple
|
||||
( [ ApcParcours ],
|
||||
{
|
||||
"TC" : [ ApcNiveau ],
|
||||
parcour.id : [ ApcNiveau ]
|
||||
}
|
||||
)
|
||||
"""
|
||||
parcours = self.parcours.order_by(ApcParcours.numero).all()
|
||||
parcours_ref = self.parcours.order_by(ApcParcours.numero).all()
|
||||
if parcour is None:
|
||||
parcours = parcours_ref
|
||||
else:
|
||||
parcours = [parcour]
|
||||
niveaux_by_parcours = {
|
||||
parcour.id: ApcNiveau.niveaux_annee_de_parcours(parcour, annee, self)
|
||||
for parcour in parcours
|
||||
for parcour in parcours_ref
|
||||
}
|
||||
# Cherche tronc commun
|
||||
if niveaux_by_parcours:
|
||||
|
@ -154,14 +181,37 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||
niveau for niveau in niveaux_parcours_1 if niveau.id in niveaux_ids_tc
|
||||
]
|
||||
niveaux_by_parcours_no_tc["TC"] = niveaux_tc
|
||||
return niveaux_by_parcours_no_tc
|
||||
return parcours, niveaux_by_parcours_no_tc
|
||||
|
||||
def get_competences_tronc_commun(self) -> list["ApcCompetence"]:
|
||||
"""Liste des compétences communes à tous les parcours du référentiel."""
|
||||
parcours = self.parcours.all()
|
||||
if not parcours:
|
||||
return []
|
||||
|
||||
ids = set.intersection(
|
||||
*[
|
||||
{competence.id for competence in parcour.query_competences()}
|
||||
for parcour in parcours
|
||||
]
|
||||
)
|
||||
return sorted(
|
||||
[
|
||||
competence
|
||||
for competence in parcours[0].query_competences()
|
||||
if competence.id in ids
|
||||
],
|
||||
key=lambda c: c.numero or 0,
|
||||
)
|
||||
|
||||
|
||||
class ApcCompetence(db.Model, XMLModel):
|
||||
"Compétence"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
referentiel_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
|
||||
db.Integer,
|
||||
db.ForeignKey("apc_referentiel_competences.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
# les compétences dans Orébut sont identifiées par leur id unique
|
||||
# (mais id_orebut n'est pas unique car le même ref. pourra être chargé dans plusieurs depts)
|
||||
|
@ -197,7 +247,7 @@ class ApcCompetence(db.Model, XMLModel):
|
|||
def __repr__(self):
|
||||
return f"<ApcCompetence {self.id} {self.titre!r}>"
|
||||
|
||||
def to_dict(self):
|
||||
def to_dict(self, with_app_critiques=True):
|
||||
"repr dict recursive sur situations, composantes, niveaux"
|
||||
return {
|
||||
"id_orebut": self.id_orebut,
|
||||
|
@ -209,7 +259,10 @@ class ApcCompetence(db.Model, XMLModel):
|
|||
"composantes_essentielles": [
|
||||
x.to_dict() for x in self.composantes_essentielles
|
||||
],
|
||||
"niveaux": {x.annee: x.to_dict() for x in self.niveaux},
|
||||
"niveaux": {
|
||||
x.annee: x.to_dict(with_app_critiques=with_app_critiques)
|
||||
for x in self.niveaux
|
||||
},
|
||||
}
|
||||
|
||||
def to_dict_bul(self) -> dict:
|
||||
|
@ -227,7 +280,9 @@ class ApcSituationPro(db.Model, XMLModel):
|
|||
"Situation professionnelle"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
competence_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
|
||||
db.Integer,
|
||||
db.ForeignKey("apc_competence.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
libelle = db.Column(db.Text(), nullable=False)
|
||||
# aucun attribut (le text devient le libellé)
|
||||
|
@ -239,7 +294,9 @@ class ApcComposanteEssentielle(db.Model, XMLModel):
|
|||
"Composante essentielle"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
competence_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
|
||||
db.Integer,
|
||||
db.ForeignKey("apc_competence.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
libelle = db.Column(db.Text(), nullable=False)
|
||||
|
||||
|
@ -257,7 +314,9 @@ 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
|
||||
db.Integer,
|
||||
db.ForeignKey("apc_competence.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
libelle = db.Column(db.Text(), nullable=False)
|
||||
annee = db.Column(db.Text(), nullable=False) # "BUT1", "BUT2", "BUT3"
|
||||
|
@ -275,13 +334,15 @@ class ApcNiveau(db.Model, XMLModel):
|
|||
return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={
|
||||
self.annee!r} {self.competence!r}>"""
|
||||
|
||||
def to_dict(self):
|
||||
"as a dict, recursif sur les AC"
|
||||
def to_dict(self, with_app_critiques=True):
|
||||
"as a dict, recursif (ou non) sur les AC"
|
||||
return {
|
||||
"libelle": self.libelle,
|
||||
"annee": self.annee,
|
||||
"ordre": self.ordre,
|
||||
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques},
|
||||
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques}
|
||||
if with_app_critiques
|
||||
else {},
|
||||
}
|
||||
|
||||
def to_dict_bul(self):
|
||||
|
@ -306,9 +367,8 @@ class ApcNiveau(db.Model, XMLModel):
|
|||
if annee not in {1, 2, 3}:
|
||||
raise ValueError("annee invalide pour un parcours BUT")
|
||||
if referentiel_competence is None:
|
||||
raise ScoValueError(
|
||||
"Pas de référentiel de compétences associé à la formation !"
|
||||
)
|
||||
raise ScoNoReferentielCompetences()
|
||||
|
||||
annee_formation = f"BUT{annee}"
|
||||
if parcour is None:
|
||||
return ApcNiveau.query.filter(
|
||||
|
@ -337,7 +397,7 @@ app_critiques_modules = db.Table(
|
|||
),
|
||||
db.Column(
|
||||
"app_crit_id",
|
||||
db.ForeignKey("apc_app_critique.id"),
|
||||
db.ForeignKey("apc_app_critique.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
),
|
||||
)
|
||||
|
@ -346,7 +406,9 @@ app_critiques_modules = db.Table(
|
|||
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)
|
||||
niveau_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_niveau.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
code = db.Column(db.Text(), nullable=False, index=True)
|
||||
libelle = db.Column(db.Text())
|
||||
|
||||
|
@ -376,7 +438,9 @@ class ApcAppCritique(db.Model, XMLModel):
|
|||
query = query.filter(ApcNiveau.competence == competence)
|
||||
return query
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
def to_dict(self, with_code=False) -> dict:
|
||||
if with_code:
|
||||
return {"code": self.code, "libelle": self.libelle}
|
||||
return {"libelle": self.libelle}
|
||||
|
||||
def get_label(self) -> str:
|
||||
|
@ -393,7 +457,10 @@ class ApcAppCritique(db.Model, XMLModel):
|
|||
parcours_modules = db.Table(
|
||||
"parcours_modules",
|
||||
db.Column(
|
||||
"parcours_id", db.Integer, db.ForeignKey("apc_parcours.id"), primary_key=True
|
||||
"parcours_id",
|
||||
db.Integer,
|
||||
db.ForeignKey("apc_parcours.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
),
|
||||
db.Column(
|
||||
"module_id",
|
||||
|
@ -407,7 +474,10 @@ parcours_modules = db.Table(
|
|||
parcours_formsemestre = db.Table(
|
||||
"parcours_formsemestre",
|
||||
db.Column(
|
||||
"parcours_id", db.Integer, db.ForeignKey("apc_parcours.id"), primary_key=True
|
||||
"parcours_id",
|
||||
db.Integer,
|
||||
db.ForeignKey("apc_parcours.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
),
|
||||
db.Column(
|
||||
"formsemestre_id",
|
||||
|
@ -420,9 +490,12 @@ parcours_formsemestre = db.Table(
|
|||
|
||||
|
||||
class ApcParcours(db.Model, XMLModel):
|
||||
"Un parcours BUT"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
referentiel_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
|
||||
db.Integer,
|
||||
db.ForeignKey("apc_referentiel_competences.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
numero = db.Column(db.Integer) # ordre de présentation
|
||||
code = db.Column(db.Text(), nullable=False)
|
||||
|
@ -433,6 +506,7 @@ class ApcParcours(db.Model, XMLModel):
|
|||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
ues = db.relationship("UniteEns", back_populates="parcour")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__} {self.id} {self.code!r} ref={self.referentiel}>"
|
||||
|
@ -450,11 +524,19 @@ class ApcParcours(db.Model, XMLModel):
|
|||
d["annees"] = {x.ordre: x.to_dict() for x in self.annees}
|
||||
return d
|
||||
|
||||
def query_competences(self) -> flask_sqlalchemy.BaseQuery:
|
||||
"Les compétences associées à ce parcours"
|
||||
return (
|
||||
ApcCompetence.query.join(ApcParcoursNiveauCompetence, ApcAnneeParcours)
|
||||
.filter_by(parcours_id=self.id)
|
||||
.order_by(ApcCompetence.numero)
|
||||
)
|
||||
|
||||
|
||||
class ApcAnneeParcours(db.Model, XMLModel):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
parcours_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_parcours.id"), nullable=False
|
||||
db.Integer, db.ForeignKey("apc_parcours.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
ordre = db.Column(db.Integer)
|
||||
"numéro de l'année: 1, 2, 3"
|
||||
|
|
|
@ -2,20 +2,18 @@
|
|||
|
||||
"""Décisions de jury (validations) des RCUE et années du BUT
|
||||
"""
|
||||
|
||||
import flask_sqlalchemy
|
||||
from sqlalchemy.sql import text
|
||||
from typing import Union
|
||||
|
||||
from app import db
|
||||
import flask_sqlalchemy
|
||||
|
||||
from app import db
|
||||
from app.models import CODE_STR_LEN
|
||||
from app.models.but_refcomp import ApcNiveau
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.formations import Formation
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc import sco_codes_parcours as sco_codes
|
||||
from app.models.ues import UniteEns
|
||||
from app.scodoc import codes_cursus as sco_codes
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
|
@ -24,7 +22,7 @@ class ApcValidationRCUE(db.Model):
|
|||
|
||||
aka "regroupements cohérents d'UE" dans le jargon BUT.
|
||||
|
||||
le formsemestre est celui du semestre PAIR du niveau de compétence
|
||||
Le formsemestre est celui du semestre PAIR du niveau de compétence
|
||||
"""
|
||||
|
||||
__tablename__ = "apc_validation_rcue"
|
||||
|
@ -48,7 +46,9 @@ class ApcValidationRCUE(db.Model):
|
|||
ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
|
||||
ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
|
||||
# optionnel, le parcours dans lequel se trouve la compétence:
|
||||
parcours_id = db.Column(db.Integer, db.ForeignKey("apc_parcours.id"), nullable=True)
|
||||
parcours_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_parcours.id", ondelete="set null"), nullable=True
|
||||
)
|
||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
|
||||
|
||||
|
@ -59,13 +59,36 @@ class ApcValidationRCUE(db.Model):
|
|||
parcour = db.relationship("ApcParcours")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} {self.etud} {self.ue1}/{self.ue2}:{self.code!r}>"
|
||||
return f"""<{self.__class__.__name__} {self.id} {self.etud} {
|
||||
self.ue1}/{self.ue2}:{self.code!r}>"""
|
||||
|
||||
def __str__(self):
|
||||
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {
|
||||
self.code} enregistrée le {self.date.strftime("%d/%m/%Y")}"""
|
||||
|
||||
def to_html(self) -> str:
|
||||
"description en HTML"
|
||||
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}:
|
||||
<b>{self.code}</b>
|
||||
<em>enregistrée le {self.date.strftime("%d/%m/%Y")}
|
||||
à {self.date.strftime("%Hh%M")}</em>"""
|
||||
|
||||
def annee(self) -> str:
|
||||
"""l'année BUT concernée: "BUT1", "BUT2" ou "BUT3" """
|
||||
niveau = self.niveau()
|
||||
return niveau.annee if niveau else None
|
||||
|
||||
def niveau(self) -> ApcNiveau:
|
||||
"""Le niveau de compétence associé à cet RCUE."""
|
||||
# Par convention, il est donné par la seconde UE
|
||||
return self.ue2.niveau_competence
|
||||
|
||||
def to_dict(self):
|
||||
"as a dict"
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
return d
|
||||
|
||||
def to_dict_bul(self) -> dict:
|
||||
"Export dict pour bulletins: le code et le niveau de compétence"
|
||||
niveau = self.niveau()
|
||||
|
@ -74,34 +97,41 @@ class ApcValidationRCUE(db.Model):
|
|||
"niveau": None if niveau is None else niveau.to_dict_bul(),
|
||||
}
|
||||
|
||||
def to_dict_codes(self) -> dict:
|
||||
"Dict avec seulement les ids et la date - pour cache table jury"
|
||||
return {
|
||||
"id": self.id,
|
||||
"code": self.code,
|
||||
"date": self.date,
|
||||
"etudid": self.etudid,
|
||||
"niveau_id": self.niveau().id,
|
||||
"formsemestre_id": self.formsemestre_id,
|
||||
}
|
||||
|
||||
|
||||
# Attention: ce n'est pas un modèle mais une classe ordinaire:
|
||||
class RegroupementCoherentUE:
|
||||
"""Le regroupement cohérent d'UE, dans la terminologie du BUT, est le couple d'UEs
|
||||
de la même année (BUT1,2,3) liées au *même niveau de compétence*.
|
||||
|
||||
La moyenne (10/20) au RCU déclenche la compensation des UE.
|
||||
La moyenne (10/20) au RCUE déclenche la compensation des UE.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
etud: Identite,
|
||||
formsemestre_1: FormSemestre,
|
||||
ue_1: UniteEns,
|
||||
dec_ue_1: "DecisionsProposeesUE",
|
||||
formsemestre_2: FormSemestre,
|
||||
ue_2: UniteEns,
|
||||
dec_ue_2: "DecisionsProposeesUE",
|
||||
inscription_etat: str,
|
||||
):
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
|
||||
ue_1 = dec_ue_1.ue
|
||||
ue_2 = dec_ue_2.ue
|
||||
# Ordonne les UE dans le sens croissant (S1,S2) ou (S3,S4)...
|
||||
if formsemestre_1.semestre_id > formsemestre_2.semestre_id:
|
||||
(ue_1, formsemestre_1), (ue_2, formsemestre_2) = (
|
||||
(
|
||||
ue_2,
|
||||
formsemestre_2,
|
||||
),
|
||||
(ue_2, formsemestre_2),
|
||||
(ue_1, formsemestre_1),
|
||||
)
|
||||
assert formsemestre_1.semestre_id % 2 == 1
|
||||
|
@ -121,21 +151,12 @@ class RegroupementCoherentUE:
|
|||
self.moy_ue_1 = self.moy_ue_2 = "-"
|
||||
self.moy_ue_1_val = self.moy_ue_2_val = 0.0
|
||||
return
|
||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_1)
|
||||
if ue_1.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_1.id]:
|
||||
self.moy_ue_1 = res.etud_moy_ue[ue_1.id][etud.id]
|
||||
self.moy_ue_1_val = self.moy_ue_1 # toujours float, peut être NaN
|
||||
else:
|
||||
self.moy_ue_1 = None
|
||||
self.moy_ue_1_val = 0.0
|
||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_2)
|
||||
if ue_2.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_2.id]:
|
||||
self.moy_ue_2 = res.etud_moy_ue[ue_2.id][etud.id]
|
||||
self.moy_ue_2_val = self.moy_ue_2
|
||||
else:
|
||||
self.moy_ue_2 = None
|
||||
self.moy_ue_2_val = 0.0
|
||||
# Calcul de la moyenne au RCUE
|
||||
self.moy_ue_1 = dec_ue_1.moy_ue_with_cap
|
||||
self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0
|
||||
self.moy_ue_2 = dec_ue_2.moy_ue_with_cap
|
||||
self.moy_ue_2_val = self.moy_ue_2 if self.moy_ue_2 is not None else 0.0
|
||||
|
||||
# Calcul de la moyenne au RCUE (utilise les moy d'UE capitalisées)
|
||||
if (self.moy_ue_1 is not None) and (self.moy_ue_2 is not None):
|
||||
# Moyenne RCUE (les pondérations par défaut sont 1.)
|
||||
self.moy_rcue = (
|
||||
|
@ -145,7 +166,14 @@ class RegroupementCoherentUE:
|
|||
self.moy_rcue = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__} {self.ue_1.acronyme}({self.moy_ue_1}) {self.ue_2.acronyme}({self.moy_ue_2})>"
|
||||
return f"""<{self.__class__.__name__} {
|
||||
self.ue_1.acronyme}({self.moy_ue_1}) {
|
||||
self.ue_2.acronyme}({self.moy_ue_2})>"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"""RCUE {
|
||||
self.ue_1.acronyme}({self.moy_ue_1}) + {
|
||||
self.ue_2.acronyme}({self.moy_ue_2})"""
|
||||
|
||||
def query_validations(
|
||||
self,
|
||||
|
@ -177,8 +205,9 @@ class RegroupementCoherentUE:
|
|||
return self.query_validations().count() > 0
|
||||
|
||||
def est_compensable(self):
|
||||
"""Vrai si ce RCUE est validable par compensation
|
||||
c'est à dire que sa moyenne est > 10 avec une UE < 10
|
||||
"""Vrai si ce RCUE est validable (uniquement) par compensation
|
||||
c'est à dire que sa moyenne est > 10 avec une UE < 10.
|
||||
Note: si ADM, est_compensable est faux.
|
||||
"""
|
||||
return (
|
||||
(self.moy_rcue is not None)
|
||||
|
@ -214,62 +243,62 @@ class RegroupementCoherentUE:
|
|||
|
||||
|
||||
# unused
|
||||
def find_rcues(
|
||||
formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str
|
||||
) -> list[RegroupementCoherentUE]:
|
||||
"""Les RCUE (niveau de compétence) à considérer pour cet étudiant dans
|
||||
ce semestre pour cette UE.
|
||||
# def find_rcues(
|
||||
# formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str
|
||||
# ) -> list[RegroupementCoherentUE]:
|
||||
# """Les RCUE (niveau de compétence) à considérer pour cet étudiant dans
|
||||
# ce semestre pour cette UE.
|
||||
|
||||
Cherche les UEs du même niveau de compétence auxquelles l'étudiant est inscrit.
|
||||
En cas de redoublement, il peut y en avoir plusieurs, donc plusieurs RCUEs.
|
||||
# Cherche les UEs du même niveau de compétence auxquelles l'étudiant est inscrit.
|
||||
# En cas de redoublement, il peut y en avoir plusieurs, donc plusieurs RCUEs.
|
||||
|
||||
Résultat: la liste peut être vide.
|
||||
"""
|
||||
if (ue.niveau_competence is None) or (ue.semestre_idx is None):
|
||||
return []
|
||||
# Résultat: la liste peut être vide.
|
||||
# """
|
||||
# if (ue.niveau_competence is None) or (ue.semestre_idx is None):
|
||||
# return []
|
||||
|
||||
if ue.semestre_idx % 2: # S1, S3, S5
|
||||
other_semestre_idx = ue.semestre_idx + 1
|
||||
else:
|
||||
other_semestre_idx = ue.semestre_idx - 1
|
||||
# if ue.semestre_idx % 2: # S1, S3, S5
|
||||
# other_semestre_idx = ue.semestre_idx + 1
|
||||
# else:
|
||||
# other_semestre_idx = ue.semestre_idx - 1
|
||||
|
||||
cursor = db.session.execute(
|
||||
text(
|
||||
"""SELECT
|
||||
ue.id, formsemestre.id
|
||||
FROM
|
||||
notes_ue ue,
|
||||
notes_formsemestre_inscription inscr,
|
||||
notes_formsemestre formsemestre
|
||||
# cursor = db.session.execute(
|
||||
# text(
|
||||
# """SELECT
|
||||
# ue.id, formsemestre.id
|
||||
# FROM
|
||||
# notes_ue ue,
|
||||
# notes_formsemestre_inscription inscr,
|
||||
# notes_formsemestre formsemestre
|
||||
|
||||
WHERE
|
||||
inscr.etudid = :etudid
|
||||
AND inscr.formsemestre_id = formsemestre.id
|
||||
|
||||
AND formsemestre.semestre_id = :other_semestre_idx
|
||||
AND ue.formation_id = formsemestre.formation_id
|
||||
AND ue.niveau_competence_id = :ue_niveau_competence_id
|
||||
AND ue.semestre_idx = :other_semestre_idx
|
||||
"""
|
||||
),
|
||||
{
|
||||
"etudid": etud.id,
|
||||
"other_semestre_idx": other_semestre_idx,
|
||||
"ue_niveau_competence_id": ue.niveau_competence_id,
|
||||
},
|
||||
)
|
||||
rcues = []
|
||||
for ue_id, formsemestre_id in cursor:
|
||||
other_ue = UniteEns.query.get(ue_id)
|
||||
other_formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||
rcues.append(
|
||||
RegroupementCoherentUE(
|
||||
etud, formsemestre, ue, other_formsemestre, other_ue, inscription_etat
|
||||
)
|
||||
)
|
||||
# safety check: 1 seul niveau de comp. concerné:
|
||||
assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1
|
||||
return rcues
|
||||
# WHERE
|
||||
# inscr.etudid = :etudid
|
||||
# AND inscr.formsemestre_id = formsemestre.id
|
||||
|
||||
# AND formsemestre.semestre_id = :other_semestre_idx
|
||||
# AND ue.formation_id = formsemestre.formation_id
|
||||
# AND ue.niveau_competence_id = :ue_niveau_competence_id
|
||||
# AND ue.semestre_idx = :other_semestre_idx
|
||||
# """
|
||||
# ),
|
||||
# {
|
||||
# "etudid": etud.id,
|
||||
# "other_semestre_idx": other_semestre_idx,
|
||||
# "ue_niveau_competence_id": ue.niveau_competence_id,
|
||||
# },
|
||||
# )
|
||||
# rcues = []
|
||||
# for ue_id, formsemestre_id in cursor:
|
||||
# other_ue = UniteEns.query.get(ue_id)
|
||||
# other_formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||
# rcues.append(
|
||||
# RegroupementCoherentUE(
|
||||
# etud, formsemestre, ue, other_formsemestre, other_ue, inscription_etat
|
||||
# )
|
||||
# )
|
||||
# # safety check: 1 seul niveau de comp. concerné:
|
||||
# assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1
|
||||
# return rcues
|
||||
|
||||
|
||||
class ApcValidationAnnee(db.Model):
|
||||
|
@ -277,7 +306,7 @@ class ApcValidationAnnee(db.Model):
|
|||
|
||||
__tablename__ = "apc_validation_annee"
|
||||
# Assure unicité de la décision:
|
||||
__table_args__ = (db.UniqueConstraint("etudid", "annee_scolaire"),)
|
||||
__table_args__ = (db.UniqueConstraint("etudid", "annee_scolaire", "ordre"),)
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
etudid = db.Column(
|
||||
db.Integer,
|
||||
|
@ -299,7 +328,11 @@ class ApcValidationAnnee(db.Model):
|
|||
formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} {self.etud} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"
|
||||
return f"""<{self.__class__.__name__} {self.id} {self.etud
|
||||
} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"""
|
||||
|
||||
def __str__(self):
|
||||
return f"""décision sur année BUT{self.ordre} {self.annee_scolaire} : {self.code}"""
|
||||
|
||||
def to_dict_bul(self) -> dict:
|
||||
"dict pour bulletins"
|
||||
|
@ -333,7 +366,8 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
|||
titres_rcues.append(f"""pas de compétence: code {dec_rcue["code"]}""")
|
||||
else:
|
||||
titres_rcues.append(
|
||||
f"""{niveau["competence"]["titre"]} {niveau["ordre"]}: {dec_rcue["code"]}"""
|
||||
f"""{niveau["competence"]["titre"]} {niveau["ordre"]}: {
|
||||
dec_rcue["code"]}"""
|
||||
)
|
||||
decisions["descr_decisions_rcue"] = ", ".join(titres_rcues)
|
||||
decisions["descr_decisions_niveaux"] = (
|
||||
|
|
|
@ -6,13 +6,14 @@
|
|||
from flask import flash
|
||||
from app import db, log
|
||||
from app.comp import bonus_spo
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
from app.scodoc.sco_codes_parcours import (
|
||||
from app.scodoc.codes_cursus import (
|
||||
ABAN,
|
||||
ABL,
|
||||
ADC,
|
||||
ADJ,
|
||||
ADJR,
|
||||
ADM,
|
||||
AJ,
|
||||
ATB,
|
||||
|
@ -34,6 +35,7 @@ CODES_SCODOC_TO_APO = {
|
|||
ABL: "ABL",
|
||||
ADC: "ADMC",
|
||||
ADJ: "ADM",
|
||||
ADJR: "ADM",
|
||||
ADM: "ADM",
|
||||
AJ: "AJ",
|
||||
ATB: "AJAC",
|
||||
|
@ -83,6 +85,8 @@ class ScoDocSiteConfig(db.Model):
|
|||
"INSTITUTION_CITY": str,
|
||||
"DEFAULT_PDF_FOOTER_TEMPLATE": str,
|
||||
"enable_entreprises": bool,
|
||||
"month_debut_annee_scolaire": int,
|
||||
"month_debut_periode2": int,
|
||||
}
|
||||
|
||||
def __init__(self, name, value):
|
||||
|
@ -223,3 +227,73 @@ class ScoDocSiteConfig(db.Model):
|
|||
db.session.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def _get_int_field(cls, name: str, default=None) -> int:
|
||||
"""Valeur d'un champs integer"""
|
||||
cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
|
||||
if (cfg is None) or cfg.value is None:
|
||||
return default
|
||||
return int(cfg.value)
|
||||
|
||||
@classmethod
|
||||
def _set_int_field(
|
||||
cls,
|
||||
name: str,
|
||||
value: int,
|
||||
default=None,
|
||||
range_values: tuple = (),
|
||||
) -> bool:
|
||||
"""Set champs integer. True si changement."""
|
||||
if value != cls._get_int_field(name, default=default):
|
||||
if not isinstance(value, int) or (
|
||||
range_values and (value < range_values[0]) or (value > range_values[1])
|
||||
):
|
||||
raise ValueError("invalid value")
|
||||
cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
|
||||
if cfg is None:
|
||||
cfg = ScoDocSiteConfig(name=name, value=str(value))
|
||||
else:
|
||||
cfg.value = str(value)
|
||||
db.session.add(cfg)
|
||||
db.session.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_month_debut_annee_scolaire(cls) -> int:
|
||||
"""Mois de début de l'année scolaire."""
|
||||
return cls._get_int_field(
|
||||
"month_debut_annee_scolaire", scu.MONTH_DEBUT_ANNEE_SCOLAIRE
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_month_debut_periode2(cls) -> int:
|
||||
"""Mois de début de l'année scolaire."""
|
||||
return cls._get_int_field("month_debut_periode2", scu.MONTH_DEBUT_PERIODE2)
|
||||
|
||||
@classmethod
|
||||
def set_month_debut_annee_scolaire(
|
||||
cls, month: int = scu.MONTH_DEBUT_ANNEE_SCOLAIRE
|
||||
) -> bool:
|
||||
"""Fixe le mois de début des années scolaires.
|
||||
True si changement.
|
||||
"""
|
||||
if cls._set_int_field(
|
||||
"month_debut_annee_scolaire", month, scu.MONTH_DEBUT_ANNEE_SCOLAIRE, (1, 12)
|
||||
):
|
||||
log(f"set_month_debut_annee_scolaire({month})")
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def set_month_debut_periode2(cls, month: int = scu.MONTH_DEBUT_PERIODE2) -> bool:
|
||||
"""Fixe le mois de début des années scolaires.
|
||||
True si changement.
|
||||
"""
|
||||
if cls._set_int_field(
|
||||
"month_debut_periode2", month, scu.MONTH_DEBUT_PERIODE2, (1, 12)
|
||||
):
|
||||
log(f"set_month_debut_periode2({month})")
|
||||
return True
|
||||
return False
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
"""ScoDoc models : departements
|
||||
"""
|
||||
from typing import Any
|
||||
|
||||
from app import db
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models.preferences import ScoPreference
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
|
@ -39,7 +39,7 @@ class Departement(db.Model):
|
|||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}(id={self.id}, acronym='{self.acronym}')>"
|
||||
|
||||
def to_dict(self):
|
||||
def to_dict(self, with_dept_name=True, with_dept_preferences=False):
|
||||
data = {
|
||||
"id": self.id,
|
||||
"acronym": self.acronym,
|
||||
|
@ -47,6 +47,17 @@ class Departement(db.Model):
|
|||
"visible": self.visible,
|
||||
"date_creation": self.date_creation,
|
||||
}
|
||||
if with_dept_name:
|
||||
pref = ScoPreference.query.filter_by(
|
||||
dept_id=self.id, name="DeptName"
|
||||
).first()
|
||||
data["dept_name"] = pref.value if pref else None
|
||||
# Ceci n'est pas encore utilisé, mais pourrait être publié
|
||||
# par l'API après nettoyage des préférences.
|
||||
if with_dept_preferences:
|
||||
data["preferences"] = {
|
||||
p.name: p.value for p in ScoPreference.query.filter_by(dept_id=self.id)
|
||||
}
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -58,6 +58,16 @@ class Identite(db.Model):
|
|||
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
|
||||
#
|
||||
admission = db.relationship("Admission", backref="identite", lazy="dynamic")
|
||||
dispense_ues = db.relationship(
|
||||
"DispenseUE",
|
||||
back_populates="etud",
|
||||
cascade="all, delete",
|
||||
passive_deletes=True,
|
||||
)
|
||||
|
||||
# Relations avec les assiduites et les justificatifs
|
||||
assiduites = db.relationship("Assiduite", backref="etudiant", lazy="dynamic")
|
||||
justificatifs = db.relationship("Justificatif", backref="etudiant", lazy="dynamic")
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
|
@ -73,6 +83,14 @@ class Identite(db.Model):
|
|||
args = make_etud_args(etudid=etudid, code_nip=code_nip)
|
||||
return Identite.query.filter_by(**args).first_or_404()
|
||||
|
||||
@classmethod
|
||||
def create_etud(cls, **args):
|
||||
"Crée un étudiant, avec admission et adresse vides."
|
||||
etud: Identite = cls(**args)
|
||||
etud.adresses.append(Adresse())
|
||||
etud.admission.append(Admission())
|
||||
return etud
|
||||
|
||||
@property
|
||||
def civilite_str(self):
|
||||
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
|
||||
|
@ -169,6 +187,10 @@ class Identite(db.Model):
|
|||
e["etudid"] = self.id
|
||||
e["date_naissance"] = ndb.DateISOtoDMY(e["date_naissance"])
|
||||
e["ne"] = self.e
|
||||
e["nomprenom"] = self.nomprenom
|
||||
adresse = self.adresses.first()
|
||||
if adresse:
|
||||
e.update(adresse.to_dict())
|
||||
return {k: e[k] or "" for k in e} # convert_null_outputs_to_empty
|
||||
|
||||
def to_dict_bul(self, include_urls=True):
|
||||
|
@ -278,11 +300,18 @@ class Identite(db.Model):
|
|||
inscription_courante = self.inscription_courante()
|
||||
if inscription_courante:
|
||||
titre_sem = inscription_courante.formsemestre.titre_mois()
|
||||
if inscription_courante.etat == scu.DEMISSION:
|
||||
inscr_txt = "Démission de"
|
||||
elif inscription_courante.etat == scu.DEF:
|
||||
inscr_txt = "Défaillant dans"
|
||||
else:
|
||||
inscr_txt = "Inscrit en"
|
||||
|
||||
return {
|
||||
"etat_in_cursem": inscription_courante.etat,
|
||||
"inscription_courante": inscription_courante,
|
||||
"inscription": titre_sem,
|
||||
"inscription_str": "Inscrit en " + titre_sem,
|
||||
"inscription_str": inscr_txt + " " + titre_sem,
|
||||
"situation": self.descr_situation_etud(),
|
||||
}
|
||||
else:
|
||||
|
@ -311,7 +340,7 @@ class Identite(db.Model):
|
|||
"situation": situation,
|
||||
}
|
||||
|
||||
def inscription_etat(self, formsemestre_id):
|
||||
def inscription_etat(self, formsemestre_id: int) -> str:
|
||||
"""État de l'inscription de cet étudiant au semestre:
|
||||
False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF
|
||||
"""
|
||||
|
@ -394,14 +423,21 @@ class Identite(db.Model):
|
|||
|
||||
return situation
|
||||
|
||||
def etat_civil_pv(self, line_sep="\n") -> str:
|
||||
def etat_civil_pv(self, with_paragraph=True, line_sep="\n") -> str:
|
||||
"""Présentation, pour PV jury
|
||||
M. Pierre Dupont
|
||||
n° 12345678
|
||||
né(e) le 7/06/1974
|
||||
à Paris
|
||||
Si with_paragraph (défaut):
|
||||
M. Pierre Dupont
|
||||
n° 12345678
|
||||
né(e) le 7/06/1974
|
||||
à Paris
|
||||
Sinon:
|
||||
M. Pierre Dupont
|
||||
"""
|
||||
return f"""{self.nomprenom}{line_sep}n°{self.code_nip or ""}{line_sep}né{self.e} le {self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""}{line_sep}à {self.lieu_naissance or ""}"""
|
||||
if with_paragraph:
|
||||
return f"""{self.nomprenom}{line_sep}n°{self.code_nip or ""}{line_sep}né{self.e} le {
|
||||
self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""}{
|
||||
line_sep}à {self.lieu_naissance or ""}"""
|
||||
return self.nomprenom
|
||||
|
||||
def photo_html(self, title=None, size="small") -> str:
|
||||
"""HTML img tag for the photo, either in small size (h90)
|
||||
|
|
|
@ -5,12 +5,16 @@
|
|||
import datetime
|
||||
|
||||
from app import db
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
from app.models.notes import NotesNotes
|
||||
from app.models.ues import UniteEns
|
||||
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
||||
DEFAULT_EVALUATION_TIME = datetime.time(8, 0)
|
||||
|
||||
|
||||
class Evaluation(db.Model):
|
||||
"""Evaluation (contrôle, examen, ...)"""
|
||||
|
@ -44,10 +48,12 @@ class Evaluation(db.Model):
|
|||
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 ''}">"""
|
||||
return f"""<Evaluation {self.id} {
|
||||
self.jour.isoformat() if self.jour else ''} "{
|
||||
self.description[:16] if self.description else ''}">"""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"Représentation dict, pour json"
|
||||
"Représentation dict (riche, compat ScoDoc 7)"
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
# ScoDoc7 output_formators
|
||||
|
@ -67,6 +73,34 @@ class Evaluation(db.Model):
|
|||
e["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
|
||||
return evaluation_enrich_dict(e)
|
||||
|
||||
def to_dict_api(self) -> dict:
|
||||
"Représentation dict pour API JSON"
|
||||
if self.jour is None:
|
||||
date_debut = None
|
||||
date_fin = None
|
||||
else:
|
||||
date_debut = datetime.datetime.combine(
|
||||
self.jour, self.heure_debut or datetime.time(0, 0)
|
||||
).isoformat()
|
||||
date_fin = datetime.datetime.combine(
|
||||
self.jour, self.heure_fin or datetime.time(0, 0)
|
||||
).isoformat()
|
||||
|
||||
return {
|
||||
"coefficient": self.coefficient,
|
||||
"date_debut": date_debut,
|
||||
"date_fin": date_fin,
|
||||
"description": self.description,
|
||||
"evaluation_type": self.evaluation_type,
|
||||
"id": self.id,
|
||||
"moduleimpl_id": self.moduleimpl_id,
|
||||
"note_max": self.note_max,
|
||||
"numero": self.numero,
|
||||
"poids": self.get_ue_poids_dict(),
|
||||
"publish_incomplete": self.publish_incomplete,
|
||||
"visi_bulletin": self.visibulletin,
|
||||
}
|
||||
|
||||
def from_dict(self, data):
|
||||
"""Set evaluation attributes from given dict values."""
|
||||
check_evaluation_args(data)
|
||||
|
@ -74,6 +108,29 @@ class Evaluation(db.Model):
|
|||
if k != "_sa_instance_state" and k != "id" and k in data:
|
||||
setattr(self, k, data[k])
|
||||
|
||||
def descr_heure(self) -> str:
|
||||
"Description de la plage horaire pour affichages"
|
||||
if self.heure_debut and (
|
||||
not self.heure_fin or self.heure_fin == self.heure_debut
|
||||
):
|
||||
return f"""à {self.heure_debut.strftime("%Hh%M")}"""
|
||||
elif self.heure_debut and self.heure_fin:
|
||||
return f"""de {self.heure_debut.strftime("%Hh%M")} à {self.heure_fin.strftime("%Hh%M")}"""
|
||||
else:
|
||||
return ""
|
||||
|
||||
def descr_duree(self) -> str:
|
||||
"Description de la durée pour affichages"
|
||||
if self.heure_debut is None and self.heure_fin is None:
|
||||
return ""
|
||||
debut = self.heure_debut or DEFAULT_EVALUATION_TIME
|
||||
fin = self.heure_fin or DEFAULT_EVALUATION_TIME
|
||||
d = (fin.hour * 60 + fin.minute) - (debut.hour * 60 + debut.minute)
|
||||
duree = f"{d//60}h"
|
||||
if d % 60:
|
||||
duree += f"{d%60:02d}"
|
||||
return duree
|
||||
|
||||
def clone(self, not_copying=()):
|
||||
"""Clone, not copying the given attrs
|
||||
Attention: la copie n'a pas d'id avant le prochain commit
|
||||
|
@ -87,6 +144,29 @@ class Evaluation(db.Model):
|
|||
db.session.add(copy)
|
||||
return copy
|
||||
|
||||
def set_default_poids(self) -> bool:
|
||||
"""Initialize les poids bvers les UE à leurs valeurs par défaut
|
||||
C'est à dire à 1 si le coef. module/UE est non nul, 0 sinon.
|
||||
Les poids existants ne sont pas modifiés.
|
||||
Return True if (uncommited) modification, False otherwise.
|
||||
"""
|
||||
ue_coef_dict = self.moduleimpl.module.get_ue_coef_dict()
|
||||
sem_ues = self.moduleimpl.formsemestre.query_ues(with_sport=False).all()
|
||||
modified = False
|
||||
for ue in sem_ues:
|
||||
existing_poids = EvaluationUEPoids.query.filter_by(
|
||||
ue=ue, evaluation=self
|
||||
).first()
|
||||
if existing_poids is None:
|
||||
coef_ue = ue_coef_dict.get(ue.id, 0.0) or 0.0
|
||||
if coef_ue > 0:
|
||||
poids = 1.0 # par défaut au départ
|
||||
else:
|
||||
poids = 0.0
|
||||
self.set_ue_poids(ue, poids)
|
||||
modified = True
|
||||
return modified
|
||||
|
||||
def set_ue_poids(self, ue, poids: float) -> None:
|
||||
"""Set poids évaluation vers cette UE"""
|
||||
self.update_ue_poids_dict({ue.id: poids})
|
||||
|
@ -99,7 +179,7 @@ class Evaluation(db.Model):
|
|||
for ue_id, poids in ue_poids_dict.items():
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
L.append(EvaluationUEPoids(evaluation=self, ue=ue, poids=poids))
|
||||
self.ue_poids = L
|
||||
self.ue_poids = L # backref # pylint:disable=attribute-defined-outside-init
|
||||
self.moduleimpl.invalidate_evaluations_poids() # inval cache
|
||||
|
||||
def update_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
||||
|
@ -108,8 +188,18 @@ class Evaluation(db.Model):
|
|||
current.update(ue_poids_dict)
|
||||
self.set_ue_poids_dict(current)
|
||||
|
||||
def get_ue_poids_dict(self) -> dict:
|
||||
"""returns { ue_id : poids }"""
|
||||
def get_ue_poids_dict(self, sort=False) -> dict:
|
||||
"""returns { ue_id : poids }
|
||||
Si sort, trie par UE
|
||||
"""
|
||||
if sort:
|
||||
return {
|
||||
p.ue.id: p.poids
|
||||
for p in sorted(
|
||||
self.ue_poids, key=lambda p: (p.ue.numero or 0, p.ue.acronyme)
|
||||
)
|
||||
}
|
||||
|
||||
return {p.ue.id: p.poids for p in self.ue_poids}
|
||||
|
||||
def get_ue_poids_str(self) -> str:
|
||||
|
@ -130,6 +220,12 @@ class Evaluation(db.Model):
|
|||
]
|
||||
)
|
||||
|
||||
def get_etud_note(self, etud: Identite) -> NotesNotes:
|
||||
"""La note de l'étudiant, ou None si pas noté.
|
||||
(nb: pas de cache, lent, ne pas utiliser pour des calculs)
|
||||
"""
|
||||
return NotesNotes.query.filter_by(etudid=etud.id, evaluation_id=self.id).first()
|
||||
|
||||
|
||||
class EvaluationUEPoids(db.Model):
|
||||
"""Poids des évaluations (BUT)
|
||||
|
@ -164,7 +260,7 @@ class EvaluationUEPoids(db.Model):
|
|||
|
||||
|
||||
# Fonction héritée de ScoDoc7 à refactorer
|
||||
def evaluation_enrich_dict(e):
|
||||
def evaluation_enrich_dict(e: dict):
|
||||
"""add or convert some fields in an evaluation dict"""
|
||||
# For ScoDoc7 compat
|
||||
heure_debut_dt = e["heure_debut"] or datetime.time(
|
||||
|
@ -173,7 +269,7 @@ def evaluation_enrich_dict(e):
|
|||
heure_fin_dt = e["heure_fin"] or datetime.time(8, 00)
|
||||
e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"])
|
||||
e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"])
|
||||
e["jouriso"] = ndb.DateDMYtoISO(e["jour"])
|
||||
e["jour_iso"] = ndb.DateDMYtoISO(e["jour"])
|
||||
heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
|
||||
d = ndb.TimeDuration(heure_debut, heure_fin)
|
||||
if d is not None:
|
||||
|
|
|
@ -13,7 +13,6 @@ from app import email
|
|||
from app import log
|
||||
from app.auth.models import User
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_preferences
|
||||
|
@ -170,10 +169,12 @@ class ScolarNews(db.Model):
|
|||
log(f"news: {news}")
|
||||
news.notify_by_mail()
|
||||
|
||||
def get_news_formsemestre(self) -> FormSemestre:
|
||||
def get_news_formsemestre(self) -> "FormSemestre":
|
||||
"""formsemestre concerné par la nouvelle
|
||||
None si inexistant
|
||||
"""
|
||||
from app.models.formsemestre import FormSemestre
|
||||
|
||||
formsemestre_id = None
|
||||
if self.type == self.NEWS_INSCR:
|
||||
formsemestre_id = self.object
|
||||
|
|
|
@ -17,9 +17,9 @@ from app.models.modules import Module
|
|||
from app.models.moduleimpls import ModuleImpl
|
||||
from app.models.ues import UniteEns
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_codes_parcours import UE_STANDARD
|
||||
from app.scodoc.codes_cursus import UE_STANDARD
|
||||
|
||||
|
||||
class Formation(db.Model):
|
||||
|
@ -36,6 +36,7 @@ class Formation(db.Model):
|
|||
titre = db.Column(db.Text(), nullable=False)
|
||||
titre_officiel = db.Column(db.Text(), nullable=False)
|
||||
version = db.Column(db.Integer, default=1, server_default="1")
|
||||
commentaire = db.Column(db.Text())
|
||||
formation_code = db.Column(
|
||||
db.String(SHORT_STR_LEN),
|
||||
server_default=db.text("notes_newid_fcod()"),
|
||||
|
@ -47,7 +48,7 @@ class Formation(db.Model):
|
|||
|
||||
# Optionnel, pour les formations type BUT
|
||||
referentiel_competence_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_referentiel_competences.id")
|
||||
db.Integer, db.ForeignKey("apc_referentiel_competences.id", ondelete="SET NULL")
|
||||
)
|
||||
ues = db.relationship("UniteEns", backref="formation", lazy="dynamic")
|
||||
formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation")
|
||||
|
@ -55,26 +56,41 @@ class Formation(db.Model):
|
|||
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!r}')>"
|
||||
return f"""<{self.__class__.__name__}(id={self.id}, dept_id={
|
||||
self.dept_id}, acronyme={self.acronyme!r}, version={self.version})>"""
|
||||
|
||||
def to_html(self) -> str:
|
||||
"titre complet pour affichage"
|
||||
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
|
||||
|
||||
def to_dict(self):
|
||||
def to_dict(self, with_refcomp_attrs=False, with_departement=True):
|
||||
"""As a dict.
|
||||
Si with_refcomp_attrs, ajoute attributs permettant de retrouver le ref. de comp.
|
||||
"""
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
e["departement"] = self.departement.to_dict()
|
||||
# ScoDoc7 output_formators: (backward compat)
|
||||
e["formation_id"] = self.id
|
||||
if "referentiel_competence" in e:
|
||||
e.pop("referentiel_competence")
|
||||
e["code_specialite"] = e["code_specialite"] or ""
|
||||
e["commentaire"] = e["commentaire"] or ""
|
||||
if with_departement and self.departement:
|
||||
e["departement"] = self.departement.to_dict()
|
||||
else:
|
||||
e.pop("departement", None)
|
||||
e["formation_id"] = self.id # ScoDoc7 backward compat
|
||||
if with_refcomp_attrs and self.referentiel_competence:
|
||||
e["refcomp_version_orebut"] = self.referentiel_competence.version_orebut
|
||||
e["refcomp_specialite"] = self.referentiel_competence.specialite
|
||||
e["refcomp_type_titre"] = self.referentiel_competence.type_titre
|
||||
|
||||
return e
|
||||
|
||||
def get_parcours(self):
|
||||
"""get l'instance de TypeParcours de cette formation
|
||||
(le TypeParcours définit le genre de formation, à ne pas confondre
|
||||
def get_cursus(self) -> codes_cursus.TypeCursus:
|
||||
"""get l'instance de TypeCursus de cette formation
|
||||
(le TypeCursus définit le genre de formation, à ne pas confondre
|
||||
avec les parcours du BUT).
|
||||
"""
|
||||
return sco_codes_parcours.get_parcours_from_code(self.type_parcours)
|
||||
return codes_cursus.get_cursus_from_code(self.type_parcours)
|
||||
|
||||
def get_titre_version(self) -> str:
|
||||
"""Titre avec version"""
|
||||
|
@ -82,7 +98,7 @@ class Formation(db.Model):
|
|||
|
||||
def is_apc(self):
|
||||
"True si formation APC avec SAE (BUT)"
|
||||
return self.get_parcours().APC_SAE
|
||||
return self.get_cursus().APC_SAE
|
||||
|
||||
def get_module_coefs(self, semestre_idx: int = None):
|
||||
"""Les coefs des modules vers les UE (accès via cache)"""
|
||||
|
@ -101,9 +117,14 @@ class Formation(db.Model):
|
|||
df_cache.ModuleCoefsCache.set(key, modules_coefficients)
|
||||
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 has_locked_sems(self, semestre_idx: int = None):
|
||||
"""True if there is a locked formsemestre in this formation.
|
||||
If semestre_idx is specified, check only this index.
|
||||
"""
|
||||
query = self.formsemestres.filter_by(etat=False)
|
||||
if semestre_idx is not None:
|
||||
query = query.filter_by(semestre_id=semestre_idx)
|
||||
return len(query.all()) > 0
|
||||
|
||||
def invalidate_module_coefs(self, semestre_idx: int = None):
|
||||
"""Invalide le cache des coefficients de modules.
|
||||
|
@ -194,12 +215,17 @@ class Formation(db.Model):
|
|||
|
||||
def query_ues_parcour(self, parcour: ApcParcours) -> flask_sqlalchemy.BaseQuery:
|
||||
"""Les UEs d'un parcours de la formation.
|
||||
Si parcour est None, les UE sans parcours.
|
||||
Exemple: pour avoir les UE du semestre 3, faire
|
||||
`formation.query_ues_parcour(parcour).filter_by(semestre_idx=3)`
|
||||
"""
|
||||
return UniteEns.query.filter_by(formation=self).filter(
|
||||
if parcour is None:
|
||||
return UniteEns.query.filter_by(
|
||||
formation=self, type=UE_STANDARD, parcour_id=None
|
||||
)
|
||||
return UniteEns.query.filter_by(formation=self, type=UE_STANDARD).filter(
|
||||
UniteEns.niveau_competence_id == ApcNiveau.id,
|
||||
UniteEns.type == UE_STANDARD,
|
||||
(UniteEns.parcour_id == parcour.id) | (UniteEns.parcour_id == None),
|
||||
ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
|
||||
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
|
||||
ApcAnneeParcours.parcours_id == parcour.id,
|
||||
|
@ -226,6 +252,21 @@ class Formation(db.Model):
|
|||
.filter(ApcAnneeParcours.parcours_id == parcour.id)
|
||||
)
|
||||
|
||||
def refcomp_desassoc(self):
|
||||
"""Désassocie la formation de son ref. de compétence"""
|
||||
self.referentiel_competence = None
|
||||
db.session.add(self)
|
||||
# Niveaux des UE
|
||||
for ue in self.ues:
|
||||
ue.niveau_competence = None
|
||||
db.session.add(ue)
|
||||
# Parcours et AC des modules
|
||||
for mod in self.modules:
|
||||
mod.parcours = []
|
||||
mod.app_critiques = []
|
||||
db.session.add(mod)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class Matiere(db.Model):
|
||||
"""Matières: regroupe les modules d'une UE
|
||||
|
@ -253,6 +294,6 @@ class Matiere(db.Model):
|
|||
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["ue_id"] = self.id
|
||||
return e
|
||||
|
|
|
@ -1,45 +1,49 @@
|
|||
# -*- coding: UTF-8 -*
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
# pylint génère trop de faux positifs avec les colonnes date:
|
||||
# pylint: disable=no-member,not-an-iterable
|
||||
|
||||
"""ScoDoc models: formsemestre
|
||||
"""
|
||||
import datetime
|
||||
from functools import cached_property
|
||||
|
||||
from flask import flash, g
|
||||
from flask_login import current_user
|
||||
import flask_sqlalchemy
|
||||
from flask import flash, g
|
||||
from sqlalchemy import and_, or_
|
||||
from sqlalchemy.sql import text
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import CODE_STR_LEN
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import db, log
|
||||
from app.auth.models import User
|
||||
from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN
|
||||
from app.models.but_refcomp import (
|
||||
ApcAnneeParcours,
|
||||
ApcNiveau,
|
||||
ApcParcours,
|
||||
ApcParcoursNiveauCompetence,
|
||||
ApcReferentielCompetences,
|
||||
parcours_formsemestre,
|
||||
)
|
||||
from app.models.groups import GroupDescr, Partition
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.models.but_refcomp import parcours_formsemestre
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formations import Formation
|
||||
from app.models.groups import GroupDescr, Partition
|
||||
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription
|
||||
from app.models.modules import Module
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
from app.models.ues import UniteEns
|
||||
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||
from app.models.validations import ScolarFormSemestreValidation
|
||||
from app.scodoc import codes_cursus, sco_preferences
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV
|
||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||
|
||||
|
||||
class FormSemestre(db.Model):
|
||||
|
@ -54,51 +58,58 @@ class FormSemestre(db.Model):
|
|||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
|
||||
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
||||
titre = db.Column(db.Text())
|
||||
date_debut = db.Column(db.Date())
|
||||
date_fin = db.Column(db.Date())
|
||||
etat = db.Column(
|
||||
db.Boolean(), nullable=False, default=True, server_default="true"
|
||||
) # False si verrouillé
|
||||
titre = db.Column(db.Text(), nullable=False)
|
||||
date_debut = db.Column(db.Date(), nullable=False)
|
||||
date_fin = db.Column(db.Date(), nullable=False)
|
||||
etat = db.Column(db.Boolean(), nullable=False, default=True, server_default="true")
|
||||
"False si verrouillé"
|
||||
modalite = db.Column(
|
||||
db.String(SHORT_STR_LEN), db.ForeignKey("notes_form_modalites.modalite")
|
||||
) # "FI", "FAP", "FC", ...
|
||||
# gestion compensation sem DUT:
|
||||
)
|
||||
"Modalité de formation: 'FI', 'FAP', 'FC', ..."
|
||||
gestion_compensation = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# ne publie pas le bulletin XML ou JSON:
|
||||
"gestion compensation sem DUT (inutilisé en APC)"
|
||||
bul_hide_xml = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# Bloque le calcul des moyennes (générale et d'UE)
|
||||
"ne publie pas le bulletin XML ou JSON"
|
||||
block_moyennes = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# semestres decales (pour gestion jurys):
|
||||
"Bloque le calcul des moyennes (générale et d'UE)"
|
||||
block_moyenne_generale = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
"Si vrai, la moyenne générale indicative BUT n'est pas calculée"
|
||||
gestion_semestrielle = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# couleur fond bulletins HTML:
|
||||
"Semestres décalés (pour gestion jurys DUT, pas implémenté ou utile en BUT)"
|
||||
bul_bgcolor = db.Column(
|
||||
db.String(SHORT_STR_LEN), default="white", server_default="white"
|
||||
db.String(SHORT_STR_LEN),
|
||||
default="white",
|
||||
server_default="white",
|
||||
nullable=False,
|
||||
)
|
||||
# autorise resp. a modifier semestre:
|
||||
"couleur fond bulletins HTML"
|
||||
resp_can_edit = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# autorise resp. a modifier slt les enseignants:
|
||||
"autorise resp. à modifier le formsemestre"
|
||||
resp_can_change_ens = db.Column(
|
||||
db.Boolean(), nullable=False, default=True, server_default="true"
|
||||
)
|
||||
# autorise les ens a creer des evals:
|
||||
"autorise resp. a modifier slt les enseignants"
|
||||
ens_can_edit_eval = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="False"
|
||||
)
|
||||
# code element semestre Apogee, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...'
|
||||
"autorise les enseignants à créer des évals dans leurs modimpls"
|
||||
elt_sem_apo = db.Column(db.Text()) # peut être fort long !
|
||||
# code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'
|
||||
"code element semestre Apogee, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...'"
|
||||
elt_annee_apo = db.Column(db.Text())
|
||||
"code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'"
|
||||
|
||||
# Relations:
|
||||
etapes = db.relationship(
|
||||
|
@ -108,6 +119,7 @@ class FormSemestre(db.Model):
|
|||
"ModuleImpl",
|
||||
backref="formsemestre",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
etuds = db.relationship(
|
||||
"Identite",
|
||||
|
@ -145,7 +157,12 @@ class FormSemestre(db.Model):
|
|||
self.modalite = FormationModalite.DEFAULT_MODALITE
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} {self.titre_num()}>"
|
||||
return f"<{self.__class__.__name__} {self.id} {self.titre_annee()}>"
|
||||
|
||||
def sort_key(self) -> tuple:
|
||||
"""clé pour tris par ordre alphabétique
|
||||
(pour avoir le plus récent d'abord, sort avec reverse=True)"""
|
||||
return (self.date_debut, self.semestre_id)
|
||||
|
||||
def to_dict(self, convert_objects=False) -> dict:
|
||||
"""dict (compatible ScoDoc7).
|
||||
|
@ -170,7 +187,7 @@ class FormSemestre(db.Model):
|
|||
d["responsables"] = [u.id for u in self.responsables]
|
||||
d["titre_formation"] = self.titre_formation()
|
||||
if convert_objects:
|
||||
d["parcours"] = [p.to_dict() for p in self.parcours]
|
||||
d["parcours"] = [p.to_dict() for p in self.get_parcours_apc()]
|
||||
d["departement"] = self.departement.to_dict()
|
||||
d["formation"] = self.formation.to_dict()
|
||||
d["etape_apo"] = self.etapes_apo_str()
|
||||
|
@ -197,9 +214,10 @@ class FormSemestre(db.Model):
|
|||
d["etape_apo"] = self.etapes_apo_str()
|
||||
d["formsemestre_id"] = self.id
|
||||
d["formation"] = self.formation.to_dict()
|
||||
d["parcours"] = [p.to_dict() for p in self.parcours]
|
||||
d["parcours"] = [p.to_dict() for p in self.get_parcours_apc()]
|
||||
d["responsables"] = [u.id for u in self.responsables]
|
||||
d["titre_court"] = self.formation.acronyme
|
||||
d["titre_formation"] = self.titre_formation()
|
||||
d["titre_num"] = self.titre_num()
|
||||
d["session_id"] = self.session_id()
|
||||
return d
|
||||
|
@ -219,7 +237,8 @@ class FormSemestre(db.Model):
|
|||
d["mois_debut_ord"] = self.date_debut.month
|
||||
d["mois_fin_ord"] = self.date_fin.month
|
||||
# La période: considère comme "S1" (ou S3) les débuts en aout-sept-octobre
|
||||
# devrait sans doute pouvoir etre changé...
|
||||
# devrait sans doute pouvoir etre changé... XXX PIVOT
|
||||
d["periode"] = self.periode()
|
||||
if self.date_debut.month >= 8 and self.date_debut.month <= 10:
|
||||
d["periode"] = 1 # typiquement, début en septembre: S1, S3...
|
||||
else:
|
||||
|
@ -238,17 +257,41 @@ class FormSemestre(db.Model):
|
|||
d["etapes_apo_str"] = self.etapes_apo_str()
|
||||
return d
|
||||
|
||||
def flip_lock(self):
|
||||
"""Flip etat (lock)"""
|
||||
self.etat = not self.etat
|
||||
db.session.add(self)
|
||||
|
||||
def get_parcours_apc(self) -> list[ApcParcours]:
|
||||
"""Liste des parcours proposés par ce semestre.
|
||||
Si aucun n'est coché et qu'il y a un référentiel, tous ceux du référentiel.
|
||||
"""
|
||||
r = self.parcours or (
|
||||
self.formation.referentiel_competence
|
||||
and self.formation.referentiel_competence.parcours
|
||||
)
|
||||
return r or []
|
||||
|
||||
def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery:
|
||||
"""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.
|
||||
- Formations APC / BUT: les UEs de la formation qui
|
||||
- ont le même numéro de semestre que ce formsemestre
|
||||
- sont associées à l'un des parcours de ce formsemestre (ou à aucun)
|
||||
|
||||
"""
|
||||
if self.formation.get_parcours().APC_SAE:
|
||||
if self.formation.get_cursus().APC_SAE:
|
||||
sem_ues = UniteEns.query.filter_by(
|
||||
formation=self.formation, semestre_idx=self.semestre_id
|
||||
)
|
||||
if self.parcours:
|
||||
# Prend toutes les UE de l'un des parcours du sem., ou déclarées sans parcours
|
||||
sem_ues = sem_ues.filter(
|
||||
(UniteEns.parcour == None)
|
||||
| (UniteEns.parcour_id.in_([p.id for p in self.parcours]))
|
||||
)
|
||||
# si le sem. ne coche aucun parcours, prend toutes les UE
|
||||
else:
|
||||
sem_ues = db.session.query(UniteEns).filter(
|
||||
ModuleImpl.formsemestre_id == self.id,
|
||||
|
@ -256,12 +299,15 @@ class FormSemestre(db.Model):
|
|||
UniteEns.id == Module.ue_id,
|
||||
)
|
||||
if not with_sport:
|
||||
sem_ues = sem_ues.filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
|
||||
sem_ues = sem_ues.filter(UniteEns.type != codes_cursus.UE_SPORT)
|
||||
return sem_ues.order_by(UniteEns.numero)
|
||||
|
||||
def query_ues_parcours_etud(self, etudid: int) -> flask_sqlalchemy.BaseQuery:
|
||||
"""UE que suit l'étudiant dans ce semestre BUT
|
||||
"""XXX inutilisé à part pour un test unitaire => supprimer ?
|
||||
UEs que suit l'étudiant dans ce semestre BUT
|
||||
en fonction du parcours dans lequel il est inscrit.
|
||||
Si l'étudiant n'est inscrit à aucun parcours,
|
||||
renvoie uniquement les UEs de tronc commun (sans parcours).
|
||||
|
||||
Si voulez les UE d'un parcours, il est plus efficace de passer par
|
||||
`formation.query_ues_parcour(parcour)`.
|
||||
|
@ -272,7 +318,13 @@ class FormSemestre(db.Model):
|
|||
UniteEns.niveau_competence_id == ApcNiveau.id,
|
||||
ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
|
||||
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
|
||||
ApcAnneeParcours.parcours_id == FormSemestreInscription.parcour_id,
|
||||
or_(
|
||||
ApcAnneeParcours.parcours_id == FormSemestreInscription.parcour_id,
|
||||
and_(
|
||||
FormSemestreInscription.parcour_id.is_(None),
|
||||
UniteEns.parcour_id.is_(None),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@cached_property
|
||||
|
@ -285,7 +337,7 @@ class FormSemestre(db.Model):
|
|||
if self.formation.is_apc():
|
||||
modimpls.sort(
|
||||
key=lambda m: (
|
||||
m.module.module_type or 0,
|
||||
m.module.module_type or 0, # ressources (2) avant SAEs (3)
|
||||
m.module.numero or 0,
|
||||
m.module.code or 0,
|
||||
)
|
||||
|
@ -324,7 +376,7 @@ class FormSemestre(db.Model):
|
|||
return [ModuleImpl.query.get(modimpl_id) for modimpl_id in cursor]
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
"""Vrai si user peut modifier ce semestre"""
|
||||
"""Vrai si user peut modifier ce semestre (est chef ou l'un des responsables)"""
|
||||
if not user.has_permission(Permission.ScoImplement): # pas chef
|
||||
if not self.resp_can_edit or user.id not in [
|
||||
resp.id for resp in self.responsables
|
||||
|
@ -338,7 +390,7 @@ class FormSemestre(db.Model):
|
|||
(les dates de début et fin sont incluses)
|
||||
"""
|
||||
today = datetime.date.today()
|
||||
return (self.date_debut <= today) and (today <= self.date_fin)
|
||||
return self.date_debut <= today <= self.date_fin
|
||||
|
||||
def contient_periode(self, date_debut, date_fin) -> bool:
|
||||
"""Vrai si l'intervalle [date_debut, date_fin] est
|
||||
|
@ -351,29 +403,105 @@ class FormSemestre(db.Model):
|
|||
"""Test si sem est entièrement sur la même année scolaire.
|
||||
(ce n'est pas obligatoire mais si ce n'est pas le
|
||||
cas les exports Apogée risquent de mal fonctionner)
|
||||
Pivot au 1er août.
|
||||
Pivot au 1er août par défaut.
|
||||
"""
|
||||
if self.date_debut > self.date_fin:
|
||||
flash(f"Dates début/fin inversées pour le semestre {self.titre_annee()}")
|
||||
log(f"Warning: semestre {self.id} begins after ending !")
|
||||
annee_debut = self.date_debut.year
|
||||
if self.date_debut.month < 8: # août
|
||||
# considere que debut sur l'anne scolaire precedente
|
||||
month_debut_annee = ScoDocSiteConfig.get_month_debut_annee_scolaire()
|
||||
if self.date_debut.month < month_debut_annee:
|
||||
# début sur l'année scolaire précédente (juillet inclus par défaut)
|
||||
annee_debut -= 1
|
||||
annee_fin = self.date_fin.year
|
||||
if self.date_fin.month < 9:
|
||||
# 9 (sept) pour autoriser un début en sept et une fin en aout
|
||||
if self.date_fin.month < (month_debut_annee + 1):
|
||||
# 9 (sept) pour autoriser un début en sept et une fin en août
|
||||
annee_fin -= 1
|
||||
return annee_debut == annee_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
|
||||
c'est à dire semestres impairs commençant (par défaut)
|
||||
entre janvier et juin et les pairs entre juillet et décembre.
|
||||
"""
|
||||
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
|
||||
return (
|
||||
# impair
|
||||
(
|
||||
self.semestre_id % 2
|
||||
and self.date_debut.month < scu.MONTH_DEBUT_ANNEE_SCOLAIRE
|
||||
)
|
||||
or
|
||||
# pair
|
||||
(
|
||||
(not self.semestre_id % 2)
|
||||
and self.date_debut.month >= scu.MONTH_DEBUT_ANNEE_SCOLAIRE
|
||||
)
|
||||
)
|
||||
|
||||
def est_terminal(self) -> bool:
|
||||
"Vrai si dernier semestre de son cursus (ou formation mono-semestre)"
|
||||
return (self.semestre_id < 0) or (
|
||||
self.semestre_id == self.formation.get_cursus().NB_SEM
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def comp_periode(
|
||||
cls,
|
||||
date_debut: datetime,
|
||||
mois_pivot_annee=scu.MONTH_DEBUT_ANNEE_SCOLAIRE,
|
||||
mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2,
|
||||
jour_pivot_annee=1,
|
||||
jour_pivot_periode=1,
|
||||
):
|
||||
"""Calcule la session associée à un formsemestre commençant en date_debut
|
||||
sous la forme (année, période)
|
||||
année: première année de l'année scolaire
|
||||
période = 1 (première période de l'année scolaire, souvent automne)
|
||||
ou 2 (deuxième période de l'année scolaire, souvent printemps)
|
||||
Les quatre derniers paramètres forment les dates pivots pour l'année
|
||||
(1er août par défaut) et pour la période (1er décembre par défaut).
|
||||
|
||||
Les calculs se font à partir de la date de début indiquée.
|
||||
Exemples dans tests/unit/test_periode
|
||||
|
||||
Implémentation:
|
||||
Cas à considérer pour le calcul de la période
|
||||
|
||||
pa < pp -----------------|-------------------|---------------->
|
||||
(A-1, P:2) pa (A, P:1) pp (A, P:2)
|
||||
pp < pa -----------------|-------------------|---------------->
|
||||
(A-1, P:1) pp (A-1, P:2) pa (A, P:1)
|
||||
"""
|
||||
pivot_annee = 100 * mois_pivot_annee + jour_pivot_annee
|
||||
pivot_periode = 100 * mois_pivot_periode + jour_pivot_periode
|
||||
pivot_sem = 100 * date_debut.month + date_debut.day
|
||||
if pivot_sem < pivot_annee:
|
||||
annee = date_debut.year - 1
|
||||
else:
|
||||
annee = date_debut.year
|
||||
if pivot_annee < pivot_periode:
|
||||
if pivot_sem < pivot_annee or pivot_sem >= pivot_periode:
|
||||
periode = 2
|
||||
else:
|
||||
periode = 1
|
||||
else:
|
||||
if pivot_sem < pivot_periode or pivot_sem >= pivot_annee:
|
||||
periode = 1
|
||||
else:
|
||||
periode = 2
|
||||
return annee, periode
|
||||
|
||||
def periode(self) -> int:
|
||||
"""La période:
|
||||
* 1 : première période: automne à Paris
|
||||
* 2 : deuxième période, printemps à Paris
|
||||
"""
|
||||
return FormSemestre.comp_periode(
|
||||
self.date_debut,
|
||||
mois_pivot_annee=ScoDocSiteConfig.get_month_debut_annee_scolaire(),
|
||||
mois_pivot_periode=ScoDocSiteConfig.get_month_debut_periode2(),
|
||||
)
|
||||
|
||||
def etapes_apo_vdi(self) -> list[ApoEtapeVDI]:
|
||||
|
@ -420,13 +548,35 @@ class FormSemestre(db.Model):
|
|||
else:
|
||||
return ", ".join([u.get_nomcomplet() for u in self.responsables])
|
||||
|
||||
def est_responsable(self, user):
|
||||
def est_responsable(self, user: User):
|
||||
"True si l'user est l'un des responsables du semestre"
|
||||
return user.id in [u.id for u in self.responsables]
|
||||
|
||||
def est_chef_or_diretud(self, user: User = None):
|
||||
"Vrai si utilisateur (par def. current) est admin, chef dept ou responsable du semestre"
|
||||
user = user or current_user
|
||||
return user.has_permission(Permission.ScoImplement) or self.est_responsable(
|
||||
user
|
||||
)
|
||||
|
||||
def can_edit_jury(self, user: User = None):
|
||||
"""Vrai si utilisateur (par def. current) peut saisir decision de jury
|
||||
dans ce semestre: vérifie permission et verrouillage.
|
||||
"""
|
||||
user = user or current_user
|
||||
return self.etat and self.est_chef_or_diretud(user)
|
||||
|
||||
def can_edit_pv(self, user: User = None):
|
||||
"Vrai si utilisateur (par def. current) peut editer un PV de jury de ce semestre"
|
||||
user = user or current_user
|
||||
# Autorise les secrétariats, repérés via la permission ScoEtudChangeAdr
|
||||
return self.est_chef_or_diretud(user) or user.has_permission(
|
||||
Permission.ScoEtudChangeAdr
|
||||
)
|
||||
|
||||
def annee_scolaire(self) -> int:
|
||||
"""L'année de début de l'année scolaire.
|
||||
Par exemple, 2022 si le semestre va de septebre 2022 à février 2023."""
|
||||
Par exemple, 2022 si le semestre va de septembre 2022 à février 2023."""
|
||||
return scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month)
|
||||
|
||||
def annee_scolaire_str(self):
|
||||
|
@ -459,7 +609,7 @@ class FormSemestre(db.Model):
|
|||
if not imputation_dept:
|
||||
imputation_dept = prefs["DeptName"]
|
||||
imputation_dept = imputation_dept.upper()
|
||||
parcours_name = self.formation.get_parcours().NAME
|
||||
cursus_name = self.formation.get_cursus().NAME
|
||||
modalite = self.modalite
|
||||
# exception pour code Apprentissage:
|
||||
modalite = (modalite or "").replace("FAP", "FA").replace("APP", "FA")
|
||||
|
@ -472,11 +622,13 @@ class FormSemestre(db.Model):
|
|||
scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month)
|
||||
)
|
||||
return scu.sanitize_string(
|
||||
f"{imputation_dept}-{parcours_name}-{modalite}-{semestre_id}-{annee_sco}"
|
||||
f"{imputation_dept}-{cursus_name}-{modalite}-{semestre_id}-{annee_sco}"
|
||||
)
|
||||
|
||||
def titre_annee(self) -> str:
|
||||
""" """
|
||||
"""Le titre avec l'année
|
||||
'DUT Réseaux et Télécommunications semestre 3 FAP 2020-2021'
|
||||
"""
|
||||
titre_annee = (
|
||||
f"{self.titre_num()} {self.modalite or ''} {self.date_debut.year}"
|
||||
)
|
||||
|
@ -484,10 +636,12 @@ class FormSemestre(db.Model):
|
|||
titre_annee += "-" + str(self.date_fin.year)
|
||||
return titre_annee
|
||||
|
||||
def titre_formation(self):
|
||||
def titre_formation(self, with_sem_idx=False):
|
||||
"""Titre avec formation, court, pour passerelle: "BUT R&T"
|
||||
(méthode de formsemestre car on pourrait ajouter le semestre, ou d'autres infos, à voir)
|
||||
"""
|
||||
if with_sem_idx and self.semestre_id > 0:
|
||||
return f"{self.formation.acronyme} S{self.semestre_id}"
|
||||
return self.formation.acronyme
|
||||
|
||||
def titre_mois(self) -> str:
|
||||
|
@ -502,9 +656,9 @@ class FormSemestre(db.Model):
|
|||
|
||||
def titre_num(self) -> str:
|
||||
"""Le titre et le semestre, ex ""DUT Informatique semestre 2"" """
|
||||
if self.semestre_id == sco_codes_parcours.NO_SEMESTRE_ID:
|
||||
if self.semestre_id == codes_cursus.NO_SEMESTRE_ID:
|
||||
return self.titre
|
||||
return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}"
|
||||
return f"{self.titre} {self.formation.get_cursus().SESSION_NAME} {self.semestre_id}"
|
||||
|
||||
def sem_modalite(self) -> str:
|
||||
"""Le semestre et la modalité, ex "S2 FI" ou "S3 APP" """
|
||||
|
@ -582,14 +736,43 @@ class FormSemestre(db.Model):
|
|||
db.session.add(partition)
|
||||
db.session.flush() # pour avoir un id
|
||||
flash("Partition Parcours créée.")
|
||||
elif partition.groups_editable:
|
||||
# Il ne faut jamais laisser éditer cette partition de parcours
|
||||
partition.groups_editable = False
|
||||
db.session.add(partition)
|
||||
|
||||
for parcour in self.parcours:
|
||||
for parcour in self.get_parcours_apc():
|
||||
if parcour.code:
|
||||
group = GroupDescr.query.filter_by(
|
||||
partition_id=partition.id, group_name=parcour.code
|
||||
).first()
|
||||
if not group:
|
||||
partition.groups.append(GroupDescr(group_name=parcour.code))
|
||||
db.session.flush()
|
||||
# S'il reste des groupes de parcours qui ne sont plus dans le semestre
|
||||
# - s'ils n'ont pas d'inscrits, supprime-les.
|
||||
# - s'ils ont des inscrits: avertissement
|
||||
for group in GroupDescr.query.filter_by(partition_id=partition.id):
|
||||
if group.group_name not in (p.code for p in self.get_parcours_apc()):
|
||||
if (
|
||||
len(
|
||||
[
|
||||
inscr
|
||||
for inscr in self.inscriptions
|
||||
if (inscr.parcour is not None)
|
||||
and inscr.parcour.code == group.group_name
|
||||
]
|
||||
)
|
||||
== 0
|
||||
):
|
||||
flash(f"Suppression du groupe de parcours vide {group.group_name}")
|
||||
db.session.delete(group)
|
||||
else:
|
||||
flash(
|
||||
f"""Attention: groupe de parcours {group.group_name} non vide:
|
||||
réaffectez ses étudiants dans des parcours du semestre"""
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
def update_inscriptions_parcours_from_groups(self) -> None:
|
||||
|
@ -648,6 +831,71 @@ class FormSemestre(db.Model):
|
|||
)
|
||||
db.session.commit()
|
||||
|
||||
def etud_validations_description_html(self, etudid: int) -> str:
|
||||
"""Description textuelle des validations de jury de cet étudiant dans ce semestre"""
|
||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||
|
||||
vals_sem = ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=etudid, formsemestre_id=self.id, ue_id=None
|
||||
).all()
|
||||
vals_ues = (
|
||||
ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=etudid, formsemestre_id=self.id
|
||||
)
|
||||
.join(UniteEns)
|
||||
.order_by(UniteEns.numero)
|
||||
.all()
|
||||
)
|
||||
# Validations BUT:
|
||||
vals_rcues = (
|
||||
ApcValidationRCUE.query.filter_by(etudid=etudid, formsemestre_id=self.id)
|
||||
.join(UniteEns, ApcValidationRCUE.ue1)
|
||||
.order_by(UniteEns.numero)
|
||||
.all()
|
||||
)
|
||||
vals_annee = (
|
||||
ApcValidationAnnee.query.filter_by(
|
||||
etudid=etudid,
|
||||
annee_scolaire=self.annee_scolaire(),
|
||||
)
|
||||
.join(ApcValidationAnnee.formsemestre)
|
||||
.join(FormSemestre.formation)
|
||||
.filter(Formation.formation_code == self.formation.formation_code)
|
||||
.all()
|
||||
)
|
||||
H = []
|
||||
for vals in (vals_sem, vals_ues, vals_rcues, vals_annee):
|
||||
if vals:
|
||||
H.append(
|
||||
f"""<ul><li>{"</li><li>".join(str(x) for x in vals)}</li></ul>"""
|
||||
)
|
||||
return "\n".join(H)
|
||||
|
||||
def etud_set_all_missing_notes(self, etud: Identite, value=None) -> int:
|
||||
"""Met toutes les notes manquantes de cet étudiant dans ce semestre
|
||||
(ie dans toutes les évaluations des modules auxquels il est inscrit et n'a pas de note)
|
||||
à la valeur donnée par value, qui est en général "ATT", "ABS", "EXC".
|
||||
"""
|
||||
from app.scodoc import sco_saisie_notes
|
||||
|
||||
inscriptions = (
|
||||
ModuleImplInscription.query.filter_by(etudid=etud.id)
|
||||
.join(ModuleImpl)
|
||||
.filter_by(formsemestre_id=self.id)
|
||||
)
|
||||
nb_recorded = 0
|
||||
for inscription in inscriptions:
|
||||
for evaluation in inscription.modimpl.evaluations:
|
||||
if evaluation.get_etud_note(etud) is None:
|
||||
if not sco_saisie_notes.do_evaluation_set_etud_note(
|
||||
evaluation, etud, value
|
||||
):
|
||||
raise ScoValueError(
|
||||
"erreur lors de l'enregistrement de la note"
|
||||
)
|
||||
nb_recorded += 1
|
||||
return nb_recorded
|
||||
|
||||
|
||||
# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
|
||||
notes_formsemestre_responsables = db.Table(
|
||||
|
@ -826,7 +1074,9 @@ class FormSemestreInscription(db.Model):
|
|||
# Etape Apogée d'inscription (ajout 2020)
|
||||
etape = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
# Parcours (pour les BUT)
|
||||
parcour_id = db.Column(db.Integer, db.ForeignKey("apc_parcours.id"), index=True)
|
||||
parcour_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_parcours.id", ondelete="SET NULL"), index=True
|
||||
)
|
||||
parcour = db.relationship(ApcParcours)
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -846,8 +1096,8 @@ class NotesSemSet(db.Model):
|
|||
|
||||
title = db.Column(db.Text)
|
||||
annee_scolaire = db.Column(db.Integer, nullable=True, default=None)
|
||||
# periode: 0 (année), 1 (Simpair), 2 (Spair)
|
||||
sem_id = db.Column(db.Integer, nullable=True, default=None)
|
||||
sem_id = db.Column(db.Integer, nullable=False, default=0)
|
||||
"période: 0 (année), 1 (Simpair), 2 (Spair)"
|
||||
|
||||
|
||||
# Association: many to many
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: UTF-8 -*
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -72,7 +72,7 @@ class Partition(db.Model):
|
|||
"""
|
||||
if not isinstance(partition_name, str):
|
||||
return False
|
||||
if not len(partition_name.strip()) > 0:
|
||||
if not (0 < len(partition_name.strip()) < SHORT_STR_LEN):
|
||||
return False
|
||||
if (not existing) and (
|
||||
partition_name in [p.partition_name for p in formsemestre.partitions]
|
||||
|
@ -87,6 +87,7 @@ class Partition(db.Model):
|
|||
def to_dict(self, with_groups=False) -> dict:
|
||||
"""as a dict, with or without groups"""
|
||||
d = dict(self.__dict__)
|
||||
d["partition_id"] = self.id
|
||||
d.pop("_sa_instance_state", None)
|
||||
d.pop("formsemestre", None)
|
||||
|
||||
|
@ -146,7 +147,7 @@ class GroupDescr(db.Model):
|
|||
"""
|
||||
if not isinstance(group_name, str):
|
||||
return False
|
||||
if not default and not len(group_name.strip()) > 0:
|
||||
if not default and not (0 < len(group_name.strip()) < GROUPNAME_STR_LEN):
|
||||
return False
|
||||
if (not existing) and (group_name in [g.group_name for g in partition.groups]):
|
||||
return False
|
||||
|
|
|
@ -5,10 +5,12 @@ import pandas as pd
|
|||
import flask_sqlalchemy
|
||||
|
||||
from app import db
|
||||
from app.auth.models import User
|
||||
from app.comp import df_cache
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.modules import Module
|
||||
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
|
@ -20,14 +22,12 @@ class ModuleImpl(db.Model):
|
|||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
moduleimpl_id = db.synonym("id")
|
||||
module_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_modules.id"),
|
||||
)
|
||||
module_id = db.Column(db.Integer, db.ForeignKey("notes_modules.id"), nullable=False)
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
index=True,
|
||||
nullable=False,
|
||||
)
|
||||
responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id"))
|
||||
# formule de calcul moyenne:
|
||||
|
@ -62,11 +62,11 @@ class ModuleImpl(db.Model):
|
|||
"""Invalide poids cachés"""
|
||||
df_cache.EvaluationsPoidsCache.delete(self.id)
|
||||
|
||||
def check_apc_conformity(self) -> bool:
|
||||
def check_apc_conformity(self, res: "ResultatsSemestreBUT") -> bool:
|
||||
"""true si les poids des évaluations du module permettent de satisfaire
|
||||
les coefficients du PN.
|
||||
"""
|
||||
if not self.module.formation.get_parcours().APC_SAE or (
|
||||
if not self.module.formation.get_cursus().APC_SAE or (
|
||||
self.module.module_type != scu.ModuleType.RESSOURCE
|
||||
and self.module.module_type != scu.ModuleType.SAE
|
||||
):
|
||||
|
@ -76,7 +76,7 @@ class ModuleImpl(db.Model):
|
|||
return moy_mod.moduleimpl_is_conforme(
|
||||
self,
|
||||
self.get_evaluations_poids(),
|
||||
self.module.formation.get_module_coefs(self.module.semestre_id),
|
||||
res.modimpl_coefs_df,
|
||||
)
|
||||
|
||||
def to_dict(self, convert_objects=False, with_module=True):
|
||||
|
@ -101,6 +101,64 @@ class ModuleImpl(db.Model):
|
|||
d.pop("module", None)
|
||||
return d
|
||||
|
||||
def can_change_ens_by(self, user: User, raise_exc=False) -> bool:
|
||||
"""Check if user can modify module resp.
|
||||
If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not.
|
||||
= Admin, et dir des etud. (si option l'y autorise)
|
||||
"""
|
||||
if not self.formsemestre.etat:
|
||||
if raise_exc:
|
||||
raise ScoLockedSemError("Modification impossible: semestre verrouille")
|
||||
return False
|
||||
# -- check access
|
||||
# admin ou resp. semestre avec flag resp_can_change_resp
|
||||
if user.has_permission(Permission.ScoImplement):
|
||||
return True
|
||||
if (
|
||||
user.id in [resp.id for resp in self.formsemestre.responsables]
|
||||
) and self.formsemestre.resp_can_change_ens:
|
||||
return True
|
||||
if raise_exc:
|
||||
raise AccessDenied(f"Modification impossible pour {user}")
|
||||
return False
|
||||
|
||||
def est_inscrit(self, etud: Identite) -> bool:
|
||||
"""
|
||||
Vérifie si l'étudiant est bien inscrit au moduleimpl
|
||||
|
||||
Retourne Vrai si c'est le cas, faux sinon
|
||||
"""
|
||||
|
||||
is_module: int = (
|
||||
ModuleImplInscription.query.filter_by(
|
||||
etudid=etud.id, moduleimpl_id=self.id
|
||||
).count()
|
||||
> 0
|
||||
)
|
||||
|
||||
return is_module
|
||||
|
||||
def can_change_ens_by(self, user: User, raise_exc=False) -> bool:
|
||||
"""Check if user can modify module resp.
|
||||
If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not.
|
||||
= Admin, et dir des etud. (si option l'y autorise)
|
||||
"""
|
||||
if not self.formsemestre.etat:
|
||||
if raise_exc:
|
||||
raise ScoLockedSemError("Modification impossible: semestre verrouille")
|
||||
return False
|
||||
# -- check access
|
||||
# admin ou resp. semestre avec flag resp_can_change_resp
|
||||
if user.has_permission(Permission.ScoImplement):
|
||||
return True
|
||||
if (
|
||||
user.id in [resp.id for resp in self.formsemestre.responsables]
|
||||
) and self.formsemestre.resp_can_change_ens:
|
||||
return True
|
||||
if raise_exc:
|
||||
raise AccessDenied(f"Modification impossible pour {user}")
|
||||
return False
|
||||
|
||||
|
||||
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
||||
notes_modules_enseignants = db.Table(
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
|
||||
from app import db
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models.but_refcomp import app_critiques_modules, parcours_modules
|
||||
from app.models.but_refcomp import ApcParcours, app_critiques_modules, parcours_modules
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
|
@ -37,7 +38,9 @@ class Module(db.Model):
|
|||
# Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum)
|
||||
module_type = db.Column(db.Integer, nullable=False, default=0, server_default="0")
|
||||
# Relations:
|
||||
modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic")
|
||||
modimpls = db.relationship(
|
||||
"ModuleImpl", backref="module", lazy="dynamic", cascade="all, delete-orphan"
|
||||
)
|
||||
ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True)
|
||||
tags = db.relationship(
|
||||
"NotesTag",
|
||||
|
@ -66,7 +69,39 @@ class Module(db.Model):
|
|||
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!r} semestre_id={self.semestre_id}>"
|
||||
return f"""<Module{ModuleType(self.module_type or ModuleType.STANDARD).name
|
||||
} id={self.id} code={self.code!r} semestre_id={self.semestre_id}>"""
|
||||
|
||||
def clone(self):
|
||||
"""Create a new copy of this module."""
|
||||
mod = Module(
|
||||
titre=self.titre,
|
||||
abbrev=self.abbrev,
|
||||
code=self.code + "-copie",
|
||||
heures_cours=self.heures_cours,
|
||||
heures_td=self.heures_td,
|
||||
heures_tp=self.heures_tp,
|
||||
coefficient=self.coefficient,
|
||||
ects=self.ects,
|
||||
ue_id=self.ue_id,
|
||||
matiere_id=self.matiere_id,
|
||||
formation_id=self.formation_id,
|
||||
semestre_id=self.semestre_id,
|
||||
numero=self.numero, # il est conseillé de renuméroter
|
||||
code_apogee="", # volontairement vide pour éviter les erreurs
|
||||
module_type=self.module_type,
|
||||
)
|
||||
|
||||
# Les tags:
|
||||
for tag in self.tags:
|
||||
mod.tags.append(tag)
|
||||
# Les parcours
|
||||
for parcour in self.parcours:
|
||||
mod.parcours.append(parcour)
|
||||
# Les AC
|
||||
for app_critique in self.app_critiques:
|
||||
mod.app_critiques.append(app_critique)
|
||||
return mod
|
||||
|
||||
def to_dict(self, convert_objects=False, with_matiere=False, with_ue=False) -> dict:
|
||||
"""If convert_objects, convert all attributes to native types
|
||||
|
@ -141,6 +176,8 @@ class Module(db.Model):
|
|||
ue_coef_dict = { ue_id : coef }
|
||||
Les coefs nuls (zéro) ne sont pas stockés: la relation est supprimée.
|
||||
"""
|
||||
if self.formation.has_locked_sems():
|
||||
raise ScoValueError("Formation verrouillée")
|
||||
changed = False
|
||||
for ue_id, coef in ue_coef_dict.items():
|
||||
# Existant ?
|
||||
|
@ -167,6 +204,8 @@ class Module(db.Model):
|
|||
|
||||
def update_ue_coef_dict(self, ue_coef_dict: dict):
|
||||
"""update coefs vers UE (ajoute aux existants)"""
|
||||
if self.formation.has_locked_sems():
|
||||
raise ScoValueError("Formation verrouillée")
|
||||
current = self.get_ue_coef_dict()
|
||||
current.update(ue_coef_dict)
|
||||
self.set_ue_coef_dict(current)
|
||||
|
@ -177,6 +216,8 @@ class Module(db.Model):
|
|||
|
||||
def delete_ue_coef(self, ue):
|
||||
"""delete coef"""
|
||||
if self.formation.has_locked_sems():
|
||||
raise ScoValueError("Formation verrouillée")
|
||||
ue_coef = ModuleUECoef.query.get((self.id, ue.id))
|
||||
if ue_coef:
|
||||
db.session.delete(ue_coef)
|
||||
|
@ -188,25 +229,31 @@ class Module(db.Model):
|
|||
# à redéfinir les relationships...
|
||||
return sorted(self.ue_coefs, key=lambda x: x.ue.numero)
|
||||
|
||||
def ue_coefs_list(self, include_zeros=True):
|
||||
def ue_coefs_list(
|
||||
self, include_zeros=True, ues: list["UniteEns"] = None
|
||||
) -> list[tuple["UniteEns", float]]:
|
||||
"""Liste des coefs vers les UE (pour les modules APC).
|
||||
Si include_zeros, liste aussi les UE sans coef (donc nul) de ce semestre,
|
||||
Si ues est spécifié, restreint aux UE indiquées.
|
||||
Sinon si include_zeros, liste aussi les UE sans coef (donc nul) de ce semestre,
|
||||
sauf UE bonus sport.
|
||||
Result: List of tuples [ (ue, coef) ]
|
||||
"""
|
||||
if not self.is_apc():
|
||||
return []
|
||||
if include_zeros:
|
||||
if include_zeros and ues is None:
|
||||
# Toutes les UE du même semestre:
|
||||
ues_semestre = (
|
||||
ues = (
|
||||
self.formation.ues.filter_by(semestre_idx=self.ue.semestre_idx)
|
||||
.filter(UniteEns.type != UE_SPORT)
|
||||
.order_by(UniteEns.numero)
|
||||
.all()
|
||||
)
|
||||
if not ues:
|
||||
return []
|
||||
if ues:
|
||||
coefs_dict = self.get_ue_coef_dict()
|
||||
coefs_list = []
|
||||
for ue in ues_semestre:
|
||||
for ue in ues:
|
||||
coefs_list.append((ue, coefs_dict.get(ue.id, 0.0)))
|
||||
return coefs_list
|
||||
# Liste seulement les coefs définis:
|
||||
|
@ -218,6 +265,19 @@ class Module(db.Model):
|
|||
return {x.strip() for x in self.code_apogee.split(",") if x}
|
||||
return set()
|
||||
|
||||
def get_parcours(self) -> list[ApcParcours]:
|
||||
"""Les parcours utilisant ce module.
|
||||
Si tous les parcours, liste vide (!).
|
||||
"""
|
||||
ref_comp = self.formation.referentiel_competence
|
||||
if not ref_comp:
|
||||
return []
|
||||
tous_parcours_ids = {p.id for p in ref_comp.parcours}
|
||||
parcours_ids = {p.id for p in self.parcours}
|
||||
if tous_parcours_ids == parcours_ids:
|
||||
return []
|
||||
return self.parcours
|
||||
|
||||
|
||||
class ModuleUECoef(db.Model):
|
||||
"""Coefficients des modules vers les UE (APC, BUT)
|
||||
|
|
|
@ -4,8 +4,6 @@
|
|||
"""
|
||||
|
||||
from app import db
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
|
@ -53,6 +51,13 @@ class NotesNotes(db.Model):
|
|||
d.pop("_sa_instance_state", None)
|
||||
return d
|
||||
|
||||
def __repr__(self):
|
||||
"pour debug"
|
||||
from app.models.evaluations import Evaluation
|
||||
|
||||
return f"""<{self.__class__.__name__} {self.id} v={self.value} {self.date.isoformat()
|
||||
} {Evaluation.query.get(self.evaluation_id) if self.evaluation_id else "X" }>"""
|
||||
|
||||
|
||||
class NotesNotesLog(db.Model):
|
||||
"""Historique des modifs sur notes (anciennes entrees de notes_notes)"""
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
"""ScoDoc 9 models : Unités d'Enseignement (UE)
|
||||
"""
|
||||
|
||||
from app import db
|
||||
import pandas as pd
|
||||
|
||||
from app import db, log
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models.but_refcomp import ApcNiveau, ApcParcours
|
||||
from app.models.modules import Module
|
||||
from app.scodoc.sco_exceptions import ScoFormationConflict
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
|
@ -46,12 +51,26 @@ class UniteEns(db.Model):
|
|||
color = db.Column(db.Text())
|
||||
|
||||
# BUT
|
||||
niveau_competence_id = db.Column(db.Integer, db.ForeignKey("apc_niveau.id"))
|
||||
niveau_competence_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_niveau.id", ondelete="SET NULL")
|
||||
)
|
||||
niveau_competence = db.relationship("ApcNiveau", back_populates="ues")
|
||||
|
||||
# Une ue appartient soit à tous les parcours (tronc commun), soit à un seul:
|
||||
parcour_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_parcours.id", ondelete="SET NULL"), index=True
|
||||
)
|
||||
parcour = db.relationship("ApcParcours", back_populates="ues")
|
||||
|
||||
# relations
|
||||
matieres = db.relationship("Matiere", lazy="dynamic", backref="ue")
|
||||
modules = db.relationship("Module", lazy="dynamic", backref="ue")
|
||||
dispense_ues = db.relationship(
|
||||
"DispenseUE",
|
||||
back_populates="ue",
|
||||
cascade="all, delete",
|
||||
passive_deletes=True,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<{self.__class__.__name__}(id={self.id}, formation_id={
|
||||
|
@ -59,6 +78,28 @@ class UniteEns(db.Model):
|
|||
self.semestre_idx} {
|
||||
'EXTERNE' if self.is_external else ''})>"""
|
||||
|
||||
def clone(self):
|
||||
"""Create a new copy of this ue.
|
||||
Ne copie pas le code, ni le code Apogée, ni les liens au réf. de comp.
|
||||
(parcours et niveau).
|
||||
"""
|
||||
ue = UniteEns(
|
||||
formation_id=self.formation_id,
|
||||
acronyme=self.acronyme + "-copie",
|
||||
numero=self.numero,
|
||||
titre=self.titre,
|
||||
semestre_idx=self.semestre_idx,
|
||||
type=self.type,
|
||||
ue_code="", # ne duplique pas le code
|
||||
ects=self.ects,
|
||||
is_external=self.is_external,
|
||||
code_apogee="", # ne copie pas les codes Apo
|
||||
coefficient=self.coefficient,
|
||||
coef_rcue=self.coef_rcue,
|
||||
color=self.color,
|
||||
)
|
||||
return ue
|
||||
|
||||
def to_dict(self, convert_objects=False, with_module_ue_coefs=True):
|
||||
"""as a dict, with the same conversions as in ScoDoc7
|
||||
(except ECTS: keep None)
|
||||
|
@ -74,6 +115,7 @@ class UniteEns(db.Model):
|
|||
e["ects"] = e["ects"]
|
||||
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
|
||||
e["code_apogee"] = e["code_apogee"] or "" # pas de None
|
||||
e["parcour"] = self.parcour.to_dict() if self.parcour else None
|
||||
if with_module_ue_coefs:
|
||||
if convert_objects:
|
||||
e["module_ue_coefs"] = [
|
||||
|
@ -83,6 +125,12 @@ class UniteEns(db.Model):
|
|||
e.pop("module_ue_coefs", None)
|
||||
return e
|
||||
|
||||
def annee(self) -> int:
|
||||
"""L'année dans la formation (commence à 1).
|
||||
En APC seulement, en classic renvoie toujours 1.
|
||||
"""
|
||||
return 1 if self.semestre_idx is None else (self.semestre_idx - 1) // 2 + 1
|
||||
|
||||
def is_locked(self):
|
||||
"""True if UE should not be modified
|
||||
(contains modules used in a locked formsemestre)
|
||||
|
@ -135,3 +183,137 @@ class UniteEns(db.Model):
|
|||
if self.code_apogee:
|
||||
return {x.strip() for x in self.code_apogee.split(",") if x}
|
||||
return set()
|
||||
|
||||
def _check_apc_conflict(self, new_niveau_id: int, new_parcour_id: int):
|
||||
"raises ScoFormationConflict si (niveau, parcours) pas unique dans ce semestre"
|
||||
# Les UE du même semestre que nous:
|
||||
ues_sem = self.formation.ues.filter_by(semestre_idx=self.semestre_idx)
|
||||
if (new_niveau_id, new_parcour_id) in (
|
||||
(oue.niveau_competence_id, oue.parcour_id)
|
||||
for oue in ues_sem
|
||||
if oue.id != self.id
|
||||
):
|
||||
log(
|
||||
f"set_ue_niveau_competence: {self}: ({new_niveau_id}, {new_parcour_id}) déjà associé"
|
||||
)
|
||||
raise ScoFormationConflict()
|
||||
|
||||
def set_niveau_competence(self, niveau: ApcNiveau):
|
||||
"""Associe cette UE au niveau de compétence indiqué.
|
||||
Le niveau doit être dans le parcours de l'UE, s'il y en a un.
|
||||
Assure que ce soit la seule dans son parcours.
|
||||
Sinon, raises ScoFormationConflict.
|
||||
|
||||
Si niveau est None, désassocie.
|
||||
"""
|
||||
if niveau is not None:
|
||||
self._check_apc_conflict(niveau.id, self.parcour_id)
|
||||
# Le niveau est-il dans le parcours ? Sinon, erreur
|
||||
if self.parcour and niveau.id not in (
|
||||
n.id
|
||||
for n in niveau.niveaux_annee_de_parcours(
|
||||
self.parcour, self.annee(), self.formation.referentiel_competence
|
||||
)
|
||||
):
|
||||
log(
|
||||
f"set_niveau_competence: niveau {niveau} hors parcours {self.parcour}"
|
||||
)
|
||||
return
|
||||
|
||||
self.niveau_competence = niveau
|
||||
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
# Invalidation du cache
|
||||
self.formation.invalidate_cached_sems()
|
||||
log(f"ue.set_niveau_competence( {self}, {niveau} )")
|
||||
|
||||
def set_parcour(self, parcour: ApcParcours):
|
||||
"""Associe cette UE au parcours indiqué.
|
||||
Assure que ce soit la seule dans son parcours.
|
||||
Sinon, raises ScoFormationConflict.
|
||||
|
||||
Si niveau est None, désassocie.
|
||||
"""
|
||||
if (parcour is not None) and self.niveau_competence is not None:
|
||||
self._check_apc_conflict(self.niveau_competence.id, parcour.id)
|
||||
self.parcour = parcour
|
||||
# Le niveau est-il dans ce parcours ? Sinon, l'enlève
|
||||
if (
|
||||
parcour
|
||||
and self.niveau_competence
|
||||
and self.niveau_competence.id
|
||||
not in (
|
||||
n.id
|
||||
for n in self.niveau_competence.niveaux_annee_de_parcours(
|
||||
parcour, self.annee(), self.formation.referentiel_competence
|
||||
)
|
||||
)
|
||||
):
|
||||
self.niveau_competence = None
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
# Invalidation du cache
|
||||
self.formation.invalidate_cached_sems()
|
||||
log(f"ue.set_parcour( {self}, {parcour} )")
|
||||
|
||||
|
||||
class DispenseUE(db.Model):
|
||||
"""Dispense d'UE
|
||||
Utilisé en APC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée
|
||||
qu'ils ne refont pas.
|
||||
La dispense d'UE n'est PAS une validation:
|
||||
- elle n'est pas affectée par les décisions de jury (pas effacée)
|
||||
- elle est associée à un formsemestre
|
||||
- elle ne permet pas la délivrance d'ECTS ou du diplôme.
|
||||
|
||||
On utilise cette dispense et non une "inscription" par souci d'efficacité:
|
||||
en général, la grande majorité des étudiants suivront toutes les UEs de leur parcours,
|
||||
la dispense étant une exception.
|
||||
"""
|
||||
|
||||
__table_args__ = (db.UniqueConstraint("formsemestre_id", "ue_id", "etudid"),)
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
formsemestre_id = formsemestre_id = db.Column(
|
||||
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
|
||||
)
|
||||
ue_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey(UniteEns.id, ondelete="CASCADE"),
|
||||
index=True,
|
||||
nullable=False,
|
||||
)
|
||||
ue = db.relationship("UniteEns", back_populates="dispense_ues")
|
||||
etudid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||
index=True,
|
||||
nullable=False,
|
||||
)
|
||||
etud = db.relationship("Identite", back_populates="dispense_ues")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"""<{self.__class__.__name__} {self.id} etud={
|
||||
repr(self.etud)} ue={repr(self.ue)}>"""
|
||||
|
||||
@classmethod
|
||||
def load_formsemestre_dispense_ues_set(
|
||||
cls, formsemestre: "FormSemestre", etudids: pd.Index, ues: list[UniteEns]
|
||||
) -> set[tuple[int, int]]:
|
||||
"""Construit l'ensemble des
|
||||
etudids = modimpl_inscr_df.index, # les etudids
|
||||
ue_ids : modimpl_coefs_df.index, # les UE du formsemestre sans les UE bonus sport
|
||||
|
||||
Résultat: set de (etudid, ue_id).
|
||||
"""
|
||||
# Prend toutes les dispenses obtenues par des étudiants de ce formsemestre,
|
||||
# puis filtre sur inscrits et ues
|
||||
ue_ids = {ue.id for ue in ues}
|
||||
dispense_ues = {
|
||||
(dispense_ue.etudid, dispense_ue.ue_id)
|
||||
for dispense_ue in DispenseUE.query.filter_by(
|
||||
formsemestre_id=formsemestre.id
|
||||
)
|
||||
if dispense_ue.etudid in etudids and dispense_ue.ue_id in ue_ids
|
||||
}
|
||||
return dispense_ues
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"""
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import CODE_STR_LEN
|
||||
from app.models.events import Scolog
|
||||
|
@ -53,11 +54,21 @@ class ScolarFormSemestreValidation(db.Model):
|
|||
)
|
||||
|
||||
ue = db.relationship("UniteEns", lazy="select", uselist=False)
|
||||
formsemestre = db.relationship(
|
||||
"FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id]
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({self.formsemestre_id}, {self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})"
|
||||
return f"{self.__class__.__name__}(sem={self.formsemestre_id}, etuid={self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})"
|
||||
|
||||
def __str__(self):
|
||||
if self.ue_id:
|
||||
# Note: si l'objet vient d'être créé, ue_id peut exister mais pas ue !
|
||||
return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id}: {self.code}"""
|
||||
return f"""décision sur semestre {self.formsemestre.titre_mois()} du {self.event_date.strftime("%d/%m/%Y")}"""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"as a dict"
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
return d
|
||||
|
@ -83,7 +94,12 @@ class ScolarAutorisationInscription(db.Model):
|
|||
db.ForeignKey("notes_formsemestre.id"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"""{self.__class__.__name__}(id={self.id}, etudid={
|
||||
self.etudid}, semestre_id={self.semestre_id})"""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"as a dict"
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
return d
|
||||
|
@ -96,8 +112,7 @@ class ScolarAutorisationInscription(db.Model):
|
|||
origin_formsemestre_id: int,
|
||||
semestre_id: int,
|
||||
):
|
||||
"""Enregistre une autorisation, remplace celle émanant du même semestre si elle existe."""
|
||||
cls.delete_autorisation_etud(etudid, origin_formsemestre_id)
|
||||
"""Ajoute une autorisation"""
|
||||
autorisation = cls(
|
||||
etudid=etudid,
|
||||
formation_code=formation_code,
|
||||
|
@ -105,7 +120,10 @@ class ScolarAutorisationInscription(db.Model):
|
|||
semestre_id=semestre_id,
|
||||
)
|
||||
db.session.add(autorisation)
|
||||
Scolog.logdb("autorise_etud", etudid=etudid, msg=f"passage vers S{semestre_id}")
|
||||
Scolog.logdb(
|
||||
"autorise_etud", etudid=etudid, msg=f"Passage vers S{semestre_id}: autorisé"
|
||||
)
|
||||
log(f"ScolarAutorisationInscription: recording {autorisation}")
|
||||
|
||||
@classmethod
|
||||
def delete_autorisation_etud(
|
||||
|
@ -119,10 +137,11 @@ class ScolarAutorisationInscription(db.Model):
|
|||
)
|
||||
for autorisation in autorisations:
|
||||
db.session.delete(autorisation)
|
||||
log(f"ScolarAutorisationInscription: deleting {autorisation}")
|
||||
Scolog.logdb(
|
||||
"autorise_etud",
|
||||
etudid=etudid,
|
||||
msg=f"annule passage vers S{autorisation.semestre_id}",
|
||||
msg=f"Passage vers S{autorisation.semestre_id}: effacé",
|
||||
)
|
||||
db.session.flush()
|
||||
|
||||
|
@ -140,11 +159,11 @@ class ScolarEvent(db.Model):
|
|||
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
db.ForeignKey("notes_formsemestre.id", ondelete="SET NULL"),
|
||||
)
|
||||
ue_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_ue.id"),
|
||||
db.ForeignKey("notes_ue.id", ondelete="SET NULL"),
|
||||
)
|
||||
# 'CREATION', 'INSCRIPTION', 'DEMISSION',
|
||||
# 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM'
|
||||
|
@ -156,8 +175,16 @@ class ScolarEvent(db.Model):
|
|||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
)
|
||||
etud = db.relationship("Identite", lazy="select", backref="events", uselist=False)
|
||||
formsemestre = db.relationship(
|
||||
"FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id]
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"as a dict"
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
return d
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.event_type}, {self.event_date.isoformat()}, {self.formsemestre})"
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -253,7 +253,7 @@ def get_annotation_PE(etudid, tag_annotation_pe):
|
|||
) # Suppression du tag d'annotation PE
|
||||
annotationPE = annotationPE.replace("\r", "") # Suppression des \r
|
||||
annotationPE = annotationPE.replace(
|
||||
"<br/>", "\n\n"
|
||||
"<br>", "\n\n"
|
||||
) # Interprète les retours chariots html
|
||||
return annotationPE
|
||||
return "" # pas d'annotations
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -48,11 +48,11 @@ from zipfile import ZipFile
|
|||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
from app.models import Formation, FormSemestre
|
||||
|
||||
from app.scodoc.gen_tables import GenTable, SeqGenTable
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_codes_parcours # sco_codes_parcours.NEXT -> sem suivant
|
||||
from app.scodoc import codes_cursus # codes_cursus.NEXT -> sem suivant
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.pe import pe_tagtable
|
||||
|
@ -65,10 +65,8 @@ def comp_nom_semestre_dans_parcours(sem):
|
|||
"""Le nom a afficher pour titrer un semestre
|
||||
par exemple: "semestre 2 FI 2015"
|
||||
"""
|
||||
from app.scodoc import sco_formations
|
||||
|
||||
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
|
||||
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
|
||||
formation: Formation = Formation.query.get_or_404(sem["formation_id"])
|
||||
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
|
||||
return "%s %s %s %s" % (
|
||||
parcours.SESSION_NAME, # eg "semestre"
|
||||
sem["semestre_id"], # eg 2
|
||||
|
@ -457,10 +455,9 @@ class JuryPE(object):
|
|||
|
||||
reponse = False
|
||||
etud = self.get_cache_etudInfo_d_un_etudiant(etudid)
|
||||
(_, parcours) = sco_report.get_codeparcoursetud(etud)
|
||||
(_, parcours) = sco_report.get_code_cursus_etud(etud)
|
||||
if (
|
||||
len(set(sco_codes_parcours.CODES_SEM_REO.keys()) & set(parcours.values()))
|
||||
> 0
|
||||
len(codes_cursus.CODES_SEM_REO & set(parcours.values())) > 0
|
||||
): # Eliminé car NAR apparait dans le parcours
|
||||
reponse = True
|
||||
if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2:
|
||||
|
@ -529,14 +526,14 @@ class JuryPE(object):
|
|||
from app.scodoc import sco_report
|
||||
|
||||
etud = self.get_cache_etudInfo_d_un_etudiant(etudid)
|
||||
(code, parcours) = sco_report.get_codeparcoursetud(
|
||||
(code, parcours) = sco_report.get_code_cursus_etud(
|
||||
etud
|
||||
) # description = '1234:A', parcours = {1:ADM, 2:NAR, ...}
|
||||
sonDernierSemestreValide = max(
|
||||
[
|
||||
int(cle)
|
||||
for (cle, code) in parcours.items()
|
||||
if code in sco_codes_parcours.CODES_SEM_VALIDES
|
||||
if code in codes_cursus.CODES_SEM_VALIDES
|
||||
]
|
||||
+ [0]
|
||||
) # n° du dernier semestre valide, 0 sinon
|
||||
|
@ -563,9 +560,8 @@ class JuryPE(object):
|
|||
dec = nt.get_etud_decision_sem(
|
||||
etudid
|
||||
) # quelle est la décision du jury ?
|
||||
if dec and dec["code"] in list(
|
||||
sco_codes_parcours.CODES_SEM_VALIDES.keys()
|
||||
): # isinstance( sesMoyennes[i+1], float) and
|
||||
if dec and (dec["code"] in codes_cursus.CODES_SEM_VALIDES):
|
||||
# isinstance( sesMoyennes[i+1], float) and
|
||||
# mT = sesMoyennes[i+1] # substitue la moyenne si le semestre suivant est "valide"
|
||||
leFid = sem["formsemestre_id"]
|
||||
else:
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -41,11 +41,12 @@ from app.comp import res_sem
|
|||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_tag_module
|
||||
from app.pe import pe_tagtable
|
||||
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_tag_module
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
class SemestreTag(pe_tagtable.TableTag):
|
||||
"""Un SemestreTag représente un tableau de notes (basé sur notesTable)
|
||||
|
@ -103,7 +104,7 @@ class SemestreTag(pe_tagtable.TableTag):
|
|||
self.inscrlist = [
|
||||
etud
|
||||
for etud in self.nt.inscrlist
|
||||
if self.nt.get_etud_etat(etud["etudid"]) == "I"
|
||||
if self.nt.get_etud_etat(etud["etudid"]) == scu.INSCRIT
|
||||
]
|
||||
self.identdict = {
|
||||
etudid: ident
|
||||
|
@ -115,7 +116,7 @@ class SemestreTag(pe_tagtable.TableTag):
|
|||
self.modimpls = [
|
||||
modimpl
|
||||
for modimpl in self.nt.formsemestre.modimpls_sorted
|
||||
if modimpl.module.ue.type == sco_codes_parcours.UE_STANDARD
|
||||
if modimpl.module.ue.type == codes_cursus.UE_STANDARD
|
||||
] # la liste des modules (objet modimpl)
|
||||
self.somme_coeffs = sum(
|
||||
[
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -40,7 +40,7 @@ Created on Thu Sep 8 09:36:33 2016
|
|||
import datetime
|
||||
import numpy as np
|
||||
|
||||
from app.scodoc import notes_table
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
class TableTag(object):
|
||||
|
@ -186,7 +186,7 @@ class TableTag(object):
|
|||
if isinstance(col[0], float)
|
||||
else 0, # remplace les None et autres chaines par des zéros
|
||||
) # triées
|
||||
self.rangs[tag] = notes_table.comp_ranks(lesMoyennesTriees) # les rangs
|
||||
self.rangs[tag] = scu.comp_ranks(lesMoyennesTriees) # les rangs
|
||||
|
||||
# calcul des stats
|
||||
self.comp_stats_d_un_tag(tag)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -55,7 +55,7 @@ def _pe_view_sem_recap_form(formsemestre_id):
|
|||
<p class="help">
|
||||
Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de
|
||||
poursuites d'études.
|
||||
<br/>
|
||||
<br>
|
||||
De nombreux aspects sont paramétrables:
|
||||
<a href="https://scodoc.org/AvisPoursuiteEtudes" target="_blank" rel="noopener">
|
||||
voir la documentation</a>.
|
||||
|
@ -65,7 +65,7 @@ def _pe_view_sem_recap_form(formsemestre_id):
|
|||
<div class="pe_template_up">
|
||||
Les templates sont généralement installés sur le serveur ou dans le
|
||||
paramétrage de ScoDoc.
|
||||
<br/>
|
||||
<br>
|
||||
Au besoin, vous pouvez spécifier ici votre propre fichier de template
|
||||
(<tt>un_avis.tex</tt>):
|
||||
<div class="pe_template_upb">Template:
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
from time import time
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Profiler:
|
||||
OUTPUT: str = "/tmp/scodoc.profiler.csv"
|
||||
|
||||
def __init__(self, tag: str) -> None:
|
||||
self.tag: str = tag
|
||||
self.start_time: time = None
|
||||
self.stop_time: time = None
|
||||
|
||||
def start(self):
|
||||
self.start_time = time()
|
||||
return self
|
||||
|
||||
def stop(self):
|
||||
self.stop_time = time()
|
||||
return self
|
||||
|
||||
def elapsed(self) -> float:
|
||||
return self.stop_time - self.start_time
|
||||
|
||||
def dates(self) -> tuple[datetime, datetime]:
|
||||
return datetime.fromtimestamp(self.start_time), datetime.fromtimestamp(
|
||||
self.stop_time
|
||||
)
|
||||
|
||||
def write(self):
|
||||
with open(Profiler.OUTPUT, "a") as file:
|
||||
dates: tuple = self.dates()
|
||||
date_str = (dates[0].isoformat(), dates[1].isoformat())
|
||||
file.write(f"\n{self.tag},{self.elapsed() : .2}")
|
||||
|
||||
@classmethod
|
||||
def write_in(cls, msg: str):
|
||||
with open(cls.OUTPUT, "a") as file:
|
||||
file.write(f"\n# {msg}")
|
||||
|
||||
@classmethod
|
||||
def clear(cls):
|
||||
with open(cls.OUTPUT, "w") as file:
|
||||
file.write("")
|
|
@ -38,6 +38,9 @@ def TrivialFormulator(
|
|||
html_foot_markup="",
|
||||
readonly=False,
|
||||
is_submitted=False,
|
||||
title="",
|
||||
after_table="",
|
||||
before_table="{title}",
|
||||
):
|
||||
"""
|
||||
form_url : URL for this form
|
||||
|
@ -74,7 +77,8 @@ def TrivialFormulator(
|
|||
HTML elements:
|
||||
input_type : 'text', 'textarea', 'password',
|
||||
'radio', 'menu', 'checkbox',
|
||||
'hidden', 'separator', 'file', 'date', 'datedmy' (avec validation),
|
||||
'hidden', 'separator', 'table_separator',
|
||||
'file', 'date', 'datedmy' (avec validation),
|
||||
'boolcheckbox', 'text_suggest',
|
||||
'color'
|
||||
(default text)
|
||||
|
@ -111,6 +115,9 @@ def TrivialFormulator(
|
|||
html_foot_markup=html_foot_markup,
|
||||
readonly=readonly,
|
||||
is_submitted=is_submitted,
|
||||
title=title,
|
||||
after_table=after_table,
|
||||
before_table=before_table,
|
||||
)
|
||||
form = t.getform()
|
||||
if t.canceled():
|
||||
|
@ -144,6 +151,9 @@ class TF(object):
|
|||
html_foot_markup="", # html snippet put at the end, just after the table
|
||||
readonly=False,
|
||||
is_submitted=False,
|
||||
title="",
|
||||
after_table="",
|
||||
before_table="{title}",
|
||||
):
|
||||
self.form_url = form_url
|
||||
self.values = values.copy()
|
||||
|
@ -165,6 +175,9 @@ class TF(object):
|
|||
self.top_buttons = top_buttons
|
||||
self.bottom_buttons = bottom_buttons
|
||||
self.html_foot_markup = html_foot_markup
|
||||
self.title = title
|
||||
self.after_table = after_table
|
||||
self.before_table = before_table
|
||||
self.readonly = readonly
|
||||
self.result = None
|
||||
self.is_submitted = is_submitted
|
||||
|
@ -357,12 +370,23 @@ class TF(object):
|
|||
self.values[field] = True
|
||||
else:
|
||||
self.values[field] = False
|
||||
# open('/tmp/toto','a').write('checkvalues: val=%s (%s) values[%s] = %s\n' % (val, type(val), field, self.values[field]))
|
||||
if descr.get("convert_numbers", False):
|
||||
if typ[:3] == "int":
|
||||
self.values[field] = int(self.values[field])
|
||||
try:
|
||||
self.values[field] = int(self.values[field])
|
||||
except ValueError:
|
||||
msg.append(
|
||||
f"valeur invalide ({self.values[field]}) pour le champs {field}"
|
||||
)
|
||||
ok = False
|
||||
elif typ == "float" or typ == "real":
|
||||
self.values[field] = float(self.values[field].replace(",", "."))
|
||||
try:
|
||||
self.values[field] = float(self.values[field].replace(",", "."))
|
||||
except ValueError:
|
||||
msg.append(
|
||||
f"valeur invalide ({self.values[field]}) pour le champs {field}"
|
||||
)
|
||||
ok = False
|
||||
if ok:
|
||||
self.result = self.values
|
||||
else:
|
||||
|
@ -426,6 +450,7 @@ class TF(object):
|
|||
R.append('<input type="hidden" name="%s_submitted" value="1">' % self.formid)
|
||||
if self.top_buttons:
|
||||
R.append(buttons_markup + "<p></p>")
|
||||
R.append(self.before_table.format(title=self.title))
|
||||
R.append('<table class="tf">')
|
||||
for field, descr in self.formdescription:
|
||||
if descr.get("readonly", False):
|
||||
|
@ -453,6 +478,16 @@ class TF(object):
|
|||
etempl = separatortemplate
|
||||
R.append(etempl % {"label": title, "item_dom_attr": item_dom_attr})
|
||||
continue
|
||||
elif input_type == "table_separator":
|
||||
etempl = ""
|
||||
# Table ouverte ?
|
||||
if len([p for p in R if "<table" in p]) > len(
|
||||
[p for p in R if "</table" in p]
|
||||
):
|
||||
R.append(f"""</table>{self.after_table}""")
|
||||
R.append(
|
||||
f"""{self.before_table.format(title=descr.get("title", ""))}<table class="tf">"""
|
||||
)
|
||||
else:
|
||||
etempl = itemtemplate
|
||||
lab = []
|
||||
|
@ -543,11 +578,8 @@ class TF(object):
|
|||
disabled_items = descr.get("disabled_items", {})
|
||||
if vertical:
|
||||
lem.append("<table>")
|
||||
for i in range(len(labels)):
|
||||
for i in range(len(labels)): # pylint: disable=consider-using-enumerate
|
||||
if input_type == "checkbox":
|
||||
# from app.scodoc.sco_utils import log # debug only
|
||||
# log('checkbox: values[%s] = "%s"' % (field,repr(values[field]) ))
|
||||
# log("descr['allowed_values'][%s] = '%s'" % (i, repr(descr['allowed_values'][i])))
|
||||
if (
|
||||
values[field]
|
||||
and descr["allowed_values"][i] in values[field]
|
||||
|
@ -563,7 +595,7 @@ class TF(object):
|
|||
else:
|
||||
try:
|
||||
v = int(values[field])
|
||||
except:
|
||||
except (ValueError, KeyError):
|
||||
v = False
|
||||
if v:
|
||||
checked = 'checked="checked"'
|
||||
|
@ -613,7 +645,7 @@ class TF(object):
|
|||
'<input type="hidden" name="%s" id="%s" value="%s" %s >'
|
||||
% (field, wid, values[field], attribs)
|
||||
)
|
||||
elif input_type == "separator":
|
||||
elif (input_type == "separator") or (input_type == "table_separator"):
|
||||
pass
|
||||
elif input_type == "file":
|
||||
lem.append(
|
||||
|
@ -644,13 +676,15 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
|
|||
)
|
||||
lem.append(('value="%(' + field + ')s" >') % values)
|
||||
else:
|
||||
raise ValueError("unkown input_type for form (%s)!" % input_type)
|
||||
raise ValueError(f"unkown input_type for form ({input_type})!")
|
||||
explanation = descr.get("explanation", "")
|
||||
if explanation:
|
||||
lem.append('<span class="tf-explanation">%s</span>' % explanation)
|
||||
lem.append(f"""<span class="tf-explanation">{explanation}</span>""")
|
||||
comment = descr.get("comment", "")
|
||||
if comment:
|
||||
lem.append('<br/><span class="tf-comment">%s</span>' % comment)
|
||||
if (input_type != "checkbox") and (input_type != "boolcheckbox"):
|
||||
lem.append("<br>")
|
||||
lem.append(f"""<span class="tf-comment">{comment}</span>""")
|
||||
R.append(
|
||||
etempl
|
||||
% {
|
||||
|
@ -660,11 +694,11 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
|
|||
}
|
||||
)
|
||||
R.append("</table>")
|
||||
|
||||
R.append(self.after_table)
|
||||
R.append(self.html_foot_markup)
|
||||
|
||||
if self.bottom_buttons:
|
||||
R.append("<br/>" + buttons_markup)
|
||||
R.append("<br>" + buttons_markup)
|
||||
|
||||
if add_no_enter_js:
|
||||
R.append(
|
||||
|
@ -756,7 +790,7 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
|
|||
|
||||
if input_type == "separator": # separator
|
||||
R.append('<td colspan="2">%s' % title)
|
||||
else:
|
||||
elif input_type != "table_separator":
|
||||
R.append('<td class="tf-ro-fieldlabel%s">' % klass)
|
||||
R.append("%s</td>" % title)
|
||||
R.append('<td class="tf-ro-field%s">' % klass)
|
||||
|
@ -789,7 +823,11 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
|
|||
R.append(
|
||||
'<div class="tf-ro-textarea">%s</div>' % html.escape(self.values[field])
|
||||
)
|
||||
elif input_type == "separator" or input_type == "hidden":
|
||||
elif (
|
||||
input_type == "separator"
|
||||
or input_type == "hidden"
|
||||
or input_type == "table_separator"
|
||||
):
|
||||
pass
|
||||
elif input_type == "file":
|
||||
R.append("'%s'" % self.values[field])
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -25,17 +25,20 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
"""Semestres: Codes gestion parcours (constantes)
|
||||
"""Semestres: Codes gestion cursus (constantes)
|
||||
Attention: ne pas confondre avec les "parcours" du BUT.
|
||||
Renommage des anciens "parcours" -> "cursus" effectué en 9.4.41
|
||||
"""
|
||||
import collections
|
||||
import enum
|
||||
|
||||
import numpy as np
|
||||
|
||||
from app import log
|
||||
|
||||
|
||||
@enum.unique
|
||||
class CodesParcours(enum.IntEnum):
|
||||
"""Codes numériques des parcours, enregistrés en base
|
||||
class CodesCursus(enum.IntEnum):
|
||||
"""Codes numériques des cursus (ex parcours), enregistrés en base
|
||||
dans notes_formations.type_parcours
|
||||
Ne pas modifier.
|
||||
"""
|
||||
|
@ -77,7 +80,7 @@ UE_STANDARD = 0 # UE "fondamentale"
|
|||
UE_SPORT = 1 # bonus "sport"
|
||||
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_ELECTIVE = 4 # UE "élective" dans certains cursus (UCAC?, ISCID)
|
||||
UE_PROFESSIONNELLE = 5 # UE "professionnelle" (ISCID, ...)
|
||||
UE_OPTIONNELLE = 6 # UE non fondamentales (ILEPS, ...)
|
||||
|
||||
|
@ -120,6 +123,7 @@ ABL = "ABL"
|
|||
ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé
|
||||
ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10)
|
||||
ADJ = "ADJ" # admis par le jury
|
||||
ADJR = "ADJR" # UE admise car son RCUE est ADJ
|
||||
ATT = "ATT" #
|
||||
ATJ = "ATJ" # pb assiduité: décision repoussée au semestre suivant
|
||||
ATB = "ATB"
|
||||
|
@ -156,6 +160,7 @@ CODES_EXPL = {
|
|||
ABL: "Année blanche",
|
||||
ADC: "Validé par compensation",
|
||||
ADJ: "Validé par le Jury",
|
||||
ADJR: "UE validée car son RCUE est validé ADJ par le jury",
|
||||
ADM: "Validé",
|
||||
AJ: "Ajourné (ou UE/BC de BUT en attente pour problème de moyenne)",
|
||||
ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)",
|
||||
|
@ -183,16 +188,25 @@ CODES_EXPL = {
|
|||
|
||||
# Les codes de semestres:
|
||||
CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT}
|
||||
CODES_SEM_VALIDES = {ADM: True, ADC: True, ADJ: True} # semestre validé
|
||||
CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente
|
||||
CODES_SEM_VALIDES_DE_DROIT = {ADM, ADC}
|
||||
CODES_SEM_VALIDES = CODES_SEM_VALIDES_DE_DROIT | {ADJ} # semestre validé
|
||||
CODES_SEM_ATTENTES = {ATT, ATB, ATJ} # semestre en attente
|
||||
|
||||
CODES_SEM_REO = {NAR: 1} # reorientation
|
||||
CODES_SEM_REO = {NAR} # reorientation
|
||||
|
||||
CODES_UE_VALIDES_DE_DROIT = {ADM, CMP} # validation "de droit"
|
||||
CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR}
|
||||
"UE validée"
|
||||
CODES_UE_CAPITALISANTS = {ADM}
|
||||
"UE capitalisée"
|
||||
|
||||
CODES_RCUE_VALIDES_DE_DROIT = {ADM, CMP}
|
||||
CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ}
|
||||
"Niveau RCUE validé"
|
||||
|
||||
CODES_UE_VALIDES = {ADM: True, CMP: True, ADJ: True} # UE validée
|
||||
CODES_RCUE_VALIDES = CODES_UE_VALIDES # Niveau RCUE validé
|
||||
# Pour le BUT:
|
||||
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD}
|
||||
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
|
||||
CODES_RCUE = {ADM, AJ, CMP}
|
||||
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
|
||||
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
|
||||
BUT_RCUE_SUFFISANT = 8.0 - NOTES_TOLERANCE
|
||||
|
@ -203,21 +217,36 @@ BUT_CODES_PASSAGE = {
|
|||
PAS1NCI,
|
||||
ATJ,
|
||||
}
|
||||
# les codes, du plus "défavorable" à l'étudiant au plus favorable:
|
||||
# (valeur par défaut 0)
|
||||
BUT_CODES_ORDERED = {
|
||||
NAR: 0,
|
||||
DEF: 0,
|
||||
AJ: 10,
|
||||
ATJ: 20,
|
||||
CMP: 50,
|
||||
ADC: 50,
|
||||
PASD: 50,
|
||||
PAS1NCI: 60,
|
||||
ADJR: 90,
|
||||
ADJ: 100,
|
||||
ADM: 100,
|
||||
}
|
||||
|
||||
|
||||
def code_semestre_validant(code: str) -> bool:
|
||||
"Vrai si ce CODE entraine la validation du semestre"
|
||||
return CODES_SEM_VALIDES.get(code, False)
|
||||
return code in CODES_SEM_VALIDES
|
||||
|
||||
|
||||
def code_semestre_attente(code: str) -> bool:
|
||||
"Vrai si ce CODE est un code d'attente (semestre validable plus tard par jury ou compensation)"
|
||||
return CODES_SEM_ATTENTES.get(code, False)
|
||||
return code in CODES_SEM_ATTENTES
|
||||
|
||||
|
||||
def code_ue_validant(code: str) -> bool:
|
||||
"Vrai si ce code d'UE est validant (ie attribue les ECTS)"
|
||||
return CODES_UE_VALIDES.get(code, False)
|
||||
return code in CODES_UE_VALIDES
|
||||
|
||||
|
||||
DEVENIR_EXPL = {
|
||||
|
@ -246,7 +275,7 @@ DEVENIRS_NEXT2 = {NEXT_OR_NEXT2: 1, NEXT2: 1}
|
|||
|
||||
NO_SEMESTRE_ID = -1 # code semestre si pas de semestres
|
||||
|
||||
# Règles gestion parcours
|
||||
# Règles gestion cursus
|
||||
class DUTRule(object):
|
||||
def __init__(self, rule_id, premise, conclusion):
|
||||
self.rule_id = rule_id
|
||||
|
@ -268,12 +297,12 @@ class DUTRule(object):
|
|||
return True
|
||||
|
||||
|
||||
# Types de parcours
|
||||
DEFAULT_TYPE_PARCOURS = 100 # pour le menu de creation nouvelle formation
|
||||
# Types de cursus
|
||||
DEFAULT_TYPE_CURSUS = 100 # pour le menu de creation nouvelle formation
|
||||
|
||||
|
||||
class TypeParcours(object):
|
||||
TYPE_PARCOURS = None # id, utilisé par notes_formation.type_parcours
|
||||
class TypeCursus:
|
||||
TYPE_CURSUS = None # id, utilisé par notes_formation.type_parcours
|
||||
NAME = None # required
|
||||
NB_SEM = 1 # Nombre de semestres
|
||||
COMPENSATION_UE = True # inutilisé
|
||||
|
@ -287,9 +316,9 @@ class TypeParcours(object):
|
|||
SESSION_NAME = "semestre"
|
||||
SESSION_NAME_A = "du "
|
||||
SESSION_ABBRV = "S" # S1, S2, ...
|
||||
UNUSED_CODES = set() # Ensemble des codes jury non autorisés dans ce parcours
|
||||
UNUSED_CODES = set() # Ensemble des codes jury non autorisés dans ce cursus
|
||||
UE_IS_MODULE = False # 1 seul module par UE (si plusieurs modules, etudiants censéments inscrits à un seul d'entre eux)
|
||||
ECTS_ONLY = False # Parcours avec progression basée uniquement sur les ECTS
|
||||
ECTS_ONLY = False # Cursus avec progression basée uniquement sur les ECTS
|
||||
ALLOWED_UE_TYPES = list(
|
||||
UE_TYPE_NAME.keys()
|
||||
) # par defaut, autorise tous les types d'UE
|
||||
|
@ -335,18 +364,18 @@ class TypeParcours(object):
|
|||
return False, """<b>%d UE sous la barre</b>""" % n
|
||||
|
||||
|
||||
# Parcours définis (instances de sous-classes de TypeParcours):
|
||||
TYPES_PARCOURS = collections.OrderedDict() # type : Parcours
|
||||
# Cursus définis (instances de sous-classes de TypeCursus):
|
||||
SCO_CURSUS: dict[int, TypeCursus] = {} # type : Cursus
|
||||
|
||||
|
||||
def register_parcours(Parcours):
|
||||
TYPES_PARCOURS[int(Parcours.TYPE_PARCOURS)] = Parcours
|
||||
def register_cursus(cursus: TypeCursus):
|
||||
SCO_CURSUS[int(cursus.TYPE_CURSUS)] = cursus
|
||||
|
||||
|
||||
class ParcoursBUT(TypeParcours):
|
||||
class CursusBUT(TypeCursus):
|
||||
"""BUT Bachelor Universitaire de Technologie"""
|
||||
|
||||
TYPE_PARCOURS = 700
|
||||
TYPE_CURSUS = 700
|
||||
NAME = "BUT"
|
||||
NB_SEM = 6
|
||||
COMPENSATION_UE = False
|
||||
|
@ -355,63 +384,63 @@ class ParcoursBUT(TypeParcours):
|
|||
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT]
|
||||
|
||||
|
||||
register_parcours(ParcoursBUT())
|
||||
register_cursus(CursusBUT())
|
||||
|
||||
|
||||
class ParcoursDUT(TypeParcours):
|
||||
class CursusDUT(TypeCursus):
|
||||
"""DUT selon l'arrêté d'août 2005"""
|
||||
|
||||
TYPE_PARCOURS = 100
|
||||
TYPE_CURSUS = 100
|
||||
NAME = "DUT"
|
||||
NB_SEM = 4
|
||||
COMPENSATION_UE = True
|
||||
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT]
|
||||
|
||||
|
||||
register_parcours(ParcoursDUT())
|
||||
register_cursus(CursusDUT())
|
||||
|
||||
|
||||
class ParcoursDUT4(ParcoursDUT):
|
||||
class CursusDUT4(CursusDUT):
|
||||
"""DUT (en 4 semestres sans compensations)"""
|
||||
|
||||
TYPE_PARCOURS = 110
|
||||
TYPE_CURSUS = 110
|
||||
NAME = "DUT4"
|
||||
COMPENSATION_UE = False
|
||||
|
||||
|
||||
register_parcours(ParcoursDUT4())
|
||||
register_cursus(CursusDUT4())
|
||||
|
||||
|
||||
class ParcoursDUTMono(TypeParcours):
|
||||
class CursusDUTMono(TypeCursus):
|
||||
"""DUT en un an (FC, Années spéciales)"""
|
||||
|
||||
TYPE_PARCOURS = 120
|
||||
TYPE_CURSUS = 120
|
||||
NAME = "DUT"
|
||||
NB_SEM = 1
|
||||
COMPENSATION_UE = False
|
||||
UNUSED_CODES = set((ADC, ATT, ATB))
|
||||
|
||||
|
||||
register_parcours(ParcoursDUTMono())
|
||||
register_cursus(CursusDUTMono())
|
||||
|
||||
|
||||
class ParcoursDUT2(ParcoursDUT):
|
||||
class CursusDUT2(CursusDUT):
|
||||
"""DUT en deux semestres (par ex.: années spéciales semestrialisées)"""
|
||||
|
||||
TYPE_PARCOURS = CodesParcours.DUT2
|
||||
TYPE_CURSUS = CodesCursus.DUT2
|
||||
NAME = "DUT2"
|
||||
NB_SEM = 2
|
||||
|
||||
|
||||
register_parcours(ParcoursDUT2())
|
||||
register_cursus(CursusDUT2())
|
||||
|
||||
|
||||
class ParcoursLP(TypeParcours):
|
||||
class CursusLP(TypeCursus):
|
||||
"""Licence Pro (en un "semestre")
|
||||
(pour anciennes LP. Après 2014, préférer ParcoursLP2014)
|
||||
(pour anciennes LP. Après 2014, préférer CursusLP2014)
|
||||
"""
|
||||
|
||||
TYPE_PARCOURS = CodesParcours.LP
|
||||
TYPE_CURSUS = CodesCursus.LP
|
||||
NAME = "LP"
|
||||
NB_SEM = 1
|
||||
COMPENSATION_UE = False
|
||||
|
@ -422,35 +451,35 @@ class ParcoursLP(TypeParcours):
|
|||
UNUSED_CODES = set((ADC, ATT, ATB))
|
||||
|
||||
|
||||
register_parcours(ParcoursLP())
|
||||
register_cursus(CursusLP())
|
||||
|
||||
|
||||
class ParcoursLP2sem(ParcoursLP):
|
||||
class CursusLP2sem(CursusLP):
|
||||
"""Licence Pro (en deux "semestres")"""
|
||||
|
||||
TYPE_PARCOURS = CodesParcours.LP2sem
|
||||
TYPE_CURSUS = CodesCursus.LP2sem
|
||||
NAME = "LP2sem"
|
||||
NB_SEM = 2
|
||||
COMPENSATION_UE = True
|
||||
UNUSED_CODES = set((ADC,)) # autorise les codes ATT et ATB, mais pas ADC.
|
||||
|
||||
|
||||
register_parcours(ParcoursLP2sem())
|
||||
register_cursus(CursusLP2sem())
|
||||
|
||||
|
||||
class ParcoursLP2semEvry(ParcoursLP):
|
||||
class CursusLP2semEvry(CursusLP):
|
||||
"""Licence Pro (en deux "semestres", U. Evry)"""
|
||||
|
||||
TYPE_PARCOURS = CodesParcours.LP2semEvry
|
||||
TYPE_CURSUS = CodesCursus.LP2semEvry
|
||||
NAME = "LP2semEvry"
|
||||
NB_SEM = 2
|
||||
COMPENSATION_UE = True
|
||||
|
||||
|
||||
register_parcours(ParcoursLP2semEvry())
|
||||
register_cursus(CursusLP2semEvry())
|
||||
|
||||
|
||||
class ParcoursLP2014(TypeParcours):
|
||||
class CursusLP2014(TypeCursus):
|
||||
"""Licence Pro (en un "semestre"), selon arrêté du 22/01/2014"""
|
||||
|
||||
# Note: texte de référence
|
||||
|
@ -467,7 +496,7 @@ class ParcoursLP2014(TypeParcours):
|
|||
# l'établissement d'un coefficient qui peut varier dans un rapport de 1 à 3. ", etc ne sont _pas_
|
||||
# vérifiés par ScoDoc)
|
||||
|
||||
TYPE_PARCOURS = CodesParcours.LP2014
|
||||
TYPE_CURSUS = CodesCursus.LP2014
|
||||
NAME = "LP2014"
|
||||
NB_SEM = 1
|
||||
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT, UE_STAGE_LP]
|
||||
|
@ -487,7 +516,7 @@ class ParcoursLP2014(TypeParcours):
|
|||
(ue_status["moy"], ue_status["coef_ue"])
|
||||
for ue_status in ues_status
|
||||
if ue_status["ue"]["type"] == UE_STAGE_LP
|
||||
and type(ue_status["moy"]) == float
|
||||
and np.issubdtype(type(ue_status["moy"]), np.floating)
|
||||
]
|
||||
# Moyenne des moyennes:
|
||||
sum_coef = sum(x[1] for x in mc_stages_proj)
|
||||
|
@ -505,74 +534,74 @@ class ParcoursLP2014(TypeParcours):
|
|||
return True, "" # pas de coef, condition ok
|
||||
|
||||
|
||||
register_parcours(ParcoursLP2014())
|
||||
register_cursus(CursusLP2014())
|
||||
|
||||
|
||||
class ParcoursLP2sem2014(ParcoursLP):
|
||||
class CursusLP2sem2014(CursusLP):
|
||||
"""Licence Pro (en deux "semestres", selon arrêté du 22/01/2014)"""
|
||||
|
||||
TYPE_PARCOURS = CodesParcours.LP2sem2014
|
||||
TYPE_CURSUS = CodesCursus.LP2sem2014
|
||||
NAME = "LP2014_2sem"
|
||||
NB_SEM = 2
|
||||
|
||||
|
||||
register_parcours(ParcoursLP2sem2014())
|
||||
register_cursus(CursusLP2sem2014())
|
||||
|
||||
|
||||
# Masters: M2 en deux semestres
|
||||
class ParcoursM2(TypeParcours):
|
||||
class CursusM2(TypeCursus):
|
||||
"""Master 2 (en deux "semestres")"""
|
||||
|
||||
TYPE_PARCOURS = CodesParcours.M2
|
||||
TYPE_CURSUS = CodesCursus.M2
|
||||
NAME = "M2sem"
|
||||
NB_SEM = 2
|
||||
COMPENSATION_UE = True
|
||||
UNUSED_CODES = set((ATT, ATB))
|
||||
|
||||
|
||||
register_parcours(ParcoursM2())
|
||||
register_cursus(CursusM2())
|
||||
|
||||
|
||||
class ParcoursM2noncomp(ParcoursM2):
|
||||
class CursusM2noncomp(CursusM2):
|
||||
"""Master 2 (en deux "semestres") sans compensation"""
|
||||
|
||||
TYPE_PARCOURS = CodesParcours.M2noncomp
|
||||
TYPE_CURSUS = CodesCursus.M2noncomp
|
||||
NAME = "M2noncomp"
|
||||
COMPENSATION_UE = False
|
||||
UNUSED_CODES = set((ADC, ATT, ATB))
|
||||
|
||||
|
||||
register_parcours(ParcoursM2noncomp())
|
||||
register_cursus(CursusM2noncomp())
|
||||
|
||||
|
||||
class ParcoursMono(TypeParcours):
|
||||
class CursusMono(TypeCursus):
|
||||
"""Formation générique en une session"""
|
||||
|
||||
TYPE_PARCOURS = CodesParcours.Mono
|
||||
TYPE_CURSUS = CodesCursus.Mono
|
||||
NAME = "Mono"
|
||||
NB_SEM = 1
|
||||
COMPENSATION_UE = False
|
||||
UNUSED_CODES = set((ADC, ATT, ATB))
|
||||
|
||||
|
||||
register_parcours(ParcoursMono())
|
||||
register_cursus(CursusMono())
|
||||
|
||||
|
||||
class ParcoursLegacy(TypeParcours):
|
||||
class CursusLegacy(TypeCursus):
|
||||
"""DUT (ancien ScoDoc, ne plus utiliser)"""
|
||||
|
||||
TYPE_PARCOURS = CodesParcours.Legacy
|
||||
TYPE_CURSUS = CodesCursus.Legacy
|
||||
NAME = "DUT"
|
||||
NB_SEM = 4
|
||||
COMPENSATION_UE = None # backward compat: defini dans formsemestre
|
||||
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT]
|
||||
|
||||
|
||||
register_parcours(ParcoursLegacy())
|
||||
register_cursus(CursusLegacy())
|
||||
|
||||
|
||||
class ParcoursISCID(TypeParcours):
|
||||
"""Superclasse pour les parcours de l'ISCID"""
|
||||
class CursusISCID(TypeCursus):
|
||||
"""Superclasse pour les cursus de l'ISCID"""
|
||||
|
||||
# SESSION_NAME = "année"
|
||||
# SESSION_NAME_A = "de l'"
|
||||
|
@ -591,32 +620,32 @@ class ParcoursISCID(TypeParcours):
|
|||
ECTS_PROF_DIPL = 0 # crédits professionnels requis pour obtenir le diplôme
|
||||
|
||||
|
||||
class ParcoursBachelorISCID6(ParcoursISCID):
|
||||
class CursusBachelorISCID6(CursusISCID):
|
||||
"""ISCID: Bachelor en 3 ans (6 sem.)"""
|
||||
|
||||
NAME = "ParcoursBachelorISCID6"
|
||||
TYPE_PARCOURS = CodesParcours.ISCID6
|
||||
NAME = "CursusBachelorISCID6"
|
||||
TYPE_CURSUS = CodesCursus.ISCID6
|
||||
NAME = ""
|
||||
NB_SEM = 6
|
||||
ECTS_PROF_DIPL = 8 # crédits professionnels requis pour obtenir le diplôme
|
||||
|
||||
|
||||
register_parcours(ParcoursBachelorISCID6())
|
||||
register_cursus(CursusBachelorISCID6())
|
||||
|
||||
|
||||
class ParcoursMasterISCID4(ParcoursISCID):
|
||||
class CursusMasterISCID4(CursusISCID):
|
||||
"ISCID: Master en 2 ans (4 sem.)"
|
||||
TYPE_PARCOURS = CodesParcours.ISCID4
|
||||
NAME = "ParcoursMasterISCID4"
|
||||
TYPE_CURSUS = CodesCursus.ISCID4
|
||||
NAME = "CursusMasterISCID4"
|
||||
NB_SEM = 4
|
||||
ECTS_PROF_DIPL = 15 # crédits professionnels requis pour obtenir le diplôme
|
||||
|
||||
|
||||
register_parcours(ParcoursMasterISCID4())
|
||||
register_cursus(CursusMasterISCID4())
|
||||
|
||||
|
||||
class ParcoursILEPS(TypeParcours):
|
||||
"""Superclasse pour les parcours de l'ILEPS"""
|
||||
class CursusILEPS(TypeCursus):
|
||||
"""Superclasse pour les cursus de l'ILEPS"""
|
||||
|
||||
# SESSION_NAME = "année"
|
||||
# SESSION_NAME_A = "de l'"
|
||||
|
@ -632,18 +661,18 @@ class ParcoursILEPS(TypeParcours):
|
|||
BARRE_UE_DEFAULT = 0.0 # pas de barre sur les autres UE
|
||||
|
||||
|
||||
class ParcoursLicenceILEPS6(ParcoursILEPS):
|
||||
class CursusLicenceILEPS6(CursusILEPS):
|
||||
"""ILEPS: Licence 6 semestres"""
|
||||
|
||||
TYPE_PARCOURS = 1010
|
||||
TYPE_CURSUS = 1010
|
||||
NAME = "LicenceILEPS6"
|
||||
NB_SEM = 6
|
||||
|
||||
|
||||
register_parcours(ParcoursLicenceILEPS6())
|
||||
register_cursus(CursusLicenceILEPS6())
|
||||
|
||||
|
||||
class ParcoursUCAC(TypeParcours):
|
||||
class CursusUCAC(TypeCursus):
|
||||
"""Règles de validation UCAC"""
|
||||
|
||||
SESSION_NAME = "année"
|
||||
|
@ -657,79 +686,79 @@ class ParcoursUCAC(TypeParcours):
|
|||
)
|
||||
|
||||
|
||||
class ParcoursLicenceUCAC3(ParcoursUCAC):
|
||||
class CursusLicenceUCAC3(CursusUCAC):
|
||||
"""UCAC: Licence en 3 sessions d'un an"""
|
||||
|
||||
TYPE_PARCOURS = CodesParcours.LicenceUCAC3
|
||||
TYPE_CURSUS = CodesCursus.LicenceUCAC3
|
||||
NAME = "Licence UCAC en 3 sessions d'un an"
|
||||
NB_SEM = 3
|
||||
|
||||
|
||||
register_parcours(ParcoursLicenceUCAC3())
|
||||
register_cursus(CursusLicenceUCAC3())
|
||||
|
||||
|
||||
class ParcoursMasterUCAC2(ParcoursUCAC):
|
||||
class CursusMasterUCAC2(CursusUCAC):
|
||||
"""UCAC: Master en 2 sessions d'un an"""
|
||||
|
||||
TYPE_PARCOURS = CodesParcours.MasterUCAC2
|
||||
TYPE_CURSUS = CodesCursus.MasterUCAC2
|
||||
NAME = "Master UCAC en 2 sessions d'un an"
|
||||
NB_SEM = 2
|
||||
|
||||
|
||||
register_parcours(ParcoursMasterUCAC2())
|
||||
register_cursus(CursusMasterUCAC2())
|
||||
|
||||
|
||||
class ParcoursMonoUCAC(ParcoursUCAC):
|
||||
class CursusMonoUCAC(CursusUCAC):
|
||||
"""UCAC: Formation en 1 session de durée variable"""
|
||||
|
||||
TYPE_PARCOURS = CodesParcours.MonoUCAC
|
||||
TYPE_CURSUS = CodesCursus.MonoUCAC
|
||||
NAME = "Formation UCAC en 1 session de durée variable"
|
||||
NB_SEM = 1
|
||||
UNUSED_CODES = set((ADC, ATT, ATB))
|
||||
|
||||
|
||||
register_parcours(ParcoursMonoUCAC())
|
||||
register_cursus(CursusMonoUCAC())
|
||||
|
||||
|
||||
class Parcours6Sem(TypeParcours):
|
||||
"""Parcours générique en 6 semestres"""
|
||||
class Cursus6Sem(TypeCursus):
|
||||
"""Cursus générique en 6 semestres"""
|
||||
|
||||
TYPE_PARCOURS = CodesParcours.GEN_6_SEM
|
||||
TYPE_CURSUS = CodesCursus.GEN_6_SEM
|
||||
NAME = "Formation en 6 semestres"
|
||||
NB_SEM = 6
|
||||
COMPENSATION_UE = True
|
||||
|
||||
|
||||
register_parcours(Parcours6Sem())
|
||||
register_cursus(Cursus6Sem())
|
||||
|
||||
# # En cours d'implémentation:
|
||||
# class ParcoursLicenceLMD(TypeParcours):
|
||||
# class CursusLicenceLMD(TypeCursus):
|
||||
# """Licence standard en 6 semestres dans le LMD"""
|
||||
# TYPE_PARCOURS = 401
|
||||
# TYPE_CURSUS = 401
|
||||
# NAME = "Licence LMD"
|
||||
# NB_SEM = 6
|
||||
# COMPENSATION_UE = True
|
||||
|
||||
# register_parcours(ParcoursLicenceLMD())
|
||||
# register_cursus(CursusLicenceLMD())
|
||||
|
||||
|
||||
class ParcoursMasterLMD(TypeParcours):
|
||||
class CursusMasterLMD(TypeCursus):
|
||||
"""Master générique en 4 semestres dans le LMD"""
|
||||
|
||||
TYPE_PARCOURS = CodesParcours.MasterLMD
|
||||
TYPE_CURSUS = CodesCursus.MasterLMD
|
||||
NAME = "Master LMD"
|
||||
NB_SEM = 4
|
||||
COMPENSATION_UE = True # variabale inutilisée
|
||||
UNUSED_CODES = set((ADC, ATT, ATB))
|
||||
|
||||
|
||||
register_parcours(ParcoursMasterLMD())
|
||||
register_cursus(CursusMasterLMD())
|
||||
|
||||
|
||||
class ParcoursMasterIG(ParcoursMasterLMD):
|
||||
class CursusMasterIG(CursusMasterLMD):
|
||||
"""Master de l'Institut Galilée (U. Paris 13) en 4 semestres (LMD)"""
|
||||
|
||||
TYPE_PARCOURS = CodesParcours.MasterIG
|
||||
TYPE_CURSUS = CodesCursus.MasterIG
|
||||
NAME = "Master IG P13"
|
||||
BARRE_MOY = 10.0
|
||||
NOTES_BARRE_VALID_UE_TH = 10.0 # seuil pour valider UE
|
||||
|
@ -739,7 +768,7 @@ class ParcoursMasterIG(ParcoursMasterLMD):
|
|||
BARRE_MOY_UE_STAGE = 10.0
|
||||
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT, UE_STAGE_10]
|
||||
|
||||
def check_barre_ues(self, ues_status): # inspire de la fonction de ParcoursLP2014
|
||||
def check_barre_ues(self, ues_status): # inspire de la fonction de CursusLP2014
|
||||
"""True si la ou les conditions sur les UE sont valides
|
||||
moyenne d'UE > 7, ou > 10 si UE de stage
|
||||
"""
|
||||
|
@ -778,10 +807,10 @@ class ParcoursMasterIG(ParcoursMasterLMD):
|
|||
return True, "" # pas de coef, condition ok
|
||||
|
||||
|
||||
register_parcours(ParcoursMasterIG())
|
||||
register_cursus(CursusMasterIG())
|
||||
|
||||
|
||||
# Ajouter ici vos parcours, le TYPE_PARCOURS devant être unique au monde
|
||||
# Ajouter ici vos cursus, le TYPE_CURSUS devant être unique au monde
|
||||
# (avisez sur la liste de diffusion)
|
||||
|
||||
|
||||
|
@ -789,16 +818,17 @@ register_parcours(ParcoursMasterIG())
|
|||
|
||||
|
||||
# -------------------------
|
||||
_tp = list(TYPES_PARCOURS.items())
|
||||
_tp = list(SCO_CURSUS.items())
|
||||
_tp.sort(key=lambda x: x[1].__doc__) # sort by intitulé
|
||||
FORMATION_PARCOURS_DESCRS = [p[1].__doc__ for p in _tp] # intitulés (eg pour menu)
|
||||
FORMATION_PARCOURS_TYPES = [p[0] for p in _tp] # codes numeriques (TYPE_PARCOURS)
|
||||
FORMATION_CURSUS_DESCRS = [p[1].__doc__ for p in _tp] # intitulés (eg pour menu)
|
||||
FORMATION_CURSUS_TYPES = [p[0] for p in _tp] # codes numeriques (TYPE_CURSUS)
|
||||
|
||||
|
||||
def get_parcours_from_code(code_parcours):
|
||||
parcours = TYPES_PARCOURS.get(code_parcours)
|
||||
if parcours is None:
|
||||
log(f"Warning: invalid code_parcours: {code_parcours}")
|
||||
def get_cursus_from_code(code_cursus: int) -> TypeCursus:
|
||||
"renvoie le cursus de code indiqué"
|
||||
cursus = SCO_CURSUS.get(code_cursus)
|
||||
if cursus is None:
|
||||
log(f"Warning: invalid code_cursus: {code_cursus}")
|
||||
# default to legacy
|
||||
parcours = TYPES_PARCOURS.get(0)
|
||||
return parcours
|
||||
cursus = SCO_CURSUS.get(0)
|
||||
return cursus
|
|
@ -4,7 +4,7 @@
|
|||
#
|
||||
# Command: ./csv2rules.py misc/parcoursDUT.csv
|
||||
#
|
||||
from app.scodoc.sco_codes_parcours import (
|
||||
from app.scodoc.codes_cursus import (
|
||||
DUTRule,
|
||||
ADC,
|
||||
ADJ,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -251,7 +251,7 @@ def sco_header(
|
|||
#gtrcontent {{
|
||||
margin-left: {params["margin_left"]};
|
||||
height: 100%%;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 16px;
|
||||
}}
|
||||
</style>
|
||||
"""
|
||||
|
@ -274,7 +274,7 @@ def sco_header(
|
|||
H.append("""<div id="gtrcontent">""")
|
||||
# En attendant le replacement complet de cette fonction,
|
||||
# inclusion ici des messages flask
|
||||
H.append(render_template("flashed_messages.html"))
|
||||
H.append(render_template("flashed_messages.j2"))
|
||||
#
|
||||
# Barre menu semestre:
|
||||
H.append(formsemestre_page_title(formsemestre_id))
|
||||
|
@ -284,8 +284,8 @@ def sco_header(
|
|||
if current_user.passwd_temp:
|
||||
H.append(
|
||||
f"""<div class="passwd_warn">
|
||||
Attention !<br/>
|
||||
Vous avez reçu un mot de passe temporaire.<br/>
|
||||
Attention !<br>
|
||||
Vous avez reçu un mot de passe temporaire.<br>
|
||||
Vous devez le changer: <a href="{scu.UsersURL}/form_change_password?user_name={current_user.user_name}">cliquez ici</a>
|
||||
</div>"""
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -48,26 +48,26 @@ def sidebar_common():
|
|||
url_for("users.user_info_page",
|
||||
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>
|
||||
<br><a id="deconnectlink" href="{url_for("auth.logout")}">déconnexion</a>
|
||||
</div>
|
||||
{sidebar_dept()}
|
||||
<h2 class="insidebar">Scolarité</h2>
|
||||
<a href="{scu.ScoURL()}" class="sidebar">Semestres</a> <br/>
|
||||
<a href="{scu.NotesURL()}" class="sidebar">Programmes</a> <br/>
|
||||
<a href="{scu.AbsencesURL()}" class="sidebar">Absences</a> <br/>
|
||||
<a href="{scu.ScoURL()}" class="sidebar">Semestres</a> <br>
|
||||
<a href="{scu.NotesURL()}" class="sidebar">Programmes</a> <br>
|
||||
<a href="{scu.AbsencesURL()}" class="sidebar">Absences</a> <br>
|
||||
"""
|
||||
]
|
||||
if current_user.has_permission(
|
||||
Permission.ScoUsersAdmin
|
||||
) or current_user.has_permission(Permission.ScoUsersView):
|
||||
H.append(
|
||||
f"""<a href="{scu.UsersURL()}" class="sidebar">Utilisateurs</a> <br/>"""
|
||||
f"""<a href="{scu.UsersURL()}" class="sidebar">Utilisateurs</a> <br>"""
|
||||
)
|
||||
|
||||
if current_user.has_permission(Permission.ScoChangePreferences):
|
||||
H.append(
|
||||
f"""<a href="{url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)}"
|
||||
class="sidebar">Paramétrage</a> <br/>"""
|
||||
class="sidebar">Paramétrage</a> <br>"""
|
||||
)
|
||||
|
||||
return "".join(H)
|
||||
|
@ -84,7 +84,7 @@ def sidebar(etudid: int = None):
|
|||
H = [
|
||||
f"""<div class="sidebar">
|
||||
{ sidebar_common() }
|
||||
<div class="box-chercheetud">Chercher étudiant:<br/>
|
||||
<div class="box-chercheetud">Chercher étudiant:<br>
|
||||
<form method="get" id="form-chercheetud"
|
||||
action="{url_for('scolar.search_etud_in_dept', scodoc_dept=g.scodoc_dept) }">
|
||||
<div><input type="text" size="12" class="in-expnom" name="expnom" spellcheck="false"></input></div>
|
||||
|
@ -101,7 +101,6 @@ def sidebar(etudid: int = None):
|
|||
etudid = request.form.get("etudid", None)
|
||||
|
||||
if etudid is not None:
|
||||
etudi = int(etudid)
|
||||
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
||||
params.update(etud)
|
||||
params["fiche_url"] = url_for(
|
||||
|
@ -121,13 +120,13 @@ def sidebar(etudid: int = None):
|
|||
nbabsnj = nbabs - nbabsjust
|
||||
H.append(
|
||||
f"""<span title="absences du { cur_sem["date_debut"] } au { cur_sem["date_fin"] }">(1/2 j.)
|
||||
<br/>{ nbabsjust } J., { nbabsnj } N.J.</span>"""
|
||||
<br>{ nbabsjust } J., { nbabsnj } N.J.</span>"""
|
||||
)
|
||||
H.append("<ul>")
|
||||
if current_user.has_permission(Permission.ScoAbsChange):
|
||||
H.append(
|
||||
f"""
|
||||
<li><a href="{ url_for('absences.SignaleAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Ajouter</a></li>
|
||||
<li><a href="{ url_for('assiduites.signal_assiduites_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Ajouter</a></li>
|
||||
<li><a href="{ url_for('absences.JustifAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Justifier</a></li>
|
||||
<li><a href="{ url_for('absences.AnnuleAbsenceEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Supprimer</a></li>
|
||||
"""
|
||||
|
@ -150,7 +149,7 @@ def sidebar(etudid: int = None):
|
|||
# Logo
|
||||
H.append(
|
||||
f"""<div class="logo-insidebar">
|
||||
<div class="sidebar-bottom"><a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }" class="sidebar">À propos</a><br/>
|
||||
<div class="sidebar-bottom"><a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }" class="sidebar">À propos</a><br>
|
||||
<a href="{ scu.SCO_USER_MANUAL }" target="_blank" class="sidebar">Aide</a>
|
||||
</div></div>
|
||||
<div class="logo-logo">
|
||||
|
@ -167,6 +166,6 @@ def sidebar(etudid: int = None):
|
|||
def sidebar_dept():
|
||||
"""Partie supérieure de la marge de gauche"""
|
||||
return render_template(
|
||||
"sidebar_dept.html",
|
||||
"sidebar_dept.j2",
|
||||
prefs=sco_preferences.SemPreferences(),
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -83,7 +83,7 @@ def histogram_notes(notes):
|
|||
return "\n".join(D)
|
||||
|
||||
|
||||
def make_menu(title, items, css_class="", alone=False):
|
||||
def make_menu(title, items, css_class="", alone=False) -> str:
|
||||
"""HTML snippet to render a simple drop down menu.
|
||||
items is a list of dicts:
|
||||
{ 'title' :
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -459,49 +459,22 @@ def dictfilter(d, fields, filter_nulls=True):
|
|||
# --- Misc Tools
|
||||
|
||||
|
||||
def DateDMYtoISO(dmy, null_is_empty=False):
|
||||
def DateDMYtoISO(dmy: str, null_is_empty=False) -> str:
|
||||
"convert date string from french format to ISO"
|
||||
if not dmy:
|
||||
if null_is_empty:
|
||||
return ""
|
||||
else:
|
||||
return None
|
||||
if not isinstance(dmy, str):
|
||||
if hasattr(dmy, "strftime"):
|
||||
return dmy.strftime("%Y-%m-%d")
|
||||
|
||||
t = dmy.split("/")
|
||||
|
||||
if len(t) != 3:
|
||||
raise ScoValueError('Format de date (j/m/a) invalide: "%s"' % str(dmy))
|
||||
day, month, year = t
|
||||
year = int(year)
|
||||
month = int(month)
|
||||
day = int(day)
|
||||
# accept years YYYY or YY, uses 1970 as pivot
|
||||
if year < 100:
|
||||
if year > 70:
|
||||
year += 1900
|
||||
else:
|
||||
year += 2000
|
||||
|
||||
if month < 1 or month > 12:
|
||||
raise ScoValueError("mois de la date invalide ! (%s)" % month)
|
||||
# compute nb of day in month:
|
||||
mo = month
|
||||
if mo > 7:
|
||||
mo = mo + 1
|
||||
if mo % 2:
|
||||
MonthNbDays = 31
|
||||
elif mo == 2:
|
||||
if year % 4 == 0 and ((year % 100 != 0) or (year % 400 == 0)):
|
||||
MonthNbDays = 29 # leap
|
||||
else:
|
||||
MonthNbDays = 28
|
||||
else:
|
||||
MonthNbDays = 30
|
||||
if day < 1 or day > MonthNbDays:
|
||||
raise ScoValueError("jour de la date invalide ! (%s)" % day)
|
||||
return "%04d-%02d-%02d" % (year, month, day)
|
||||
if not isinstance(dmy, str):
|
||||
raise ScoValueError(f'Date (j/m/a) invalide: "{dmy}"')
|
||||
try:
|
||||
dt = datetime.datetime.strptime(dmy, "%d/%m/%Y")
|
||||
except ValueError as exc:
|
||||
raise ScoValueError(f'Date (j/m/a) invalide: "{dmy}"') from exc
|
||||
return dt.date().isoformat()
|
||||
|
||||
|
||||
def DateISOtoDMY(isodate):
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -51,7 +51,7 @@ def convert_html_to_text(s):
|
|||
|
||||
|
||||
def newline_to_br(text):
|
||||
return text.replace("\n", "<br/>")
|
||||
return text.replace("\n", "<br>")
|
||||
|
||||
|
||||
class HTMLSanitizer(HTMLParser):
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -42,6 +42,8 @@ from app.scodoc import sco_cache
|
|||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_preferences
|
||||
from app.models import Assiduite, Justificatif
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
# --- Misc tools.... ------------------
|
||||
|
@ -420,7 +422,6 @@ WHERE A.ETUDID = %(etudid)s
|
|||
|
||||
def list_abs_date(etudid, beg_date=None, end_date=None):
|
||||
"""Liste des absences et justifs entre deux dates (inclues)."""
|
||||
print("On rentre")
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
|
||||
|
@ -1053,6 +1054,45 @@ def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso):
|
|||
return r
|
||||
|
||||
|
||||
def get_assiduites_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, nb abs justifiées)
|
||||
Utilise un cache.
|
||||
"""
|
||||
key = str(etudid) + "_" + date_debut_iso + "_" + date_fin_iso + "_assiduites"
|
||||
r = sco_cache.AbsSemEtudCache.get(key)
|
||||
if not r:
|
||||
|
||||
date_debut: datetime.datetime = scu.is_iso_formated(date_debut_iso, True)
|
||||
date_fin: datetime.datetime = scu.is_iso_formated(date_fin_iso, True)
|
||||
|
||||
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid)
|
||||
justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid)
|
||||
|
||||
assiduites = scass.filter_by_date(assiduites, Assiduite, date_debut, date_fin)
|
||||
justificatifs = scass.filter_by_date(
|
||||
justificatifs, Justificatif, date_debut, date_fin
|
||||
)
|
||||
|
||||
calculator: scass.CountCalculator = scass.CountCalculator()
|
||||
calculator.compute_assiduites(assiduites)
|
||||
nb_abs: dict = calculator.to_dict()["demi"]
|
||||
|
||||
abs_just: list[Assiduite] = scass.get_all_justified(
|
||||
etudid, date_debut, date_fin
|
||||
)
|
||||
|
||||
calculator.reset()
|
||||
calculator.compute_assiduites(abs_just)
|
||||
nb_abs_just: dict = calculator.to_dict()["demi"]
|
||||
|
||||
r = (nb_abs, nb_abs_just)
|
||||
ans = sco_cache.AbsSemEtudCache.set(key, r)
|
||||
if not ans:
|
||||
log("warning: get_assiduites_count failed to cache")
|
||||
return r
|
||||
|
||||
|
||||
def invalidate_abs_count(etudid, sem):
|
||||
"""Invalidate (clear) cached counts"""
|
||||
date_debut = sem["date_debut_iso"]
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -78,6 +78,15 @@ def table_billets(
|
|||
for billet in billets:
|
||||
billet_dict = billet.to_dict()
|
||||
rows.append(billet_dict)
|
||||
billet_dict["_nomprenom_order"] = (
|
||||
billet.etudiant.sort_key if billet.etudiant else ""
|
||||
)
|
||||
billet_dict["_abs_begin_str_order"] = (
|
||||
billet.abs_begin.isoformat() if billet.abs_begin else ""
|
||||
)
|
||||
billet_dict["_abs_begin_str_order"] = (
|
||||
billet.abs_begin.isoformat() if billet.abs_end else ""
|
||||
)
|
||||
if billet_dict["abs_begin"].hour < 12:
|
||||
m = " matin"
|
||||
else:
|
||||
|
@ -116,7 +125,7 @@ def table_billets(
|
|||
billet_dict["nomprenom"] = "???" # should not occur
|
||||
else:
|
||||
billet_dict["nomprenom"] = billet.etudiant.nomprenom
|
||||
billet_dict["_nomprenom_disp_order"] = billet.etudiant.sort_key
|
||||
billet_dict["_nomprenom_order"] = billet.etudiant.sort_key
|
||||
billet_dict[
|
||||
"_nomprenom_td_attrs"
|
||||
] = f'id="{billet.etudiant.id}" class="etudinfo"'
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue