Compare commits
988 commits
qwann/k-fe
...
master
Author | SHA1 | Date | |
---|---|---|---|
635b3d1607 | |||
7709f8a652 | |||
db4d1264c1 | |||
a9be316aaa | |||
622770aec4 | |||
7acc4609d3 | |||
65c483e935 | |||
|
e6930d3ebb | ||
8d7ccccc9b | |||
4d4b536781 | |||
0674217526 | |||
bf30a9e510 | |||
47cf999359 | |||
e1bd6bc6ad | |||
9178511005 | |||
3997f48eb8 | |||
bb80716cb4 | |||
2ec15ad2d5 | |||
81af13a216 | |||
1a8fe48d05 | |||
18c0f0f699 | |||
2d1357c4ff | |||
853611556c | |||
29b1581ab7 | |||
2fb4c6a95c | |||
e1c9a4474d | |||
e74e03d36b | |||
c8dc0ca34e | |||
982c82ba31 | |||
531ba575c8 | |||
def9b68a90 | |||
7cd4338a3a | |||
f5bfbddfbe | |||
9e185c8cb5 | |||
b8c963779a | |||
57ff8fe131 | |||
0fdc1a2a46 | |||
aafbab29ff | |||
fb66c064cd | |||
2126224e15 | |||
d6109a9312 | |||
a69bd0426f | |||
81a4dbef7c | |||
4fedf3453d | |||
8607d77c84 | |||
bc55a3067e | |||
dd68ad91cd | |||
2f71246509 | |||
a20a1c11d6 | |||
0e1ff1765a | |||
177d413f4c | |||
01dd16c795 | |||
84c87c1b4b | |||
1b143b322f | |||
f9456e3c29 | |||
dd1350f1c2 | |||
8f4cb68d31 | |||
3a3f96a8df | |||
7d8926e459 | |||
3fee014384 | |||
cd351786bb | |||
7362c2fa2a | |||
efbc947145 | |||
0c45262fbc | |||
cabd277b4a | |||
4108efe8c9 | |||
693e4252d5 | |||
3aa928e8f0 | |||
5e2e68960b | |||
91e9beee11 | |||
ac286209ed | |||
b03cda5962 | |||
4feb567af7 | |||
1ac47885d0 | |||
071c810605 | |||
7f00ce0ff1 | |||
95136cb4eb | |||
e299997aa8 | |||
40f34926bb | |||
69976a878a | |||
6621ae3950 | |||
9288daaf9e | |||
e92c500940 | |||
|
d75eaf583f | ||
55bd3ab51d | |||
|
f640a25f59 | ||
|
f881c7cd8b | ||
b548b87c25 | |||
|
a72302291f | ||
a0bde75f50 | |||
|
44b19c12e5 | ||
f97d339a1c | |||
|
094116e88d | ||
b32a07fc22 | |||
|
4fc9902cf6 | ||
7164cfa37a | |||
|
90f96fb5c9 | ||
e50249355d | |||
|
c304d734d9 | ||
c36dd30bce | |||
2571cc955e | |||
3eaac5c68f | |||
af4c8e0744 | |||
14e0a3ef0a | |||
|
83078d4726 | ||
cb262ad479 | |||
5c47118834 | |||
30e842ce80 | |||
892bf51163 | |||
e20d7ca6c2 | |||
|
1b09293206 | ||
|
e7da476697 | ||
|
761ab6df90 | ||
|
a8d4035d33 | ||
b80426c56f | |||
429e611daa | |||
|
5160da7862 | ||
aad3775222 | |||
4b92716092 | |||
dfa5b4bf69 | |||
|
1be5dcb6af | ||
a891ec56a6 | |||
7a52690a63 | |||
a2f396ce7a | |||
85e30056a6 | |||
|
69de48f285 | ||
|
eba36f2712 | ||
|
bfdb34aae7 | ||
fcf2002cd7 | |||
b236d6a950 | |||
4b29097f02 | |||
87f383bef1 | |||
|
1ad025e046 | ||
|
b3c047738a | ||
|
17a9ae3302 | ||
|
e41bcbb6d7 | ||
|
e384bfb0f3 | ||
373ff1f62c | |||
|
65eb95a3c9 | ||
|
1c880b265e | ||
|
75fbdc7efb | ||
1b8dd971b0 | |||
713d686047 | |||
|
77aa269c90 | ||
|
9a143521d5 | ||
|
a77cf59b18 | ||
|
d8cabda678 | ||
|
f086140dad | ||
|
4d1ae8f540 | ||
|
6d824a58be | ||
20880114aa | |||
|
df180d7446 | ||
ef1793a348 | |||
2d677b2093 | |||
f70eacfc37 | |||
264a0a852f | |||
7ca7f7298a | |||
|
a5c822e7f7 | ||
|
6b316c482b | ||
|
4060730ec5 | ||
|
c6cfc311e0 | ||
|
4326ba9016 | ||
|
4205e0ad0e | ||
|
964eec6ab1 | ||
|
29236e0b0e | ||
|
1939a54fef | ||
|
348881d207 | ||
|
ef8c1b8bf2 | ||
|
8743301105 | ||
|
6a11139588 | ||
|
a34b83c236 | ||
|
02584982f6 | ||
|
7bf0c5f09e | ||
|
b9aaf6a19c | ||
|
16dee0c143 | ||
|
a947b9d3f2 | ||
|
93d283fecb | ||
|
63738e8e02 | ||
|
1e44550e12 | ||
|
4136cb6868 | ||
|
99809209e0 | ||
|
0351f6728b | ||
|
7efc7e6b94 | ||
7d21a5a1fc | |||
dba785bf13 | |||
71878caf2c | |||
|
db42028228 | ||
|
7171a7567c | ||
|
339223bec0 | ||
|
d62a8d61de | ||
|
f6c83dc692 | ||
|
a984d1fd6f | ||
|
f901ea9396 | ||
|
17d96f1775 | ||
|
c10e5fe45c | ||
|
9bbe3f50cb | ||
|
1f4a4ec76f | ||
|
2befa584aa | ||
|
b48d32f4bc | ||
|
e36e88e77a | ||
|
8e9fc341ca | ||
|
c14c2d54a5 | ||
|
6adfaba8e9 | ||
|
06005014f9 | ||
|
4268a30d51 | ||
|
c71e6d22bf | ||
|
4df3ef4dd9 | ||
|
af95e64344 | ||
|
ac8ad15ad1 | ||
|
47dd078b6a | ||
|
472a44c30f | ||
|
b72ea9ebf9 | ||
|
f9958e4da0 | ||
|
47f406e09e | ||
|
1450b65dcd | ||
|
aac94afcd0 | ||
|
209360f535 | ||
|
b224fedf28 | ||
|
1ab071d16e | ||
|
1cf6f6f3e7 | ||
|
a421bec625 | ||
|
4e758fbba0 | ||
|
2350109a33 | ||
|
778637d60e | ||
|
23f7865140 | ||
|
cc7c4306f4 | ||
|
1183e50f60 | ||
|
a8de7e0ae0 | ||
|
30a39ef2f6 | ||
|
9a635148bb | ||
|
4b95b65be2 | ||
|
884ec2535b | ||
|
beba3052dd | ||
|
46242ad2c0 | ||
|
fa8c57269c | ||
|
b97bc8bfa8 | ||
|
89fc309c01 | ||
|
d7367476bc | ||
|
7297baaf7e | ||
|
8bf7914728 | ||
|
71fbbcff8a | ||
|
9303772f9a | ||
559b36b6f0 | |||
|
fbafdb7134 | ||
|
a53bd94737 | ||
|
46ef12309a | ||
|
4f60ba35eb | ||
|
f29b3f0187 | ||
|
aa3462aaee | ||
|
7c35357060 | ||
|
726b3f55a0 | ||
|
63eeb5b7a9 | ||
|
7081380058 | ||
|
288de95c49 | ||
|
9a01d1e877 | ||
|
10746c0469 | ||
|
5c8eca15b6 | ||
|
ef64f9ce5c | ||
|
9762838921 | ||
|
bf6d6d6430 | ||
|
ba9aa06b4f | ||
|
880dc31353 | ||
|
9a78fca507 | ||
|
4bc56d34e0 | ||
|
79f0757e9f | ||
|
a2eed13717 | ||
|
830aba984e | ||
|
33319cfe76 | ||
|
44b001bd3c | ||
|
ab9d95055e | ||
|
40391d8814 | ||
|
681507f211 | ||
|
7f133316a4 | ||
|
0bdbcf59fa | ||
|
0bad404b71 | ||
|
c100f2fc8d | ||
|
ba74779f95 | ||
|
035bbe68a5 | ||
|
319db68655 | ||
|
9d2c13e67c | ||
|
73c068055b | ||
|
30ce8d13af | ||
|
340f8f16a7 | ||
|
0ce1e62586 | ||
|
783fe1de32 | ||
|
49fde85187 | ||
|
479e751b7c | ||
|
f2c1ff2abd | ||
|
8b73460165 | ||
|
411d7e7dce | ||
|
f952d50b12 | ||
|
7324a72e6e | ||
|
ad73cc987d | ||
|
e9e0c79b40 | ||
|
badee498a3 | ||
|
72cd55716b | ||
|
a7cbd2d451 | ||
|
b9699637aa | ||
|
521be6db85 | ||
|
f3701d91fc | ||
|
59dacda37d | ||
|
7f58b5fa00 | ||
|
df222f18a3 | ||
|
5d22a4cac4 | ||
|
cc3a436750 | ||
|
404d3f4f4c | ||
|
43fcdc8526 | ||
|
35f896b40f | ||
|
fb1a38cff3 | ||
|
f88795a60e | ||
|
0ffebdf82f | ||
|
147b8514ef | ||
|
d535cf24a3 | ||
|
84dab59c72 | ||
|
22cf0d403e | ||
|
a525cffaff | ||
|
1af602c9f7 | ||
|
8a17aa2caa | ||
|
7a9d96d83a | ||
|
3869c02dfa | ||
|
848eb2274a | ||
|
ebd8b7ccdb | ||
|
3c6ab35390 | ||
|
57901c0013 | ||
|
d172dad0ab | ||
|
d0b7000747 | ||
|
84ff0d7182 | ||
|
600927b21c | ||
|
d965050563 | ||
|
a14c9d9574 | ||
|
8f9c94fe10 | ||
|
46f447ec5d | ||
|
4dbf11f91e | ||
|
569ce0ba25 | ||
|
aa955a06ef | ||
|
a9eb32217f | ||
|
c7998f56f0 | ||
|
a6e58dcd68 | ||
|
49591fa67e | ||
|
9f9724b1d1 | ||
|
205dc93f4b | ||
|
7f6d4527ed | ||
|
43a2f8db53 | ||
|
ba4cc01ed4 | ||
|
11d94ecba8 | ||
|
82d58d23c9 | ||
|
6377dd5c95 | ||
|
f364928004 | ||
|
dc070278f7 | ||
|
561a121e04 | ||
|
b03cf05ef7 | ||
|
edf6a03bc4 | ||
|
eb3cba31a7 | ||
|
1ffda1a5c4 | ||
|
2bc97a115c | ||
|
b8072f4346 | ||
|
cedd3cf816 | ||
|
2c833daa7f | ||
|
2a05c2247c | ||
|
c957ab2b72 | ||
|
98cce25f4c | ||
|
ba6ddfc516 | ||
|
c5d7eb9d30 | ||
|
d3185f25c3 | ||
|
007b5006d4 | ||
|
2d36c85085 | ||
|
d6fa738a25 | ||
|
c145191e55 | ||
|
91852bd4a0 | ||
|
6f5fa19fc3 | ||
|
e92d50593c | ||
|
0590bc3aab | ||
|
3286ad09df | ||
|
3da0a613f7 | ||
|
34be9e2393 | ||
|
97bdeed97a | ||
|
8016b16904 | ||
|
6e8926595d | ||
|
a9b6bc65a2 | ||
|
b1fd6e6021 | ||
|
576d43f44d | ||
|
c55a2c8c8e | ||
|
65c979ea59 | ||
|
dcd592ed11 | ||
|
e401303a08 | ||
|
2b72f3b40b | ||
|
2aae281120 | ||
|
1387da3b54 | ||
|
e868e6eb18 | ||
|
fc988e3fad | ||
|
205b5c206b | ||
|
ab9b4d14ef | ||
|
5f8b8661bf | ||
|
858a0c61e2 | ||
|
12d3ef0c02 | ||
|
da40ed1d8c | ||
|
f10bd1eea2 | ||
|
8576023b42 | ||
|
24eaaa277f | ||
|
8f4e3bb048 | ||
|
7775e45b60 | ||
|
b3ada0eb89 | ||
|
74b9721fbd | ||
|
7db75c0060 | ||
|
d96b3d26b6 | ||
|
72210e1980 | ||
|
359f85a42d | ||
|
95cac47f4e | ||
|
7dc32add35 | ||
|
0011cfe8f7 | ||
|
5a7c4f64d5 | ||
|
46893a8df5 | ||
|
81b45f74e3 | ||
|
c6dfcea5e2 | ||
|
198e456c22 | ||
|
1518f4c703 | ||
|
2d59565f61 | ||
|
d3384dc5fc | ||
|
e7cc705350 | ||
|
8fa635773c | ||
|
1ac3e0f976 | ||
|
f811230c25 | ||
|
74c3afe9ca | ||
|
0875ef1278 | ||
|
1d707aad41 | ||
|
fef19024d8 | ||
|
566e968849 | ||
|
55c69ae42b | ||
|
40839458a5 | ||
|
a259dd524f | ||
|
85c750d380 | ||
|
826e45f619 | ||
|
5989f65154 | ||
|
2cf0ccbb6b | ||
|
1677768177 | ||
|
7a7e02adab | ||
|
62d26560d9 | ||
|
df9639715b | ||
|
72237fef60 | ||
|
8fa07bb845 | ||
|
7931f50611 | ||
|
910536c6d3 | ||
|
3d830884b1 | ||
|
9110e5b185 | ||
|
22f60163fe | ||
|
8661716df9 | ||
|
ae64f09869 | ||
|
422e2f7b42 | ||
|
f990934425 | ||
|
effed1b5c5 | ||
|
efbb9c2be3 | ||
|
a6c9cf11bd | ||
|
26fa9dc898 | ||
|
ee1d158f2d | ||
|
5e5b224f89 | ||
|
f33416b712 | ||
|
b6626093e5 | ||
|
15936751c0 | ||
|
9efc200f74 | ||
|
54e8f95667 | ||
|
a5ccd40ec1 | ||
|
b4fbc3edf8 | ||
|
eb10c904e0 | ||
|
c8c8c6abc8 | ||
|
e64f405299 | ||
|
aa2f691f1e | ||
|
8cd9434664 | ||
|
e323f2f755 | ||
|
deae1c4639 | ||
|
62281cb3b7 | ||
|
6454931e70 | ||
|
6e88f1a887 | ||
|
2e28986503 | ||
|
b24935b938 | ||
|
ac06211841 | ||
|
5c1e2e9cda | ||
|
c1e48579f1 | ||
|
c6a6e7fafa | ||
|
be064262da | ||
|
9ac030fd16 | ||
|
7caee5665b | ||
|
e7517195cd | ||
|
30783d677b | ||
|
c863b2010e | ||
|
24d7d90c28 | ||
|
f2b1962e1c | ||
|
68ccd4722f | ||
|
9a90f19502 | ||
|
fbbc9937f6 | ||
|
e9f00b4f06 | ||
|
c7ca96bce5 | ||
|
637572ab58 | ||
|
28370c8e67 | ||
|
701ea96a90 | ||
|
de1bba3695 | ||
|
56f1edebe3 | ||
|
c52bac05b3 | ||
|
5d24786e20 | ||
|
bca75dbf98 | ||
|
0789da7bed | ||
|
f6458074b2 | ||
|
eadfd1d3cd | ||
|
3a34ab4462 | ||
|
25b603d667 | ||
|
f26d330973 | ||
|
7a52e841e6 | ||
|
9a3914ece6 | ||
|
6a32a72c15 | ||
|
d464b69b2e | ||
|
d16bf5e6b0 | ||
|
1ba6b5753f | ||
|
21fcb5daa9 | ||
|
c5adc6b7d8 | ||
|
b9ba0a3829 | ||
|
028b6f6cb7 | ||
|
3ca8b45014 | ||
|
90fc6aa3e7 | ||
|
707b7b76db | ||
|
6fff995ccd | ||
|
9b0440429c | ||
|
50266f2466 | ||
|
65171d1276 | ||
|
3b43ad84b5 | ||
|
bb72a16b64 | ||
|
b1c69eddb5 | ||
|
88c9187e2e | ||
|
bbe831a226 | ||
|
f642b218d0 | ||
|
cc72f47f00 | ||
|
24180e747e | ||
|
5a0cf58d8a | ||
|
d7d4d73af3 | ||
|
c2f6622a9f | ||
|
8778695e95 | ||
|
e0fd3db638 | ||
|
d5e9d09044 | ||
|
6e9dc03bc7 | ||
|
6384cfc701 | ||
|
67d7dafc14 | ||
|
1ada8645b8 | ||
|
d4a9e96e38 | ||
|
abb8cc5a2d | ||
|
5a9ea4234e | ||
|
c9136dbcfa | ||
|
61e4ad9741 | ||
|
c9dad9465a | ||
|
f10d6d1a71 | ||
|
97cb9d1f3b | ||
|
c66fb7eb6f | ||
|
48ad5cd1c7 | ||
|
ef35f45ad2 | ||
|
26bcd729bb | ||
|
78ad4402b0 | ||
|
6767ba8e8c | ||
|
4f15bb9624 | ||
|
3b2251a1d6 | ||
|
64ceb813c6 | ||
|
3b0d4ba58f | ||
|
5298a19667 | ||
|
b1d8bb04c4 | ||
|
b8cd5f1da5 | ||
|
a259b04d9c | ||
|
e45ee3fb40 | ||
|
d2c6c9da7a | ||
|
914888d18a | ||
|
c8b8c90580 | ||
|
922190d20f | ||
|
6362740a77 | ||
|
9eebc7fb22 | ||
|
2aa06d2954 | ||
|
931b2c4e1f | ||
|
b450cb09e6 | ||
|
8d11044610 | ||
|
786c8f132f | ||
|
677ba5b92e | ||
|
fb4455af39 | ||
|
7438445110 | ||
|
f7ce2edd87 | ||
|
0221221d53 | ||
|
49ef8b3c15 | ||
|
550a073d51 | ||
|
af0de33d4c | ||
|
c95e1818b2 | ||
|
41ad2a15ac | ||
|
36d6a4a1cd | ||
|
9b2c4c1f98 | ||
|
c3b5de336a | ||
|
bf117ec070 | ||
|
a3b0ea9b8d | ||
|
9dabab51db | ||
|
2ad400c5e7 | ||
|
8a27f70e89 | ||
|
fcf29fe6df | ||
|
31e4658766 | ||
|
7b554e4778 | ||
|
fe2f8aaa5a | ||
|
137dd655d1 | ||
|
494cd5ddc1 | ||
|
80188fa88d | ||
|
4580f8bf0f | ||
|
03e6fe3ef6 | ||
|
8c75189ce1 | ||
|
68b7219cf5 | ||
|
7a828760b3 | ||
|
5280ec2d18 | ||
|
b0d8b0b7f8 | ||
|
bc90de76b6 | ||
|
ed97ff466d | ||
|
6cce9779fa | ||
|
2c2872275a | ||
|
fb3f6b9073 | ||
|
28cb35e0b0 | ||
|
79ad1346d3 | ||
|
bb05edfd6b | ||
|
4d3531c2cb | ||
|
ff968b68b2 | ||
|
84c36b9903 | ||
|
3088098a0a | ||
|
f9feff4b24 | ||
|
08d7e12c38 | ||
|
bd74f4098c | ||
|
ee79281f53 | ||
|
ee4d2d7f0e | ||
|
f19b257afd | ||
|
87e3795c76 | ||
|
f5d6d91e51 | ||
|
a1a2aac1f3 | ||
|
c1449d50ce | ||
|
e13a5b0e60 | ||
|
858759865e | ||
|
8bae013152 | ||
|
4d5419fdbc | ||
|
2e4d7101ce | ||
|
229b6e55f5 | ||
|
d2ba9471da | ||
|
1f945d1af3 | ||
|
64c792b11f | ||
|
67e28c704f | ||
|
ac901e5b77 | ||
|
21fc91c3a4 | ||
|
00bad52570 | ||
|
59d93900a3 | ||
|
2df4e931d4 | ||
|
36e802082e | ||
|
d7e1583a8e | ||
|
2c848a564c | ||
|
e97c873b4f | ||
|
f151ad75c6 | ||
|
83ce873e25 | ||
|
71e3c210f2 | ||
|
a4fdb578bc | ||
|
0498db1140 | ||
|
77ceae37ef | ||
|
085013b256 | ||
|
381b52f46c | ||
|
0bd3bd63aa | ||
|
c6c4814519 | ||
|
b1747f61fe | ||
|
85aa56d030 | ||
|
361ad46be4 | ||
|
4e15ab8041 | ||
|
091208b66c | ||
|
7df8a9ef6b | ||
|
4c7993f48f | ||
|
94d5e0f0ac | ||
|
a521caba8d | ||
|
797f0356f6 | ||
|
8dcc1f012a | ||
|
1115960107 | ||
|
727b3042a1 | ||
|
61efded673 | ||
|
38aecdd741 | ||
|
e0ffee295d | ||
|
e62756ed29 | ||
|
ac3bfbe368 | ||
|
affdf43e0b | ||
|
20ceec0e64 | ||
|
4c9ee8a57d | ||
|
5c581d8984 | ||
|
ac4d5cf7d5 | ||
|
b90e749a7f | ||
|
d04b79bcb5 | ||
|
14164ec4a5 | ||
|
f9b461a08d | ||
|
8c5d09dbee | ||
|
e2af45929e | ||
|
481cb5e478 | ||
|
a3ca2e66bf | ||
|
82746f1492 | ||
|
ea45eb1f55 | ||
|
a60df91b04 | ||
|
c1a99453d5 | ||
|
be5218f7e1 | ||
|
dd5fe36ee1 | ||
|
4a4dae9951 | ||
|
5d87d7f249 | ||
|
f9cde30e08 | ||
|
b376114bee | ||
|
57088cda03 | ||
|
b542f805f0 | ||
|
28a129f69c | ||
|
c4f5e168f2 | ||
|
5b2dad6fff | ||
|
b22a77e603 | ||
|
4d9c66cb51 | ||
|
9254e3f8f7 | ||
|
635c14ff8f | ||
|
898abd15c6 | ||
|
337453c6c6 | ||
|
bdb8f06e1d | ||
|
64b4c2c08a | ||
|
8a6a3d0994 | ||
|
8ea0cb84d1 | ||
|
099857e226 | ||
|
f8e954ff79 | ||
|
435bb392ea | ||
|
de10392a7f | ||
|
e421792906 | ||
|
abb3823a8b | ||
|
b343c6c6e0 | ||
|
22cfaf9b44 | ||
|
a9dce881bd | ||
|
0100a9a62e | ||
|
6fba63846a | ||
|
2a06cc5806 | ||
|
380e38519b | ||
|
8ccaf8beaf | ||
|
7892f42e3e | ||
|
2964d3a4aa | ||
|
46da197507 | ||
|
b005e772ea | ||
|
f83eeb7a5f | ||
|
e94015a142 | ||
|
98fe68d0be | ||
|
f9aee86a1c | ||
|
e2a7e1f6de | ||
|
53efb4b542 | ||
|
53ea6f24ee | ||
|
b3e7b59903 | ||
|
bc7c30e2ee | ||
|
4da5add25a | ||
|
f5766e9207 | ||
|
a8fd04e4c0 | ||
|
33bc3c5882 | ||
|
41a3c4c161 | ||
|
83c83d791b | ||
|
d5f0060e2e | ||
|
41256154ad | ||
|
0814cfe1ef | ||
|
730611039b | ||
|
9b355d5b56 | ||
|
6781122fc1 | ||
|
e9ca8eb8dd | ||
|
97f682dfcd | ||
|
9f2004bb54 | ||
|
8119591c62 | ||
|
34e552f760 | ||
|
ef97afd86c | ||
|
9a7a447246 | ||
|
f85f014bc9 | ||
|
fb1519cece | ||
|
51e8058f97 | ||
|
fba6b592f5 | ||
|
db158ad312 | ||
|
8c9de4303b | ||
|
9661751df2 | ||
|
d1c9d27a65 | ||
|
838bf325ba | ||
|
b99fd03df2 | ||
|
d37c41e99f | ||
|
a4ecd344d0 | ||
|
e0285607a0 | ||
|
96adadce5e | ||
|
e8a9e808f5 | ||
|
966cf6ce15 | ||
|
6406b493a2 | ||
|
0701213225 | ||
|
411e66b13c | ||
|
5db7eef1d7 | ||
|
0a1b20dd4e | ||
|
92ebf0d233 | ||
|
405f95a43b | ||
|
ab89002cfc | ||
|
c319780ab5 | ||
|
9f23f85b87 | ||
|
1cf333f0fc | ||
|
bf372a1ce2 | ||
|
96430d852c | ||
|
fc8c8fdf29 | ||
|
8d30c5c7e5 | ||
|
fb56293273 | ||
|
baa3826a42 | ||
|
4598abc721 | ||
|
d7d0daea0d | ||
|
46e7305953 | ||
|
4f15b820a5 | ||
|
edd92beadf | ||
|
20bb9fe54b | ||
|
ba5aa6da5f | ||
|
b11e35616c | ||
|
fa98bb34bd | ||
|
a67446048e | ||
|
b37e7c4c41 | ||
|
29111059f9 | ||
|
9776a18e4c | ||
|
7f1adf7c4e | ||
|
56bc281b30 | ||
|
c4948be1f7 | ||
|
f3dbb72f69 | ||
|
173affd8eb | ||
|
51fe9cc9f8 | ||
|
d4be8b426e | ||
|
65dd7e5fa3 | ||
|
123e2b84df | ||
|
f12370a6cd | ||
|
52521e89a6 | ||
|
08ac0ac890 | ||
|
63fff6ca7c | ||
|
85b1e974ff | ||
|
3a5eceba83 | ||
|
018865967d | ||
|
8dd003f81f | ||
|
f03e708280 | ||
|
198658f5f9 | ||
|
03c74a7940 | ||
|
7717d1ed34 | ||
|
72560397a2 | ||
|
413a9cddb1 | ||
|
4064218010 | ||
|
a2fcc05672 | ||
|
8fc6f96324 | ||
|
271732f40d | ||
|
019acb90ac | ||
|
759b6d9489 | ||
|
f32e4a9b0d | ||
|
209cb1fbe5 | ||
|
ceff3ed6c9 | ||
|
2c49b25d59 | ||
|
5f963d5451 | ||
|
a1ead1bfc8 | ||
|
cef75e56d7 | ||
|
8bfb8029b2 | ||
|
fd0f387dbc | ||
|
676239ad24 | ||
|
5fd4cb5c78 | ||
|
2b3a8760ff | ||
|
0eccfcf886 | ||
|
f90663bf97 | ||
|
1043e5725a | ||
|
3bf0906697 | ||
|
85642d00d8 | ||
|
fabb30cec2 | ||
|
641bdd9464 | ||
|
4f6579c3d1 | ||
|
1c45dd833d | ||
|
f3117c9e69 | ||
|
fc72425c05 | ||
|
f74277af66 | ||
|
f64c7a6e69 | ||
|
73508c0251 | ||
|
2c4ba3258d | ||
|
0f1e05acdd | ||
|
86dbdbd2b6 | ||
|
c134e1ae0f | ||
|
abf05131d5 | ||
|
869e634adb | ||
|
be155f2f2d | ||
|
68cead600f | ||
|
d85eeb5801 | ||
|
2140a59777 | ||
|
512868ee14 | ||
|
d7d819dde0 | ||
|
eb83c58f05 | ||
|
f883082814 | ||
|
b65d37b141 | ||
|
aa498e0261 | ||
|
670fda3c9c | ||
|
dd5d773405 | ||
|
d08cec06d3 | ||
|
1f3e65fa68 | ||
|
55c1d7f02c | ||
|
71cae7f5ca | ||
|
445745ee15 | ||
|
6e51ca749d | ||
|
babb458aa4 | ||
|
288e3f15b6 | ||
|
bb23c45fad | ||
|
47c02d72af | ||
|
ae0abb5cb3 | ||
|
f66a54bb73 | ||
|
010ce0df3e | ||
|
519ef9dc20 | ||
|
a30955fb75 | ||
|
31223aaed9 | ||
|
5d14fef032 | ||
|
7ca0144004 | ||
|
7c1d1df1a9 | ||
|
a6bf1fc16a | ||
|
84c88dfd5e | ||
|
39eaf4b109 | ||
|
376cc96343 | ||
|
e0e75b53bb | ||
|
aba0be7960 | ||
|
a057776e1f | ||
|
e21154e869 | ||
|
f105225abf | ||
|
debbf265c4 | ||
|
8db55d792c | ||
|
09e99ee3a3 | ||
|
3e38e48d7a | ||
|
1664554083 | ||
|
f8610d4ff1 | ||
|
a71fbb0ed3 | ||
|
57a2af285a | ||
|
e6e4a13440 | ||
|
f9ddee60ed | ||
|
66104e1137 | ||
|
15ab316909 | ||
|
6be42d57ca | ||
|
5c8164dd3b | ||
|
625825cf3f | ||
|
dee2f4badc | ||
|
633cd49094 | ||
|
d58d08a4d4 | ||
|
5f9695ef8e | ||
|
2e08951d44 | ||
|
2b8f81c94b | ||
|
c960d97b67 | ||
|
d82c9baf20 | ||
|
4ae0b3c5f0 | ||
|
4e34583e3f | ||
|
a2116bf290 | ||
|
d48cb3aaed | ||
|
dbd017f680 | ||
|
712588af7d | ||
|
6524f89ebd | ||
|
7f2f25cb71 | ||
|
042ce80b78 | ||
|
d7f4d32c92 | ||
|
b80927efa3 | ||
|
511981e762 | ||
|
e09fa2b847 | ||
|
39d82a573f | ||
|
7124821f7c | ||
|
ba21de683b | ||
|
00a1e79af6 | ||
|
95ba7798d7 | ||
|
0fe63d3eae | ||
|
3d27dc9a41 | ||
|
1a5bbf32a4 | ||
|
25dfe2f496 | ||
|
8be913cbf9 | ||
|
49a74e8e1e | ||
|
b69f1b6dbc | ||
|
d7ca072af3 | ||
|
928abc5a06 | ||
|
0ba7100110 | ||
|
b795a06b9c | ||
|
e478beee5c | ||
|
128a9e32c0 | ||
|
1e3850bb6b | ||
|
9da9649a45 | ||
|
954a6fdb53 | ||
|
6d6ba70bd7 | ||
|
6d72644ee3 | ||
|
3d091f50b5 | ||
|
347497602c | ||
|
6d6c995563 | ||
|
63ce694b4d | ||
|
59116f2d46 | ||
|
88e911ff9d | ||
|
ba87044638 | ||
|
f8952225d6 | ||
|
c11ccf2ecc | ||
|
8488beeb4e | ||
|
8551ffcfd3 | ||
|
ea495e8f29 | ||
|
adf43889e1 | ||
|
5a22b1cd37 | ||
|
0e19abb51a | ||
|
38af50a866 | ||
|
7853987ebb | ||
|
09e63bf00c | ||
|
53658589f8 | ||
|
f5778fed2a | ||
|
66fc364739 | ||
|
6023211ab0 | ||
|
65d7a66eb8 |
738 changed files with 76026 additions and 49968 deletions
1
.credentials/HCAPTCHA_SECRET
Normal file
1
.credentials/HCAPTCHA_SECRET
Normal file
|
@ -0,0 +1 @@
|
||||||
|
0x0000000000000000000000000000000000000000
|
1
.credentials/HCAPTCHA_SITEKEY
Normal file
1
.credentials/HCAPTCHA_SITEKEY
Normal file
|
@ -0,0 +1 @@
|
||||||
|
10000000-ffff-ffff-ffff-000000000001
|
1
.credentials/KFETOPEN_TOKEN
Normal file
1
.credentials/KFETOPEN_TOKEN
Normal file
|
@ -0,0 +1 @@
|
||||||
|
k-feste_token
|
1
.credentials/SECRET_KEY
Normal file
1
.credentials/SECRET_KEY
Normal file
|
@ -0,0 +1 @@
|
||||||
|
insecure-key
|
1
.credentials/SYMPA_PASSWORD
Normal file
1
.credentials/SYMPA_PASSWORD
Normal file
|
@ -0,0 +1 @@
|
||||||
|
toto
|
1
.credentials/SYMPA_USERNAME
Normal file
1
.credentials/SYMPA_USERNAME
Normal file
|
@ -0,0 +1 @@
|
||||||
|
sympa
|
1
.envrc
Normal file
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
use nix
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -5,13 +5,20 @@ cof/settings.py
|
||||||
settings.py
|
settings.py
|
||||||
*~
|
*~
|
||||||
venv/
|
venv/
|
||||||
|
.venv/
|
||||||
.vagrant
|
.vagrant
|
||||||
/src
|
/src
|
||||||
media/
|
media/
|
||||||
*.log
|
*.log
|
||||||
|
.sass-cache/
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
.coverage
|
.coverage
|
||||||
|
|
||||||
# PyCharm
|
# PyCharm
|
||||||
.idea
|
.idea
|
||||||
.cache
|
.cache
|
||||||
|
|
||||||
|
# VSCode
|
||||||
|
.vscode/
|
||||||
|
.direnv
|
||||||
|
.static
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
image: "python:3.5"
|
image: "python:3.7"
|
||||||
|
|
||||||
services:
|
|
||||||
- postgres:latest
|
|
||||||
- redis:latest
|
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
# GestioCOF settings
|
# GestioCOF settings
|
||||||
DJANGO_SETTINGS_MODULE: "cof.settings.prod"
|
|
||||||
DBHOST: "postgres"
|
DBHOST: "postgres"
|
||||||
REDIS_HOST: "redis"
|
REDIS_HOST: "redis"
|
||||||
REDIS_PASSWD: "dummy"
|
REDIS_PASSWD: "dummy"
|
||||||
|
@ -22,21 +17,24 @@ variables:
|
||||||
# psql password authentication
|
# psql password authentication
|
||||||
PGPASSWORD: $POSTGRES_PASSWORD
|
PGPASSWORD: $POSTGRES_PASSWORD
|
||||||
|
|
||||||
test:
|
# apps to check migrations for
|
||||||
stage: test
|
MIGRATION_APPS: "bda bds cofcms clubs events gestioncof kfet kfetauth kfetcms open petitscours shared"
|
||||||
|
|
||||||
|
.test_template:
|
||||||
before_script:
|
before_script:
|
||||||
- mkdir -p vendor/{pip,apt}
|
- mkdir -p vendor/{pip,apt}
|
||||||
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client
|
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client libldap2-dev libsasl2-dev
|
||||||
- sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' cof/settings/secret_example.py > cof/settings/secret.py
|
- sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' gestioasso/settings/secret_example.py > gestioasso/settings/secret.py
|
||||||
- sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' cof/settings/secret.py
|
- sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' gestioasso/settings/secret.py
|
||||||
# Remove the old test database if it has not been done yet
|
# Remove the old test database if it has not been done yet
|
||||||
- psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB"
|
- psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB"
|
||||||
- pip install --upgrade -r requirements.txt coverage
|
- pip install --upgrade -r requirements-prod.txt coverage tblib
|
||||||
- python --version
|
- python --version
|
||||||
script:
|
|
||||||
- coverage run manage.py test
|
|
||||||
after_script:
|
after_script:
|
||||||
- coverage report
|
- coverage report
|
||||||
|
services:
|
||||||
|
- postgres:11.7
|
||||||
|
- redis:latest
|
||||||
cache:
|
cache:
|
||||||
key: test
|
key: test
|
||||||
paths:
|
paths:
|
||||||
|
@ -45,18 +43,61 @@ test:
|
||||||
# Keep this disabled for now, as it may kill GitLab...
|
# Keep this disabled for now, as it may kill GitLab...
|
||||||
# coverage: '/TOTAL.*\s(\d+\.\d+)\%$/'
|
# coverage: '/TOTAL.*\s(\d+\.\d+)\%$/'
|
||||||
|
|
||||||
|
kfettest:
|
||||||
|
stage: test
|
||||||
|
extends: .test_template
|
||||||
|
variables:
|
||||||
|
DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod"
|
||||||
|
script:
|
||||||
|
- coverage run manage.py test kfet
|
||||||
|
|
||||||
|
coftest:
|
||||||
|
stage: test
|
||||||
|
extends: .test_template
|
||||||
|
variables:
|
||||||
|
DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod"
|
||||||
|
script:
|
||||||
|
- coverage run manage.py test gestioncof bda petitscours shared --parallel
|
||||||
|
|
||||||
|
bdstest:
|
||||||
|
stage: test
|
||||||
|
extends: .test_template
|
||||||
|
variables:
|
||||||
|
DJANGO_SETTINGS_MODULE: "gestioasso.settings.bds_prod"
|
||||||
|
script:
|
||||||
|
- coverage run manage.py test bds clubs events --parallel
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
image: python:3.6
|
|
||||||
stage: test
|
stage: test
|
||||||
before_script:
|
before_script:
|
||||||
- mkdir -p vendor/pip
|
- mkdir -p vendor/pip
|
||||||
- pip install --upgrade black isort flake8
|
- pip install --upgrade black isort flake8
|
||||||
script:
|
script:
|
||||||
- black --check .
|
- black --check .
|
||||||
- isort --recursive --check-only --diff bda cof gestioncof kfet provisioning shared utils
|
- isort --check --diff .
|
||||||
# Print errors only
|
# Print errors only
|
||||||
- flake8 --exit-zero bda cof gestioncof kfet provisioning shared utils
|
- flake8 --exit-zero bda bds clubs gestioasso events gestioncof kfet petitscours provisioning shared
|
||||||
cache:
|
cache:
|
||||||
key: linters
|
key: linters
|
||||||
paths:
|
paths:
|
||||||
- vendor/
|
- vendor/
|
||||||
|
|
||||||
|
# Check whether there are some missing migrations.
|
||||||
|
migration_checks:
|
||||||
|
stage: test
|
||||||
|
variables:
|
||||||
|
DJANGO_SETTINGS_MODULE: "gestioasso.settings.local"
|
||||||
|
before_script:
|
||||||
|
- mkdir -p vendor/{pip,apt}
|
||||||
|
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client libldap2-dev libsasl2-dev
|
||||||
|
- cp gestioasso/settings/secret_example.py gestioasso/settings/secret.py
|
||||||
|
- pip install --upgrade -r requirements-devel.txt
|
||||||
|
- python --version
|
||||||
|
script: python manage.py makemigrations --dry-run --check $MIGRATION_APPS
|
||||||
|
services:
|
||||||
|
# this should not be necessary…
|
||||||
|
- postgres:11.7
|
||||||
|
cache:
|
||||||
|
key: migration_checks
|
||||||
|
paths:
|
||||||
|
- vendor/
|
||||||
|
|
|
@ -48,7 +48,7 @@ if type isort &>/dev/null; then
|
||||||
ISORT_OUTPUT="/tmp/gc-isort-output.log"
|
ISORT_OUTPUT="/tmp/gc-isort-output.log"
|
||||||
touch $ISORT_OUTPUT
|
touch $ISORT_OUTPUT
|
||||||
|
|
||||||
if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort --check-only &>$ISORT_OUTPUT; then
|
if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort --check &>$ISORT_OUTPUT; then
|
||||||
echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort &>$ISORT_OUTPUT
|
echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort &>$ISORT_OUTPUT
|
||||||
printf "Reformatted.\n"
|
printf "Reformatted.\n"
|
||||||
formatter_updated=1
|
formatter_updated=1
|
||||||
|
|
303
CHANGELOG.md
Normal file
303
CHANGELOG.md
Normal file
|
@ -0,0 +1,303 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre
|
||||||
|
2018).
|
||||||
|
|
||||||
|
## Le FUTUR ! (pas prêt pour la prod)
|
||||||
|
|
||||||
|
### Nouveau module de gestion des événements
|
||||||
|
|
||||||
|
- Désormais complet niveau modèles
|
||||||
|
- Export des participants implémenté
|
||||||
|
|
||||||
|
#### TODO
|
||||||
|
|
||||||
|
- Vue de création d'événements ergonomique
|
||||||
|
- Vue d'inscription à un événement **ou** intégration propre dans la vue
|
||||||
|
"inscription d'un nouveau membre"
|
||||||
|
|
||||||
|
### Nouveau module de gestion des clubs
|
||||||
|
|
||||||
|
Uniquement un modèle simple de clubs avec des respos. Aucune gestion des
|
||||||
|
adhérents ni des cotisations.
|
||||||
|
|
||||||
|
## TODO Prod
|
||||||
|
|
||||||
|
- Lancer `python manage.py update_translation_fields` après la migration
|
||||||
|
- Mettre à jour les units systemd `daphne.service` et `worker.service`
|
||||||
|
|
||||||
|
- Créer un compte hCaptcha (https://www.hcaptcha.com/), au COF, et remplacer les secrets associés
|
||||||
|
|
||||||
|
## Version ??? - ??/??/????
|
||||||
|
|
||||||
|
## Version 0.15.1 - 15/06/2023
|
||||||
|
|
||||||
|
### K-Fêt
|
||||||
|
|
||||||
|
- Rattrape les erreurs d'envoi de mail de négatif
|
||||||
|
- Utilise l'adresse chefs pour les envois de négatifs
|
||||||
|
|
||||||
|
## Version 0.15 - 22/05/2023
|
||||||
|
|
||||||
|
### K-Fêt
|
||||||
|
|
||||||
|
- Rajoute un formulaire de contact
|
||||||
|
- Rajoute un formulaire de demande de soirée
|
||||||
|
- Désactive les mails d'envoi de négatifs sur les comptes gelés
|
||||||
|
|
||||||
|
## Version 0.14 - 19/05/2023
|
||||||
|
|
||||||
|
- Répare les dépendances en spécifiant toutes les versions
|
||||||
|
|
||||||
|
### K-Fêt
|
||||||
|
|
||||||
|
- Répare la gestion des changement d'heure via moment.js
|
||||||
|
|
||||||
|
## Version 0.13 - 19/02/2023
|
||||||
|
|
||||||
|
### K-Fêt
|
||||||
|
|
||||||
|
- Rajoute la valeur des inventaires
|
||||||
|
- Résout les problèmes de négatif ne disparaissant pas
|
||||||
|
- Affiche son surnom s'il y en a un
|
||||||
|
- Bugfixes
|
||||||
|
|
||||||
|
## Version 0.12.1 - 03/10/2022
|
||||||
|
|
||||||
|
### K-Fêt
|
||||||
|
|
||||||
|
- Fixe un problème de rendu causé par l'agrandissement du menu
|
||||||
|
|
||||||
|
- Mise à jour vers Channels 3.x et Django 3.2
|
||||||
|
|
||||||
|
## Version 0.12 - 17/06/2022
|
||||||
|
|
||||||
|
### K-Fêt
|
||||||
|
|
||||||
|
- Ajoute une exception à la limite d'historique pour les comptes `LIQ` et `#13`
|
||||||
|
- Répare le problème des étiquettes LIQ/Comptes K-Fêt inversées dans les stats des articles K-Fêt
|
||||||
|
|
||||||
|
## Version 0.11 - 26/10/2021
|
||||||
|
|
||||||
|
### COF
|
||||||
|
|
||||||
|
- Répare un problème de rendu sur le wagtail du COF
|
||||||
|
|
||||||
|
### K-Fêt
|
||||||
|
|
||||||
|
- Ajoute de mails de rappels pour les comptes en négatif
|
||||||
|
- La recherche de comptes sur K-Psul remarche normalement
|
||||||
|
- Le pointeur de la souris change de forme quand on survole un item d'autocomplétion
|
||||||
|
- Modification du gel de compte:
|
||||||
|
- on ne peut plus geler/dégeler son compte soi-même (il faut la permission "Gérer les permissions K-Fêt")
|
||||||
|
- on ne peut rien compter sur un compte gelé (aucune override possible), et les K-Fêteux·ses dont le compte est gelé perdent tout accès à K-Psul
|
||||||
|
- les comptes actuellement gelés (sur l'ancien système) sont dégelés automatiquement
|
||||||
|
- Modification du fonctionnement des négatifs
|
||||||
|
- impossible d'avoir des négatifs inférieurs à `kfet_config.overdraft_amount`
|
||||||
|
- il n'y a plus de limite de temps sur les négatifs
|
||||||
|
- supression des autorisations de négatif
|
||||||
|
- il n'est plus possible de réinitialiser la durée d'un négatif en faisant puis en annulant une charge
|
||||||
|
- La gestion des erreurs passe du client au serveur, ce qui permet d'avoir des messages plus explicites
|
||||||
|
- La supression d'opérations anciennes est réparée
|
||||||
|
|
||||||
|
## Version 0.10 - 18/04/2021
|
||||||
|
|
||||||
|
### K-Fêt
|
||||||
|
|
||||||
|
- On fait sauter la limite qui empêchait de vendre plus de 24 unités d'un item à
|
||||||
|
la fois.
|
||||||
|
- L'interface indique plus clairement quand on fait une erreur en modifiant un
|
||||||
|
compte.
|
||||||
|
- On supprime la fonction "décalage de balance".
|
||||||
|
- L'accès à l'historique est maintenant limité à 7 jours pour raison de
|
||||||
|
confidentialité. Les chefs/trez peuvent disposer d'une permission
|
||||||
|
supplémentaire pour accéder à jusqu'à 30 jours en cas de problème de compta.
|
||||||
|
L'accès à son historique personnel n'est pas limité. Les durées sont
|
||||||
|
configurables dans `settings/cof_prod.py`.
|
||||||
|
|
||||||
|
### COF
|
||||||
|
|
||||||
|
- Le Captcha sur la page de demande de petits cours utilise maintenant hCaptcha
|
||||||
|
au lieu de ReCaptcha, pour mieux respecter la vie privée des utilisateur·ices
|
||||||
|
|
||||||
|
## Version 0.9 - 06/02/2020
|
||||||
|
|
||||||
|
### COF / BdA
|
||||||
|
|
||||||
|
- Le COF peut remettre à zéro la liste de ses adhérents en août (sans passer par
|
||||||
|
KDE).
|
||||||
|
- La page d'accueil affiche la date de fermeture des tirages BdA.
|
||||||
|
- On peut revendre une place dès qu'on l'a payée, plus besoin de payer toutes
|
||||||
|
ses places pour pouvoir revendre.
|
||||||
|
- On s'assure que l'email fourni lors d'une demande de petit cours est valide.
|
||||||
|
|
||||||
|
### BDS
|
||||||
|
|
||||||
|
- Le burô peut maintenant accorder ou révoquer le statut de membre du Burô
|
||||||
|
en modifiant le profil d'un membre du BDS.
|
||||||
|
- Le burô peut exporter la liste de ses membres avec email au format CSV depuis
|
||||||
|
la page d'accueil.
|
||||||
|
|
||||||
|
### K-Fêt
|
||||||
|
|
||||||
|
- On affiche les articles actuellement en vente en premier lors des inventaires
|
||||||
|
et des commandes.
|
||||||
|
- On peut supprimer un inventaire. Seuls les articles dont c'est le dernier
|
||||||
|
inventaire sont affectés.
|
||||||
|
|
||||||
|
## Version 0.8 - 03/12/2020
|
||||||
|
|
||||||
|
### COF
|
||||||
|
|
||||||
|
- La page "Mes places" dans la section BdA indique quelles places sont sur
|
||||||
|
listing.
|
||||||
|
- ergonomie de l'interface admin du BdA : moins d'options inutiles lors de
|
||||||
|
la sélection de participants.
|
||||||
|
- les tirages sont maintenant archivables pour éviter d'avoir encore d'autres
|
||||||
|
options inutiles.
|
||||||
|
- l'autocomplétion dans l'admin BdA est réparée.
|
||||||
|
- Les icones de la page de gestion des petits cours sont (à nouveau) réparées.
|
||||||
|
- On a supprimé la possibilité de modifier les mails automatiques depuis
|
||||||
|
l'interface admin car trop problématique. Faute de mieux, envoyer un mail à
|
||||||
|
KDE pour modifier ces mails.
|
||||||
|
- corrige un crash sporadique sur la page d'inscription au système de petits
|
||||||
|
cours
|
||||||
|
|
||||||
|
### K-Fêt
|
||||||
|
|
||||||
|
- (fix partiel) Empêche la K-Fêt de modifier des données COF (e.g. nom, prénom,
|
||||||
|
username) lors de la création d'un nouveau compte.
|
||||||
|
- Les statistiques de conso globales montrent deux courbes COF / non-COF au
|
||||||
|
lieu de LIQ / sur compte.
|
||||||
|
- Un bug empêchait de fermer manuellement la K-Fêt depuis un compte non
|
||||||
|
privilégié en tapant un mot de passe. C'est corrigé.
|
||||||
|
|
||||||
|
## Version 0.7.2 - 08/09/2020
|
||||||
|
|
||||||
|
- Nouvelle page 404
|
||||||
|
- Correction de bug en K-Fêt : le lien pour créer un nouveau compte exté apparaît
|
||||||
|
à nouveau dans l'autocomplétion
|
||||||
|
|
||||||
|
## Version 0.7.1 - 05/09/2020
|
||||||
|
|
||||||
|
Petits ajustements sur le site du COF :
|
||||||
|
|
||||||
|
- Possibilité d'ajouter des champs d'infos supplémentaires en plus de l'email et
|
||||||
|
de la page web dans les annuaires (clubs et partenaires).
|
||||||
|
- Corrige un bug d'affichage des adresses emails de clubs
|
||||||
|
|
||||||
|
## Version 0.7 - 29/08/2020
|
||||||
|
|
||||||
|
### GestioBDS
|
||||||
|
|
||||||
|
- Ajout d'un bouton pour supprimer un compte
|
||||||
|
- Le nombre d'adhérent⋅es est affiché sur la page d'accueil
|
||||||
|
- le groupe BDS a les bonnes permissions
|
||||||
|
|
||||||
|
### Site du COF
|
||||||
|
|
||||||
|
- Captcha fonctionnel pour les mailing-listes
|
||||||
|
|
||||||
|
### K-Fêt
|
||||||
|
|
||||||
|
- L'autocomplétion pour la création de compte K-Fêt se lance à 3 caractères seulement,
|
||||||
|
donc est plus rapide.
|
||||||
|
|
||||||
|
## Version 0.6 - 27/07/2020
|
||||||
|
|
||||||
|
Arrivée du BDS !
|
||||||
|
GestioCOF et GestioBDS ont du code en commun mais tournent de façon séparée, les
|
||||||
|
deux bases de données sont distinctes.
|
||||||
|
|
||||||
|
## Version 0.5 - 11/07/2020
|
||||||
|
|
||||||
|
### Problèmes corrigés
|
||||||
|
|
||||||
|
- La recherche d'utilisateurices (COF + K-Fêt) fonctionne de nouveau
|
||||||
|
- Bug d'affichage quand on a beaucoup de clubs dans le cadre "Accès rapide" sur
|
||||||
|
la page des clubs (nouveau site du COF)
|
||||||
|
- Version mobile plus ergonimique sur le nouveau site du COF
|
||||||
|
- Cliquer sur "visualiser" sur les pages de clubs dans wagtail ne provoque plus
|
||||||
|
d'erreurs 500 (nouveau site du COF)
|
||||||
|
- L'historique des ventes des articles K-Fêt fonctionne à nouveau
|
||||||
|
- Les montants en K-Fêt sont à nouveau affichés en UKF (et non en €).
|
||||||
|
- Les boutons "afficher/cacher" des mails et noms des participant⋅e⋅s à un
|
||||||
|
spectacle BdA fonctionnent à nouveau.
|
||||||
|
- on ne peut plus compter de consos sur ☠☠☠, ni éditer les comptes spéciaux
|
||||||
|
(LIQ, GNR, ☠☠☠, #13).
|
||||||
|
|
||||||
|
### Nouvelles fonctionnalités
|
||||||
|
|
||||||
|
- On n'affiche que 4 articles sur la pages "nouveautés" (nouveau site du COF)
|
||||||
|
- Plus de traductions sur le nouveau site du COF
|
||||||
|
- Les transferts apparaissent maintenant dans l'historique K-Fêt et l'historique
|
||||||
|
personnel.
|
||||||
|
- les statistiques K-Fêt remontent à plus d'un an (et le code est simplifié)
|
||||||
|
|
||||||
|
## Version 0.4.1 - 17/01/2020
|
||||||
|
|
||||||
|
- Corrige un bug sur K-Psul lorsqu'un trigramme contient des caractères réservés
|
||||||
|
aux urls (\#, /...)
|
||||||
|
|
||||||
|
## Version 0.4 - 15/01/2020
|
||||||
|
|
||||||
|
- Corrige un bug d'affichage d'images sur l'interface des petits cours
|
||||||
|
- La page des transferts permet de créer un nombre illimité de transferts en
|
||||||
|
une fois.
|
||||||
|
- Nouveau site du COF : les liens sont optionnels dans les descriptions de clubs
|
||||||
|
- Mise à jour du lien vers le calendire de la K-Fêt sur la page d'accueil
|
||||||
|
- Certaines opérations sont à nouveau accessibles depuis la session partagée
|
||||||
|
K-Fêt.
|
||||||
|
- Le bouton "déconnexion" déconnecte vraiment du CAS pour les comptes clipper
|
||||||
|
- Corrige un crash sur la page des reventes pour les nouveaux participants.
|
||||||
|
- Corrige un bug d'affichage pour les trigrammes avec caractères spéciaux
|
||||||
|
|
||||||
|
## Version 0.3.3 - 30/11/2019
|
||||||
|
|
||||||
|
- Corrige un problème de redirection lors de la déconnexion (CAS seulement)
|
||||||
|
- Les catégories d'articles K-Fêt peuvent être exemptées de subvention COF
|
||||||
|
- Corrige un bug d'affichage dans K-Psul quand on annule une transaction sur LIQ
|
||||||
|
- Corrige une privilege escalation liée aux sessions partagées en K-Fêt
|
||||||
|
https://git.eleves.ens.fr/klub-dev-ens/gestioCOF/issues/240
|
||||||
|
|
||||||
|
## Version 0.3.2 - 04/11/2019
|
||||||
|
|
||||||
|
- Bugfix: modifier un compte K-Fêt ne supprime plus nom/prénom
|
||||||
|
|
||||||
|
## Version 0.3.1 - 19/10/2019
|
||||||
|
|
||||||
|
- Bugfix: l'historique des utilisateurices s'affiche à nouveau
|
||||||
|
|
||||||
|
## Version 0.3 - 16/10/2019
|
||||||
|
|
||||||
|
- Comptes extés: lien pour changer son mot de passe sur la page d'accueil
|
||||||
|
- Les utilisateurices non-COF peuvent éditer leur profil
|
||||||
|
- Un peu de pub pour KDEns sur la page d'accueil
|
||||||
|
- Fix erreur 500 sur /bda/revente/<tirage_id>/manage
|
||||||
|
- Si on essaie d'accéder au compte que qqn d'autre on a une 404 (et plus une 403)
|
||||||
|
- On ne peut plus modifier des comptes COF depuis l'interface K-Fêt
|
||||||
|
- Le champ de paiement BdA se fait au niveau des attributions
|
||||||
|
- Affiche un message d'erreur plutôt que de crasher si échec de l'envoi du mail
|
||||||
|
de bienvenue aux nouveaux membres
|
||||||
|
- On peut supprimer des comptes et des articles K-Fêt
|
||||||
|
- Passage à Django2
|
||||||
|
- Dev : on peut désactiver la barre de debug avec une variable shell
|
||||||
|
- Remplace les CSS de Google par des polices de proximité
|
||||||
|
- Passage du site du COF et de la K-Fêt en Wagtail 2.3 et Wagtail-modeltranslation 0.9
|
||||||
|
- Ajoute un lien vers l'administration générale depuis les petits cours
|
||||||
|
- Abandon de l'ancien catalogue BdA (déjà plus utilisé depuis longtemps)
|
||||||
|
- Force l'unicité des logins clipper
|
||||||
|
- Nouveau site du COF en wagtail
|
||||||
|
- Meilleurs affichage des longues listes de spectacles à cocher dans BdA-Revente
|
||||||
|
- Bugfix : les pages de la revente ne sont plus accessibles qu'aux membres du
|
||||||
|
COF
|
||||||
|
|
||||||
|
## Version 0.2 - 07/11/2018
|
||||||
|
|
||||||
|
- Corrections de bugs d'interface dans l'inscription aux tirages BdA
|
||||||
|
- On peut annuler une revente à tout moment
|
||||||
|
- Pleiiiiin de tests
|
||||||
|
|
||||||
|
## Version 0.1 - 09/09/2018
|
||||||
|
|
||||||
|
Début de la numérotation des versions, début du changelog
|
32
README.md
32
README.md
|
@ -1,4 +1,4 @@
|
||||||
# GestioCOF
|
# GestioCOF / GestioBDS
|
||||||
|
|
||||||
[](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master)
|
[](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master)
|
||||||
[](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master)
|
[](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master)
|
||||||
|
@ -18,7 +18,7 @@ Il vous faudra installer pip, les librairies de développement de python ainsi
|
||||||
que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous
|
que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous
|
||||||
Debian et dérivées (Ubuntu, ...) :
|
Debian et dérivées (Ubuntu, ...) :
|
||||||
|
|
||||||
sudo apt-get install python3-pip python3-dev python3-venv sqlite3
|
sudo apt-get install python3-pip python3-dev python3-venv sqlite3 libsasl2-dev python-dev-is-python3 libldap2-dev libssl-dev
|
||||||
|
|
||||||
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
|
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
|
||||||
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
|
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
|
||||||
|
@ -30,7 +30,15 @@ Pour l'activer, il faut taper
|
||||||
|
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
|
|
||||||
depuis le même dossier.
|
depuis le même dossier. Pour préparer l'environnement à l'utilisation de `./manage.py`
|
||||||
|
(qui permet de faire des tests en local), il faut également taper
|
||||||
|
|
||||||
|
export CREDENTIALS_DIRECTORY=$(realpath .credentials)
|
||||||
|
export DJANGO_SETTINGS_MODULE=gestioasso.settings.local
|
||||||
|
export GESTIOCOF_DEBUG=true
|
||||||
|
export GESTIOCOF_STATIC_ROOT=$(realpath .static)
|
||||||
|
export GESTIOBDS_DEBUG=true
|
||||||
|
export GESTIOBDS_STATIC_ROOT=$(realpath .static)
|
||||||
|
|
||||||
Vous pouvez maintenant installer les dépendances Python depuis le fichier
|
Vous pouvez maintenant installer les dépendances Python depuis le fichier
|
||||||
`requirements-devel.txt` :
|
`requirements-devel.txt` :
|
||||||
|
@ -38,11 +46,11 @@ Vous pouvez maintenant installer les dépendances Python depuis le fichier
|
||||||
pip install -U pip # parfois nécessaire la première fois
|
pip install -U pip # parfois nécessaire la première fois
|
||||||
pip install -r requirements-devel.txt
|
pip install -r requirements-devel.txt
|
||||||
|
|
||||||
Pour terminer, copier le fichier `cof/settings/secret_example.py` vers
|
Pour terminer, copier le fichier `gestioasso/settings/secret_example.py` vers
|
||||||
`cof/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique
|
`gestioasso/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique
|
||||||
pour profiter de façon transparente des mises à jour du fichier:
|
pour profiter de façon transparente des mises à jour du fichier:
|
||||||
|
|
||||||
ln -s secret_example.py cof/settings/secret.py
|
ln -s secret_example.py gestioasso/settings/secret.py
|
||||||
|
|
||||||
Nous avons un git hook de pre-commit pour formatter et vérifier que votre code
|
Nous avons un git hook de pre-commit pour formatter et vérifier que votre code
|
||||||
vérifie nos conventions. Pour bénéficier des mises à jour du hook, préférez
|
vérifie nos conventions. Pour bénéficier des mises à jour du hook, préférez
|
||||||
|
@ -186,6 +194,18 @@ Pour lancer les tests :
|
||||||
python manage.py test
|
python manage.py test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Astuces
|
||||||
|
|
||||||
|
- En développement on utilise la django debug toolbar parce que c'est utile pour
|
||||||
|
débuguer les templates ou les requêtes SQL mais des fois c'est pénible parce
|
||||||
|
ça fait ramer GestioCOF (surtout dans wagtail).
|
||||||
|
Vous pouvez la désactiver temporairement en définissant la variable
|
||||||
|
d'environnement `DJANGO_NO_DDT` dans votre shell : par exemple dans
|
||||||
|
bash/zsh/…:
|
||||||
|
```
|
||||||
|
$ export DJANGO_NO_DDT=1
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Documentation utilisateur
|
## Documentation utilisateur
|
||||||
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
- Changer les urls dans les mails "bda-revente" et "bda-shotgun"
|
|
42
Vagrantfile
vendored
42
Vagrantfile
vendored
|
@ -1,47 +1,19 @@
|
||||||
# -*- mode: ruby -*-
|
# -*- mode: ruby -*-
|
||||||
# vi: set ft=ruby :
|
# vi: set ft=ruby :
|
||||||
|
|
||||||
# All Vagrant configuration is done below. The "2" in Vagrant.configure
|
# Configuration de base pour GestioCOF.
|
||||||
# configures the configuration version (we support older styles for
|
# Voir https://docs.vagrantup.com pour plus d'informations.
|
||||||
# backwards compatibility). Please don't change it unless you know what
|
|
||||||
# you're doing.
|
|
||||||
Vagrant.configure(2) do |config|
|
Vagrant.configure(2) do |config|
|
||||||
# The most common configuration options are documented and commented below.
|
# On se base sur Debian 10 (Buster) pour avoir le même environnement qu'en
|
||||||
# For a complete reference, please see the online documentation at
|
# production.
|
||||||
# https://docs.vagrantup.com.
|
config.vm.box = "debian/contrib-buster64"
|
||||||
|
|
||||||
config.vm.box = "ubuntu/xenial64"
|
|
||||||
|
|
||||||
# On associe le port 80 dans la machine virtuelle avec le port 8080 de notre
|
# On associe le port 80 dans la machine virtuelle avec le port 8080 de notre
|
||||||
# ordinateur, et le port 8000 avec le port 8000.
|
# ordinateur, et le port 8000 avec le port 8000.
|
||||||
config.vm.network :forwarded_port, guest: 80, host: 8080
|
config.vm.network :forwarded_port, guest: 80, host: 8080
|
||||||
config.vm.network :forwarded_port, guest: 8000, host: 8000
|
config.vm.network :forwarded_port, guest: 8000, host: 8000
|
||||||
|
|
||||||
# Create a private network, which allows host-only access to the machine
|
# Le restes de la configuration (installation de paquets, etc) est géré un
|
||||||
# using a specific IP.
|
# script shell.
|
||||||
# config.vm.network "private_network", ip: "192.168.33.10"
|
|
||||||
|
|
||||||
# Provider-specific configuration so you can fine-tune various
|
|
||||||
# backing providers for Vagrant. These expose provider-specific options.
|
|
||||||
# Example for VirtualBox:
|
|
||||||
#
|
|
||||||
# config.vm.provider "virtualbox" do |vb|
|
|
||||||
# # Display the VirtualBox GUI when booting the machine
|
|
||||||
# vb.gui = true
|
|
||||||
#
|
|
||||||
# # Customize the amount of memory on the VM:
|
|
||||||
# vb.memory = "1024"
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# View the documentation for the provider you are using for more
|
|
||||||
# information on available options.
|
|
||||||
|
|
||||||
# Enable provisioning with a shell script. Additional provisioners such as
|
|
||||||
# Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
|
|
||||||
# documentation for more information about their specific syntax and use.
|
|
||||||
# config.vm.provision "shell", inline: <<-SHELL
|
|
||||||
# sudo apt-get update
|
|
||||||
# sudo apt-get install -y apache2
|
|
||||||
# SHELL
|
|
||||||
config.vm.provision :shell, path: "provisioning/bootstrap.sh"
|
config.vm.provision :shell, path: "provisioning/bootstrap.sh"
|
||||||
end
|
end
|
||||||
|
|
138
bda/admin.py
138
bda/admin.py
|
@ -1,10 +1,11 @@
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from custommail.shortcuts import send_mass_custom_mail
|
|
||||||
from dal.autocomplete import ModelSelect2
|
from dal.autocomplete import ModelSelect2
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.db.models import Count, Sum
|
from django.core.mail import send_mass_mail
|
||||||
|
from django.db.models import Count, Q, Sum
|
||||||
|
from django.template import loader
|
||||||
from django.template.defaultfilters import pluralize
|
from django.template.defaultfilters import pluralize
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
@ -32,20 +33,6 @@ class ReadOnlyMixin(object):
|
||||||
return readonly_fields + self.readonly_fields_update
|
return readonly_fields + self.readonly_fields_update
|
||||||
|
|
||||||
|
|
||||||
class ChoixSpectacleAdminForm(forms.ModelForm):
|
|
||||||
class Meta:
|
|
||||||
widgets = {
|
|
||||||
"participant": ModelSelect2(url="bda-participant-autocomplete"),
|
|
||||||
"spectacle": ModelSelect2(url="bda-spectacle-autocomplete"),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ChoixSpectacleInline(admin.TabularInline):
|
|
||||||
model = ChoixSpectacle
|
|
||||||
form = ChoixSpectacleAdminForm
|
|
||||||
sortable_field_name = "priority"
|
|
||||||
|
|
||||||
|
|
||||||
class AttributionTabularAdminForm(forms.ModelForm):
|
class AttributionTabularAdminForm(forms.ModelForm):
|
||||||
listing = None
|
listing = None
|
||||||
|
|
||||||
|
@ -81,27 +68,51 @@ class WithListingAttributionInline(AttributionInline):
|
||||||
exclude = ("given",)
|
exclude = ("given",)
|
||||||
form = WithListingAttributionTabularAdminForm
|
form = WithListingAttributionTabularAdminForm
|
||||||
listing = True
|
listing = True
|
||||||
|
verbose_name_plural = "Attributions sur listing"
|
||||||
|
|
||||||
|
|
||||||
class WithoutListingAttributionInline(AttributionInline):
|
class WithoutListingAttributionInline(AttributionInline):
|
||||||
form = WithoutListingAttributionTabularAdminForm
|
form = WithoutListingAttributionTabularAdminForm
|
||||||
listing = False
|
listing = False
|
||||||
|
verbose_name_plural = "Attributions hors listing"
|
||||||
|
|
||||||
|
|
||||||
class ParticipantAdminForm(forms.ModelForm):
|
class ParticipantAdminForm(forms.ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields["choicesrevente"].queryset = Spectacle.objects.select_related(
|
queryset = Spectacle.objects.select_related("location")
|
||||||
"location"
|
|
||||||
)
|
if self.instance.pk is not None:
|
||||||
|
queryset = queryset.filter(tirage=self.instance.tirage)
|
||||||
|
|
||||||
|
self.fields["choicesrevente"].queryset = queryset
|
||||||
|
|
||||||
|
|
||||||
|
class ParticipantPaidFilter(admin.SimpleListFilter):
|
||||||
|
"""
|
||||||
|
Permet de filtrer les participants sur s'ils ont payé leurs places ou pas
|
||||||
|
"""
|
||||||
|
|
||||||
|
title = "A payé"
|
||||||
|
parameter_name = "paid"
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
return ((True, "Oui"), (False, "Non"))
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
return queryset.filter(paid=self.value())
|
||||||
|
|
||||||
|
|
||||||
class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
||||||
inlines = [WithListingAttributionInline, WithoutListingAttributionInline]
|
inlines = [WithListingAttributionInline, WithoutListingAttributionInline]
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
return Participant.objects.annotate(
|
return self.model.objects.annotate_paid().annotate(
|
||||||
nb_places=Count("attributions"), total=Sum("attributions__price")
|
nb_places=Count("attributions"),
|
||||||
|
remain=Sum(
|
||||||
|
"attribution__spectacle__price", filter=Q(attribution__paid=False)
|
||||||
|
),
|
||||||
|
total=Sum("attributions__price"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def nb_places(self, obj):
|
def nb_places(self, obj):
|
||||||
|
@ -110,6 +121,13 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
||||||
nb_places.admin_order_field = "nb_places"
|
nb_places.admin_order_field = "nb_places"
|
||||||
nb_places.short_description = "Nombre de places"
|
nb_places.short_description = "Nombre de places"
|
||||||
|
|
||||||
|
def paid(self, obj):
|
||||||
|
return obj.paid
|
||||||
|
|
||||||
|
paid.short_description = "A payé"
|
||||||
|
paid.boolean = True
|
||||||
|
paid.admin_order_field = "paid"
|
||||||
|
|
||||||
def total(self, obj):
|
def total(self, obj):
|
||||||
tot = obj.total
|
tot = obj.total
|
||||||
if tot:
|
if tot:
|
||||||
|
@ -118,31 +136,46 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
||||||
return "0 €"
|
return "0 €"
|
||||||
|
|
||||||
total.admin_order_field = "total"
|
total.admin_order_field = "total"
|
||||||
total.short_description = "Total à payer"
|
total.short_description = "Total des places"
|
||||||
list_display = ("user", "nb_places", "total", "paid", "paymenttype", "tirage")
|
|
||||||
list_filter = ("paid", "tirage")
|
def remain(self, obj):
|
||||||
|
rem = obj.remain
|
||||||
|
if rem:
|
||||||
|
return "%.02f €" % rem
|
||||||
|
else:
|
||||||
|
return "0 €"
|
||||||
|
|
||||||
|
remain.admin_order_field = "remain"
|
||||||
|
remain.short_description = "Reste à payer"
|
||||||
|
|
||||||
|
list_display = ("user", "nb_places", "total", "paid", "remain", "tirage")
|
||||||
|
list_filter = (ParticipantPaidFilter, "tirage")
|
||||||
search_fields = ("user__username", "user__first_name", "user__last_name")
|
search_fields = ("user__username", "user__first_name", "user__last_name")
|
||||||
actions = ["send_attribs"]
|
actions = ["send_attribs"]
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
list_per_page = 400
|
list_per_page = 400
|
||||||
readonly_fields = ("total",)
|
readonly_fields = ("total", "paid")
|
||||||
readonly_fields_update = ("user", "tirage")
|
readonly_fields_update = ("user", "tirage")
|
||||||
form = ParticipantAdminForm
|
form = ParticipantAdminForm
|
||||||
|
|
||||||
def send_attribs(self, request, queryset):
|
def send_attribs(self, request, queryset):
|
||||||
datatuple = []
|
emails = []
|
||||||
for member in queryset.all():
|
for member in queryset.all():
|
||||||
|
subject = "Résultats du tirage au sort"
|
||||||
attribs = member.attributions.all()
|
attribs = member.attributions.all()
|
||||||
context = {"member": member.user}
|
context = {"member": member.user}
|
||||||
shortname = ""
|
|
||||||
|
template_name = ""
|
||||||
if len(attribs) == 0:
|
if len(attribs) == 0:
|
||||||
shortname = "bda-attributions-decus"
|
template_name = "bda/mails/attributions-decus.txt"
|
||||||
else:
|
else:
|
||||||
shortname = "bda-attributions"
|
template_name = "bda/mails/attributions.txt"
|
||||||
context["places"] = attribs
|
context["places"] = attribs
|
||||||
print(context)
|
|
||||||
datatuple.append((shortname, context, "bda@ens.fr", [member.user.email]))
|
message = loader.render_to_string(template_name, context)
|
||||||
send_mass_custom_mail(datatuple)
|
emails.append((subject, message, "bda@ens.fr", [member.user.email]))
|
||||||
|
|
||||||
|
send_mass_mail(emails)
|
||||||
count = len(queryset.all())
|
count = len(queryset.all())
|
||||||
if count == 1:
|
if count == 1:
|
||||||
message_bit = "1 membre a"
|
message_bit = "1 membre a"
|
||||||
|
@ -158,17 +191,6 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
||||||
|
|
||||||
|
|
||||||
class AttributionAdminForm(forms.ModelForm):
|
class AttributionAdminForm(forms.ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
if "spectacle" in self.fields:
|
|
||||||
self.fields["spectacle"].queryset = Spectacle.objects.select_related(
|
|
||||||
"location"
|
|
||||||
)
|
|
||||||
if "participant" in self.fields:
|
|
||||||
self.fields["participant"].queryset = Participant.objects.select_related(
|
|
||||||
"user", "tirage"
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
participant = cleaned_data.get("participant")
|
participant = cleaned_data.get("participant")
|
||||||
|
@ -181,13 +203,14 @@ class AttributionAdminForm(forms.ModelForm):
|
||||||
)
|
)
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
widgets = {
|
||||||
|
"participant": ModelSelect2(url="bda-participant-autocomplete"),
|
||||||
|
"spectacle": ModelSelect2(url="bda-spectacle-autocomplete"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
||||||
def paid(self, obj):
|
|
||||||
return obj.participant.paid
|
|
||||||
|
|
||||||
paid.short_description = "A payé"
|
|
||||||
paid.boolean = True
|
|
||||||
list_display = ("id", "spectacle", "participant", "given", "paid")
|
list_display = ("id", "spectacle", "participant", "given", "paid")
|
||||||
search_fields = (
|
search_fields = (
|
||||||
"spectacle__title",
|
"spectacle__title",
|
||||||
|
@ -200,7 +223,7 @@ class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
||||||
|
|
||||||
|
|
||||||
class ChoixSpectacleAdmin(admin.ModelAdmin):
|
class ChoixSpectacleAdmin(admin.ModelAdmin):
|
||||||
form = ChoixSpectacleAdminForm
|
autocomplete_fields = ["participant", "spectacle"]
|
||||||
|
|
||||||
def tirage(self, obj):
|
def tirage(self, obj):
|
||||||
return obj.participant.tirage
|
return obj.participant.tirage
|
||||||
|
@ -244,15 +267,14 @@ class SalleAdmin(admin.ModelAdmin):
|
||||||
class SpectacleReventeAdminForm(forms.ModelForm):
|
class SpectacleReventeAdminForm(forms.ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields["confirmed_entry"].queryset = Participant.objects.select_related(
|
qset = Participant.objects.select_related("user", "tirage")
|
||||||
"user", "tirage"
|
|
||||||
)
|
if self.instance.pk is not None:
|
||||||
self.fields["seller"].queryset = Participant.objects.select_related(
|
qset = qset.filter(tirage=self.instance.seller.tirage)
|
||||||
"user", "tirage"
|
|
||||||
)
|
self.fields["confirmed_entry"].queryset = qset
|
||||||
self.fields["soldTo"].queryset = Participant.objects.select_related(
|
self.fields["seller"].queryset = qset
|
||||||
"user", "tirage"
|
self.fields["soldTo"].queryset = qset
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SpectacleReventeAdmin(admin.ModelAdmin):
|
class SpectacleReventeAdmin(admin.ModelAdmin):
|
||||||
|
|
|
@ -2,7 +2,6 @@ import random
|
||||||
|
|
||||||
|
|
||||||
class Algorithm(object):
|
class Algorithm(object):
|
||||||
|
|
||||||
shows = None
|
shows = None
|
||||||
ranks = None
|
ranks = None
|
||||||
origranks = None
|
origranks = None
|
||||||
|
|
211
bda/forms.py
211
bda/forms.py
|
@ -1,8 +1,9 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms.models import BaseInlineFormSet
|
from django.forms.models import BaseInlineFormSet
|
||||||
|
from django.template import loader
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from bda.models import Attribution, Spectacle, SpectacleRevente
|
from bda.models import SpectacleRevente
|
||||||
|
|
||||||
|
|
||||||
class InscriptionInlineFormSet(BaseInlineFormSet):
|
class InscriptionInlineFormSet(BaseInlineFormSet):
|
||||||
|
@ -38,142 +39,146 @@ class TokenForm(forms.Form):
|
||||||
token = forms.CharField(widget=forms.widgets.Textarea())
|
token = forms.CharField(widget=forms.widgets.Textarea())
|
||||||
|
|
||||||
|
|
||||||
class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
class TemplateLabelField(forms.ModelMultipleChoiceField):
|
||||||
def label_from_instance(self, obj):
|
"""
|
||||||
return str(obj.spectacle)
|
Extends ModelMultipleChoiceField to offer two more customization options :
|
||||||
|
- `label_from_instance` can be used with a template file
|
||||||
|
- the widget rendering template can be specified with `option_template_name`
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
class ReventeModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
self,
|
||||||
def __init__(self, *args, own=True, **kwargs):
|
label_template_name=None,
|
||||||
|
context_object_name="obj",
|
||||||
|
option_template_name=None,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.own = own
|
self.label_template_name = label_template_name
|
||||||
|
self.context_object_name = context_object_name
|
||||||
|
if option_template_name is not None:
|
||||||
|
self.widget.option_template_name = option_template_name
|
||||||
|
|
||||||
def label_from_instance(self, obj):
|
def label_from_instance(self, obj):
|
||||||
label = "{show}{suffix}"
|
if self.label_template_name is None:
|
||||||
suffix = ""
|
return super().label_from_instance(obj)
|
||||||
if self.own:
|
|
||||||
# C'est notre propre revente : informations sur le statut
|
|
||||||
if obj.soldTo is not None:
|
|
||||||
suffix = " -- Vendue à {firstname} {lastname}".format(
|
|
||||||
firstname=obj.soldTo.user.first_name,
|
|
||||||
lastname=obj.soldTo.user.last_name,
|
|
||||||
)
|
|
||||||
elif obj.shotgun:
|
|
||||||
suffix = " -- Tirage infructueux"
|
|
||||||
elif obj.notif_sent:
|
|
||||||
suffix = " -- Inscriptions au tirage en cours"
|
|
||||||
else:
|
else:
|
||||||
# Ce n'est pas à nous : on ne voit jamais l'acheteur
|
return loader.render_to_string(
|
||||||
suffix = " -- Vendue par {firstname} {lastname}".format(
|
self.label_template_name, context={self.context_object_name: obj}
|
||||||
firstname=obj.seller.user.first_name, lastname=obj.seller.user.last_name
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return label.format(show=str(obj.attribution.spectacle), suffix=suffix)
|
|
||||||
|
# Formulaires pour revente_manage
|
||||||
|
|
||||||
|
|
||||||
class ResellForm(forms.Form):
|
class ResellForm(forms.Form):
|
||||||
attributions = AttributionModelMultipleChoiceField(
|
|
||||||
label="",
|
|
||||||
queryset=Attribution.objects.none(),
|
|
||||||
widget=forms.CheckboxSelectMultiple,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, participant, *args, **kwargs):
|
def __init__(self, participant, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields["attributions"].queryset = (
|
self.fields["attributions"] = TemplateLabelField(
|
||||||
participant.attribution_set.filter(spectacle__date__gte=timezone.now())
|
queryset=participant.attribution_set.filter(
|
||||||
|
spectacle__date__gte=timezone.now(), paid=True
|
||||||
|
)
|
||||||
.exclude(revente__seller=participant)
|
.exclude(revente__seller=participant)
|
||||||
.select_related("spectacle", "spectacle__location", "participant__user")
|
.select_related("spectacle", "spectacle__location", "participant__user"),
|
||||||
|
widget=forms.CheckboxSelectMultiple,
|
||||||
|
required=False,
|
||||||
|
label_template_name="bda/forms/attribution_label_table.html",
|
||||||
|
option_template_name="bda/forms/checkbox_table.html",
|
||||||
|
context_object_name="attribution",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AnnulForm(forms.Form):
|
class AnnulForm(forms.Form):
|
||||||
reventes = ReventeModelMultipleChoiceField(
|
|
||||||
own=True,
|
|
||||||
label="",
|
|
||||||
queryset=Attribution.objects.none(),
|
|
||||||
widget=forms.CheckboxSelectMultiple,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, participant, *args, **kwargs):
|
def __init__(self, participant, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields["reventes"].queryset = (
|
self.fields["reventes"] = TemplateLabelField(
|
||||||
participant.original_shows.filter(
|
label="",
|
||||||
|
queryset=participant.original_shows.filter(
|
||||||
attribution__spectacle__date__gte=timezone.now(), soldTo__isnull=True
|
attribution__spectacle__date__gte=timezone.now(), soldTo__isnull=True
|
||||||
)
|
)
|
||||||
.select_related(
|
.select_related(
|
||||||
"attribution__spectacle", "attribution__spectacle__location"
|
"attribution__spectacle", "attribution__spectacle__location"
|
||||||
)
|
)
|
||||||
.order_by("-date")
|
.order_by("-date"),
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class InscriptionReventeForm(forms.Form):
|
|
||||||
spectacles = forms.ModelMultipleChoiceField(
|
|
||||||
queryset=Spectacle.objects.none(),
|
|
||||||
widget=forms.CheckboxSelectMultiple,
|
widget=forms.CheckboxSelectMultiple,
|
||||||
required=False,
|
required=False,
|
||||||
)
|
label_template_name="bda/forms/revente_self_label_table.html",
|
||||||
|
option_template_name="bda/forms/checkbox_table.html",
|
||||||
def __init__(self, tirage, *args, **kwargs):
|
context_object_name="revente",
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields["spectacles"].queryset = tirage.spectacle_set.select_related(
|
|
||||||
"location"
|
|
||||||
).filter(date__gte=timezone.now())
|
|
||||||
|
|
||||||
|
|
||||||
class ReventeTirageAnnulForm(forms.Form):
|
|
||||||
reventes = ReventeModelMultipleChoiceField(
|
|
||||||
own=False,
|
|
||||||
label="",
|
|
||||||
queryset=SpectacleRevente.objects.none(),
|
|
||||||
widget=forms.CheckboxSelectMultiple,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, participant, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields["reventes"].queryset = participant.entered.filter(
|
|
||||||
soldTo__isnull=True
|
|
||||||
).select_related("attribution__spectacle", "seller__user")
|
|
||||||
|
|
||||||
|
|
||||||
class ReventeTirageForm(forms.Form):
|
|
||||||
reventes = ReventeModelMultipleChoiceField(
|
|
||||||
own=False,
|
|
||||||
label="",
|
|
||||||
queryset=SpectacleRevente.objects.none(),
|
|
||||||
widget=forms.CheckboxSelectMultiple,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, participant, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields["reventes"].queryset = (
|
|
||||||
SpectacleRevente.objects.filter(
|
|
||||||
notif_sent=True, shotgun=False, tirage_done=False
|
|
||||||
)
|
|
||||||
.exclude(confirmed_entry=participant)
|
|
||||||
.select_related("attribution__spectacle")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SoldForm(forms.Form):
|
class SoldForm(forms.Form):
|
||||||
reventes = ReventeModelMultipleChoiceField(
|
|
||||||
own=True,
|
|
||||||
label="",
|
|
||||||
queryset=Attribution.objects.none(),
|
|
||||||
widget=forms.CheckboxSelectMultiple,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, participant, *args, **kwargs):
|
def __init__(self, participant, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields["reventes"].queryset = (
|
self.fields["reventes"] = TemplateLabelField(
|
||||||
participant.original_shows.filter(soldTo__isnull=False)
|
queryset=participant.original_shows.filter(soldTo__isnull=False)
|
||||||
.exclude(soldTo=participant)
|
.exclude(soldTo=participant)
|
||||||
.select_related(
|
.select_related(
|
||||||
"attribution__spectacle", "attribution__spectacle__location"
|
"attribution__spectacle", "attribution__spectacle__location"
|
||||||
|
),
|
||||||
|
widget=forms.CheckboxSelectMultiple,
|
||||||
|
label_template_name="bda/forms/revente_sold_label_table.html",
|
||||||
|
option_template_name="bda/forms/checkbox_table.html",
|
||||||
|
context_object_name="revente",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Formulaire pour revente_subscribe
|
||||||
|
|
||||||
|
|
||||||
|
class InscriptionReventeForm(forms.Form):
|
||||||
|
def __init__(self, tirage, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields["spectacles"] = TemplateLabelField(
|
||||||
|
queryset=tirage.spectacle_set.select_related("location").filter(
|
||||||
|
date__gte=timezone.now()
|
||||||
|
),
|
||||||
|
widget=forms.CheckboxSelectMultiple,
|
||||||
|
required=False,
|
||||||
|
label_template_name="bda/forms/spectacle_label_table.html",
|
||||||
|
option_template_name="bda/forms/checkbox_table.html",
|
||||||
|
context_object_name="spectacle",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Formulaires pour revente_tirages
|
||||||
|
|
||||||
|
|
||||||
|
class ReventeTirageAnnulForm(forms.Form):
|
||||||
|
def __init__(self, participant, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields["reventes"] = TemplateLabelField(
|
||||||
|
queryset=participant.entered.filter(soldTo__isnull=True).select_related(
|
||||||
|
"attribution__spectacle", "seller__user"
|
||||||
|
),
|
||||||
|
widget=forms.CheckboxSelectMultiple,
|
||||||
|
required=False,
|
||||||
|
label_template_name="bda/forms/revente_other_label_table.html",
|
||||||
|
option_template_name="bda/forms/checkbox_table.html",
|
||||||
|
context_object_name="revente",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReventeTirageForm(forms.Form):
|
||||||
|
def __init__(self, participant, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields["reventes"] = TemplateLabelField(
|
||||||
|
queryset=(
|
||||||
|
SpectacleRevente.objects.filter(
|
||||||
|
notif_sent=True,
|
||||||
|
shotgun=False,
|
||||||
|
tirage_done=False,
|
||||||
|
attribution__spectacle__tirage=participant.tirage,
|
||||||
|
)
|
||||||
|
.exclude(confirmed_entry=participant)
|
||||||
|
.select_related("attribution__spectacle")
|
||||||
|
),
|
||||||
|
widget=forms.CheckboxSelectMultiple,
|
||||||
|
required=False,
|
||||||
|
label_template_name="bda/forms/revente_other_label_table.html",
|
||||||
|
option_template_name="bda/forms/checkbox_table.html",
|
||||||
|
context_object_name="revente",
|
||||||
)
|
)
|
||||||
|
|
|
@ -81,7 +81,7 @@ class Command(MyBaseCommand):
|
||||||
shows = random.sample(
|
shows = random.sample(
|
||||||
list(tirage.spectacle_set.all()), tirage.spectacle_set.count() // 2
|
list(tirage.spectacle_set.all()), tirage.spectacle_set.count() // 2
|
||||||
)
|
)
|
||||||
for (rank, show) in enumerate(shows):
|
for rank, show in enumerate(shows):
|
||||||
choices.append(
|
choices.append(
|
||||||
ChoixSpectacle(
|
ChoixSpectacle(
|
||||||
participant=part,
|
participant=part,
|
||||||
|
|
|
@ -6,7 +6,6 @@ from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
|
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
|
@ -35,7 +35,6 @@ def fill_tirage_fields(apps, schema_editor):
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("bda", "0001_initial")]
|
dependencies = [("bda", "0001_initial")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
|
@ -5,7 +5,6 @@ from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("bda", "0002_add_tirage")]
|
dependencies = [("bda", "0002_add_tirage")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
|
@ -5,7 +5,6 @@ from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("bda", "0003_update_tirage_and_spectacle")]
|
dependencies = [("bda", "0003_update_tirage_and_spectacle")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
|
@ -5,7 +5,6 @@ from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("bda", "0004_mails-rappel")]
|
dependencies = [("bda", "0004_mails-rappel")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
|
@ -18,7 +18,6 @@ def forwards_func(apps, schema_editor):
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("bda", "0005_encoding")]
|
dependencies = [("bda", "0005_encoding")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
|
@ -5,7 +5,6 @@ from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("bda", "0006_add_tirage_switch")]
|
dependencies = [("bda", "0006_add_tirage_switch")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
|
@ -5,7 +5,6 @@ from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("bda", "0007_extends_spectacle")]
|
dependencies = [("bda", "0007_extends_spectacle")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
|
@ -6,7 +6,6 @@ from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("bda", "0008_py3")]
|
dependencies = [("bda", "0008_py3")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
|
@ -12,15 +12,15 @@ def forwards_func(apps, schema_editor):
|
||||||
|
|
||||||
for revente in SpectacleRevente.objects.all():
|
for revente in SpectacleRevente.objects.all():
|
||||||
is_expired = timezone.now() > revente.date_tirage()
|
is_expired = timezone.now() > revente.date_tirage()
|
||||||
is_direct = revente.attribution.spectacle.date >= revente.date and timezone.now() > revente.date + timedelta(
|
is_direct = (
|
||||||
minutes=15
|
revente.attribution.spectacle.date >= revente.date
|
||||||
|
and timezone.now() > revente.date + timedelta(minutes=15)
|
||||||
)
|
)
|
||||||
revente.shotgun = is_expired or is_direct
|
revente.shotgun = is_expired or is_direct
|
||||||
revente.save()
|
revente.save()
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("bda", "0009_revente")]
|
dependencies = [("bda", "0009_revente")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
|
@ -5,7 +5,6 @@ from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("bda", "0010_spectaclerevente_shotgun")]
|
dependencies = [("bda", "0010_spectaclerevente_shotgun")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
|
@ -5,7 +5,6 @@ from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("bda", "0011_tirage_appear_catalogue")]
|
dependencies = [("bda", "0011_tirage_appear_catalogue")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
|
@ -13,7 +13,6 @@ def swap_double_choice(apps, schema_editor):
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("bda", "0011_tirage_appear_catalogue")]
|
dependencies = [("bda", "0011_tirage_appear_catalogue")]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
|
@ -6,7 +6,6 @@ from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [("bda", "0012_notif_time"), ("bda", "0012_swap_double_choice")]
|
dependencies = [("bda", "0012_notif_time"), ("bda", "0012_swap_double_choice")]
|
||||||
|
|
||||||
operations = []
|
operations = []
|
||||||
|
|
30
bda/migrations/0014_attribution_paid_field.py
Normal file
30
bda/migrations/0014_attribution_paid_field.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 2.2 on 2019-06-03 19:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [("bda", "0013_merge_20180524_2123")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="attribution",
|
||||||
|
name="paid",
|
||||||
|
field=models.BooleanField(default=False, verbose_name="Payée"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="attribution",
|
||||||
|
name="paymenttype",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("cash", "Cash"),
|
||||||
|
("cb", "CB"),
|
||||||
|
("cheque", "Chèque"),
|
||||||
|
("autre", "Autre"),
|
||||||
|
],
|
||||||
|
max_length=6,
|
||||||
|
verbose_name="Moyen de paiement",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
36
bda/migrations/0015_move_bda_payment.py
Normal file
36
bda/migrations/0015_move_bda_payment.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# Generated by Django 2.2 on 2019-06-03 19:30
|
||||||
|
|
||||||
|
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def set_attr_payment(apps, schema_editor):
|
||||||
|
Attribution = apps.get_model("bda", "Attribution")
|
||||||
|
for attr in Attribution.objects.all():
|
||||||
|
attr.paid = attr.participant.paid
|
||||||
|
attr.paymenttype = attr.participant.paymenttype
|
||||||
|
attr.save()
|
||||||
|
|
||||||
|
|
||||||
|
def set_participant_payment(apps, schema_editor):
|
||||||
|
Participant = apps.get_model("bda", "Participant")
|
||||||
|
for part in Participant.objects.all():
|
||||||
|
attr_set = part.attribution_set
|
||||||
|
part.paid = attr_set.exists() and not attr_set.filter(paid=False).exists()
|
||||||
|
try:
|
||||||
|
# S'il n'y a qu'un seul type de paiement, on le set
|
||||||
|
part.paymenttype = (
|
||||||
|
attr_set.values_list("paymenttype", flat=True).distinct().get()
|
||||||
|
)
|
||||||
|
# Sinon, whatever
|
||||||
|
except (ObjectDoesNotExist, MultipleObjectsReturned):
|
||||||
|
pass
|
||||||
|
part.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [("bda", "0014_attribution_paid_field")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(set_attr_payment, set_participant_payment, atomic=True)
|
||||||
|
]
|
12
bda/migrations/0016_delete_participant_paid.py
Normal file
12
bda/migrations/0016_delete_participant_paid.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# Generated by Django 2.2 on 2019-06-03 19:30
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [("bda", "0015_move_bda_payment")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(model_name="participant", name="paid"),
|
||||||
|
migrations.RemoveField(model_name="participant", name="paymenttype"),
|
||||||
|
]
|
17
bda/migrations/0017_participant_accepte_charte.py
Normal file
17
bda/migrations/0017_participant_accepte_charte.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 2.2 on 2019-09-18 16:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [("bda", "0016_delete_participant_paid")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="participant",
|
||||||
|
name="accepte_charte",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False, verbose_name="A accepté la charte BdA"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
37
bda/migrations/0018_auto_20201021_1818.py
Normal file
37
bda/migrations/0018_auto_20201021_1818.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# Generated by Django 2.2.12 on 2020-10-21 16:18
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("bda", "0017_participant_accepte_charte"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="participant",
|
||||||
|
options={"ordering": ("-tirage", "user__last_name", "user__first_name")},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="tirage",
|
||||||
|
name="archived",
|
||||||
|
field=models.BooleanField(default=False, verbose_name="Archivé"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="participant",
|
||||||
|
name="tirage",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
limit_choices_to={"archived": False},
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="bda.Tirage",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="participant",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("tirage", "user"), name="unique_tirage"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
23
bda/migrations/0019_auto_20220630_1245.py
Normal file
23
bda/migrations/0019_auto_20220630_1245.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 3.2.13 on 2022-06-30 10:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bda", "0018_auto_20201021_1818"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="choixspectacle",
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="choixspectacle",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("participant", "spectacle"), name="unique_participation"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
18
bda/migrations/0019_auto_20240707_1359.py
Normal file
18
bda/migrations/0019_auto_20240707_1359.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.28 on 2024-07-07 11:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bda', '0018_auto_20201021_1818'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='attribution',
|
||||||
|
name='paymenttype',
|
||||||
|
field=models.CharField(blank=True, choices=[('cash', 'Cash'), ('cb', 'CB'), ('cheque', 'Chèque'), ('virement', 'Virement'), ('autre', 'Autre')], max_length=8, verbose_name='Moyen de paiement'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Generated by Django 4.2.16 on 2025-02-26 08:23
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bda", "0019_auto_20220630_1245"),
|
||||||
|
("bda", "0019_auto_20240707_1359"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = []
|
151
bda/models.py
151
bda/models.py
|
@ -2,14 +2,14 @@ import calendar
|
||||||
import random
|
import random
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from custommail.models import CustomMail
|
|
||||||
from custommail.shortcuts import send_mass_custom_mail
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
|
from django.core.mail import EmailMessage, send_mass_mail
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Count
|
from django.db.models import Count, Exists
|
||||||
|
from django.template import loader
|
||||||
from django.utils import formats, timezone
|
from django.utils import formats, timezone
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ class Tirage(models.Model):
|
||||||
"Tirage à afficher dans le catalogue", default=False
|
"Tirage à afficher dans le catalogue", default=False
|
||||||
)
|
)
|
||||||
enable_do_tirage = models.BooleanField("Le tirage peut être lancé", default=False)
|
enable_do_tirage = models.BooleanField("Le tirage peut être lancé", default=False)
|
||||||
|
archived = models.BooleanField("Archivé", default=False)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s - %s" % (
|
return "%s - %s" % (
|
||||||
|
@ -116,16 +117,19 @@ class Spectacle(models.Model):
|
||||||
bda_generic.nb_attr = 1
|
bda_generic.nb_attr = 1
|
||||||
members.append(bda_generic)
|
members.append(bda_generic)
|
||||||
# On écrit un mail personnalisé à chaque participant
|
# On écrit un mail personnalisé à chaque participant
|
||||||
datatuple = [
|
mails = [
|
||||||
(
|
(
|
||||||
"bda-rappel",
|
str(self),
|
||||||
{"member": member, "nb_attr": member.nb_attr, "show": self},
|
loader.render_to_string(
|
||||||
|
"bda/mails/rappel.txt",
|
||||||
|
context={"member": member, "nb_attr": member.nb_attr, "show": self},
|
||||||
|
),
|
||||||
settings.MAIL_DATA["rappels"]["FROM"],
|
settings.MAIL_DATA["rappels"]["FROM"],
|
||||||
[member.email],
|
[member.email],
|
||||||
)
|
)
|
||||||
for member in members
|
for member in members
|
||||||
]
|
]
|
||||||
send_mass_custom_mail(datatuple)
|
send_mass_mail(mails)
|
||||||
# On enregistre le fait que l'envoi a bien eu lieu
|
# On enregistre le fait que l'envoi a bien eu lieu
|
||||||
self.rappel_sent = timezone.now()
|
self.rappel_sent = timezone.now()
|
||||||
self.save()
|
self.save()
|
||||||
|
@ -147,10 +151,46 @@ PAYMENT_TYPES = (
|
||||||
("cash", "Cash"),
|
("cash", "Cash"),
|
||||||
("cb", "CB"),
|
("cb", "CB"),
|
||||||
("cheque", "Chèque"),
|
("cheque", "Chèque"),
|
||||||
|
("virement", "Virement"),
|
||||||
("autre", "Autre"),
|
("autre", "Autre"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Attribution(models.Model):
|
||||||
|
participant = models.ForeignKey("Participant", on_delete=models.CASCADE)
|
||||||
|
spectacle = models.ForeignKey(
|
||||||
|
Spectacle, on_delete=models.CASCADE, related_name="attribues"
|
||||||
|
)
|
||||||
|
given = models.BooleanField("Donnée", default=False)
|
||||||
|
paid = models.BooleanField("Payée", default=False)
|
||||||
|
paymenttype = models.CharField(
|
||||||
|
"Moyen de paiement", max_length=8, choices=PAYMENT_TYPES, blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s -- %s, %s" % (
|
||||||
|
self.participant.user,
|
||||||
|
self.spectacle.title,
|
||||||
|
self.spectacle.date,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ParticipantPaidQueryset(models.QuerySet):
|
||||||
|
"""
|
||||||
|
Un manager qui annote le queryset avec un champ `paid`,
|
||||||
|
indiquant si un participant a payé toutes ses attributions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def annotate_paid(self):
|
||||||
|
# OuterRef permet de se référer à un champ d'un modèle non encore fixé
|
||||||
|
# Voir:
|
||||||
|
# https://docs.djangoproject.com/en/2.2/ref/models/expressions/#django.db.models.OuterRef
|
||||||
|
unpaid = Attribution.objects.filter(
|
||||||
|
participant=models.OuterRef("pk"), paid=False
|
||||||
|
)
|
||||||
|
return self.annotate(paid=~Exists(unpaid))
|
||||||
|
|
||||||
|
|
||||||
class Participant(models.Model):
|
class Participant(models.Model):
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
choices = models.ManyToManyField(
|
choices = models.ManyToManyField(
|
||||||
|
@ -159,18 +199,25 @@ class Participant(models.Model):
|
||||||
attributions = models.ManyToManyField(
|
attributions = models.ManyToManyField(
|
||||||
Spectacle, through="Attribution", related_name="attributed_to"
|
Spectacle, through="Attribution", related_name="attributed_to"
|
||||||
)
|
)
|
||||||
paid = models.BooleanField("A payé", default=False)
|
tirage = models.ForeignKey(
|
||||||
paymenttype = models.CharField(
|
Tirage, on_delete=models.CASCADE, limit_choices_to={"archived": False}
|
||||||
"Moyen de paiement", max_length=6, choices=PAYMENT_TYPES, blank=True
|
|
||||||
)
|
)
|
||||||
tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE)
|
accepte_charte = models.BooleanField("A accepté la charte BdA", default=False)
|
||||||
choicesrevente = models.ManyToManyField(
|
choicesrevente = models.ManyToManyField(
|
||||||
Spectacle, related_name="subscribed", blank=True
|
Spectacle, related_name="subscribed", blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
objects = ParticipantPaidQueryset.as_manager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s - %s" % (self.user, self.tirage.title)
|
return "%s - %s" % (self.user, self.tirage.title)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ("-tirage", "user__last_name", "user__first_name")
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(fields=("tirage", "user"), name="unique_tirage"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
DOUBLE_CHOICES = (
|
DOUBLE_CHOICES = (
|
||||||
("1", "1 place"),
|
("1", "1 place"),
|
||||||
|
@ -207,26 +254,15 @@ class ChoixSpectacle(models.Model):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("priority",)
|
ordering = ("priority",)
|
||||||
unique_together = (("participant", "spectacle"),)
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["participant", "spectacle"], name="unique_participation"
|
||||||
|
)
|
||||||
|
]
|
||||||
verbose_name = "voeu"
|
verbose_name = "voeu"
|
||||||
verbose_name_plural = "voeux"
|
verbose_name_plural = "voeux"
|
||||||
|
|
||||||
|
|
||||||
class Attribution(models.Model):
|
|
||||||
participant = models.ForeignKey(Participant, on_delete=models.CASCADE)
|
|
||||||
spectacle = models.ForeignKey(
|
|
||||||
Spectacle, on_delete=models.CASCADE, related_name="attribues"
|
|
||||||
)
|
|
||||||
given = models.BooleanField("Donnée", default=False)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "%s -- %s, %s" % (
|
|
||||||
self.participant.user,
|
|
||||||
self.spectacle.title,
|
|
||||||
self.spectacle.date,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SpectacleRevente(models.Model):
|
class SpectacleRevente(models.Model):
|
||||||
attribution = models.OneToOneField(
|
attribution = models.OneToOneField(
|
||||||
Attribution, on_delete=models.CASCADE, related_name="revente"
|
Attribution, on_delete=models.CASCADE, related_name="revente"
|
||||||
|
@ -329,21 +365,24 @@ class SpectacleRevente(models.Model):
|
||||||
BdA-Revente à tous les intéressés.
|
BdA-Revente à tous les intéressés.
|
||||||
"""
|
"""
|
||||||
inscrits = self.attribution.spectacle.subscribed.select_related("user")
|
inscrits = self.attribution.spectacle.subscribed.select_related("user")
|
||||||
datatuple = [
|
mails = [
|
||||||
(
|
(
|
||||||
"bda-revente",
|
"BdA-Revente : {}".format(self.attribution.spectacle.title),
|
||||||
{
|
loader.render_to_string(
|
||||||
|
"bda/mails/revente-new.txt",
|
||||||
|
context={
|
||||||
"member": participant.user,
|
"member": participant.user,
|
||||||
"show": self.attribution.spectacle,
|
"show": self.attribution.spectacle,
|
||||||
"revente": self,
|
"revente": self,
|
||||||
"site": Site.objects.get_current(),
|
"site": Site.objects.get_current(),
|
||||||
},
|
},
|
||||||
|
),
|
||||||
settings.MAIL_DATA["revente"]["FROM"],
|
settings.MAIL_DATA["revente"]["FROM"],
|
||||||
[participant.user.email],
|
[participant.user.email],
|
||||||
)
|
)
|
||||||
for participant in inscrits
|
for participant in inscrits
|
||||||
]
|
]
|
||||||
send_mass_custom_mail(datatuple)
|
send_mass_mail(mails)
|
||||||
self.notif_sent = True
|
self.notif_sent = True
|
||||||
self.notif_time = timezone.now()
|
self.notif_time = timezone.now()
|
||||||
self.save()
|
self.save()
|
||||||
|
@ -354,20 +393,23 @@ class SpectacleRevente(models.Model):
|
||||||
leur indiquer qu'il est désormais disponible au shotgun.
|
leur indiquer qu'il est désormais disponible au shotgun.
|
||||||
"""
|
"""
|
||||||
inscrits = self.attribution.spectacle.subscribed.select_related("user")
|
inscrits = self.attribution.spectacle.subscribed.select_related("user")
|
||||||
datatuple = [
|
mails = [
|
||||||
(
|
(
|
||||||
"bda-shotgun",
|
"BdA-Revente : {}".format(self.attribution.spectacle.title),
|
||||||
{
|
loader.render_to_string(
|
||||||
|
"bda/mails/revente-shotgun.txt",
|
||||||
|
context={
|
||||||
"member": participant.user,
|
"member": participant.user,
|
||||||
"show": self.attribution.spectacle,
|
"show": self.attribution.spectacle,
|
||||||
"site": Site.objects.get_current(),
|
"site": Site.objects.get_current(),
|
||||||
},
|
},
|
||||||
|
),
|
||||||
settings.MAIL_DATA["revente"]["FROM"],
|
settings.MAIL_DATA["revente"]["FROM"],
|
||||||
[participant.user.email],
|
[participant.user.email],
|
||||||
)
|
)
|
||||||
for participant in inscrits
|
for participant in inscrits
|
||||||
]
|
]
|
||||||
send_mass_custom_mail(datatuple)
|
send_mass_mail(mails)
|
||||||
self.notif_sent = True
|
self.notif_sent = True
|
||||||
self.notif_time = timezone.now()
|
self.notif_time = timezone.now()
|
||||||
# Flag inutile, sauf si l'horloge interne merde
|
# Flag inutile, sauf si l'horloge interne merde
|
||||||
|
@ -399,31 +441,30 @@ class SpectacleRevente(models.Model):
|
||||||
"show": spectacle,
|
"show": spectacle,
|
||||||
}
|
}
|
||||||
|
|
||||||
c_mails_qs = CustomMail.objects.filter(
|
subject = "BdA-Revente : {}".format(spectacle.title)
|
||||||
shortname__in=[
|
|
||||||
"bda-revente-winner",
|
|
||||||
"bda-revente-loser",
|
|
||||||
"bda-revente-seller",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
c_mails = {cm.shortname: cm for cm in c_mails_qs}
|
|
||||||
|
|
||||||
mails.append(
|
mails.append(
|
||||||
c_mails["bda-revente-winner"].get_message(
|
EmailMessage(
|
||||||
context,
|
subject=subject,
|
||||||
|
body=loader.render_to_string(
|
||||||
|
"bda/mails/revente-tirage-winner.txt",
|
||||||
|
context=context,
|
||||||
|
),
|
||||||
from_email=settings.MAIL_DATA["revente"]["FROM"],
|
from_email=settings.MAIL_DATA["revente"]["FROM"],
|
||||||
to=[winner.user.email],
|
to=[winner.user.email],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
mails.append(
|
mails.append(
|
||||||
c_mails["bda-revente-seller"].get_message(
|
EmailMessage(
|
||||||
context,
|
subject=subject,
|
||||||
|
body=loader.render_to_string(
|
||||||
|
"bda/mails/revente-tirage-seller.txt",
|
||||||
|
context=context,
|
||||||
|
),
|
||||||
from_email=settings.MAIL_DATA["revente"]["FROM"],
|
from_email=settings.MAIL_DATA["revente"]["FROM"],
|
||||||
to=[seller.user.email],
|
to=[seller.user.email],
|
||||||
reply_to=[winner.user.email],
|
reply_to=[winner.user.email],
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Envoie un mail aux perdants
|
# Envoie un mail aux perdants
|
||||||
|
@ -433,11 +474,15 @@ class SpectacleRevente(models.Model):
|
||||||
new_context["acheteur"] = inscrit.user
|
new_context["acheteur"] = inscrit.user
|
||||||
|
|
||||||
mails.append(
|
mails.append(
|
||||||
c_mails["bda-revente-loser"].get_message(
|
EmailMessage(
|
||||||
new_context,
|
subject=subject,
|
||||||
|
body=loader.render_to_string(
|
||||||
|
"bda/mails/revente-tirage-loser.txt",
|
||||||
|
context=new_context,
|
||||||
|
),
|
||||||
from_email=settings.MAIL_DATA["revente"]["FROM"],
|
from_email=settings.MAIL_DATA["revente"]["FROM"],
|
||||||
to=[inscrit.user.email],
|
to=[inscrit.user.email],
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
mail_conn = mail.get_connection()
|
mail_conn = mail.get_connection()
|
||||||
|
|
125
bda/static/bda/css/bda.css
Normal file
125
bda/static/bda/css/bda.css
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
form#tokenform {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin-right: 10px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
form#tokenform textarea {
|
||||||
|
font-size: 2em;
|
||||||
|
width: 350px;
|
||||||
|
height: 200px;
|
||||||
|
font-family: 'Droif Serif', serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* wft ?
|
||||||
|
input {
|
||||||
|
width: 400px;
|
||||||
|
font-size: 2em;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
ul.losers {
|
||||||
|
display: inline;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.losers li {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.details {
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border: 0px solid black;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
.attribresult {
|
||||||
|
margin: 10px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spectacle-passe {
|
||||||
|
opacity:0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** JQuery-Confirm box **/
|
||||||
|
|
||||||
|
.jconfirm .jconfirm-bg {
|
||||||
|
background-color: rgb(0,0,0,0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jconfirm .jconfirm-box {
|
||||||
|
padding:0;
|
||||||
|
border-radius:0 !important;
|
||||||
|
font-family:Roboto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jconfirm .jconfirm-box .content-pane {
|
||||||
|
border-bottom:1px solid #ddd;
|
||||||
|
margin: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jconfirm .jconfirm-box .content {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jconfirm .jconfirm-box .content-pane {
|
||||||
|
border-bottom:1px solid #ddd;
|
||||||
|
margin: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jconfirm .jconfirm-box .content {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jconfirm .jconfirm-box .content a,
|
||||||
|
.jconfirm .jconfirm-box .content a:hover {
|
||||||
|
color: #D81138;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jconfirm .jconfirm-box .buttons {
|
||||||
|
margin-top:-6px; /* j'arrive pas à voir pk y'a un espace au dessus sinon... */
|
||||||
|
padding:0;
|
||||||
|
height:40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jconfirm .jconfirm-box .buttons button {
|
||||||
|
min-width:40px;
|
||||||
|
height:100%;
|
||||||
|
margin:0;
|
||||||
|
margin:0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jconfirm .jconfirm-box .buttons button:first-child:focus,
|
||||||
|
.jconfirm .jconfirm-box .buttons button:first-child:hover {
|
||||||
|
color:#FFF !important;
|
||||||
|
background:forestgreen !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jconfirm .jconfirm-box .buttons button:nth-child(2):focus,
|
||||||
|
.jconfirm .jconfirm-box .buttons button:nth-child(2):hover {
|
||||||
|
color:#FFF !important;
|
||||||
|
background:#D93A32 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jconfirm .jconfirm-box div.title-c .title {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
padding:0 15px;
|
||||||
|
height:40px;
|
||||||
|
line-height:40px;
|
||||||
|
|
||||||
|
font-family:Dosis;
|
||||||
|
font-size:20px;
|
||||||
|
font-weight:bold;
|
||||||
|
|
||||||
|
color:#FFF;
|
||||||
|
background-color:rgb(222, 130, 107);
|
||||||
|
}
|
|
@ -1,48 +0,0 @@
|
||||||
form#tokenform {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
margin-right: 10px;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
form#tokenform textarea {
|
|
||||||
font-size: 2em;
|
|
||||||
width: 350px;
|
|
||||||
height: 200px;
|
|
||||||
font-family: 'Droif Serif', serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* wft ?
|
|
||||||
input {
|
|
||||||
width: 400px;
|
|
||||||
font-size: 2em;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
ul.losers {
|
|
||||||
display: inline;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.losers li {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.details {
|
|
||||||
font-size: 0.7em;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
border: 0px solid black;
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
.attribresult {
|
|
||||||
margin: 10px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spectacle-passe {
|
|
||||||
opacity:0.5;
|
|
||||||
}
|
|
Binary file not shown.
|
@ -1,8 +1,8 @@
|
||||||
{% extends "base_title.html" %}
|
{% extends "base_title.html" %}
|
||||||
{% load staticfiles %}
|
{% load static %}
|
||||||
|
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
|
<link type="text/css" rel="stylesheet" href="{% static "bda/css/bda.css" %}" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends "base_title.html" %}
|
{% extends "base_title.html" %}
|
||||||
{% load staticfiles %}
|
{% load static %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
<h2>État des inscriptions BdA</h2>
|
<h2>État des inscriptions BdA</h2>
|
||||||
|
@ -42,9 +42,6 @@
|
||||||
Total : {{ total }} place{{ total|pluralize }} demandée{{ total|pluralize }}
|
Total : {{ total }} place{{ total|pluralize }} demandée{{ total|pluralize }}
|
||||||
sur {{ proposed }} place{{ proposed|pluralize }} proposée{{ proposed|pluralize }}
|
sur {{ proposed }} place{{ proposed|pluralize }} proposée{{ proposed|pluralize }}
|
||||||
</span>
|
</span>
|
||||||
<script type="text/javascript"
|
|
||||||
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}">
|
|
||||||
</script>
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(function(){
|
$(function(){
|
||||||
$("table.etat-bda").stupidtable();
|
$("table.etat-bda").stupidtable();
|
||||||
|
|
1
bda/templates/bda/forms/attribution_label_table.html
Normal file
1
bda/templates/bda/forms/attribution_label_table.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{% include 'bda/forms/spectacle_label_table.html' with spectacle=attribution.spectacle %}
|
4
bda/templates/bda/forms/checkbox_table.html
Normal file
4
bda/templates/bda/forms/checkbox_table.html
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<tr>
|
||||||
|
<td><input type="{{ widget.type }}" name="{{ widget.name }}" {% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %}></td>
|
||||||
|
{{ widget.label }}
|
||||||
|
</tr>
|
1
bda/templates/bda/forms/date_tirage.html
Normal file
1
bda/templates/bda/forms/date_tirage.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<td data-sort-value="{{ revente.date_tirage | date:"U" }}">{{ revente.date_tirage }}</td>
|
3
bda/templates/bda/forms/revente_other_label_table.html
Normal file
3
bda/templates/bda/forms/revente_other_label_table.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{% include 'bda/forms/spectacle_label_table.html' with spectacle=revente.attribution.spectacle %}
|
||||||
|
{% with user=revente.seller.user %} <td>{{user.first_name}} {{user.last_name}}</td> {% endwith%}
|
||||||
|
{% include 'bda/forms/date_tirage.html' %}
|
2
bda/templates/bda/forms/revente_self_label_table.html
Normal file
2
bda/templates/bda/forms/revente_self_label_table.html
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
{% include 'bda/forms/spectacle_label_table.html' with spectacle=revente.attribution.spectacle %}
|
||||||
|
{% include 'bda/forms/date_tirage.html' %}
|
4
bda/templates/bda/forms/revente_sold_label_table.html
Normal file
4
bda/templates/bda/forms/revente_sold_label_table.html
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{% include 'bda/forms/spectacle_label_table.html' with spectacle=revente.attribution.spectacle %}
|
||||||
|
{% with user=revente.soldTo.user %}
|
||||||
|
<td><a href="mailto:{{ user.email }}">{{user.first_name}} {{user.last_name}}</a></td>
|
||||||
|
{% endwith %}
|
4
bda/templates/bda/forms/spectacle_label_table.html
Normal file
4
bda/templates/bda/forms/spectacle_label_table.html
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<td>{{ spectacle.title }}</td>
|
||||||
|
<td data-sort-value="{{ spectacle.timestamp }}">{{ spectacle.date }}</td>
|
||||||
|
<td data-sort-value="{{ spectacle.location }}">{{ spectacle.location }}</td>
|
||||||
|
<td data-sort-value="{{ spectacle.price |stringformat:".3f" }}">{{ spectacle.price |floatformat }}€</td>
|
|
@ -1,11 +1,13 @@
|
||||||
{% extends "base_title.html" %}
|
{% extends "base_title.html" %}
|
||||||
{% load staticfiles %}
|
{% load static %}
|
||||||
|
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
<script src="{% static 'js/jquery-ui.min.js' %}" type="text/javascript"></script>
|
<script type="text/javascript" src="{% static 'vendor/jquery/jquery-ui.min.js' %}" ></script>
|
||||||
<script src="{% static "js/jquery.ui.touch-punch.min.js" %}" type="text/javascript"></script>
|
<script type="text/javascript" src="{% static "vendor/jquery/jquery-confirm.js" %}"></script>
|
||||||
<link type="text/css" rel="stylesheet" href="{% static "css/jquery-ui.min.css" %}" />
|
<script type="text/javascript" src="{% static 'gestioncof/vendor/jquery.ui.touch-punch.min.js' %}" ></script>
|
||||||
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
|
<link type="text/css" rel="stylesheet" href="{% static 'vendor/jquery/jquery-confirm.css' %}">
|
||||||
|
<link type="text/css" rel="stylesheet" href="{% static 'vendor/jquery/jquery-ui.min.css' %}" />
|
||||||
|
<link type="text/css" rel="stylesheet" href="{% static 'bda/css/bda.css' %}" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
|
@ -52,6 +54,11 @@ var django = {
|
||||||
} else {
|
} else {
|
||||||
deleteInput.attr("checked", true);
|
deleteInput.attr("checked", true);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Reset the default values
|
||||||
|
var selects = $(form).find("select");
|
||||||
|
$(selects[0]).val("");
|
||||||
|
$(selects[1]).val("1");
|
||||||
}
|
}
|
||||||
// callback
|
// callback
|
||||||
});
|
});
|
||||||
|
@ -113,11 +120,50 @@ var django = {
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<input type="hidden" name="dbstate" value="{{ dbstate }}" />
|
<input type="hidden" name="dbstate" value="{{ dbstate }}" />
|
||||||
<input type="submit" class="btn btn-primary" value="Enregistrer" />
|
<input type="submit" class="btn btn-primary" id="bda-inscr" value="Enregistrer" />
|
||||||
</div>
|
</div>
|
||||||
<p class="footnotes">
|
<p class="footnotes">
|
||||||
<sup>1</sup>: cette liste de vœux est ordonnée (du plus important au moins important), pour ajuster la priorité vous pouvez déplacer chaque vœu.<br />
|
<sup>1</sup>: cette liste de vœux est ordonnée (du plus important au moins important), pour ajuster la priorité vous pouvez déplacer chaque vœu.<br />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{% if not charte %}
|
||||||
|
<script>
|
||||||
|
(function ($) {
|
||||||
|
var charte_ok = false ;
|
||||||
|
function link_charte() {
|
||||||
|
$.confirm({
|
||||||
|
title: 'Charte du BdA',
|
||||||
|
columnClass: 'col-md-6 col-md-offset-3',
|
||||||
|
content: `
|
||||||
|
<div>
|
||||||
|
En vous inscrivant à ce tirage du Bureau des Arts, vous vous engagez à \
|
||||||
|
respecter la charte du BdA:</br> \
|
||||||
|
<a target="_blank" href='https://bda.ens.fr/lequipe/charte-bda/'>https://bda.ens.fr/lequipe/charte-bda/</a>
|
||||||
|
</div>`,
|
||||||
|
backgroundDismiss: true,
|
||||||
|
opacity: 1,
|
||||||
|
animation:'top',
|
||||||
|
closeAnimation:'bottom',
|
||||||
|
keyboardEnabled: true,
|
||||||
|
confirmButton: '<span class="glyphicon glyphicon-ok"></span>',
|
||||||
|
cancelButton: '<span class="glyphicon glyphicon-remove"></span>',
|
||||||
|
confirm: function() {
|
||||||
|
charte_ok = true ;
|
||||||
|
$("#bda_form").submit();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$(document).ready(function($) {
|
||||||
|
$("#bda_form").submit(function(e) {
|
||||||
|
if (!charte_ok) {
|
||||||
|
e.preventDefault();
|
||||||
|
link_charte();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})(django.jQuery);
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -26,13 +26,6 @@
|
||||||
|
|
||||||
<hr \>
|
<hr \>
|
||||||
|
|
||||||
<p>
|
|
||||||
<em>Note :</em> le template de ce mail peut être modifié à
|
|
||||||
<a href="{% url 'admin:custommail_custommail_change' custommail.pk %}">cette adresse</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<hr \>
|
|
||||||
|
|
||||||
<h3>Forme des mails</h3>
|
<h3>Forme des mails</h3>
|
||||||
|
|
||||||
<h4>Une seule place</h4>
|
<h4>Une seule place</h4>
|
||||||
|
|
10
bda/templates/bda/mails/attributions-decus.txt
Normal file
10
bda/templates/bda/mails/attributions-decus.txt
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
Cher-e {{ member.first_name }},
|
||||||
|
|
||||||
|
Tu t'es inscrit-e pour le tirage au sort du BdA. Malheureusement, tu n'as
|
||||||
|
obtenu aucune place.
|
||||||
|
|
||||||
|
Nous proposons cependant de nombreuses offres hors-tirage tout au long de
|
||||||
|
l'année, et nous t'invitons à nous contacter si l'une d'entre elles
|
||||||
|
t'intéresse !
|
||||||
|
--
|
||||||
|
Le Bureau des Arts
|
29
bda/templates/bda/mails/attributions.txt
Normal file
29
bda/templates/bda/mails/attributions.txt
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
Cher-e {{ member.first_name }},
|
||||||
|
|
||||||
|
Tu t'es inscrit-e pour le tirage au sort du BdA. Tu as été sélectionné-e
|
||||||
|
pour les spectacles suivants :
|
||||||
|
{% for place in places %}
|
||||||
|
- 1 place pour {{ place }}{% endfor %}
|
||||||
|
|
||||||
|
*Paiement*
|
||||||
|
Au burô :
|
||||||
|
L'intégralité de ces places de spectacles est à régler dès maintenant, au bureau du COF pendant les heures de permanences (lundi, mardi, jeudi entre 12h et 14h et entre 18h30 et 19h30, mercredi entre 18h30 et 19h30, vendredi entre 12h et 14h). Les places sont à régler AVANT les représentations. Si vous êtes en vacances, vous pourrez venir les régler dès votre retour. Il est demandé à chacun·e de prendre garde à honorer l’ensemble des places qui lui sont attribuées et de s'engager de fait à payer la ou les place(s) qui lui sont attribuées.
|
||||||
|
|
||||||
|
Par virements :
|
||||||
|
L'intégralité de ces places de spectacles est à régler dès maintenant par virements. Il vous sera demandé d'envoyer une confirmation de l'envoi de virement à bda@ens.fr.
|
||||||
|
IBAN AEENS : FR76 4255 9100 0008 0263 8331 927
|
||||||
|
Motif de virements : AVR25(ou MAI25)-tirageprintemps-NOM-prénom
|
||||||
|
|
||||||
|
Les places sont à régler AVANT les représentations. Il est demandé à chacun·e de prendre garde à honorer l’ensemble des places qui lui sont attribuées et de s'engager de fait à payer la ou les place(s) qui lui sont attribuées.
|
||||||
|
Des facilités de paiement sont bien évidemment possibles : nous pouvons ne pas encaisser le chèque immédiatement, ou bien découper votre paiement en deux fois. Pour ceux qui ne pourraient pas venir payer au bureau, merci de nous contacter par mail.
|
||||||
|
|
||||||
|
*Mode de retrait des places*
|
||||||
|
Au moment du paiement, certaines places vous seront remises directement, d'autres seront à récupérer au cours de l'année, d'autres encore seront nominatives et à retirer le soir même dans les théâtres correspondants. Pour chaque spectacle, vous recevrez un mail quelques jours avant la représentation vous indiquant le mode de retrait.
|
||||||
|
Nous vous rappelons que l'obtention de places du BdA vous engage à respecter les règles de fonctionnement :
|
||||||
|
https://bda.ens.fr/lequipe/charte-bda/
|
||||||
|
|
||||||
|
Un système de revente des places via les mails BdA-revente est disponible directement sur votre compte GestioCOF. Pour pouvoir l'utiliser, il faut que vous ayez payé vos places en amont.
|
||||||
|
|
||||||
|
En vous souhaitant de très beaux spectacles tout au long de l'année,
|
||||||
|
--
|
||||||
|
Le Bureau des Arts
|
23
bda/templates/bda/mails/rappel.txt
Normal file
23
bda/templates/bda/mails/rappel.txt
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
Bonjour {{ member.first_name }},
|
||||||
|
|
||||||
|
Nous te rappellons que tu as eu la chance d'obtenir {{ nb_attr|pluralize:"une place,deux places" }}
|
||||||
|
pour {{ show.title }}, le {{ show.date }} au {{ show.location }}. N'oublie pas de t'y rendre !
|
||||||
|
{% if nb_attr == 2 %}
|
||||||
|
Tu as obtenu deux places pour ce spectacle. Nous te rappelons que
|
||||||
|
ces places sont strictement réservées aux personnes de moins de 28 ans.
|
||||||
|
{% endif %}
|
||||||
|
{% if show.listing %}Pour ce spectacle, tu as reçu {{ nb_attr|pluralize:"une place,des places" }} sur
|
||||||
|
listing. Il te faudra donc te rendre 15 minutes en avance sur les lieux de la représentation
|
||||||
|
pour {{ nb_attr|pluralize:"la,les" }} retirer.
|
||||||
|
{% else %}Pour assister à ce spectacle, tu dois présenter les billets qui ont
|
||||||
|
été distribués au burô.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
Si tu ne peux plus assister à cette représentation, tu peux
|
||||||
|
revendre ta place via BdA-revente, accessible directement sur
|
||||||
|
GestioCOF (lien "revendre une place du premier tirage" sur la page
|
||||||
|
d'accueil https://www.cof.ens.fr/gestion/).
|
||||||
|
|
||||||
|
En te souhaitant un excellent spectacle,
|
||||||
|
--
|
||||||
|
Le Bureau des Arts
|
12
bda/templates/bda/mails/revente-new.txt
Normal file
12
bda/templates/bda/mails/revente-new.txt
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
Bonjour {{ member.first_name }}
|
||||||
|
|
||||||
|
Une place pour le spectacle {{ show.title }} ({{ show.date }})
|
||||||
|
a été postée sur BdA-Revente.
|
||||||
|
|
||||||
|
Si ce spectacle t'intéresse toujours, merci de nous le signaler en cliquant
|
||||||
|
sur ce lien : https://{{ site }}{% url "bda-revente-confirm" revente.id %}.
|
||||||
|
Dans le cas où plusieurs personnes seraient intéressées, nous procèderons à
|
||||||
|
un tirage au sort le {{ revente.date_tirage|date:"DATE_FORMAT" }}.
|
||||||
|
|
||||||
|
Chaleureusement,
|
||||||
|
Le BdA
|
13
bda/templates/bda/mails/revente-seller.txt
Normal file
13
bda/templates/bda/mails/revente-seller.txt
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
Bonjour {{ vendeur.first_name }},
|
||||||
|
|
||||||
|
Tu t’es bien inscrit·e pour revendre une place pour {{ show.title }}.
|
||||||
|
|
||||||
|
{% with revente.date_tirage as time %}
|
||||||
|
Le tirage au sort entre tout·e·s les racheteuse·eur·s potentiel·le·s aura lieu
|
||||||
|
le {{ time|date:"DATE_FORMAT" }} à {{ time|time:"TIME_FORMAT" }} (dans {{time|timeuntil }}).
|
||||||
|
Si personne ne s’est inscrit pour racheter la place, celle-ci apparaîtra parmi
|
||||||
|
les « Places disponibles immédiatement à la revente » sur GestioCOF.
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
Bonne revente !
|
||||||
|
Le Bureau des Arts
|
6
bda/templates/bda/mails/revente-shotgun-seller.txt
Normal file
6
bda/templates/bda/mails/revente-shotgun-seller.txt
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
Bonjour {{ vendeur.first_name }} !
|
||||||
|
|
||||||
|
Je souhaiterais racheter ta place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) à {{ show.price|floatformat:2 }}€.
|
||||||
|
Contacte-moi si tu es toujours intéressé·e !
|
||||||
|
|
||||||
|
{{ acheteur.get_full_name }} ({{ acheteur.email }})
|
11
bda/templates/bda/mails/revente-shotgun.txt
Normal file
11
bda/templates/bda/mails/revente-shotgun.txt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
Bonjour {{ member.first_name }}
|
||||||
|
|
||||||
|
Une place pour le spectacle {{ show.title }} ({{ show.date }})
|
||||||
|
a été postée sur BdA-Revente.
|
||||||
|
|
||||||
|
Puisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour
|
||||||
|
cette place : elle est disponible immédiatement à l'adresse
|
||||||
|
https://{{ site }}{% url "bda-revente-buy" show.id %}, à la disposition de tous.
|
||||||
|
|
||||||
|
Chaleureusement,
|
||||||
|
Le BdA
|
9
bda/templates/bda/mails/revente-tirage-loser.txt
Normal file
9
bda/templates/bda/mails/revente-tirage-loser.txt
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
Bonjour {{ acheteur.first_name }},
|
||||||
|
|
||||||
|
Tu t'étais inscrit·e pour la revente de la place de {{ vendeur.get_full_name }}
|
||||||
|
pour {{ show.title }}.
|
||||||
|
Malheureusement, une autre personne a été tirée au sort pour racheter la place.
|
||||||
|
Tu pourras certainement retenter ta chance pour une autre revente !
|
||||||
|
|
||||||
|
À très bientôt,
|
||||||
|
Le Bureau des Arts
|
7
bda/templates/bda/mails/revente-tirage-seller.txt
Normal file
7
bda/templates/bda/mails/revente-tirage-seller.txt
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
Bonjour {{ vendeur.first_name }},
|
||||||
|
|
||||||
|
La personne tirée au sort pour racheter ta place pour {{ show.title }} est {{ acheteur.get_full_name }}.
|
||||||
|
Tu peux le/la contacter à l'adresse {{ acheteur.email }}, ou en répondant à ce mail.
|
||||||
|
|
||||||
|
Chaleureusement,
|
||||||
|
Le BdA
|
7
bda/templates/bda/mails/revente-tirage-winner.txt
Normal file
7
bda/templates/bda/mails/revente-tirage-winner.txt
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
Bonjour {{ acheteur.first_name }},
|
||||||
|
|
||||||
|
Tu as été tiré·e au sort pour racheter une place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) à {{ show.price|floatformat:2 }}€.
|
||||||
|
Tu peux contacter le/la vendeur·se à l'adresse {{ vendeur.email }}.
|
||||||
|
|
||||||
|
Chaleureusement,
|
||||||
|
Le BdA
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends "base_title.html" %}
|
{% extends "base_title.html" %}
|
||||||
{% load staticfiles %}
|
{% load static %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
<h2>{{ spectacle }}</h2>
|
<h2>{{ spectacle }}</h2>
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for participant in participants %}
|
{% for participant in participants %}
|
||||||
<tr>
|
<tr>
|
||||||
<td data-sort-value="{{ participan.name}}">{{participant.name}}</td>
|
<td data-sort-value="{{ participant.name}}">{{participant.name}}</td>
|
||||||
<td data-sort-value="{{participant.nb_places}}">{{participant.nb_places}} place{{participant.nb_places|pluralize}}</td>
|
<td data-sort-value="{{participant.nb_places}}">{{participant.nb_places}} place{{participant.nb_places|pluralize}}</td>
|
||||||
<td data-sort-value="{{participant.email}}">{{participant.email}}</td>
|
<td data-sort-value="{{participant.email}}">{{participant.email}}</td>
|
||||||
<td data-sort-value="{{ participant.paid}}" class={%if participant.paid %}"greenratio"{%else%}"redratio"{%endif%}>
|
<td data-sort-value="{{ participant.paid}}" class={%if participant.paid %}"greenratio"{%else%}"redratio"{%endif%}>
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
<h3><a href="{% url "admin:bda_attribution_add" %}?spectacle={{spectacle.id}}"><span class="glyphicon glyphicon-plus-sign"></span> Ajouter une attribution</a></h3>
|
<h3><a href="{% url "admin:bda_attribution_add" %}?spectacle={{spectacle.id}}"><span class="glyphicon glyphicon-plus-sign"></span> Ajouter une attribution</a></h3>
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-default" type="button" onclick="toggle('export-mails')">Afficher/Cacher mails participants</button>
|
<button class="btn btn-default" type="button" onclick="toggle('export-mails')">Afficher/Cacher mails participant⋅e⋅s</button>
|
||||||
<pre id="export-mails" style="display:none">{% spaceless %}
|
<pre id="export-mails" style="display:none">{% spaceless %}
|
||||||
{% for participant in participants %}{{ participant.email }}, {% endfor %}
|
{% for participant in participants %}{{ participant.email }}, {% endfor %}
|
||||||
{% endspaceless %}</pre>
|
{% endspaceless %}</pre>
|
||||||
|
@ -56,9 +56,7 @@
|
||||||
<a href="{% url 'bda-rappels' spectacle.id %}">Page d'envoi manuel des mails de rappel</a>
|
<a href="{% url 'bda-rappels' spectacle.id %}">Page d'envoi manuel des mails de rappel</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript"
|
<script type="text/javascript">
|
||||||
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}"></script>
|
|
||||||
<script>
|
|
||||||
function toggle(id) {
|
function toggle(id) {
|
||||||
var pre = document.getElementById(id) ;
|
var pre = document.getElementById(id) ;
|
||||||
pre.style.display = pre.style.display == "none" ? "block" : "none" ;
|
pre.style.display = pre.style.display == "none" ? "block" : "none" ;
|
||||||
|
|
|
@ -10,9 +10,20 @@
|
||||||
<td>{{place.spectacle.location}}</td>
|
<td>{{place.spectacle.location}}</td>
|
||||||
<td>{{place.spectacle.date}}</td>
|
<td>{{place.spectacle.date}}</td>
|
||||||
<td>{% if place.double %}deux places{%else%}une place{% endif %}</td>
|
<td>{% if place.double %}deux places{%else%}une place{% endif %}</td>
|
||||||
|
<td>{% if place.spectacle.listing %}sur listing{% else %}place physique{% endif %}</td>
|
||||||
|
<td>
|
||||||
|
{% if place.unpaid == 0 %}
|
||||||
|
Payé
|
||||||
|
{% elif place.unpaid == 1 %}
|
||||||
|
Une place à payer ({{place.unpaid_price|floatformat}}€)
|
||||||
|
{% else %}
|
||||||
|
Deux places à payer ({{place.unpaid_price|floatformat}}€)
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
<h4 class="bda-prix">Reste à payer : {{ unpaid|floatformat }}€</h4>
|
||||||
<h4 class="bda-prix">Total à payer : {{ total|floatformat }}€</h4>
|
<h4 class="bda-prix">Total à payer : {{ total|floatformat }}€</h4>
|
||||||
<br/>
|
<br/>
|
||||||
<p>Ne manque pas un spectacle avec le
|
<p>Ne manque pas un spectacle avec le
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends "base_title.html" %}
|
{% extends "base_title.html" %}
|
||||||
{% load staticfiles %}
|
{% load static %}
|
||||||
|
|
||||||
|
|
||||||
{%block realcontent %}
|
{%block realcontent %}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends "base_title.html" %}
|
{% extends "base_title.html" %}
|
||||||
{% load staticfiles %}
|
{% load static %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
<h2>Inscription à une revente</h2>
|
<h2>Inscription à une revente</h2>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends "base_title.html" %}
|
{% extends "base_title.html" %}
|
||||||
{% load staticfiles %}
|
{% load static %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
{% extends "base_title.html" %}
|
{% extends "base_title.html" %}
|
||||||
{% load bootstrap %}
|
{% load static %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
|
|
||||||
<h2>Gestion des places que je revends</h2>
|
<h2>Gestion des places que je revends</h2>
|
||||||
{% with resell_attributions=resellform.attributions annul_reventes=annulform.reventes sold_reventes=soldform.reventes %}
|
|
||||||
|
|
||||||
{% if resell_attributions %}
|
{% if resell_exists %}
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<h3>Places non revendues</h3>
|
<h3>Places non revendues</h3>
|
||||||
|
@ -17,10 +16,21 @@
|
||||||
ensuite 1h pour changer d'avis avant que la revente soit confirmée et
|
ensuite 1h pour changer d'avis avant que la revente soit confirmée et
|
||||||
que les notifications soient envoyées aux intéressé·e·s.
|
que les notifications soient envoyées aux intéressé·e·s.
|
||||||
</div>
|
</div>
|
||||||
<div class="bootstrap-form-reduce">
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ resellform|bootstrap }}
|
<table class="table table-striped stupidtable">
|
||||||
</div>
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th data-sort="string">Titre</th>
|
||||||
|
<th data-sort="string">Lieu</th>
|
||||||
|
<th data-sort="int">Date</th>
|
||||||
|
<th data-sort="int">Prix</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for checkbox in resellform.attributions %}{{ checkbox }}{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
|
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
|
||||||
</div>
|
</div>
|
||||||
|
@ -29,7 +39,7 @@
|
||||||
<hr />
|
<hr />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if annul_reventes %}
|
{% if annul_exists %}
|
||||||
<h3>Places en cours de revente</h3>
|
<h3>Places en cours de revente</h3>
|
||||||
<form action="" method="post">
|
<form action="" method="post">
|
||||||
<div class="bg-info text-info center-block">
|
<div class="bg-info text-info center-block">
|
||||||
|
@ -37,22 +47,28 @@
|
||||||
Vous pouvez annuler les reventes qui n'ont pas encore trouvé preneur·se.
|
Vous pouvez annuler les reventes qui n'ont pas encore trouvé preneur·se.
|
||||||
</div>
|
</div>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class='form-group'>
|
<table class="table table-striped stupidtable">
|
||||||
<div class='multiple-checkbox'>
|
<thead>
|
||||||
<ul>
|
<tr>
|
||||||
{% for revente in annul_reventes %}
|
<th></th>
|
||||||
<li>{{ revente.tag }} {{ revente.choice_label }}</li>
|
<th data-sort="string">Titre</th>
|
||||||
{% endfor %}
|
<th data-sort="int">Date</th>
|
||||||
</ul>
|
<th data-sort="string">Lieu</th>
|
||||||
</div>
|
<th data-sort="int">Prix</th>
|
||||||
</div>
|
<th data-sort="int">Tirage le</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for checkbox in annulform.reventes %}{{ checkbox }}{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
|
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if sold_reventes %}
|
{% if sold_exists %}
|
||||||
<h3>Places revendues</h3>
|
<h3>Places revendues</h3>
|
||||||
<form action="" method="post">
|
<form action="" method="post">
|
||||||
<div class="bg-info text-info center-block">
|
<div class="bg-info text-info center-block">
|
||||||
|
@ -64,17 +80,42 @@
|
||||||
vous ne parvenez pas à entrer en contact avec la personne tirée au
|
vous ne parvenez pas à entrer en contact avec la personne tirée au
|
||||||
sort.
|
sort.
|
||||||
</div>
|
</div>
|
||||||
<div class="bootstrap-form-reduce">
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ soldform|bootstrap }}
|
<table class="table table-striped stupidtable">
|
||||||
</div>
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th data-sort="string">Titre</th>
|
||||||
|
<th data-sort="int">Date</th>
|
||||||
|
<th data-sort="string">Lieu</th>
|
||||||
|
<th data-sort="int">Prix</th>
|
||||||
|
<th>Vendue à</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for checkbox in soldform.reventes %}{{ checkbox }}{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
<button type="submit" class="btn btn-primary" name="transfer">Transférer</button>
|
<button type="submit" class="btn btn-primary" name="transfer">Transférer</button>
|
||||||
<button type="submit" class="btn btn-primary" name="reinit">Réinitialiser</button>
|
<button type="submit" class="btn btn-primary" name="reinit">Réinitialiser</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not resell_attributions and not annul_reventes and not sold_reventes %}
|
{% if not resell_exists and not annul_exists and not sold_exists %}
|
||||||
<p>Plus de reventes possibles !</p>
|
<p>Plus de reventes possibles !</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endwith %}
|
<script language="JavaScript">
|
||||||
|
$(function(){
|
||||||
|
$("table.stupidtable").stupidtable();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("tr").click(function() {
|
||||||
|
$(this).find("input[type=checkbox]").click()
|
||||||
|
});
|
||||||
|
|
||||||
|
$("input[type=checkbox]").click(function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
{% extends "base_title.html" %}
|
|
||||||
|
|
||||||
{% block realcontent %}
|
|
||||||
<h2><strong>Nope</strong></h2>
|
|
||||||
<p>Avant de revendre des places, il faut aller les payer !</p>
|
|
||||||
{% endblock %}
|
|
|
@ -2,11 +2,26 @@
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
<h2>Places disponibles immédiatement</h2>
|
<h2>Places disponibles immédiatement</h2>
|
||||||
{% if shotgun %}
|
{% if spectacles %}
|
||||||
<ul class="list-unstyled">
|
<table class="table table-striped stupidtable" id="bda-shotgun">
|
||||||
{% for spectacle in shotgun %}
|
<thead>
|
||||||
<li><a href="{% url "bda-revente-buy" spectacle.id %}">{{spectacle}}</a></li>
|
<tr>
|
||||||
|
<th data-sort="string">Titre</th>
|
||||||
|
<th data-sort="int">Date</th>
|
||||||
|
<th data-sort="string">Lieu</th>
|
||||||
|
<th data-sort="int">Prix</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for spectacle in spectacles %}
|
||||||
|
<tr>
|
||||||
|
{% include "bda/forms/spectacle_label_table.html" with spectacle=spectacle %}
|
||||||
|
<td class="button"><a role="button" class="btn btn-primary" href="{% url 'bda-revente-buy' spectacle.id %}">Racheter</a>
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p> Pas de places disponibles immédiatement, désolé !</p>
|
<p> Pas de places disponibles immédiatement, désolé !</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{% extends "base_title.html" %}
|
{% extends "base_title.html" %}
|
||||||
{% load bootstrap %}
|
{% load static %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
<h2>Inscriptions pour BdA-Revente</h2>
|
<h2>Inscriptions pour BdA-Revente</h2>
|
||||||
<form action="" class="form-horizontal" method="post">
|
<form action="" class="form-horizontal" method="post">
|
||||||
<div class="bg-info text-info center-block">
|
<div class="bg-info text-info center-block">
|
||||||
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
|
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
|
||||||
Cochez les spectacles pour lesquels vous souhaitez recevoir un
|
Cochez les spectacles pour lesquels vous souhaitez recevoir une
|
||||||
notification quand une place est disponible en revente. <br />
|
notification quand une place est disponible en revente. <br />
|
||||||
Lorsque vous validez vos choix, si un tirage au sort est en cours pour
|
Lorsque vous validez vos choix, si un tirage au sort est en cours pour
|
||||||
un des spectacles que vous avez sélectionné, vous serez automatiquement
|
un des spectacles que vous avez sélectionné, vous serez automatiquement
|
||||||
|
@ -21,14 +21,21 @@
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
onClick="select(false)">Tout désélectionner</button>
|
onClick="select(false)">Tout désélectionner</button>
|
||||||
|
<table class="table table-striped stupidtable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th data-sort="string">Titre</th>
|
||||||
|
<th data-sort="int">Date</th>
|
||||||
|
<th data-sort="string">Lieu</th>
|
||||||
|
<th data-sort="int">Prix</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for checkbox in form.spectacles %}{{ checkbox }}{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
<div class="multiple-checkbox">
|
|
||||||
<ul>
|
|
||||||
{% for checkbox in form.spectacles %}
|
|
||||||
<li>{{ checkbox }}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<input type="submit"
|
<input type="submit"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
|
@ -36,11 +43,22 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script language="JavaScript">
|
<script language="JavaScript">
|
||||||
function select(check) {
|
function select(check) {
|
||||||
checkboxes = document.getElementsByName("spectacles");
|
checkboxes = document.getElementsByName("spectacles");
|
||||||
for(var i=0, n=checkboxes.length; i < n; i++) {
|
for(var i=0, n=checkboxes.length; i < n; i++) {
|
||||||
checkboxes[i].checked = check;
|
checkboxes[i].checked = check;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$(function(){
|
||||||
|
$("table.stupidtable").stupidtable();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("tr").click(function() {
|
||||||
|
$(this).find("input[type=checkbox]").click()
|
||||||
|
});
|
||||||
|
|
||||||
|
$("input[type=checkbox]").click(function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,45 +1,70 @@
|
||||||
{% extends "base_title.html" %}
|
{% extends "base_title.html" %}
|
||||||
{% load bootstrap %}
|
{% load static %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
|
|
||||||
<h2>Tirages au sort de reventes</h2>
|
<h2>Tirages au sort de reventes</h2>
|
||||||
|
|
||||||
{% if annulform.reventes %}
|
{% if annul_exists %}
|
||||||
<h3>Les reventes auxquelles vous êtes inscrit·e</h3>
|
<h3>Les reventes auxquelles vous êtes inscrit·e</h3>
|
||||||
<form class="form-horizontal" action="" method="post">
|
<form class="form-horizontal" action="" method="post">
|
||||||
<div class="bg-info text-info center-block">
|
<div class="bg-info text-info center-block">
|
||||||
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
|
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
|
||||||
Vous pouvez vous désinscrire des reventes suivantes tant que le tirage n'a
|
Voici la liste des reventes auxquelles vous êtes inscrit·e ; si vous ne souhaitez plus participer au tirage au sort vous pouvez vous en désister.
|
||||||
pas eu lieu.
|
|
||||||
</div>
|
</div>
|
||||||
<div class="bootstrap-form-reduce">
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ annulform|bootstrap }}
|
<table class="table table-striped stupidtable">
|
||||||
</div>
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th data-sort="string">Titre</th>
|
||||||
|
<th data-sort="int">Date</th>
|
||||||
|
<th data-sort="string">Lieu</th>
|
||||||
|
<th data-sort="int">Prix</th>
|
||||||
|
<th>Vendue par</th>
|
||||||
|
<th data-sort="int">Tirage le</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for checkbox in annulform.reventes %}{{ checkbox }}{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<input type="submit"
|
<input type="submit"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
name="annul"
|
name="annul"
|
||||||
value="Se désinscrire des tirages sélectionnés">
|
value="Se désister des tirages sélectionnés">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<hr />
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if subform.reventes %}
|
<hr />
|
||||||
|
|
||||||
|
{% if sub_exists %}
|
||||||
|
|
||||||
<h3>Tirages en cours</h3>
|
<h3>Tirages en cours</h3>
|
||||||
<form class="form-horizontal" action="" method="post">
|
<form class="form-horizontal" action="" method="post">
|
||||||
<div class="bg-info text-info center-block">
|
<div class="bg-info text-info center-block">
|
||||||
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
|
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
|
||||||
Vous pouvez vous inscrire aux tirage en cours suivants.
|
Vous pouvez vous inscrire aux tirages en cours suivants.
|
||||||
</div>
|
</div>
|
||||||
<div class="bootstrap-form-reduce">
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ subform|bootstrap }}
|
<table class="table table-striped stupidtable">
|
||||||
</div>
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th data-sort="string">Titre</th>
|
||||||
|
<th data-sort="int">Date</th>
|
||||||
|
<th data-sort="string">Lieu</th>
|
||||||
|
<th data-sort="int">Prix</th>
|
||||||
|
<th>Vendue par</th>
|
||||||
|
<th data-sort="int">Tirage le</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for checkbox in subform.reventes %}{{ checkbox }}{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<input type="submit"
|
<input type="submit"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
|
@ -49,4 +74,26 @@
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not annul_exists and not sub_exists %}
|
||||||
|
<div class="bg-info text-info center-block">
|
||||||
|
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
|
||||||
|
Aucune revente n'est active pour le moment !
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<script language="JavaScript">
|
||||||
|
$(function(){
|
||||||
|
$("table.stupidtable").stupidtable();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("tr").click(function() {
|
||||||
|
$(this).find("input[type=checkbox]").click()
|
||||||
|
});
|
||||||
|
|
||||||
|
$("input[type=checkbox]").click(function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,113 +0,0 @@
|
||||||
{% load staticfiles %}
|
|
||||||
|
|
||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<base target="_parent"/>
|
|
||||||
<style>
|
|
||||||
@font-face {
|
|
||||||
font-family: josefinsans;
|
|
||||||
src: url({% static "fonts/josefinsans.ttf" %});
|
|
||||||
}
|
|
||||||
|
|
||||||
*::-moz-selection {
|
|
||||||
background: #B0B0B0;
|
|
||||||
}
|
|
||||||
|
|
||||||
*::selection {
|
|
||||||
background: #B0B0B0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.descTable{
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto 1em;
|
|
||||||
border-bottom: 2px solid;
|
|
||||||
border-collapse: collapse;
|
|
||||||
border-spacing: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 2;
|
|
||||||
max-width: 100%;
|
|
||||||
background-color: transparent;
|
|
||||||
font-family: 'josefinsans', 'Arial';
|
|
||||||
font-weight: 700;
|
|
||||||
color: #5a5a5a;
|
|
||||||
}
|
|
||||||
|
|
||||||
img{
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<meta charset="utf8" />
|
|
||||||
</head>
|
|
||||||
<script src="https://code.jquery.com/jquery-3.1.0.min.js"></script>
|
|
||||||
<body>
|
|
||||||
{% for show in shows %}
|
|
||||||
<table class="descTable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th colspan="2"><p style="text-align:center;font-size:22px;">{{ show.title }}</p></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><p style="text-align: left;">{{ show.location }}</p></td><td class="column-2"><p style="text-align: right;">{{ show.category }}</p></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><p style="text-align: left;">{{ show.date|date:"l j F Y - H\hi" }}</p></td><td class="column-2"><p style="text-align: right;">{{ show.slots }} place{{ show.slots|pluralize}} {% if show.slots_description != "" %}({{ show.slots_description }}){% endif %} - {{ show.price }} euro{{ show.price|pluralize}}</p></td>
|
|
||||||
</tr>
|
|
||||||
{% if show.vips %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="2"><p style="text-align: justify;">{{ show.vips }}</p></td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="2">
|
|
||||||
<p style="text-align: justify;">{{ show.description }}</p>
|
|
||||||
{% for quote in show.quote_set.all %}
|
|
||||||
<p style="text-align:center; font-style: italic;">«{{ quote.text }}»{% if quote.author %} - {{ quote.author }}{% endif %}</p>
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% if show.image %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="2"><p style="text-align:center;"><a href="{{ show.ext_link }}"><img class="imgDesc" style="display: inline;" src="{{ MEDIA_URL }}{{ show.image }}" alt="{{ show.title }}"></a></p></td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endfor %}
|
|
||||||
<script>
|
|
||||||
// Correction de la taille des images
|
|
||||||
|
|
||||||
/*$(document).ready(function() {
|
|
||||||
$(".descTable").each(function() {
|
|
||||||
$(this).width($("body").width());
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".imgDesc").on("load", function() {
|
|
||||||
|
|
||||||
// Dimensions
|
|
||||||
origHeight = 500; // Hauteur souhaitée
|
|
||||||
|
|
||||||
w = $(this).width();
|
|
||||||
h = $(this).height();
|
|
||||||
r = w/h; // Ratio de l'image
|
|
||||||
maxWidth = $("body").width();
|
|
||||||
|
|
||||||
if (r * origHeight > maxWidth)
|
|
||||||
{
|
|
||||||
$(this).width(maxWidth);
|
|
||||||
$(this).height(maxWidth/r);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$(this).width(r * origHeight);
|
|
||||||
$(this).height(origHeight);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});*/
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "base_title.html" %}
|
{% extends "base_title.html" %}
|
||||||
{% load staticfiles %}
|
{% load static %}
|
||||||
|
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
|
<link type="text/css" rel="stylesheet" href="{% static "bda/css/bda.css" %}" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
|
@ -32,9 +32,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<script type="text/javascript"
|
|
||||||
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}">
|
|
||||||
</script>
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(function(){
|
$(function(){
|
||||||
$("table.etat-bda").stupidtable();
|
$("table.etat-bda").stupidtable();
|
||||||
|
@ -51,6 +49,5 @@
|
||||||
<h3> Exports </h3>
|
<h3> Exports </h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{% url 'bda-unpaid' tirage_id %}">Mailing list impayés</a>
|
<li><a href="{% url 'bda-unpaid' tirage_id %}">Mailing list impayés</a>
|
||||||
<li><a href="{% url 'bda-descriptions' tirage_id %}">Lien vers les descriptions des spectacles, à utiliser dans une page wordpress</a>
|
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
65
bda/tests/mixins.py
Normal file
65
bda/tests/mixins.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from shared.tests.mixins import ViewTestCaseMixin
|
||||||
|
|
||||||
|
from ..models import CategorieSpectacle, Salle, Spectacle, Tirage
|
||||||
|
from .utils import create_user
|
||||||
|
|
||||||
|
|
||||||
|
class BdAViewTestCaseMixin(ViewTestCaseMixin):
|
||||||
|
def get_users_base(self):
|
||||||
|
return {
|
||||||
|
"bda_other": create_user(username="bda_other"),
|
||||||
|
"bda_member": create_user(username="bda_member", is_cof=True),
|
||||||
|
"bda_staff": create_user(username="bda_staff", is_cof=True, is_buro=True),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BdATestHelpers:
|
||||||
|
bda_testdata = False
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
if self.bda_testdata:
|
||||||
|
self.load_bda_testdata()
|
||||||
|
|
||||||
|
def load_bda_testdata(self):
|
||||||
|
self.tirage = Tirage.objects.create(
|
||||||
|
title="Test tirage",
|
||||||
|
appear_catalogue=True,
|
||||||
|
ouverture=timezone.now(),
|
||||||
|
fermeture=timezone.now(),
|
||||||
|
)
|
||||||
|
self.category = CategorieSpectacle.objects.create(name="Category")
|
||||||
|
self.location = Salle.objects.create(name="here")
|
||||||
|
self.show1 = Spectacle.objects.create(
|
||||||
|
title="foo",
|
||||||
|
date=timezone.now(),
|
||||||
|
location=self.location,
|
||||||
|
price=0,
|
||||||
|
slots=42,
|
||||||
|
tirage=self.tirage,
|
||||||
|
listing=False,
|
||||||
|
category=self.category,
|
||||||
|
)
|
||||||
|
self.show2 = Spectacle.objects.create(
|
||||||
|
title="bar",
|
||||||
|
date=timezone.now(),
|
||||||
|
location=self.location,
|
||||||
|
price=1,
|
||||||
|
slots=142,
|
||||||
|
tirage=self.tirage,
|
||||||
|
listing=False,
|
||||||
|
category=self.category,
|
||||||
|
)
|
||||||
|
self.show3 = Spectacle.objects.create(
|
||||||
|
title="baz",
|
||||||
|
date=timezone.now(),
|
||||||
|
location=self.location,
|
||||||
|
price=2,
|
||||||
|
slots=242,
|
||||||
|
tirage=self.tirage,
|
||||||
|
listing=False,
|
||||||
|
category=self.category,
|
||||||
|
)
|
|
@ -19,8 +19,6 @@ User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class SpectacleReventeTests(TestCase):
|
class SpectacleReventeTests(TestCase):
|
||||||
fixtures = ["gestioncof/management/data/custommail.json"]
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
|
|
||||||
|
@ -69,7 +67,7 @@ class SpectacleReventeTests(TestCase):
|
||||||
revente = self.rev
|
revente = self.rev
|
||||||
|
|
||||||
wanted_by = [self.p1, self.p2, self.p3]
|
wanted_by = [self.p1, self.p2, self.p3]
|
||||||
revente.confirmed_entry = wanted_by
|
revente.confirmed_entry.set(wanted_by)
|
||||||
|
|
||||||
with mock.patch("bda.models.random.choice") as mc:
|
with mock.patch("bda.models.random.choice") as mc:
|
||||||
# Set winner to self.p1.
|
# Set winner to self.p1.
|
||||||
|
|
|
@ -1,221 +1,306 @@
|
||||||
import json
|
import json
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from urllib.parse import urlencode
|
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
from django.utils import timezone
|
from django.urls import reverse
|
||||||
|
from django.utils import formats, timezone
|
||||||
|
|
||||||
from ..models import CategorieSpectacle, Salle, Spectacle, Tirage
|
from ..models import Participant, Tirage
|
||||||
|
from .mixins import BdATestHelpers, BdAViewTestCaseMixin
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
def create_user(username, is_cof=False, is_buro=False):
|
class InscriptionViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
|
||||||
user = User.objects.create_user(username=username, password=username)
|
url_name = "bda-tirage-inscription"
|
||||||
user.profile.is_cof = is_cof
|
|
||||||
user.profile.is_buro = is_buro
|
|
||||||
user.profile.save()
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
http_methods = ["GET", "POST"]
|
||||||
|
|
||||||
def user_is_cof(user):
|
auth_user = "bda_member"
|
||||||
return (user is not None) and user.profile.is_cof
|
auth_forbidden = [None, "bda_other"]
|
||||||
|
|
||||||
|
bda_testdata = True
|
||||||
|
|
||||||
def user_is_staff(user):
|
@property
|
||||||
return (user is not None) and user.profile.is_buro
|
def url_kwargs(self):
|
||||||
|
return {"tirage_id": self.tirage.id}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_expected(self):
|
||||||
|
return "/gestion/bda/inscription/{}".format(self.tirage.id)
|
||||||
|
|
||||||
class BdATestHelpers:
|
def test_get_opened(self):
|
||||||
def setUp(self):
|
self.tirage.ouverture = timezone.now() - timedelta(days=1)
|
||||||
# Some user with different access privileges
|
self.tirage.fermeture = timezone.now() + timedelta(days=1)
|
||||||
staff = create_user(username="bda_staff", is_cof=True, is_buro=True)
|
self.tirage.save()
|
||||||
staff_c = Client()
|
|
||||||
staff_c.force_login(staff)
|
|
||||||
|
|
||||||
member = create_user(username="bda_member", is_cof=True)
|
resp = self.client.get(self.url)
|
||||||
member_c = Client()
|
|
||||||
member_c.force_login(member)
|
|
||||||
|
|
||||||
other = create_user(username="bda_other")
|
self.assertEqual(resp.status_code, 200)
|
||||||
other_c = Client()
|
self.assertFalse(resp.context["messages"])
|
||||||
other_c.force_login(other)
|
|
||||||
|
|
||||||
self.client_matrix = [
|
def test_get_closed_future(self):
|
||||||
(staff, staff_c),
|
self.tirage.ouverture = timezone.now() + timedelta(days=1)
|
||||||
(member, member_c),
|
self.tirage.fermeture = timezone.now() + timedelta(days=2)
|
||||||
(other, other_c),
|
self.tirage.save()
|
||||||
(None, Client()),
|
|
||||||
]
|
|
||||||
|
|
||||||
def require_custommails(self):
|
resp = self.client.get(self.url)
|
||||||
from django.core.management import call_command
|
|
||||||
|
|
||||||
call_command("syncmails", verbosity=0)
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertIn(
|
||||||
def check_restricted_access(
|
"Le tirage n'est pas encore ouvert : ouverture le {}".format(
|
||||||
self, url, validate_user=user_is_cof, redirect_url=None
|
formats.localize(timezone.template_localtime(self.tirage.ouverture))
|
||||||
):
|
|
||||||
def craft_redirect_url(user):
|
|
||||||
if redirect_url:
|
|
||||||
return redirect_url
|
|
||||||
elif user is None:
|
|
||||||
# client is not logged in
|
|
||||||
login_url = "/login"
|
|
||||||
if url:
|
|
||||||
login_url += "?{}".format(urlencode({"next": url}, safe="/"))
|
|
||||||
return login_url
|
|
||||||
else:
|
|
||||||
return "/"
|
|
||||||
|
|
||||||
for (user, client) in self.client_matrix:
|
|
||||||
resp = client.get(url, follow=True)
|
|
||||||
if validate_user(user):
|
|
||||||
self.assertEqual(200, resp.status_code)
|
|
||||||
else:
|
|
||||||
self.assertRedirects(resp, craft_redirect_url(user))
|
|
||||||
|
|
||||||
|
|
||||||
class TestBdAViews(BdATestHelpers, TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
# Signals handlers on login/logout send messages.
|
|
||||||
# Due to the way the Django' test Client performs login, this raise an
|
|
||||||
# error. As workaround, we mock the Django' messages module.
|
|
||||||
patcher_messages = mock.patch("gestioncof.signals.messages")
|
|
||||||
patcher_messages.start()
|
|
||||||
self.addCleanup(patcher_messages.stop)
|
|
||||||
# Set up the helpers
|
|
||||||
super().setUp()
|
|
||||||
# Some BdA stuff
|
|
||||||
self.tirage = Tirage.objects.create(
|
|
||||||
title="Test tirage",
|
|
||||||
appear_catalogue=True,
|
|
||||||
ouverture=timezone.now(),
|
|
||||||
fermeture=timezone.now(),
|
|
||||||
)
|
|
||||||
self.category = CategorieSpectacle.objects.create(name="Category")
|
|
||||||
self.location = Salle.objects.create(name="here")
|
|
||||||
Spectacle.objects.bulk_create(
|
|
||||||
[
|
|
||||||
Spectacle(
|
|
||||||
title="foo",
|
|
||||||
date=timezone.now(),
|
|
||||||
location=self.location,
|
|
||||||
price=0,
|
|
||||||
slots=42,
|
|
||||||
tirage=self.tirage,
|
|
||||||
listing=False,
|
|
||||||
category=self.category,
|
|
||||||
),
|
),
|
||||||
Spectacle(
|
[str(msg) for msg in resp.context["messages"]],
|
||||||
title="bar",
|
|
||||||
date=timezone.now(),
|
|
||||||
location=self.location,
|
|
||||||
price=1,
|
|
||||||
slots=142,
|
|
||||||
tirage=self.tirage,
|
|
||||||
listing=False,
|
|
||||||
category=self.category,
|
|
||||||
),
|
|
||||||
Spectacle(
|
|
||||||
title="baz",
|
|
||||||
date=timezone.now(),
|
|
||||||
location=self.location,
|
|
||||||
price=2,
|
|
||||||
slots=242,
|
|
||||||
tirage=self.tirage,
|
|
||||||
listing=False,
|
|
||||||
category=self.category,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_bda_inscriptions(self):
|
def test_get_closed_past(self):
|
||||||
# TODO: test the form
|
self.tirage.ouverture = timezone.now() - timedelta(days=2)
|
||||||
url = "/bda/inscription/{}".format(self.tirage.id)
|
self.tirage.fermeture = timezone.now() - timedelta(days=1)
|
||||||
self.check_restricted_access(url)
|
self.tirage.save()
|
||||||
|
|
||||||
def test_bda_places(self):
|
resp = self.client.get(self.url)
|
||||||
url = "/bda/places/{}".format(self.tirage.id)
|
|
||||||
self.check_restricted_access(url)
|
|
||||||
|
|
||||||
def test_etat_places(self):
|
self.assertEqual(resp.status_code, 200)
|
||||||
url = "/bda/etat-places/{}".format(self.tirage.id)
|
self.assertIn(
|
||||||
self.check_restricted_access(url)
|
" C'est fini : tirage au sort dans la journée !",
|
||||||
|
[str(msg) for msg in resp.context["messages"]],
|
||||||
|
)
|
||||||
|
|
||||||
def test_perform_tirage(self):
|
def get_base_post_data(self):
|
||||||
# Only staff member can perform a tirage
|
return {
|
||||||
url = "/bda/tirage/{}".format(self.tirage.id)
|
"choixspectacle_set-TOTAL_FORMS": "3",
|
||||||
self.check_restricted_access(url, validate_user=user_is_staff)
|
"choixspectacle_set-INITIAL_FORMS": "0",
|
||||||
|
"choixspectacle_set-MIN_NUM_FORMS": "0",
|
||||||
|
"choixspectacle_set-MAX_NUM_FORMS": "1000",
|
||||||
|
}
|
||||||
|
|
||||||
_, staff_c = self.client_matrix[0]
|
base_post_data = property(get_base_post_data)
|
||||||
|
|
||||||
|
def test_post(self):
|
||||||
|
self.tirage.ouverture = timezone.now() - timedelta(days=1)
|
||||||
|
self.tirage.fermeture = timezone.now() + timedelta(days=1)
|
||||||
|
self.tirage.save()
|
||||||
|
|
||||||
|
data = dict(
|
||||||
|
self.base_post_data,
|
||||||
|
**{
|
||||||
|
"choixspectacle_set-TOTAL_FORMS": "2",
|
||||||
|
"choixspectacle_set-0-id": "",
|
||||||
|
"choixspectacle_set-0-participant": "",
|
||||||
|
"choixspectacle_set-0-spectacle": str(self.show1.pk),
|
||||||
|
"choixspectacle_set-0-double_choice": "1",
|
||||||
|
"choixspectacle_set-0-priority": "2",
|
||||||
|
"choixspectacle_set-1-id": "",
|
||||||
|
"choixspectacle_set-1-participant": "",
|
||||||
|
"choixspectacle_set-1-spectacle": str(self.show2.pk),
|
||||||
|
"choixspectacle_set-1-double_choice": "autoquit",
|
||||||
|
"choixspectacle_set-1-priority": "1",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertIn(
|
||||||
|
"Votre inscription a été mise à jour avec succès !",
|
||||||
|
[str(msg) for msg in resp.context["messages"]],
|
||||||
|
)
|
||||||
|
participant = Participant.objects.get(
|
||||||
|
user=self.users["bda_member"], tirage=self.tirage
|
||||||
|
)
|
||||||
|
self.assertSetEqual(
|
||||||
|
set(
|
||||||
|
participant.choixspectacle_set.values_list(
|
||||||
|
"priority", "spectacle_id", "double_choice"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
{(1, self.show2.pk, "autoquit"), (2, self.show1.pk, "1")},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_post_state_changed(self):
|
||||||
|
self.tirage.ouverture = timezone.now() - timedelta(days=1)
|
||||||
|
self.tirage.fermeture = timezone.now() + timedelta(days=1)
|
||||||
|
self.tirage.save()
|
||||||
|
|
||||||
|
data = {"dbstate": "different"}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertIn(
|
||||||
|
"Impossible d'enregistrer vos modifications : vous avez apporté d'autres "
|
||||||
|
"modifications entre temps.",
|
||||||
|
[str(msg) for msg in resp.context["messages"]],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PlacesViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
|
||||||
|
url_name = "bda-places-attribuees"
|
||||||
|
|
||||||
|
auth_user = "bda_member"
|
||||||
|
auth_forbidden = [None, "bda_other"]
|
||||||
|
|
||||||
|
bda_testdata = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_kwargs(self):
|
||||||
|
return {"tirage_id": self.tirage.id}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_expected(self):
|
||||||
|
return "/gestion/bda/places/{}".format(self.tirage.id)
|
||||||
|
|
||||||
|
|
||||||
|
class EtatPlacesViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
|
||||||
|
url_name = "bda-etat-places"
|
||||||
|
|
||||||
|
auth_user = "bda_member"
|
||||||
|
auth_forbidden = [None, "bda_other"]
|
||||||
|
|
||||||
|
bda_testdata = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_kwargs(self):
|
||||||
|
return {"tirage_id": self.tirage.id}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_expected(self):
|
||||||
|
return "/gestion/bda/etat-places/{}".format(self.tirage.id)
|
||||||
|
|
||||||
|
|
||||||
|
class TirageViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
|
||||||
|
url_name = "bda-tirage"
|
||||||
|
|
||||||
|
http_methods = ["GET", "POST"]
|
||||||
|
|
||||||
|
auth_user = "bda_staff"
|
||||||
|
auth_forbidden = [None, "bda_other", "bda_member"]
|
||||||
|
|
||||||
|
bda_testdata = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_kwargs(self):
|
||||||
|
return {"tirage_id": self.tirage.id}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_expected(self):
|
||||||
|
return "/gestion/bda/tirage/{}".format(self.tirage.id)
|
||||||
|
|
||||||
|
def test_perform_tirage_disabled(self):
|
||||||
# Cannot be performed if disabled
|
# Cannot be performed if disabled
|
||||||
self.tirage.enable_do_tirage = False
|
self.tirage.enable_do_tirage = False
|
||||||
self.tirage.save()
|
self.tirage.save()
|
||||||
resp = staff_c.get(url)
|
resp = self.client.get(self.url)
|
||||||
self.assertTemplateUsed(resp, "tirage-failed.html")
|
self.assertTemplateUsed(resp, "tirage-failed.html")
|
||||||
|
|
||||||
|
def test_perform_tirage_opened_registrations(self):
|
||||||
# Cannot be performed if registrations are still open
|
# Cannot be performed if registrations are still open
|
||||||
self.tirage.enable_do_tirage = True
|
self.tirage.enable_do_tirage = True
|
||||||
self.tirage.fermeture = timezone.now() + timedelta(seconds=3600)
|
self.tirage.fermeture = timezone.now() + timedelta(seconds=3600)
|
||||||
self.tirage.save()
|
self.tirage.save()
|
||||||
resp = staff_c.get(url)
|
resp = self.client.get(self.url)
|
||||||
self.assertTemplateUsed(resp, "tirage-failed.html")
|
self.assertTemplateUsed(resp, "tirage-failed.html")
|
||||||
|
|
||||||
|
def test_perform_tirage(self):
|
||||||
# Otherwise, perform the tirage
|
# Otherwise, perform the tirage
|
||||||
|
self.tirage.enable_do_tirage = True
|
||||||
self.tirage.fermeture = timezone.now()
|
self.tirage.fermeture = timezone.now()
|
||||||
self.tirage.save()
|
self.tirage.save()
|
||||||
resp = staff_c.get(url)
|
resp = self.client.get(self.url)
|
||||||
self.assertTemplateNotUsed(resp, "tirage-failed.html")
|
self.assertTemplateNotUsed(resp, "tirage-failed.html")
|
||||||
|
|
||||||
def test_spectacles_list(self):
|
|
||||||
url = "/bda/spectacles/{}".format(self.tirage.id)
|
|
||||||
self.check_restricted_access(url, validate_user=user_is_staff)
|
|
||||||
|
|
||||||
def test_spectacle_detail(self):
|
class SpectacleListViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
|
||||||
show = self.tirage.spectacle_set.first()
|
url_name = "bda-liste-spectacles"
|
||||||
url = "/bda/spectacles/{}/{}".format(self.tirage.id, show.id)
|
|
||||||
self.check_restricted_access(url, validate_user=user_is_staff)
|
|
||||||
|
|
||||||
def test_tirage_unpaid(self):
|
auth_user = "bda_staff"
|
||||||
url = "/bda/spectacles/unpaid/{}".format(self.tirage.id)
|
auth_forbidden = [None, "bda_other", "bda_member"]
|
||||||
self.check_restricted_access(url, validate_user=user_is_staff)
|
|
||||||
|
|
||||||
def test_send_reminders(self):
|
bda_testdata = True
|
||||||
self.require_custommails()
|
|
||||||
# Just get the page
|
@property
|
||||||
show = self.tirage.spectacle_set.first()
|
def url_kwargs(self):
|
||||||
url = "/bda/mails-rappel/{}".format(show.id)
|
return {"tirage_id": self.tirage.id}
|
||||||
self.check_restricted_access(url, validate_user=user_is_staff)
|
|
||||||
# Actually send the reminder emails
|
@property
|
||||||
_, staff_c = self.client_matrix[0]
|
def url_expected(self):
|
||||||
resp = staff_c.post(url)
|
return "/gestion/bda/spectacles/{}".format(self.tirage.id)
|
||||||
|
|
||||||
|
|
||||||
|
class SpectacleViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
|
||||||
|
url_name = "bda-spectacle"
|
||||||
|
|
||||||
|
auth_user = "bda_staff"
|
||||||
|
auth_forbidden = [None, "bda_other", "bda_member"]
|
||||||
|
|
||||||
|
bda_testdata = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_kwargs(self):
|
||||||
|
return {"tirage_id": self.tirage.id, "spectacle_id": self.show1.id}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_expected(self):
|
||||||
|
return "/gestion/bda/spectacles/{}/{}".format(self.tirage.id, self.show1.id)
|
||||||
|
|
||||||
|
|
||||||
|
class UnpaidViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
|
||||||
|
url_name = "bda-unpaid"
|
||||||
|
|
||||||
|
auth_user = "bda_staff"
|
||||||
|
auth_forbidden = [None, "bda_other", "bda_member"]
|
||||||
|
|
||||||
|
bda_testdata = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_kwargs(self):
|
||||||
|
return {"tirage_id": self.tirage.id}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_expected(self):
|
||||||
|
return "/gestion/bda/spectacles/unpaid/{}".format(self.tirage.id)
|
||||||
|
|
||||||
|
|
||||||
|
class SendRemindersViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
|
||||||
|
url_name = "bda-rappels"
|
||||||
|
|
||||||
|
auth_user = "bda_staff"
|
||||||
|
auth_forbidden = [None, "bda_other", "bda_member"]
|
||||||
|
|
||||||
|
bda_testdata = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_kwargs(self):
|
||||||
|
return {"spectacle_id": self.show1.id}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_expected(self):
|
||||||
|
return "/gestion/bda/mails-rappel/{}".format(self.show1.id)
|
||||||
|
|
||||||
|
def test_post(self):
|
||||||
|
resp = self.client.post(self.url)
|
||||||
self.assertEqual(200, resp.status_code)
|
self.assertEqual(200, resp.status_code)
|
||||||
# TODO: check that emails are sent
|
# TODO: check that emails are sent
|
||||||
|
|
||||||
def test_catalogue_api(self):
|
|
||||||
url_list = "/bda/catalogue/list"
|
|
||||||
url_details = "/bda/catalogue/details?id={}".format(self.tirage.id)
|
|
||||||
url_descriptions = "/bda/catalogue/descriptions?id={}".format(self.tirage.id)
|
|
||||||
|
|
||||||
# Anyone can get
|
class CatalogueViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
|
||||||
def anyone_can_get(url):
|
auth_user = None
|
||||||
self.check_restricted_access(url, validate_user=lambda user: True)
|
auth_forbidden = []
|
||||||
|
|
||||||
anyone_can_get(url_list)
|
bda_testdata = True
|
||||||
anyone_can_get(url_details)
|
|
||||||
anyone_can_get(url_descriptions)
|
|
||||||
|
|
||||||
# The resulting JSON contains the information
|
def test_api_list(self):
|
||||||
_, client = self.client_matrix[0]
|
url_list = "/gestion/bda/catalogue/list"
|
||||||
|
resp = self.client.get(url_list)
|
||||||
# List
|
|
||||||
resp = client.get(url_list)
|
|
||||||
self.assertJSONEqual(
|
self.assertJSONEqual(
|
||||||
resp.content.decode("utf-8"),
|
resp.content.decode("utf-8"),
|
||||||
[{"id": self.tirage.id, "title": self.tirage.title}],
|
[{"id": self.tirage.id, "title": self.tirage.title}],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Details
|
def test_api_details(self):
|
||||||
resp = client.get(url_details)
|
url_details = "/gestion/bda/catalogue/details?id={}".format(self.tirage.id)
|
||||||
|
resp = self.client.get(url_details)
|
||||||
self.assertJSONEqual(
|
self.assertJSONEqual(
|
||||||
resp.content.decode("utf-8"),
|
resp.content.decode("utf-8"),
|
||||||
{
|
{
|
||||||
|
@ -224,8 +309,11 @@ class TestBdAViews(BdATestHelpers, TestCase):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Descriptions
|
def test_api_descriptions(self):
|
||||||
resp = client.get(url_descriptions)
|
url_descriptions = "/gestion/bda/catalogue/descriptions?id={}".format(
|
||||||
|
self.tirage.id
|
||||||
|
)
|
||||||
|
resp = self.client.get(url_descriptions)
|
||||||
raw = resp.content.decode("utf-8")
|
raw = resp.content.decode("utf-8")
|
||||||
try:
|
try:
|
||||||
results = json.loads(raw)
|
results = json.loads(raw)
|
||||||
|
@ -238,6 +326,43 @@ class TestBdAViews(BdATestHelpers, TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ----- BdA Revente --------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
def make_participant(name: str, tirage: Tirage) -> User:
|
||||||
|
user = User.objects.create_user(username=name, password=name)
|
||||||
|
user.profile.is_cof = True
|
||||||
|
user.profile.save()
|
||||||
|
Participant.objects.create(user=user, tirage=tirage)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
class TestReventeManageTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tirage = Tirage.objects.create(
|
||||||
|
title="tirage1",
|
||||||
|
ouverture=timezone.now(),
|
||||||
|
fermeture=timezone.now() + timedelta(days=90),
|
||||||
|
)
|
||||||
|
self.user = make_participant("toto", self.tirage)
|
||||||
|
self.url = reverse("bda-revente-manage", args=[self.tirage.id])
|
||||||
|
|
||||||
|
# Signals handlers on login/logout send messages.
|
||||||
|
# Due to the way the Django' test Client performs login, this raise an
|
||||||
|
# error. As workaround, we mock the Django' messages module.
|
||||||
|
patcher_messages = mock.patch("gestioncof.signals.messages")
|
||||||
|
patcher_messages.start()
|
||||||
|
self.addCleanup(patcher_messages.stop)
|
||||||
|
|
||||||
|
def test_can_get(self):
|
||||||
|
client = Client()
|
||||||
|
client.force_login(
|
||||||
|
self.user, backend="django.contrib.auth.backends.ModelBackend"
|
||||||
|
)
|
||||||
|
r = client.get(self.url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
class TestBdaRevente:
|
class TestBdaRevente:
|
||||||
pass
|
pass
|
||||||
# TODO
|
# TODO
|
||||||
|
|
36
bda/tests/utils.py
Normal file
36
bda/tests/utils.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from ..models import CategorieSpectacle, Salle, Spectacle, Tirage
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(username, is_cof=False, is_buro=False):
|
||||||
|
user = User.objects.create_user(username=username, password=username)
|
||||||
|
user.profile.is_cof = is_cof
|
||||||
|
user.profile.is_buro = is_buro
|
||||||
|
user.profile.save()
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def user_is_cof(user):
|
||||||
|
return (user is not None) and user.profile.is_cof
|
||||||
|
|
||||||
|
|
||||||
|
def user_is_staff(user):
|
||||||
|
return (user is not None) and user.profile.is_buro
|
||||||
|
|
||||||
|
|
||||||
|
def create_spectacle(**kwargs):
|
||||||
|
defaults = {
|
||||||
|
"title": "Title",
|
||||||
|
"category": CategorieSpectacle.objects.first(),
|
||||||
|
"date": (timezone.now() + timedelta(days=7)).date(),
|
||||||
|
"location": Salle.objects.first(),
|
||||||
|
"price": 10.0,
|
||||||
|
"slots": 20,
|
||||||
|
"tirage": Tirage.objects.first(),
|
||||||
|
"listing": False,
|
||||||
|
}
|
||||||
|
return Spectacle.objects.create(**dict(defaults, **kwargs))
|
49
bda/urls.py
49
bda/urls.py
|
@ -1,75 +1,80 @@
|
||||||
from django.conf.urls import url
|
from django.urls import re_path
|
||||||
|
|
||||||
from bda import views
|
from bda import views
|
||||||
from bda.views import SpectacleListView
|
from bda.views import SpectacleListView
|
||||||
from gestioncof.decorators import buro_required
|
from gestioncof.decorators import buro_required
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(
|
re_path(
|
||||||
r"^inscription/(?P<tirage_id>\d+)$",
|
r"^inscription/(?P<tirage_id>\d+)$",
|
||||||
views.inscription,
|
views.inscription,
|
||||||
name="bda-tirage-inscription",
|
name="bda-tirage-inscription",
|
||||||
),
|
),
|
||||||
url(r"^places/(?P<tirage_id>\d+)$", views.places, name="bda-places-attribuees"),
|
re_path(r"^places/(?P<tirage_id>\d+)$", views.places, name="bda-places-attribuees"),
|
||||||
url(r"^etat-places/(?P<tirage_id>\d+)$", views.etat_places, name="bda-etat-places"),
|
re_path(
|
||||||
url(r"^tirage/(?P<tirage_id>\d+)$", views.tirage),
|
r"^etat-places/(?P<tirage_id>\d+)$", views.etat_places, name="bda-etat-places"
|
||||||
url(
|
),
|
||||||
|
re_path(r"^tirage/(?P<tirage_id>\d+)$", views.tirage, name="bda-tirage"),
|
||||||
|
re_path(
|
||||||
r"^spectacles/(?P<tirage_id>\d+)$",
|
r"^spectacles/(?P<tirage_id>\d+)$",
|
||||||
buro_required(SpectacleListView.as_view()),
|
buro_required(SpectacleListView.as_view()),
|
||||||
name="bda-liste-spectacles",
|
name="bda-liste-spectacles",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^spectacles/(?P<tirage_id>\d+)/(?P<spectacle_id>\d+)$",
|
r"^spectacles/(?P<tirage_id>\d+)/(?P<spectacle_id>\d+)$",
|
||||||
views.spectacle,
|
views.spectacle,
|
||||||
name="bda-spectacle",
|
name="bda-spectacle",
|
||||||
),
|
),
|
||||||
url(r"^spectacles/unpaid/(?P<tirage_id>\d+)$", views.unpaid, name="bda-unpaid"),
|
re_path(
|
||||||
url(
|
r"^spectacles/unpaid/(?P<tirage_id>\d+)$",
|
||||||
|
views.UnpaidParticipants.as_view(),
|
||||||
|
name="bda-unpaid",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
r"^spectacles/autocomplete$",
|
r"^spectacles/autocomplete$",
|
||||||
views.spectacle_autocomplete,
|
views.spectacle_autocomplete,
|
||||||
name="bda-spectacle-autocomplete",
|
name="bda-spectacle-autocomplete",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^participants/autocomplete$",
|
r"^participants/autocomplete$",
|
||||||
views.participant_autocomplete,
|
views.participant_autocomplete,
|
||||||
name="bda-participant-autocomplete",
|
name="bda-participant-autocomplete",
|
||||||
),
|
),
|
||||||
# Urls BdA-Revente
|
# Urls BdA-Revente
|
||||||
url(
|
re_path(
|
||||||
r"^revente/(?P<tirage_id>\d+)/manage$",
|
r"^revente/(?P<tirage_id>\d+)/manage$",
|
||||||
views.revente_manage,
|
views.revente_manage,
|
||||||
name="bda-revente-manage",
|
name="bda-revente-manage",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^revente/(?P<tirage_id>\d+)/subscribe$",
|
r"^revente/(?P<tirage_id>\d+)/subscribe$",
|
||||||
views.revente_subscribe,
|
views.revente_subscribe,
|
||||||
name="bda-revente-subscribe",
|
name="bda-revente-subscribe",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^revente/(?P<tirage_id>\d+)/tirages$",
|
r"^revente/(?P<tirage_id>\d+)/tirages$",
|
||||||
views.revente_tirages,
|
views.revente_tirages,
|
||||||
name="bda-revente-tirages",
|
name="bda-revente-tirages",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^revente/(?P<spectacle_id>\d+)/buy$",
|
r"^revente/(?P<spectacle_id>\d+)/buy$",
|
||||||
views.revente_buy,
|
views.revente_buy,
|
||||||
name="bda-revente-buy",
|
name="bda-revente-buy",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^revente/(?P<revente_id>\d+)/confirm$",
|
r"^revente/(?P<revente_id>\d+)/confirm$",
|
||||||
views.revente_confirm,
|
views.revente_confirm,
|
||||||
name="bda-revente-confirm",
|
name="bda-revente-confirm",
|
||||||
),
|
),
|
||||||
url(
|
re_path(
|
||||||
r"^revente/(?P<tirage_id>\d+)/shotgun$",
|
r"^revente/(?P<tirage_id>\d+)/shotgun$",
|
||||||
views.revente_shotgun,
|
views.revente_shotgun,
|
||||||
name="bda-revente-shotgun",
|
name="bda-revente-shotgun",
|
||||||
),
|
),
|
||||||
url(r"^mails-rappel/(?P<spectacle_id>\d+)$", views.send_rappel, name="bda-rappels"),
|
re_path(
|
||||||
url(
|
r"^mails-rappel/(?P<spectacle_id>\d+)$", views.send_rappel, name="bda-rappels"
|
||||||
r"^descriptions/(?P<tirage_id>\d+)$",
|
),
|
||||||
views.descriptions_spectacles,
|
re_path(
|
||||||
name="bda-descriptions",
|
r"^catalogue/(?P<request_type>[a-z]+)$", views.catalogue, name="bda-catalogue"
|
||||||
),
|
),
|
||||||
url(r"^catalogue/(?P<request_type>[a-z]+)$", views.catalogue, name="bda-catalogue"),
|
|
||||||
]
|
]
|
||||||
|
|
193
bda/views.py
193
bda/views.py
|
@ -4,19 +4,19 @@ import random
|
||||||
import time
|
import time
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from custommail.models import CustomMail
|
|
||||||
from custommail.shortcuts import send_custom_mail, send_mass_custom_mail
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.exceptions import NON_FIELD_ERRORS
|
||||||
|
from django.core.mail import send_mail, send_mass_mail
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Count, Prefetch
|
from django.db.models import Count, Prefetch
|
||||||
from django.forms.models import inlineformset_factory
|
from django.forms.models import inlineformset_factory
|
||||||
from django.http import HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
|
from django.http import HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
from django.template import loader
|
||||||
from django.template.defaultfilters import pluralize
|
from django.template.defaultfilters import pluralize
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils import formats, timezone
|
from django.utils import formats, timezone
|
||||||
from django.views.generic.list import ListView
|
from django.views.generic.list import ListView
|
||||||
|
|
||||||
|
@ -41,8 +41,8 @@ from bda.models import (
|
||||||
SpectacleRevente,
|
SpectacleRevente,
|
||||||
Tirage,
|
Tirage,
|
||||||
)
|
)
|
||||||
from gestioncof.decorators import buro_required, cof_required
|
from gestioncof.decorators import BuroRequiredMixin, buro_required, cof_required
|
||||||
from utils.views.autocomplete import Select2QuerySetView
|
from shared.views import Select2QuerySetView
|
||||||
|
|
||||||
|
|
||||||
@cof_required
|
@cof_required
|
||||||
|
@ -114,6 +114,7 @@ def places(request, tirage_id):
|
||||||
"spectacle__date", "spectacle"
|
"spectacle__date", "spectacle"
|
||||||
).select_related("spectacle", "spectacle__location")
|
).select_related("spectacle", "spectacle__location")
|
||||||
total = sum(place.spectacle.price for place in places)
|
total = sum(place.spectacle.price for place in places)
|
||||||
|
unpaid = 0
|
||||||
filtered_places = []
|
filtered_places = []
|
||||||
places_dict = {}
|
places_dict = {}
|
||||||
spectacles = []
|
spectacles = []
|
||||||
|
@ -124,6 +125,8 @@ def places(request, tirage_id):
|
||||||
places_dict[place.spectacle].double = True
|
places_dict[place.spectacle].double = True
|
||||||
else:
|
else:
|
||||||
place.double = False
|
place.double = False
|
||||||
|
place.unpaid = 0
|
||||||
|
place.unpaid_price = 0
|
||||||
places_dict[place.spectacle] = place
|
places_dict[place.spectacle] = place
|
||||||
spectacles.append(place.spectacle)
|
spectacles.append(place.spectacle)
|
||||||
filtered_places.append(place)
|
filtered_places.append(place)
|
||||||
|
@ -132,6 +135,12 @@ def places(request, tirage_id):
|
||||||
warning = True
|
warning = True
|
||||||
else:
|
else:
|
||||||
dates.append(date)
|
dates.append(date)
|
||||||
|
|
||||||
|
if not place.paid:
|
||||||
|
unpaid += place.spectacle.price
|
||||||
|
places_dict[place.spectacle].unpaid += 1
|
||||||
|
places_dict[place.spectacle].unpaid_price += place.spectacle.price
|
||||||
|
|
||||||
# On prévient l'utilisateur s'il a deux places à la même date
|
# On prévient l'utilisateur s'il a deux places à la même date
|
||||||
if warning:
|
if warning:
|
||||||
messages.warning(
|
messages.warning(
|
||||||
|
@ -147,6 +156,7 @@ def places(request, tirage_id):
|
||||||
"places": filtered_places,
|
"places": filtered_places,
|
||||||
"tirage": tirage,
|
"tirage": tirage,
|
||||||
"total": total,
|
"total": total,
|
||||||
|
"unpaid": unpaid,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -188,22 +198,40 @@ def inscription(request, tirage_id):
|
||||||
ChoixSpectacle,
|
ChoixSpectacle,
|
||||||
fields=("spectacle", "double_choice", "priority"),
|
fields=("spectacle", "double_choice", "priority"),
|
||||||
formset=InscriptionInlineFormSet,
|
formset=InscriptionInlineFormSet,
|
||||||
|
error_messages={
|
||||||
|
NON_FIELD_ERRORS: {
|
||||||
|
"unique_together": "Vous avez déjà demandé ce voeu plus haut !"
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
success = False
|
|
||||||
stateerror = False
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
# use *this* queryset
|
# use *this* queryset
|
||||||
dbstate = _hash_queryset(participant.choixspectacle_set.all())
|
dbstate = _hash_queryset(participant.choixspectacle_set.all())
|
||||||
if "dbstate" in request.POST and dbstate != request.POST["dbstate"]:
|
if "dbstate" in request.POST and dbstate != request.POST["dbstate"]:
|
||||||
stateerror = True
|
|
||||||
formset = BdaFormSet(instance=participant)
|
formset = BdaFormSet(instance=participant)
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
"Impossible d'enregistrer vos modifications "
|
||||||
|
": vous avez apporté d'autres modifications "
|
||||||
|
"entre temps.",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
formset = BdaFormSet(request.POST, instance=participant)
|
formset = BdaFormSet(request.POST, instance=participant)
|
||||||
if formset.is_valid():
|
if formset.is_valid():
|
||||||
formset.save()
|
formset.save()
|
||||||
success = True
|
|
||||||
formset = BdaFormSet(instance=participant)
|
formset = BdaFormSet(instance=participant)
|
||||||
|
participant.accepte_charte = True
|
||||||
|
participant.save()
|
||||||
|
messages.success(
|
||||||
|
request, "Votre inscription a été mise à jour avec succès !"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
"Une erreur s'est produite lors de l'enregistrement de vos vœux. "
|
||||||
|
"Avez-vous demandé plusieurs fois le même spectacle ?",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
formset = BdaFormSet(instance=participant)
|
formset = BdaFormSet(instance=participant)
|
||||||
# use *this* queryset
|
# use *this* queryset
|
||||||
|
@ -214,18 +242,6 @@ def inscription(request, tirage_id):
|
||||||
total_price += choice.spectacle.price
|
total_price += choice.spectacle.price
|
||||||
if choice.double:
|
if choice.double:
|
||||||
total_price += choice.spectacle.price
|
total_price += choice.spectacle.price
|
||||||
# Messages
|
|
||||||
if success:
|
|
||||||
messages.success(
|
|
||||||
request, "Votre inscription a été mise à jour avec " "succès !"
|
|
||||||
)
|
|
||||||
if stateerror:
|
|
||||||
messages.error(
|
|
||||||
request,
|
|
||||||
"Impossible d'enregistrer vos modifications "
|
|
||||||
": vous avez apporté d'autres modifications "
|
|
||||||
"entre temps.",
|
|
||||||
)
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"bda/inscription-tirage.html",
|
"bda/inscription-tirage.html",
|
||||||
|
@ -234,6 +250,7 @@ def inscription(request, tirage_id):
|
||||||
"total_price": total_price,
|
"total_price": total_price,
|
||||||
"dbstate": dbstate,
|
"dbstate": dbstate,
|
||||||
"tirage": tirage,
|
"tirage": tirage,
|
||||||
|
"charte": participant.accepte_charte,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -267,13 +284,13 @@ def do_tirage(tirage_elt, token):
|
||||||
results = Algorithm(data["shows"], data["members"], choices)(token)
|
results = Algorithm(data["shows"], data["members"], choices)(token)
|
||||||
|
|
||||||
# On compte les places attribuées et les déçus
|
# On compte les places attribuées et les déçus
|
||||||
for (_, members, losers) in results:
|
for _, members, losers in results:
|
||||||
data["total_slots"] += len(members)
|
data["total_slots"] += len(members)
|
||||||
data["total_losers"] += len(losers)
|
data["total_losers"] += len(losers)
|
||||||
|
|
||||||
# On calcule le déficit et les bénéfices pour le BdA
|
# On calcule le déficit et les bénéfices pour le BdA
|
||||||
# FIXME: le traitement de l'opéra est sale
|
# FIXME: le traitement de l'opéra est sale
|
||||||
for (show, members, _) in results:
|
for show, members, _ in results:
|
||||||
deficit = (show.slots - len(members)) * show.price
|
deficit = (show.slots - len(members)) * show.price
|
||||||
data["total_sold"] += show.slots * show.price
|
data["total_sold"] += show.slots * show.price
|
||||||
if deficit >= 0:
|
if deficit >= 0:
|
||||||
|
@ -286,8 +303,8 @@ def do_tirage(tirage_elt, token):
|
||||||
# so assign a single object for each Participant id
|
# so assign a single object for each Participant id
|
||||||
members_uniq = {}
|
members_uniq = {}
|
||||||
members2 = {}
|
members2 = {}
|
||||||
for (show, members, _) in results:
|
for show, members, _ in results:
|
||||||
for (member, _, _, _) in members:
|
for member, _, _, _ in members:
|
||||||
if member.id not in members_uniq:
|
if member.id not in members_uniq:
|
||||||
members_uniq[member.id] = member
|
members_uniq[member.id] = member
|
||||||
members2[member] = []
|
members2[member] = []
|
||||||
|
@ -363,7 +380,7 @@ def tirage(request, tirage_id):
|
||||||
return render(request, "bda-token.html", {"form": form})
|
return render(request, "bda-token.html", {"form": form})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@cof_required
|
||||||
def revente_manage(request, tirage_id):
|
def revente_manage(request, tirage_id):
|
||||||
"""
|
"""
|
||||||
Gestion de ses propres reventes :
|
Gestion de ses propres reventes :
|
||||||
|
@ -374,13 +391,10 @@ def revente_manage(request, tirage_id):
|
||||||
- Annulation d'une revente après que le tirage a eu lieu
|
- Annulation d'une revente après que le tirage a eu lieu
|
||||||
"""
|
"""
|
||||||
tirage = get_object_or_404(Tirage, id=tirage_id)
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
participant, created = Participant.objects.get_or_create(
|
participant, created = Participant.objects.annotate_paid().get_or_create(
|
||||||
user=request.user, tirage=tirage
|
user=request.user, tirage=tirage
|
||||||
)
|
)
|
||||||
|
|
||||||
if not participant.paid:
|
|
||||||
return render(request, "bda/revente/notpaid.html", {})
|
|
||||||
|
|
||||||
resellform = ResellForm(participant, prefix="resell")
|
resellform = ResellForm(participant, prefix="resell")
|
||||||
annulform = AnnulForm(participant, prefix="annul")
|
annulform = AnnulForm(participant, prefix="annul")
|
||||||
soldform = SoldForm(participant, prefix="sold")
|
soldform = SoldForm(participant, prefix="sold")
|
||||||
|
@ -390,7 +404,7 @@ def revente_manage(request, tirage_id):
|
||||||
if "resell" in request.POST:
|
if "resell" in request.POST:
|
||||||
resellform = ResellForm(participant, request.POST, prefix="resell")
|
resellform = ResellForm(participant, request.POST, prefix="resell")
|
||||||
if resellform.is_valid():
|
if resellform.is_valid():
|
||||||
datatuple = []
|
mails = []
|
||||||
attributions = resellform.cleaned_data["attributions"]
|
attributions = resellform.cleaned_data["attributions"]
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
for attribution in attributions:
|
for attribution in attributions:
|
||||||
|
@ -405,16 +419,17 @@ def revente_manage(request, tirage_id):
|
||||||
"show": attribution.spectacle,
|
"show": attribution.spectacle,
|
||||||
"revente": revente,
|
"revente": revente,
|
||||||
}
|
}
|
||||||
datatuple.append(
|
mails.append(
|
||||||
(
|
(
|
||||||
"bda-revente-new",
|
"BdA-Revente : {}".format(attribution.spectacle),
|
||||||
context,
|
loader.render_to_string(
|
||||||
|
"bda/mails/revente-seller.txt", context=context
|
||||||
|
),
|
||||||
settings.MAIL_DATA["revente"]["FROM"],
|
settings.MAIL_DATA["revente"]["FROM"],
|
||||||
[participant.user.email],
|
[participant.user.email],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
revente.save()
|
send_mass_mail(mails)
|
||||||
send_mass_custom_mail(datatuple)
|
|
||||||
# On annule une revente
|
# On annule une revente
|
||||||
elif "annul" in request.POST:
|
elif "annul" in request.POST:
|
||||||
annulform = AnnulForm(participant, request.POST, prefix="annul")
|
annulform = AnnulForm(participant, request.POST, prefix="annul")
|
||||||
|
@ -445,6 +460,10 @@ def revente_manage(request, tirage_id):
|
||||||
new_date = timezone.now() - SpectacleRevente.remorse_time
|
new_date = timezone.now() - SpectacleRevente.remorse_time
|
||||||
revente.reset(new_date=new_date)
|
revente.reset(new_date=new_date)
|
||||||
|
|
||||||
|
sold_exists = soldform.fields["reventes"].queryset.exists()
|
||||||
|
annul_exists = annulform.fields["reventes"].queryset.exists()
|
||||||
|
resell_exists = resellform.fields["attributions"].queryset.exists()
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"bda/revente/manage.html",
|
"bda/revente/manage.html",
|
||||||
|
@ -453,11 +472,14 @@ def revente_manage(request, tirage_id):
|
||||||
"soldform": soldform,
|
"soldform": soldform,
|
||||||
"annulform": annulform,
|
"annulform": annulform,
|
||||||
"resellform": resellform,
|
"resellform": resellform,
|
||||||
|
"sold_exists": sold_exists,
|
||||||
|
"annul_exists": annul_exists,
|
||||||
|
"resell_exists": resell_exists,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@cof_required
|
||||||
def revente_tirages(request, tirage_id):
|
def revente_tirages(request, tirage_id):
|
||||||
"""
|
"""
|
||||||
Affiche à un participant la liste de toutes les reventes en cours (pour un
|
Affiche à un participant la liste de toutes les reventes en cours (pour un
|
||||||
|
@ -500,14 +522,22 @@ def revente_tirages(request, tirage_id):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
annul_exists = annulform.fields["reventes"].queryset.exists()
|
||||||
|
sub_exists = subform.fields["reventes"].queryset.exists()
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"bda/revente/tirages.html",
|
"bda/revente/tirages.html",
|
||||||
{"annulform": annulform, "subform": subform},
|
{
|
||||||
|
"annulform": annulform,
|
||||||
|
"subform": subform,
|
||||||
|
"annul_exists": annul_exists,
|
||||||
|
"sub_exists": sub_exists,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@cof_required
|
||||||
def revente_confirm(request, revente_id):
|
def revente_confirm(request, revente_id):
|
||||||
revente = get_object_or_404(SpectacleRevente, id=revente_id)
|
revente = get_object_or_404(SpectacleRevente, id=revente_id)
|
||||||
participant, _ = Participant.objects.get_or_create(
|
participant, _ = Participant.objects.get_or_create(
|
||||||
|
@ -524,7 +554,7 @@ def revente_confirm(request, revente_id):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@cof_required
|
||||||
def revente_subscribe(request, tirage_id):
|
def revente_subscribe(request, tirage_id):
|
||||||
"""
|
"""
|
||||||
Permet à un participant de sélectionner ses préférences pour les reventes.
|
Permet à un participant de sélectionner ses préférences pour les reventes.
|
||||||
|
@ -541,7 +571,7 @@ def revente_subscribe(request, tirage_id):
|
||||||
form = InscriptionReventeForm(tirage, request.POST)
|
form = InscriptionReventeForm(tirage, request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
choices = form.cleaned_data["spectacles"]
|
choices = form.cleaned_data["spectacles"]
|
||||||
participant.choicesrevente = choices
|
participant.choicesrevente.set(choices)
|
||||||
participant.save()
|
participant.save()
|
||||||
for spectacle in choices:
|
for spectacle in choices:
|
||||||
qset = SpectacleRevente.objects.filter(attribution__spectacle=spectacle)
|
qset = SpectacleRevente.objects.filter(attribution__spectacle=spectacle)
|
||||||
|
@ -569,18 +599,18 @@ def revente_subscribe(request, tirage_id):
|
||||||
)
|
)
|
||||||
# Messages
|
# Messages
|
||||||
if success:
|
if success:
|
||||||
messages.success(request, "Ton inscription a bien été prise en compte")
|
messages.success(request, "Votre inscription a bien été prise en compte")
|
||||||
if deja_revente:
|
if deja_revente:
|
||||||
messages.info(
|
messages.info(
|
||||||
request,
|
request,
|
||||||
"Des reventes existent déjà pour certains de "
|
"Des reventes existent déjà pour certains de "
|
||||||
"ces spectacles, vérifie les places "
|
"ces spectacles, vérifiez les places "
|
||||||
"disponibles sans tirage !",
|
"disponibles sans tirage !",
|
||||||
)
|
)
|
||||||
if inscrit_revente:
|
if inscrit_revente:
|
||||||
shows = map("<li>{!s}</li>".format, inscrit_revente)
|
shows = map("<li>{!s}</li>".format, inscrit_revente)
|
||||||
msg = (
|
msg = (
|
||||||
"Tu as été inscrit à des reventes en cours pour les spectacles "
|
"Vous avez été inscrit·e à des reventes en cours pour les spectacles "
|
||||||
"<ul>{:s}</ul>".format("\n".join(shows))
|
"<ul>{:s}</ul>".format("\n".join(shows))
|
||||||
)
|
)
|
||||||
messages.info(request, msg, extra_tags="safe")
|
messages.info(request, msg, extra_tags="safe")
|
||||||
|
@ -588,7 +618,7 @@ def revente_subscribe(request, tirage_id):
|
||||||
return render(request, "bda/revente/subscribe.html", {"form": form})
|
return render(request, "bda/revente/subscribe.html", {"form": form})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@cof_required
|
||||||
def revente_buy(request, spectacle_id):
|
def revente_buy(request, spectacle_id):
|
||||||
spectacle = get_object_or_404(Spectacle, id=spectacle_id)
|
spectacle = get_object_or_404(Spectacle, id=spectacle_id)
|
||||||
tirage = spectacle.tirage
|
tirage = spectacle.tirage
|
||||||
|
@ -618,12 +648,16 @@ def revente_buy(request, spectacle_id):
|
||||||
"acheteur": request.user,
|
"acheteur": request.user,
|
||||||
"vendeur": revente.seller.user,
|
"vendeur": revente.seller.user,
|
||||||
}
|
}
|
||||||
send_custom_mail(
|
|
||||||
"bda-buy-shotgun",
|
send_mail(
|
||||||
"bda@ens.fr",
|
"BdA-Revente : {}".format(spectacle.title),
|
||||||
|
loader.render_to_string(
|
||||||
|
"bda/mails/revente-shotgun-seller.txt", context=context
|
||||||
|
),
|
||||||
|
request.user.email,
|
||||||
[revente.seller.user.email],
|
[revente.seller.user.email],
|
||||||
context=context,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"bda/revente/mail-success.html",
|
"bda/revente/mail-success.html",
|
||||||
|
@ -637,7 +671,7 @@ def revente_buy(request, spectacle_id):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@cof_required
|
||||||
def revente_shotgun(request, tirage_id):
|
def revente_shotgun(request, tirage_id):
|
||||||
tirage = get_object_or_404(Tirage, id=tirage_id)
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
spectacles = (
|
spectacles = (
|
||||||
|
@ -657,7 +691,7 @@ def revente_shotgun(request, tirage_id):
|
||||||
)
|
)
|
||||||
shotgun = [sp for sp in spectacles if len(sp.shotguns) > 0]
|
shotgun = [sp for sp in spectacles if len(sp.shotguns) > 0]
|
||||||
|
|
||||||
return render(request, "bda/revente/shotgun.html", {"shotgun": shotgun})
|
return render(request, "bda/revente/shotgun.html", {"spectacles": shotgun})
|
||||||
|
|
||||||
|
|
||||||
@buro_required
|
@buro_required
|
||||||
|
@ -676,12 +710,13 @@ def spectacle(request, tirage_id, spectacle_id):
|
||||||
"username": participant.user.username,
|
"username": participant.user.username,
|
||||||
"email": participant.user.email,
|
"email": participant.user.email,
|
||||||
"given": int(attrib.given),
|
"given": int(attrib.given),
|
||||||
"paid": participant.paid,
|
"paid": attrib.paid,
|
||||||
"nb_places": 1,
|
"nb_places": 1,
|
||||||
}
|
}
|
||||||
if participant.id in participants:
|
if participant.id in participants:
|
||||||
participants[participant.id]["nb_places"] += 1
|
participants[participant.id]["nb_places"] += 1
|
||||||
participants[participant.id]["given"] += attrib.given
|
participants[participant.id]["given"] += attrib.given
|
||||||
|
participants[participant.id]["paid"] &= attrib.paid
|
||||||
else:
|
else:
|
||||||
participants[participant.id] = participant_info
|
participants[participant.id] = participant_info
|
||||||
|
|
||||||
|
@ -709,34 +744,37 @@ class SpectacleListView(ListView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@buro_required
|
class UnpaidParticipants(BuroRequiredMixin, ListView):
|
||||||
def unpaid(request, tirage_id):
|
context_object_name = "unpaid"
|
||||||
tirage = get_object_or_404(Tirage, id=tirage_id)
|
template_name = "bda-unpaid.html"
|
||||||
unpaid = (
|
|
||||||
tirage.participant_set.annotate(nb_attributions=Count("attribution"))
|
def get_queryset(self):
|
||||||
.filter(paid=False, nb_attributions__gt=0)
|
return (
|
||||||
|
Participant.objects.annotate_paid()
|
||||||
|
.filter(tirage__id=self.kwargs["tirage_id"], paid=False)
|
||||||
.select_related("user")
|
.select_related("user")
|
||||||
)
|
)
|
||||||
return render(request, "bda-unpaid.html", {"unpaid": unpaid})
|
|
||||||
|
|
||||||
|
|
||||||
@buro_required
|
@buro_required
|
||||||
def send_rappel(request, spectacle_id):
|
def send_rappel(request, spectacle_id):
|
||||||
show = get_object_or_404(Spectacle, id=spectacle_id)
|
show = get_object_or_404(Spectacle, id=spectacle_id)
|
||||||
# Mails d'exemples
|
# Mails d'exemples
|
||||||
custommail = CustomMail.objects.get(shortname="bda-rappel")
|
subject = show.title
|
||||||
exemple_mail_1place = custommail.render(
|
body_mail_1place = loader.render_to_string(
|
||||||
{"member": request.user, "show": show, "nb_attr": 1}
|
"bda/mails/rappel.txt",
|
||||||
|
context={"member": request.user, "show": show, "nb_attr": 1},
|
||||||
)
|
)
|
||||||
exemple_mail_2places = custommail.render(
|
body_mail_2places = loader.render_to_string(
|
||||||
{"member": request.user, "show": show, "nb_attr": 2}
|
"bda/mails/rappel.txt",
|
||||||
|
context={"member": request.user, "show": show, "nb_attr": 2},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Contexte
|
# Contexte
|
||||||
ctxt = {
|
ctxt = {
|
||||||
"show": show,
|
"show": show,
|
||||||
"exemple_mail_1place": exemple_mail_1place,
|
"exemple_mail_1place": (subject, body_mail_1place),
|
||||||
"exemple_mail_2places": exemple_mail_2places,
|
"exemple_mail_2places": (subject, body_mail_2places),
|
||||||
"custommail": custommail,
|
|
||||||
}
|
}
|
||||||
# Envoi confirmé
|
# Envoi confirmé
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
@ -757,25 +795,6 @@ def send_rappel(request, spectacle_id):
|
||||||
return render(request, "bda/mails-rappel.html", ctxt)
|
return render(request, "bda/mails-rappel.html", ctxt)
|
||||||
|
|
||||||
|
|
||||||
def descriptions_spectacles(request, tirage_id):
|
|
||||||
tirage = get_object_or_404(Tirage, id=tirage_id)
|
|
||||||
shows_qs = tirage.spectacle_set.select_related("location").prefetch_related(
|
|
||||||
"quote_set"
|
|
||||||
)
|
|
||||||
category_name = request.GET.get("category", "")
|
|
||||||
location_id = request.GET.get("location", "")
|
|
||||||
if category_name:
|
|
||||||
shows_qs = shows_qs.filter(category__name=category_name)
|
|
||||||
if location_id:
|
|
||||||
try:
|
|
||||||
shows_qs = shows_qs.filter(location__id=int(location_id))
|
|
||||||
except ValueError:
|
|
||||||
return HttpResponseBadRequest(
|
|
||||||
"La variable GET 'location' doit contenir un entier"
|
|
||||||
)
|
|
||||||
return render(request, "descriptions.html", {"shows": shows_qs})
|
|
||||||
|
|
||||||
|
|
||||||
def catalogue(request, request_type):
|
def catalogue(request, request_type):
|
||||||
"""
|
"""
|
||||||
Vue destinée à communiquer avec un client AJAX, fournissant soit :
|
Vue destinée à communiquer avec un client AJAX, fournissant soit :
|
||||||
|
|
5
bds/admin.py
Normal file
5
bds/admin.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from bds.models import BDSProfile
|
||||||
|
|
||||||
|
admin.site.register(BDSProfile)
|
28
bds/apps.py
Normal file
28
bds/apps.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
from django.apps import AppConfig, apps as global_apps
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.db.models.signals import post_migrate
|
||||||
|
|
||||||
|
|
||||||
|
def bds_group_perms(app_config, apps=global_apps, **kwargs):
|
||||||
|
try:
|
||||||
|
Permission = apps.get_model("auth", "Permission")
|
||||||
|
Group = apps.get_model("auth", "Group")
|
||||||
|
|
||||||
|
group = Group.objects.get(name="Burô du BDS")
|
||||||
|
perms = Permission.objects.filter(
|
||||||
|
Q(content_type__app_label="bds")
|
||||||
|
| Q(content_type__app_label="auth") & Q(content_type__model="user")
|
||||||
|
)
|
||||||
|
group.permissions.set(perms)
|
||||||
|
group.save()
|
||||||
|
|
||||||
|
except (LookupError, Group.DoesNotExist):
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class BdsConfig(AppConfig):
|
||||||
|
name = "bds"
|
||||||
|
verbose_name = "Gestion des adhérent·e·s du BDS"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
post_migrate.connect(bds_group_perms, sender=self)
|
63
bds/autocomplete.py
Normal file
63
bds/autocomplete.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from shared import autocomplete
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class BDSMemberSearch(autocomplete.ModelSearch):
|
||||||
|
model = User
|
||||||
|
search_fields = ["username", "first_name", "last_name"]
|
||||||
|
verbose_name = _("Membres du BDS")
|
||||||
|
|
||||||
|
def get_queryset_filter(self, *args, **kwargs):
|
||||||
|
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
||||||
|
qset_filter &= Q(bds__is_member=True)
|
||||||
|
return qset_filter
|
||||||
|
|
||||||
|
def result_uuid(self, user):
|
||||||
|
return user.username
|
||||||
|
|
||||||
|
def result_link(self, user):
|
||||||
|
return reverse("bds:user.update", args=(user.pk,))
|
||||||
|
|
||||||
|
|
||||||
|
class BDSOthersSearch(autocomplete.ModelSearch):
|
||||||
|
model = User
|
||||||
|
search_fields = ["username", "first_name", "last_name"]
|
||||||
|
verbose_name = _("Non-membres du BDS")
|
||||||
|
|
||||||
|
def get_queryset_filter(self, *args, **kwargs):
|
||||||
|
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
||||||
|
qset_filter &= Q(bds__isnull=True) | Q(bds__is_member=False)
|
||||||
|
return qset_filter
|
||||||
|
|
||||||
|
def result_uuid(self, user):
|
||||||
|
return user.username
|
||||||
|
|
||||||
|
def result_link(self, user):
|
||||||
|
return reverse("bds:user.update", args=(user.pk,))
|
||||||
|
|
||||||
|
|
||||||
|
class BDSLDAPSearch(autocomplete.LDAPSearch):
|
||||||
|
def result_link(self, clipper):
|
||||||
|
url = reverse("bds:user.create.fromclipper", args=(clipper.clipper,))
|
||||||
|
get = {"fullname": clipper.fullname, "mail": clipper.mail}
|
||||||
|
|
||||||
|
return "{}?{}".format(url, urlencode(get))
|
||||||
|
|
||||||
|
|
||||||
|
class BDSSearch(autocomplete.Compose):
|
||||||
|
search_units = [
|
||||||
|
("members", BDSMemberSearch()),
|
||||||
|
("others", BDSOthersSearch()),
|
||||||
|
("clippers", BDSLDAPSearch()),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
bds_search = BDSSearch()
|
41
bds/forms.py
Normal file
41
bds/forms.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.forms import UserCreationForm
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from bds.models import BDSProfile
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class UserForm(forms.ModelForm):
|
||||||
|
is_buro = forms.BooleanField(label=_("Membre du Burô"), required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ["email", "first_name", "last_name"]
|
||||||
|
|
||||||
|
|
||||||
|
class UserFromClipperForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields["username"].disabled = True
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ["username", "email", "first_name", "last_name"]
|
||||||
|
|
||||||
|
|
||||||
|
class UserFromScratchForm(UserCreationForm):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ["username", "email", "first_name", "last_name"]
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = BDSProfile
|
||||||
|
exclude = ["user"]
|
||||||
|
widgets = {
|
||||||
|
"birthdate": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d")
|
||||||
|
}
|
141
bds/migrations/0001_initial.py
Normal file
141
bds/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
# Generated by Django 2.2 on 2019-07-17 12:48
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import bds.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="BDSProfile",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"phone",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=20, verbose_name="téléphone"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"occupation",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("EXT", "Extérieur"),
|
||||||
|
("1A", "1A"),
|
||||||
|
("2A", "2A"),
|
||||||
|
("3A", "3A"),
|
||||||
|
("4A", "4A"),
|
||||||
|
("MAG", "Magistérien"),
|
||||||
|
("ARC", "Archicube"),
|
||||||
|
("DOC", "Doctorant"),
|
||||||
|
("CST", "CST"),
|
||||||
|
("PER", "Personnel ENS"),
|
||||||
|
],
|
||||||
|
default="1A",
|
||||||
|
max_length=3,
|
||||||
|
verbose_name="occupation",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"departement",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=50, verbose_name="département"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"birthdate",
|
||||||
|
models.DateField(
|
||||||
|
blank=True, null=True, verbose_name="date de naissance"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"mails_bds",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False, verbose_name="recevoir les mails du BDS"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_buro",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False, verbose_name="membre du Burô du BDS"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"has_certificate",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False, verbose_name="certificat médical"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"certificate_file",
|
||||||
|
models.FileField(
|
||||||
|
blank=True,
|
||||||
|
upload_to=bds.models.BDSProfile.get_certificate_filename,
|
||||||
|
verbose_name="fichier de certificat médical",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"ASPSL_number",
|
||||||
|
models.CharField(
|
||||||
|
blank=True,
|
||||||
|
max_length=50,
|
||||||
|
null=True,
|
||||||
|
verbose_name="numéro AS PSL",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"FFSU_number",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=50, null=True, verbose_name="numéro FFSU"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cotisation_period",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("ANN", "Année"),
|
||||||
|
("SE1", "Premier semestre"),
|
||||||
|
("SE2", "Deuxième semestre"),
|
||||||
|
("NO", "Aucune"),
|
||||||
|
],
|
||||||
|
default="NO",
|
||||||
|
max_length=3,
|
||||||
|
verbose_name="inscription",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"registration_date",
|
||||||
|
models.DateField(
|
||||||
|
auto_now_add=True, verbose_name="date d'inscription"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="bds",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Profil BDS",
|
||||||
|
"verbose_name_plural": "Profils BDS",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
16
bds/migrations/0002_bds_group.py
Normal file
16
bds/migrations/0002_bds_group.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# Generated by Django 2.2 on 2019-07-17 14:56
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def create_bds_buro_group(apps, schema_editor):
|
||||||
|
Group = apps.get_model("auth", "Group")
|
||||||
|
Group.objects.get_or_create(name="Burô du BDS")
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [("bds", "0001_initial")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(create_bds_buro_group, migrations.RunPython.noop)
|
||||||
|
]
|
24
bds/migrations/0003_staff_permission.py
Normal file
24
bds/migrations/0003_staff_permission.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 2.2.8 on 2019-12-20 22:48
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("bds", "0002_bds_group"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="bdsprofile",
|
||||||
|
options={
|
||||||
|
"permissions": (("is_team", "est membre du burô"),),
|
||||||
|
"verbose_name": "Profil BDS",
|
||||||
|
"verbose_name_plural": "Profils BDS",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="bdsprofile",
|
||||||
|
name="is_buro",
|
||||||
|
),
|
||||||
|
]
|
33
bds/migrations/0004_is_member_cotiz_type.py
Normal file
33
bds/migrations/0004_is_member_cotiz_type.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# Generated by Django 2.2.8 on 2019-12-22 10:20
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("bds", "0003_staff_permission"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="bdsprofile",
|
||||||
|
name="cotisation_type",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("ETU", "Étudiant"),
|
||||||
|
("NOR", "Normalien"),
|
||||||
|
("EXT", "Extérieur"),
|
||||||
|
("ARC", "Archicube"),
|
||||||
|
],
|
||||||
|
default="Normalien",
|
||||||
|
max_length=9,
|
||||||
|
verbose_name="type de cotisation",
|
||||||
|
),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="bdsprofile",
|
||||||
|
name="is_member",
|
||||||
|
field=models.BooleanField(default=False, verbose_name="adhérent⋅e du BDS"),
|
||||||
|
),
|
||||||
|
]
|
16
bds/migrations/0005_remove_bdsprofile_certificate_file.py
Normal file
16
bds/migrations/0005_remove_bdsprofile_certificate_file.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# Generated by Django 2.2.14 on 2020-07-27 20:14
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("bds", "0004_is_member_cotiz_type"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="bdsprofile",
|
||||||
|
name="certificate_file",
|
||||||
|
),
|
||||||
|
]
|
22
bds/migrations/0006_bdsprofile_comments.py
Normal file
22
bds/migrations/0006_bdsprofile_comments.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# Generated by Django 2.2.12 on 2020-08-28 12:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("bds", "0005_remove_bdsprofile_certificate_file"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="bdsprofile",
|
||||||
|
name="comments",
|
||||||
|
field=models.TextField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Attention : l'utilisateur·ice dispose d'un droit d'accès"
|
||||||
|
" aux données le/la concernant, dont le contenu de ce champ !",
|
||||||
|
verbose_name="commentaires",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
122
bds/mixins.py
Normal file
122
bds/mixins.py
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
|
||||||
|
|
||||||
|
|
||||||
|
class StaffRequiredMixin(PermissionRequiredMixin):
|
||||||
|
permission_required = "bds.is_team"
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleFormMixin(ContextMixin):
|
||||||
|
"""Mixin pour gérer plusieurs formulaires dans la même vue.
|
||||||
|
Le fonctionnement est relativement identique à celui de
|
||||||
|
FormMixin, dont la documentation est disponible ici :
|
||||||
|
https://docs.djangoproject.com/en/3.0/ref/class-based-views/mixins-editing/
|
||||||
|
|
||||||
|
Les principales différences sont :
|
||||||
|
- au lieu de form_class, il faut donner comme attribut un dict de la forme
|
||||||
|
{<form_name>: <form_class>}, avec tous les formulaires à instancier. On
|
||||||
|
peut aussi redéfinir `get_form_classes`
|
||||||
|
|
||||||
|
- les données initiales se récupèrent pour chaque form via l'attribut
|
||||||
|
`<form_name>_initial` ou la fonction `get_<form_name>_initial`. De même,
|
||||||
|
si certaines forms sont des `ModelForm`s, on peut définir la fonction
|
||||||
|
`get_<form_name>_instance`.
|
||||||
|
|
||||||
|
- chaque form a un préfixe rajouté, par défaut <form_name>, mais qui peut
|
||||||
|
être customisé via `prefixes` ou `get_prefixes`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
form_classes = {}
|
||||||
|
prefixes = {}
|
||||||
|
initial = {}
|
||||||
|
|
||||||
|
success_url = None
|
||||||
|
|
||||||
|
def get_form_classes(self):
|
||||||
|
return self.form_classes
|
||||||
|
|
||||||
|
def get_initial(self, form_name):
|
||||||
|
initial_attr = "%s_initial" % form_name
|
||||||
|
|
||||||
|
initial_method = "get_%s_initial" % form_name
|
||||||
|
initial_method = getattr(self, initial_method, None)
|
||||||
|
|
||||||
|
if hasattr(self, initial_attr):
|
||||||
|
return getattr(self, initial_attr)
|
||||||
|
elif callable(initial_method):
|
||||||
|
return initial_method()
|
||||||
|
else:
|
||||||
|
return self.initial.copy()
|
||||||
|
|
||||||
|
def get_prefix(self, form_name):
|
||||||
|
return self.prefixes.get(form_name, form_name)
|
||||||
|
|
||||||
|
def get_instance(self, form_name):
|
||||||
|
# Au cas où certaines des forms soient des ModelForms
|
||||||
|
instance_method = "get_%s_instance" % form_name
|
||||||
|
instance_method = getattr(self, instance_method, None)
|
||||||
|
|
||||||
|
if callable(instance_method):
|
||||||
|
return instance_method()
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_form_kwargs(self, form_name):
|
||||||
|
kwargs = {
|
||||||
|
"initial": self.get_initial(form_name),
|
||||||
|
"prefix": self.get_prefix(form_name),
|
||||||
|
"instance": self.get_instance(form_name),
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.request.method in ("POST", "PUT"):
|
||||||
|
kwargs.update({"data": self.request.POST, "files": self.request.FILES})
|
||||||
|
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def get_forms(self):
|
||||||
|
form_classes = self.get_form_classes()
|
||||||
|
return {
|
||||||
|
form_name: form_class(**self.get_form_kwargs(form_name))
|
||||||
|
for form_name, form_class in form_classes.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
if not self.success_url:
|
||||||
|
raise ImproperlyConfigured("No URL to redirect to. Provide a success_url.")
|
||||||
|
return str(self.success_url)
|
||||||
|
|
||||||
|
def form_valid(self, forms):
|
||||||
|
# on garde le nom form_valid pour l'interface avec SuccessMessageMixin
|
||||||
|
return HttpResponseRedirect(self.get_success_url())
|
||||||
|
|
||||||
|
def form_invalid(self, forms):
|
||||||
|
"""If the form is invalid, render the invalid form."""
|
||||||
|
return self.render_to_response(self.get_context_data(forms=forms))
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessMultipleFormView(View):
|
||||||
|
"""Équivalent de `ProcessFormView` pour plusieurs forms.
|
||||||
|
Note : il faut que *tous* les formulaires soient valides pour
|
||||||
|
qu'ils soient sauvegardés !
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
forms = self.get_forms()
|
||||||
|
return self.render_to_response(self.get_context_data(forms=forms))
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
forms = self.get_forms()
|
||||||
|
if all(form.is_valid() for form in forms.values()):
|
||||||
|
return self.form_valid(forms)
|
||||||
|
else:
|
||||||
|
return self.form_invalid(forms)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMultipleFormView(MultipleFormMixin, ProcessMultipleFormView):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleFormView(TemplateResponseMixin, BaseMultipleFormView):
|
||||||
|
pass
|
113
bds/models.py
Normal file
113
bds/models.py
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
from datetime import date
|
||||||
|
from os.path import splitext
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from shared.utils import choices_length
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class BDSProfile(models.Model):
|
||||||
|
OCCUPATION_CHOICES = (
|
||||||
|
("EXT", "Extérieur"),
|
||||||
|
("1A", "1A"),
|
||||||
|
("2A", "2A"),
|
||||||
|
("3A", "3A"),
|
||||||
|
("4A", "4A"),
|
||||||
|
("MAG", "Magistérien"),
|
||||||
|
("ARC", "Archicube"),
|
||||||
|
("DOC", "Doctorant"),
|
||||||
|
("CST", "CST"),
|
||||||
|
("PER", "Personnel ENS"),
|
||||||
|
)
|
||||||
|
|
||||||
|
TYPE_COTIZ_CHOICES = (
|
||||||
|
("ETU", "Étudiant"),
|
||||||
|
("NOR", "Normalien"),
|
||||||
|
("EXT", "Extérieur"),
|
||||||
|
("ARC", "Archicube"),
|
||||||
|
)
|
||||||
|
|
||||||
|
COTIZ_DURATION_CHOICES = (
|
||||||
|
("ANN", "Année"),
|
||||||
|
("SE1", "Premier semestre"),
|
||||||
|
("SE2", "Deuxième semestre"),
|
||||||
|
("NO", "Aucune"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_certificate_filename(instance, filename):
|
||||||
|
_, ext = splitext(filename) # récupère l'extension du fichier
|
||||||
|
year = str(date.now().year)
|
||||||
|
return "certifs/{username}-{year}.{ext}".format(
|
||||||
|
username=instance.user.username, year=year, ext=ext
|
||||||
|
)
|
||||||
|
|
||||||
|
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="bds")
|
||||||
|
phone = models.CharField(_("téléphone"), max_length=20, blank=True)
|
||||||
|
occupation = models.CharField(
|
||||||
|
_("occupation"),
|
||||||
|
default="1A",
|
||||||
|
choices=OCCUPATION_CHOICES,
|
||||||
|
max_length=choices_length(OCCUPATION_CHOICES),
|
||||||
|
)
|
||||||
|
departement = models.CharField(_("département"), max_length=50, blank=True)
|
||||||
|
birthdate = models.DateField(
|
||||||
|
auto_now_add=False,
|
||||||
|
auto_now=False,
|
||||||
|
verbose_name=_("date de naissance"),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
is_member = models.BooleanField(_("adhérent⋅e du BDS"), default=False)
|
||||||
|
|
||||||
|
mails_bds = models.BooleanField(_("recevoir les mails du BDS"), default=False)
|
||||||
|
|
||||||
|
has_certificate = models.BooleanField(_("certificat médical"), default=False)
|
||||||
|
|
||||||
|
ASPSL_number = models.CharField(
|
||||||
|
_("numéro AS PSL"), max_length=50, blank=True, null=True
|
||||||
|
)
|
||||||
|
FFSU_number = models.CharField(
|
||||||
|
_("numéro FFSU"), max_length=50, blank=True, null=True
|
||||||
|
)
|
||||||
|
cotisation_period = models.CharField(
|
||||||
|
_("inscription"), default="NO", choices=COTIZ_DURATION_CHOICES, max_length=3
|
||||||
|
)
|
||||||
|
registration_date = models.DateField(
|
||||||
|
auto_now_add=True, verbose_name=_("date d'inscription")
|
||||||
|
)
|
||||||
|
cotisation_type = models.CharField(
|
||||||
|
_("type de cotisation"), choices=TYPE_COTIZ_CHOICES, max_length=9
|
||||||
|
)
|
||||||
|
|
||||||
|
comments = models.TextField(
|
||||||
|
_("commentaires"),
|
||||||
|
blank=True,
|
||||||
|
help_text=_(
|
||||||
|
"Attention : l'utilisateur·ice dispose d'un droit d'accès aux données "
|
||||||
|
"le/la concernant, dont le contenu de ce champ !"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def expired_members(cls):
|
||||||
|
now = timezone.now()
|
||||||
|
qs = cls.objects.filter(is_member=True)
|
||||||
|
if now.month > 1 and now.month < 7:
|
||||||
|
return qs.filter(cotisation_period="SE1")
|
||||||
|
elif now.month < 2 or now.month > 8:
|
||||||
|
return qs.none()
|
||||||
|
return qs
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Profil BDS")
|
||||||
|
verbose_name_plural = _("Profils BDS")
|
||||||
|
permissions = (("is_team", _("est membre du burô")),)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.user.username
|
1
bds/static/bds/css/bds.css
Normal file
1
bds/static/bds/css/bds.css
Normal file
File diff suppressed because one or more lines are too long
1
bds/static/bds/css/bds.css.map
Normal file
1
bds/static/bds/css/bds.css.map
Normal file
File diff suppressed because one or more lines are too long
15
bds/static/bds/images/logo.svg
Normal file
15
bds/static/bds/images/logo.svg
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="300px" height="246px" viewBox="0 0 3000 2460" preserveAspectRatio="xMidYMid meet">
|
||||||
|
<g id="layer101" fill="#ffffff" stroke="none">
|
||||||
|
<path d="M0 1230 l0 -1230 1500 0 1500 0 0 1230 0 1230 -1500 0 -1500 0 0 -1230z"/>
|
||||||
|
</g>
|
||||||
|
<g id="layer102" fill="#3e2263" stroke="none">
|
||||||
|
<path d="M0 1230 l0 -1230 1500 0 1500 0 0 1230 0 1230 -1500 0 -1500 0 0 -1230z m1716 765 c103 -17 204 -46 284 -83 l55 -25 -40 -8 c-80 -16 -169 -10 -245 16 -123 42 -232 59 -385 59 -126 0 -278 -18 -341 -40 -13 -5 -16 -4 -9 4 14 15 166 60 254 76 102 19 315 19 427 1z m-703 -217 c15 -6 27 -13 27 -17 0 -4 -37 -25 -82 -47 -86 -43 -182 -120 -193 -155 -8 -26 32 -113 78 -167 44 -52 46 -62 10 -62 -39 1 -106 33 -165 80 -40 31 -54 38 -64 29 -21 -18 -17 -50 16 -116 57 -113 151 -199 259 -235 49 -16 51 -18 16 -13 -123 19 -260 151 -322 310 -15 39 -14 -81 1 -147 25 -107 55 -176 112 -251 49 -65 52 -73 38 -88 -21 -23 -104 -62 -216 -100 -51 -18 -97 -35 -102 -40 -4 -4 32 -10 80 -14 106 -9 256 13 317 48 l39 22 56 -33 c32 -18 114 -61 184 -95 80 -39 126 -67 123 -74 -6 -19 12 -16 19 3 9 24 28 5 22 -22 -7 -26 8 -34 18 -9 11 29 28 16 21 -18 -6 -30 -5 -31 10 -13 8 11 15 24 15 29 0 4 5 5 10 2 6 -3 8 -18 4 -33 -5 -26 -5 -26 10 -8 15 19 18 19 118 -12 113 -35 284 -72 329 -72 24 0 29 4 29 25 0 34 -36 125 -63 158 l-22 28 -59 -30 c-74 -39 -165 -43 -233 -10 -24 11 -43 25 -43 30 0 4 28 6 63 2 49 -4 71 -2 98 12 46 23 79 55 79 75 0 16 -48 66 -135 139 l-40 33 43 -23 c55 -30 166 -121 188 -154 9 -13 13 -29 9 -36 -15 -23 -100 -69 -129 -69 -17 0 -38 -5 -46 -10 -12 -8 -10 -10 10 -10 49 1 104 19 149 51 l45 31 24 -28 c33 -40 72 -128 79 -180 10 -79 -25 -96 -233 -114 -84 -7 -148 -7 -218 0 -106 12 -269 51 -317 76 -28 14 -31 13 -62 -10 -56 -43 -167 -89 -225 -94 l-55 -5 6 29 c2 16 14 55 26 86 21 57 27 112 15 142 -3 9 -34 38 -68 64 l-62 49 -104 7 c-166 12 -192 34 -83 70 34 12 65 24 68 28 4 4 -7 27 -24 52 -118 171 -124 402 -16 571 18 29 50 88 70 131 42 93 99 154 168 184 82 35 212 44 280 18z m540 -39 c60 -16 70 -41 75 -196 l5 -134 -39 3 -39 3 -5 114 c-4 93 -8 116 -22 125 -31 19 -41 -7 -47 -126 l-6 -113 -32 -3 c-41 -4 -44 6 -34 151 9 143 25 172 101 186 3 0 22 -4 43 -10z m322 -34 c0 -30 0 -30 -64 -33 l-64 -3 7 -130 7 -129 -35 0 -36 0 0 165 0 166 93 -3 92 -3 0 -30z m115 -55 l5 -84 18 27 c21 33 39 34 60 5 15 -22 16 -18 16 60 l1 82 35 0 35 0 -2 -162 -3 -163 -40 0 c-38 0 -41 2 -53 43 -16 49 -24 48 -42 -7 -12 -38 -16 -41 -50 -41 -34 0 -38 3 -44 31 -8 43 -8 284 1 293 4 4 19 6 33 4 24 -3 25 -6 30 -88z m370 -42 c193 -243 228 -505 100 -759 -61 -120 -195 -260 -328 -343 -66 -41 -60 -26 13 33 36 29 92 85 125 126 182 230 209 500 75 753 -42 79 -45 89 -45 163 0 44 4 79 9 79 5 0 28 -24 51 -52z m-1111 -294 c76 -30 94 -102 41 -156 -27 -27 -29 -32 -17 -47 34 -40 25 -100 -19 -120 -30 -13 -143 -15 -164 -1 -11 7 -15 42 -18 164 l-4 156 23 9 c37 15 115 13 158 -5z m571 -9 c84 -43 61 -135 -43 -173 -46 -18 -52 -23 -52 -48 0 -39 30 -42 79 -9 l37 25 -3 -41 c-4 -49 -32 -76 -90 -85 -32 -6 -41 -2 -68 24 -24 25 -30 39 -30 74 0 57 21 85 87 114 40 18 54 29 51 42 -5 29 -71 28 -111 -2 -19 -14 -36 -26 -39 -26 -11 0 -16 55 -7 72 24 45 131 63 189 33z m-303 -12 c56 -26 75 -65 71 -150 -3 -67 -6 -75 -38 -108 -43 -44 -94 -61 -146 -48 l-39 9 -3 144 c-1 79 0 150 2 157 8 19 110 17 153 -4z"/>
|
||||||
|
<path d="M1503 633 c4 -3 10 -3 14 0 3 4 0 7 -7 7 -7 0 -10 -3 -7 -7z"/>
|
||||||
|
<path d="M1532 448 c3 -7 15 -14 29 -16 23 -2 23 -2 5 13 -24 18 -39 20 -34 3z"/>
|
||||||
|
<path d="M1140 1231 c0 -45 10 -61 36 -61 26 0 64 32 64 53 0 23 -23 37 -62 37 -35 0 -38 -2 -38 -29z"/>
|
||||||
|
<path d="M1140 1070 c0 -35 17 -45 56 -36 20 5 25 12 22 34 -3 23 -8 27 -40 30 -36 3 -38 2 -38 -28z"/>
|
||||||
|
<path d="M1430 1151 c0 -72 3 -91 14 -91 28 0 47 13 61 41 31 60 10 121 -47 135 l-28 6 0 -91z"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.8 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue