mirror of
https://github.com/ae-utbm/sith.git
synced 2025-07-12 21:09:24 +00:00
Compare commits
911 Commits
feature/im
...
openapi
Author | SHA1 | Date | |
---|---|---|---|
106dc32a3d | |||
05edf33062 | |||
98175e397c | |||
62246f342d | |||
bff6513192 | |||
bf0779a096 | |||
9991507297 | |||
222ff762da | |||
6b27a97e7b | |||
87f790a044 | |||
75c4c55a32 | |||
728ad157e9 | |||
e542fe11b9 | |||
aa66fc61ab | |||
7f8304e407 | |||
3b80b36ed6 | |||
ba6e2a6402 | |||
6841d96455 | |||
8528820d89 | |||
2c9b72fe1d | |||
fe417b0c29 | |||
b3f67657d7 | |||
602c57c001 | |||
6a17e4480e | |||
1f1cd2ce0f | |||
a653f98fc1 | |||
a01ea13f5b | |||
10701ccdfa | |||
07028c8dd8 | |||
4890fcf0e1 | |||
2e71275f5b | |||
be87af5e06 | |||
f9c36c8f99 | |||
92d282f4ba | |||
a1bf86dabf | |||
21284546c4 | |||
e936f0d285 | |||
6af03240a1 | |||
01c92feb40 | |||
94d2c5660a | |||
71b3588577 | |||
2def57d82c | |||
0e88260c31 | |||
86c2ea7fd9 | |||
fc3b82c35c | |||
1d177412c3 | |||
c272cad2ea | |||
e757fb43a1 | |||
8705fbe4b2 | |||
aa60462653 | |||
9c0d89de83 | |||
809febc353 | |||
f4ff247862 | |||
1978658b9c | |||
219700f0bc | |||
2918048b16 | |||
a87016a23f | |||
f7ff77b88f | |||
e8db68b960 | |||
93a5c3a02a | |||
e46cba7a06 | |||
ba21738bd9 | |||
b1db52d2b6 | |||
2bed89aaba | |||
86c68eeb32 | |||
8cb53ceba2 | |||
a96b374ad7 | |||
9945993f0b | |||
59e90ec754 | |||
41bff53853 | |||
88b3f7c322 | |||
b31445fefb | |||
2dc32f8b20 | |||
b43b531c3b | |||
bf388e68f0 | |||
5252d450a9 | |||
8f17c3d830 | |||
6627ea417c | |||
92b2befd55 | |||
43207455b8 | |||
5fa431e29b | |||
78f3caa455 | |||
6d519e3a07 | |||
85c8b7d11c | |||
fa02f4b5f0 | |||
3df33261ce | |||
ee1bcf2011 | |||
571b3a4e02 | |||
6bf02cecd9 | |||
05d4a09f8c | |||
169e9ea55a | |||
9b916f6204 | |||
2123e83010 | |||
294b59b4d6 | |||
820ceb48dd | |||
73ce681307 | |||
faa757b54f | |||
36076aefcc | |||
b9482a6f08 | |||
d573182f4b | |||
75be6454eb | |||
428fe68cdb | |||
18967cf3d6 | |||
14ed43aaa5 | |||
c555d5c78c | |||
5db9819560 | |||
dd2cd0a18d | |||
c7ae70972f | |||
17cf0c67b3 | |||
7d40387f43 | |||
20a535429c | |||
5ff7bb3259 | |||
0d95c3b9c9 | |||
170f9dde61 | |||
61170c0918 | |||
80940765fe | |||
9d98a20e40 | |||
6f9f1ac1e7 | |||
71b096f9ef | |||
9272f53bea | |||
9b5f08e13c | |||
d0b1a49300 | |||
e500cf92ee | |||
551091f650 | |||
0c01ad1770 | |||
cba915c34d | |||
7ac41ac5cb | |||
c6bb509fc3 | |||
4d0d7adce1 | |||
6bcc420af1 | |||
9f35f5356b | |||
8d73ec797b | |||
c3fc8538cc | |||
600657b1a8 | |||
d3f21c8f16 | |||
895d51586e | |||
e200f28267 | |||
a4c6439981 | |||
6ee2e8c5da | |||
f4af29acb4 | |||
a8810816f0 | |||
b7bf3fd375 | |||
b26e85ebb2 | |||
8b8a295e16 | |||
894690a97f | |||
843ce2e3a7 | |||
9f33ddd883 | |||
a2dc4f1964 | |||
cca486f2b9 | |||
b9e27ef191 | |||
29e875bcde | |||
686d67410a | |||
4226ba88ae | |||
672bc91e36 | |||
bc9cb9b36c | |||
edafc06c3f | |||
134f8a7989 | |||
771cbdbd77 | |||
a491baddb9 | |||
8d10a5e0ab | |||
cbe42d3a60 | |||
0c4d72e17a | |||
2db3290bed | |||
429df81ec9 | |||
bb24516474 | |||
16de128fdb | |||
8e339c3d4b | |||
25298518bc | |||
2e26ff2cde | |||
a8702d4f5e | |||
7f4cc5fb0f | |||
e7215be00e | |||
4f35cc00bc | |||
af47587116 | |||
3c4daeadb0 | |||
348ab19ac6 | |||
ada74a3e42 | |||
785ac9bdab | |||
d1e604e7a5 | |||
2749a88704 | |||
eb3db134f8 | |||
fa7f5d24b0 | |||
ba76015c71 | |||
1887a2790f | |||
5d0fc38107 | |||
65df55a635 | |||
a60e1f1fdc | |||
0a0f44607e | |||
007080ee48 | |||
a13e3e95b7 | |||
169938e1da | |||
e5fb875968 | |||
9bd14f1b4e | |||
fd2295119d | |||
eac2709e86 | |||
48f6d134bf | |||
6d7467e746 | |||
0d1629495b | |||
63839dc22b | |||
c627944bd1 | |||
f0be4b270b | |||
728065e771 | |||
849fac490d | |||
5752229312 | |||
6eb860579a | |||
d08d54b4c9 | |||
bb210f8d47 | |||
efca10e252 | |||
b8f851b009 | |||
1e29ae4171 | |||
0ae1e850f4 | |||
d380668c0f | |||
9a72c5eb72 | |||
407cfbe02b | |||
6400b2c2c2 | |||
0d3fd954a3 | |||
cce7ecbe73 | |||
d200c1e381 | |||
673c427485 | |||
2f9e5bfee1 | |||
11702d3d7c | |||
43f47e2087 | |||
4b881903f0 | |||
761e37ade6 | |||
10ed2f7404 | |||
43768f1691 | |||
280d27343d | |||
138e1662c7 | |||
c80fe094a2 | |||
139221dd22 | |||
72c2981d66 | |||
6f003ffa53 | |||
7f6fd7dc47 | |||
ccf5118c9d | |||
022c19c020 | |||
2e5e217842 | |||
9c93c004ec | |||
472800eff6 | |||
b8d43a629b | |||
f6693e12cf | |||
38f491cf57 | |||
3464d5d860 | |||
81773dc800 | |||
da400155eb | |||
5079938a5b | |||
b8430adc50 | |||
eed434aeb2 | |||
372470b44b | |||
7071553c3b | |||
eea237b813 | |||
c37288c285 | |||
ccf5767a01 | |||
ffe6fc8c2a | |||
5f0b4d2050 | |||
f9d7dc7d3a | |||
8ebea00896 | |||
a548f4744e | |||
a383f3e717 | |||
60f18669c8 | |||
a36946529b | |||
eaac0c728f | |||
9ca95774a3 | |||
fa66851889 | |||
ab81f11199 | |||
bea7741d35 | |||
81e163812e | |||
4f233538e0 | |||
4ac09ac08b | |||
6d02970676 | |||
b773a05bb5 | |||
accf1befce | |||
6953eaa9d0 | |||
180bae59c8 | |||
9cafc163e8 | |||
8f8eef4107 | |||
7af745087e | |||
aab093200b | |||
1a9556f811 | |||
39b36aa509 | |||
3fc260a12c | |||
1696a2f579 | |||
baebc0b690 | |||
9f3a10ca71 | |||
38ceaf3106 | |||
87b619794d | |||
29c4a36479 | |||
ddeb12f08c | |||
a7b1406e06 | |||
871ef60cf6 | |||
7e9071a533 | |||
8c660e9856 | |||
6ca641ab7f | |||
8d6609566f | |||
17e4c63737 | |||
fad470b670 | |||
c5646b1e59 | |||
5da27bb266 | |||
be6a077c8e | |||
8d643fc6b4 | |||
47876e3971 | |||
c79c251ba7 | |||
483670e798 | |||
6c8a6008d5 | |||
e680124d7b | |||
b06a06f50c | |||
c1be55a719 | |||
6416de237f | |||
ad44fd52a4 | |||
03c27b10e5 | |||
fc0ef29738 | |||
a0eb53a607 | |||
66e5ef64fd | |||
f5d5cc18a8 | |||
4c65939bbe | |||
379527cd58 | |||
f63fb59cbf | |||
cde864fdc7 | |||
e9361697f7 | |||
830c752971 | |||
6bdc1b73ae | |||
0f003870bb | |||
0631c77a1c | |||
2cc4308a58 | |||
4975475e85 | |||
466fe58763 | |||
3b7e338808 | |||
53b13e7aef | |||
fa60ecb25a | |||
a975824481 | |||
c51e5eb6cb | |||
f0bc502ec9 | |||
902cafc5e4 | |||
b2f54aa23e | |||
29a5425259 | |||
e2a34c75ea | |||
de7aa6f6a6 | |||
9acb421b2e | |||
66d2dc74e7 | |||
2f613607af | |||
d4b9c3afb1 | |||
b81cf49d0a | |||
1da45fdffc | |||
10dde3f002 | |||
c2d6af12ab | |||
6e48f88c06 | |||
7a91a71565 | |||
c4764110d8 | |||
ff68e65250 | |||
c9d83e5916 | |||
5dc99dbfcb | |||
8dbec85c8e | |||
84d7e40e66 | |||
0b509f2200 | |||
9591162cc9 | |||
007e17fd8b | |||
35c5f96672 | |||
95f8e7517c | |||
9667c79162 | |||
1c79c25262 | |||
04b4b34bfe | |||
fc0e689d4e | |||
83bb4b3b12 | |||
8dcfc604a0 | |||
d2d639e5f6 | |||
b3eb7693e3 | |||
10f42b1522 | |||
76e9f3b1dc | |||
d0ff9bc16c | |||
5e4ebd16f9 | |||
d2b19424ff | |||
08286254cd | |||
4805c39b45 | |||
f845bbf20a | |||
71c7158124 | |||
c4643ee52c | |||
b46b0882f3 | |||
1c4efc9431 | |||
4133e0ccdd | |||
de415e7e75 | |||
9d17524f45 | |||
68ad9650af | |||
8d4d8a3abc | |||
9617e29ed5 | |||
75406f7b58 | |||
70f5ae4f9c | |||
ff307f1d65 | |||
d7ae601c52 | |||
33b9ff78bb | |||
0739ce2fb4 | |||
8fc1a754de | |||
ca8c1c9d92 | |||
0485ab1120 | |||
8a8851847c | |||
7b41051d0d | |||
3db1f592e2 | |||
6853ec0b69 | |||
3b39049c20 | |||
37d1669a72 | |||
ee9f36d883 | |||
e712f9fdb8 | |||
9991f5dc64 | |||
fce6c3d29c | |||
346439076e | |||
5e8d8b8d5d | |||
db9f86c41e | |||
c7adde62eb | |||
34559dda08 | |||
37c4621e9e | |||
dd7ed290f5 | |||
dc1e1fc897 | |||
0a5ddcea68 | |||
37abde04d7 | |||
40f2f7033e | |||
aebf909dc6 | |||
ec7d45fd91 | |||
3af5d96bf5 | |||
c7a8a1a91c | |||
2dd434d987 | |||
5e954bae6a | |||
a97dba18c2 | |||
26770de40e | |||
583d4ddfb8 | |||
486047b929 | |||
b65ec6463b | |||
7cc13ea669 | |||
c2efc969d0 | |||
b091fee035 | |||
2a0f2454f4 | |||
97ea1763f1 | |||
b9f51596e9 | |||
0610794dbe | |||
a6b32fcad1 | |||
e583e78a4e | |||
3eb3feea49 | |||
935914428b | |||
ab63ba1c54 | |||
afdc6b69df | |||
8b419dcee6 | |||
e7181257e3 | |||
8e7c09332f | |||
d9ea5e5538 | |||
a21460a1b8 | |||
b6a480ff61 | |||
84ee6dd2f5 | |||
a950585a02 | |||
7f8a2c1eaf | |||
125157fdf4 | |||
517263dd58 | |||
301fc73687 | |||
45441c351d | |||
be5ce414ba | |||
bb3f277ba5 | |||
23049a8ae2 | |||
8bbebfdb13 | |||
662b4b5c53 | |||
9675b6372c | |||
03afd49115 | |||
0af3505c2a | |||
f78b968075 | |||
7d40316044 | |||
e3dcad62cc | |||
db6a871854 | |||
ce4f57bd8f | |||
8be8328830 | |||
0a0092e189 | |||
c50f0a2ac5 | |||
6b3012d21c | |||
729f848c14 | |||
56cc4776a6 | |||
e6f25fb707 | |||
b9cbba2309 | |||
4165f8d4af | |||
cac185634d | |||
66dceefcf0 | |||
677ff51ea5 | |||
645b8a543e | |||
74a506c48b | |||
deda2b4055 | |||
67ebb90ffa | |||
5d16ba135a | |||
150d08dc45 | |||
c1a85486cc | |||
d16a207a83 | |||
d114b01bcc | |||
dee54c3b41 | |||
670d2fa12e | |||
a68e47ce8c | |||
0314aa6733 | |||
496ad7ce9b | |||
efdd4a6b16 | |||
0b31b215f6 | |||
7e1734aed5 | |||
19cd51043a | |||
5348a451e9 | |||
83ae21140d | |||
cdf9519a9f | |||
d77358eaac | |||
9609a7615b | |||
361a06e5b3 | |||
1720307c21 | |||
15ae24f0bd | |||
143713fac1 | |||
e4845b580b | |||
40c623b202 | |||
092ace8432 | |||
00cf619c68 | |||
b6e1c3bc88 | |||
3b1d06a71d | |||
a5d8c96bab | |||
564d95f701 | |||
19e21c80df | |||
768e2867b5 | |||
f07a855e7e | |||
2fa9daf627 | |||
a1bae7ced3 | |||
7312580a8d | |||
1c774aa4a0 | |||
cbcdc6171f | |||
444a2936e2 | |||
6a31f38ceb | |||
29b32f6cbf | |||
465e0f31d9 | |||
5a8052ae47 | |||
6a64e05247 | |||
81a64eed08 | |||
29b27dc626 | |||
ca25a12be0 | |||
c0a6f5eb30 | |||
4bc4d266c2 | |||
8f0ee4df6d | |||
579d077b35 | |||
32444fac90 | |||
849177562d | |||
86bbc4cf6e | |||
46e58bb49e | |||
9199f91151 | |||
9247696c1c | |||
37f62e15cf | |||
a98c924b24 | |||
a71ca60270 | |||
76cc730d8f | |||
12bb7e9294 | |||
1dca0ea003 | |||
b340a6568f | |||
6f4e93bb76 | |||
93eb09887e | |||
09081b03b6 | |||
ceee393bd8 | |||
b969513d94 | |||
2111a2c67e | |||
7405241b82 | |||
b0884c6b04 | |||
20bea62542 | |||
24925f7726 | |||
d0c18d4538 | |||
37eaa4b912 | |||
a3cca056ae | |||
ee965008d1 | |||
c57d2ece9c | |||
e5aa7aa866 | |||
cacdf600f4 | |||
5ee0ee8efb | |||
08f20796a7 | |||
58d3a7ee2c | |||
f6be360eab | |||
543a48b4ab | |||
0f657b934d | |||
c4e42212aa | |||
a4fe4996aa | |||
fbcacb24f8 | |||
0eaa20e09d | |||
271d57051e | |||
3d6c260e53 | |||
d0f17bd41a | |||
819cd257a8 | |||
655d72a2b1 | |||
71c96fdf62 | |||
3f2327dee4 | |||
06eecfce40 | |||
67af1485b3 | |||
a00a85a56a | |||
bb953a6139 | |||
140dc26dc6 | |||
3548deebf6 | |||
c67155f02c | |||
c10e1e8cbf | |||
c5f5ad3f75 | |||
8ec3074488 | |||
1b1284d3d0 | |||
f71518ed6f | |||
1800785b80 | |||
6449724ed5 | |||
6179c3e7d4 | |||
3e5d4c5fbb | |||
3f2b63aaa5 | |||
d29a5cdb44 | |||
bbcc7ffeaa | |||
93f4dede3e | |||
683f8235b1 | |||
43917317b4 | |||
f182de5929 | |||
c6657bffd2 | |||
3d138d404f | |||
9c93162741 | |||
6068c6048a | |||
d47461ba40 | |||
66e88ac6fb | |||
d3cada4c95 | |||
27443bcd21 | |||
b246e171b7 | |||
ec434bec56 | |||
7458f622f5 | |||
ab72e01707 | |||
acad74528d | |||
813bbbb94a | |||
a2a858262a | |||
727e5cb199 | |||
71602b43bd | |||
bc40b92744 | |||
d545becf24 | |||
48f605dbe0 | |||
b0d7bbbb79 | |||
f624b7c66d | |||
bf96d8a10c | |||
e8b496cfdc | |||
79ef151ad3 | |||
8e48103fd2 | |||
ed4c65600c | |||
ae16a1bd89 | |||
e2b42145e1 | |||
55ad1f99fd | |||
5b427bee35 | |||
d1c88a5cef | |||
99a25d5e9b | |||
d148d6b3a5 | |||
66189d3ab2 | |||
f1afa3b436 | |||
6380fb193c | |||
341ffc9a55 | |||
6962b39fc9 | |||
d04b4c77c6 | |||
453b7df0be | |||
878ee99fe4 | |||
6918e3044f | |||
cf46c3800f | |||
7c0c132f40 | |||
e0bf797876 | |||
dd07c374d7 | |||
b3e59b3829 | |||
352b09d9cd | |||
93cc6d99f8 | |||
85a99fc8fa | |||
a4d801bed4 | |||
fbff38c5c3 | |||
14402f7537 | |||
88d24f8067 | |||
cc1d700f7d | |||
e82acdabb0 | |||
ea42c98571 | |||
cc5df9b171 | |||
b4749f297b | |||
e564c6604c | |||
712615a312 | |||
d95d4901d2 | |||
9373654306 | |||
4a9d9f03a8 | |||
b7261ec629 | |||
2e1f16fa04 | |||
d295cc5223 | |||
ff088009d9 | |||
52c19e9962 | |||
68d0a16d1c | |||
a422e8d39a | |||
ef80c1be61 | |||
85d9816aaa | |||
93b66d980d | |||
07d617da91 | |||
34aac40e65 | |||
f54bf2b8af | |||
e7d04d9817 | |||
ef1537ac2c | |||
d1f86fe3d9 | |||
d13b79552b | |||
4036bfd703 | |||
759e360a1d | |||
8865529b39 | |||
cdb73ee49c | |||
9188c28ee7 | |||
2a6c1f050d | |||
2ec1f8cdc0 | |||
121b388d85 | |||
589119c9ee | |||
b35e1a476e | |||
8174bce720 | |||
d8a7d62b23 | |||
a75730d91f | |||
a2b5f929dd | |||
7a0fa9f1a0 | |||
28ff7f24c5 | |||
e6db25357b | |||
72ea6b6fdd | |||
bf5f72fd9d | |||
af724a1e0e | |||
0eeaf1ce21 | |||
57a8215c6b | |||
9163e4dee6 | |||
c56d6e3f6b | |||
20e8854467 | |||
3ef38fabdb | |||
f5cee10761 | |||
d1cbb765c0 | |||
7ea9a5ca2d | |||
20c015c312 | |||
ecb48ce663 | |||
00dc03a235 | |||
d3b203a4a1 | |||
4506440a62 | |||
da6bd84cdf | |||
0b9ccf6a57 | |||
a056bd177f | |||
d2ea8f2898 | |||
5cce4269bb | |||
0a2ed6dd94 | |||
417f328206 | |||
cca9732925 | |||
f02864b752 | |||
62bb15317c | |||
b35751126f | |||
28d6d8ba96 | |||
6bdb16e293 | |||
eb45cf6175 | |||
d6d8f56570 | |||
5322dc1de8 | |||
51bb1a5c9d | |||
996dadf6f5 | |||
29bb0f6712 | |||
f6fbad8403 | |||
e37ce4172e | |||
1dfd871169 | |||
a637742bb0 | |||
a5e4db99fb | |||
a9f66e2cd9 | |||
7bc7af8245 | |||
e5dfe1e638 | |||
284f064cbf | |||
e5c6f00283 | |||
12d316ebe4 | |||
cbd8932075 | |||
feb6dcbc94 | |||
181e74b1d1 | |||
eb04e26b22 | |||
7b97f0bf47 | |||
19fdaf4c89 | |||
7ca9c8dc42 | |||
946f35c601 | |||
c7b47bdd02 | |||
eef15e05f4 | |||
f265346a10 | |||
a321bd79ed | |||
819e2b5f9f | |||
91344741a5 | |||
4d2b82235c | |||
ffa3936878 | |||
26c70aa071 | |||
1bd887567e | |||
3304f32ef0 | |||
0790ae2298 | |||
39151b61e7 | |||
3f49d70745 | |||
e5434961de | |||
aab2d3a03f | |||
b022ebb80e | |||
2737cae4ab | |||
c4e6272535 | |||
aa0c98bf34 | |||
63b6b262c6 | |||
424639ea80 | |||
594776f3a6 | |||
918e93d211 | |||
b82f98c87f | |||
6c4251a91f | |||
2261782920 | |||
043dcfb283 | |||
3c76c5e0f1 | |||
d348e6314a | |||
b3fa6f352b | |||
191b05c305 | |||
215fdce411 | |||
b25805e0a1 | |||
13d0d2a300 | |||
15f51fb03f | |||
a24b1f5c2a | |||
04e7f65e8e | |||
41b9318028 | |||
378e8b53f2 | |||
c832e8b1a7 | |||
fee7ade1a5 | |||
d51dbf8a53 | |||
c03a1b57c5 | |||
0c566cfbde | |||
9295325d21 | |||
cb1aa8bef0 | |||
b9d19be183 | |||
293369f165 | |||
3046438cb1 | |||
811e5a5ad1 | |||
2c8f18d7fc | |||
c7f8cdd098 | |||
58ff5b934a | |||
03d15ddded | |||
002d8f80a6 | |||
82d3791859 | |||
d9531838f2 | |||
c7b5c77395 | |||
223aa37161 | |||
c1acadbf3d | |||
54af894b82 | |||
e1ac75f394 | |||
8c69a94488 | |||
07b625d4aa | |||
a1296dc7af | |||
e5a2236d72 | |||
588a82426e | |||
8245ddf2a6 | |||
775a0c6478 | |||
bad67a8b65 | |||
7e98e184a0 | |||
6240eff160 | |||
a8918ebe86 | |||
b852176958 | |||
c9e398b7ec | |||
e84d5626df | |||
0fb61938ce | |||
d6b27f2f21 | |||
e15bcfae07 | |||
72cf5a3d5e | |||
7de2e00c94 | |||
efe5d75798 | |||
9f1eedbe1b | |||
7fe495179f | |||
7f6c4f6236 | |||
30948f1701 | |||
02ec3607b2 | |||
3c2dcfbfa2 | |||
8bcf59aaf0 | |||
c6d2ac9100 | |||
2ac578c3ad | |||
f941435232 | |||
171a1cb876 | |||
cfc19434d0 | |||
688871a680 | |||
44c8558aa3 | |||
6b923d2310 | |||
09e0b31bc9 | |||
eb2454eded | |||
3014d8cead | |||
70fdc2edf2 | |||
e47f29aa38 | |||
d811896e21 | |||
5c999b6ef1 | |||
79a6d9e771 | |||
e1cf1c786d | |||
71fe9559b1 | |||
f1fa8d34bf | |||
aa07fa9207 | |||
47fec973bc | |||
ea8247aa16 | |||
bf18284450 | |||
cd58d5a357 | |||
75bb3f992c | |||
ae1fcdb8c0 | |||
507080f75e | |||
5bcf043d97 | |||
99605b98d4 | |||
6dfd43a8da | |||
c7135875b8 | |||
e29e1101cd | |||
d97602e60b | |||
a5cbac1f97 | |||
3143d3d91a | |||
9bdf3fc4ac | |||
e06bc7dba3 | |||
a8b9f38000 | |||
ca27b89a8b | |||
e1bf7caa9a | |||
e681c17a0f | |||
5416d88c97 | |||
0dca152436 | |||
9ce7abd31d | |||
f41ff281fb | |||
ee437649f0 | |||
321cb72ca8 | |||
4303d51c0a | |||
d16bf12611 | |||
4231a7972d | |||
c436d39014 | |||
51a12814f9 | |||
00ae6e4623 | |||
4b587e8711 | |||
d2f377b54f | |||
193c820757 | |||
b9298792ae | |||
aaf30ab965 | |||
2db66e6154 | |||
38295e591d | |||
544b0248b2 | |||
2bccf633d5 | |||
4f9d5ae7b1 | |||
259337dff1 | |||
84768eb74e | |||
288764b551 |
17
.env.example
Normal file
17
.env.example
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
HTTPS=off
|
||||||
|
SITH_DEBUG=true
|
||||||
|
|
||||||
|
# This is not the real key used in prod
|
||||||
|
SECRET_KEY=(4sjxvhz@m5$0a$j0_pqicnc$s!vbve)z+&++m%g%bjhlz4+g2
|
||||||
|
|
||||||
|
# comment the sqlite line and uncomment the postgres one to switch the dbms
|
||||||
|
DATABASE_URL=sqlite:///db.sqlite3
|
||||||
|
#DATABASE_URL=postgres://user:password@127.0.0.1:5432/sith
|
||||||
|
|
||||||
|
REDIS_PORT=7963
|
||||||
|
CACHE_URL=redis://127.0.0.1:${REDIS_PORT}/0
|
||||||
|
|
||||||
|
# Used to select which other services to run alongside
|
||||||
|
# manage.py, pytest and runserver
|
||||||
|
PROCFILE_STATIC=Procfile.static
|
||||||
|
PROCFILE_SERVICE=Procfile.service
|
14
.envrc
14
.envrc
@ -1,14 +1,6 @@
|
|||||||
if [[ ! -f pyproject.toml ]]; then
|
if [[ ! -d .venv ]]; then
|
||||||
log_error 'No pyproject.toml found. Use `poetry new` or `poetry init` to create one first.'
|
log_error 'No .venv folder found. Use `uv sync` to create one first.'
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local VENV=$(poetry env list --full-path | cut -d' ' -f1)
|
. .venv/bin/activate
|
||||||
if [[ -z $VENV || ! -d $VENV/bin ]]; then
|
|
||||||
log_error 'No poetry virtual environment found. Use `poetry install` to create one first.'
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
export VIRTUAL_ENV=$VENV
|
|
||||||
export POETRY_ACTIVE=1
|
|
||||||
PATH_add "$VENV/bin"
|
|
8
.github/actions/compile_messages/action.yml
vendored
8
.github/actions/compile_messages/action.yml
vendored
@ -1,8 +0,0 @@
|
|||||||
name: "Compile messages"
|
|
||||||
description: "Compile the gettext translation messages"
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- name: Setup project
|
|
||||||
run: poetry run ./manage.py compilemessages
|
|
||||||
shell: bash
|
|
64
.github/actions/setup_project/action.yml
vendored
64
.github/actions/setup_project/action.yml
vendored
@ -4,50 +4,48 @@ runs:
|
|||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- name: Install apt packages
|
- name: Install apt packages
|
||||||
uses: awalsh128/cache-apt-pkgs-action@latest
|
uses: awalsh128/cache-apt-pkgs-action@v1.4.3
|
||||||
with:
|
with:
|
||||||
packages: gettext libxapian-dev libgraphviz-dev
|
packages: gettext
|
||||||
version: 1.0 # increment to reset cache
|
version: 1.0 # increment to reset cache
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install Redis
|
||||||
run: |
|
uses: shogo82148/actions-setup-redis@v1
|
||||||
sudo apt update
|
|
||||||
sudo apt install gettext libxapian-dev libgraphviz-dev
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Set up python
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
redis-version: "7.x"
|
||||||
|
|
||||||
- name: Load cached Poetry installation
|
- name: Install uv
|
||||||
id: cached-poetry
|
uses: astral-sh/setup-uv@v5
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
with:
|
||||||
path: ~/.local
|
version: "0.5.14"
|
||||||
key: poetry-0 # increment to reset cache
|
enable-cache: true
|
||||||
|
cache-dependency-glob: "uv.lock"
|
||||||
|
|
||||||
- name: Install Poetry
|
- name: "Set up Python"
|
||||||
if: steps.cached-poetry.outputs.cache-hit != 'true'
|
uses: actions/setup-python@v5
|
||||||
shell: bash
|
|
||||||
run: curl -sSL https://install.python-poetry.org | python3 -
|
|
||||||
|
|
||||||
- name: Check pyproject.toml syntax
|
|
||||||
shell: bash
|
|
||||||
run: poetry check
|
|
||||||
|
|
||||||
- name: Load cached dependencies
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pypoetry
|
python-version-file: ".python-version"
|
||||||
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
|
|
||||||
restore-keys: |
|
- name: Restore cached virtualenv
|
||||||
${{ runner.os }}-poetry-
|
uses: actions/cache/restore@v4
|
||||||
|
with:
|
||||||
|
key: venv-${{ runner.os }}-${{ hashFiles('.python-version') }}-${{ hashFiles('pyproject.toml') }}-${{ env.CACHE_SUFFIX }}
|
||||||
|
path: .venv
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: poetry install -E testing -E docs
|
run: uv sync
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
|
- name: Install Xapian
|
||||||
|
run: uv run ./manage.py install_xapian
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Save cached virtualenv
|
||||||
|
uses: actions/cache/save@v4
|
||||||
|
with:
|
||||||
|
key: venv-${{ runner.os }}-${{ hashFiles('.python-version') }}-${{ hashFiles('pyproject.toml') }}-${{ env.CACHE_SUFFIX }}
|
||||||
|
path: .venv
|
||||||
|
|
||||||
- name: Compile gettext messages
|
- name: Compile gettext messages
|
||||||
run: poetry run ./manage.py compilemessages
|
run: uv run ./manage.py compilemessages
|
||||||
shell: bash
|
shell: bash
|
||||||
|
10
.github/actions/setup_xapian/action.yml
vendored
10
.github/actions/setup_xapian/action.yml
vendored
@ -1,10 +0,0 @@
|
|||||||
name: "Setup xapian"
|
|
||||||
description: "Setup the xapian indexes"
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- name: Setup xapian index
|
|
||||||
run: |
|
|
||||||
mkdir -p /dev/shm/search_indexes
|
|
||||||
ln -s /dev/shm/search_indexes sith/search_indexes
|
|
||||||
shell: bash
|
|
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@ -8,11 +8,7 @@ updates:
|
|||||||
- package-ecosystem: "pip" # See documentation for possible values
|
- package-ecosystem: "pip" # See documentation for possible values
|
||||||
directory: "/" # Location of package manifests
|
directory: "/" # Location of package manifests
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "weekly"
|
||||||
# Raise pull requests for version updates
|
|
||||||
# to pip against the `develop` branch
|
|
||||||
target-branch: "taiste"
|
target-branch: "taiste"
|
||||||
reviewers:
|
|
||||||
- "ae-utbm/developpers-v3"
|
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "[UPDATE] "
|
prefix: "[UPDATE] "
|
53
.github/workflows/ci.yml
vendored
53
.github/workflows/ci.yml
vendored
@ -1,43 +1,52 @@
|
|||||||
name: Sith 3 CI
|
name: Sith CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: [master, taiste]
|
||||||
- master
|
|
||||||
- taiste
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches: [master, taiste]
|
||||||
- master
|
workflow_dispatch:
|
||||||
- taiste
|
|
||||||
|
env:
|
||||||
|
SECRET_KEY: notTheRealOne
|
||||||
|
DATABASE_URL: sqlite:///db.sqlite3
|
||||||
|
CACHE_URL: redis://127.0.0.1:6379/0
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
black:
|
pre-commit:
|
||||||
name: Black format
|
name: Launch pre-commits checks (ruff)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v3
|
- uses: actions/setup-python@v5
|
||||||
- name: Setup Project
|
with:
|
||||||
uses: ./.github/actions/setup_project
|
python-version-file: ".python-version"
|
||||||
- run: poetry run black --check .
|
- uses: pre-commit/action@v3.0.1
|
||||||
|
with:
|
||||||
|
extra_args: --all-files
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
name: Run tests and generate coverage report
|
name: Run tests and generate coverage report
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false # don't interrupt the other test processes
|
||||||
|
matrix:
|
||||||
|
pytest-mark: [slow, not slow]
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository
|
- name: Check out repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/setup_project
|
- uses: ./.github/actions/setup_project
|
||||||
- uses: ./.github/actions/setup_xapian
|
env:
|
||||||
- uses: ./.github/actions/compile_messages
|
# To avoid race conditions on environment cache
|
||||||
|
CACHE_SUFFIX: ${{ matrix.pytest-mark }}
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: poetry run coverage run ./manage.py test
|
run: uv run coverage run -m pytest -m "${{ matrix.pytest-mark }}"
|
||||||
- name: Generate coverage report
|
- name: Generate coverage report
|
||||||
run: |
|
run: |
|
||||||
poetry run coverage report
|
uv run coverage report
|
||||||
poetry run coverage html
|
uv run coverage html
|
||||||
- name: Archive code coverage results
|
- name: Archive code coverage results
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: coverage-report
|
name: coverage-report-${{ matrix.pytest-mark }}
|
||||||
path: coverage_report
|
path: coverage_report
|
||||||
|
27
.github/workflows/deploy.yml
vendored
27
.github/workflows/deploy.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: SSH Remote Commands
|
- name: SSH Remote Commands
|
||||||
uses: appleboy/ssh-action@dce9d565de8d876c11d93fa4fe677c0285a66d78
|
uses: appleboy/ssh-action@v1.1.0
|
||||||
with:
|
with:
|
||||||
# Proxy
|
# Proxy
|
||||||
proxy_host : ${{secrets.PROXY_HOST}}
|
proxy_host : ${{secrets.PROXY_HOST}}
|
||||||
@ -31,17 +31,18 @@ jobs:
|
|||||||
|
|
||||||
script_stop: true
|
script_stop: true
|
||||||
|
|
||||||
# See https://github.com/ae-utbm/sith3/wiki/GitHub-Actions#deployment-action
|
# See https://github.com/ae-utbm/sith/wiki/GitHub-Actions#deployment-action
|
||||||
script: |
|
script: |
|
||||||
export PATH="/home/sith/.local/bin:$PATH"
|
cd ${{secrets.SITH_PATH}}
|
||||||
pushd ${{secrets.SITH_PATH}}
|
|
||||||
|
|
||||||
git pull
|
git fetch
|
||||||
poetry install
|
git reset --hard origin/master
|
||||||
poetry run ./manage.py migrate
|
uv sync --group prod
|
||||||
echo "yes" | poetry run ./manage.py collectstatic
|
npm install
|
||||||
poetry run ./manage.py compilestatic
|
uv run ./manage.py install_xapian
|
||||||
poetry run ./manage.py compilemessages
|
uv run ./manage.py migrate
|
||||||
|
uv run ./manage.py collectstatic --clear --noinput
|
||||||
|
uv run ./manage.py compilemessages
|
||||||
|
|
||||||
sudo systemctl restart uwsgi
|
sudo systemctl restart uwsgi
|
||||||
|
|
||||||
@ -51,14 +52,14 @@ jobs:
|
|||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
needs: deployment
|
needs: deployment
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Sentry Release
|
- name: Sentry Release
|
||||||
uses: getsentry/action-release@v1.2.0
|
uses: getsentry/action-release@v1.7.0
|
||||||
env:
|
env:
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||||
with:
|
with:
|
||||||
environment: production
|
environment: production
|
21
.github/workflows/deploy_docs.yml
vendored
Normal file
21
.github/workflows/deploy_docs.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
name: deploy_docs
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.github/actions/setup_project
|
||||||
|
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
||||||
|
- uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
key: mkdocs-material-${{ env.cache_id }}
|
||||||
|
path: .cache
|
||||||
|
restore-keys: |
|
||||||
|
mkdocs-material-
|
||||||
|
- run: uv run mkdocs gh-deploy --force
|
44
.github/workflows/taiste.yml
vendored
44
.github/workflows/taiste.yml
vendored
@ -1,8 +1,9 @@
|
|||||||
name: Sith3 taiste
|
name: Sith taiste
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ taiste ]
|
branches: [taiste]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deployment:
|
deployment:
|
||||||
@ -12,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: SSH Remote Commands
|
- name: SSH Remote Commands
|
||||||
uses: appleboy/ssh-action@dce9d565de8d876c11d93fa4fe677c0285a66d78
|
uses: appleboy/ssh-action@v1.1.0
|
||||||
with:
|
with:
|
||||||
# Proxy
|
# Proxy
|
||||||
proxy_host : ${{secrets.PROXY_HOST}}
|
proxy_host : ${{secrets.PROXY_HOST}}
|
||||||
@ -29,34 +30,17 @@ jobs:
|
|||||||
|
|
||||||
script_stop: true
|
script_stop: true
|
||||||
|
|
||||||
# See https://github.com/ae-utbm/sith3/wiki/GitHub-Actions#deployment-action
|
# See https://github.com/ae-utbm/sith/wiki/GitHub-Actions#deployment-action
|
||||||
script: |
|
script: |
|
||||||
export PATH="$HOME/.poetry/bin:$PATH"
|
cd ${{secrets.SITH_PATH}}
|
||||||
pushd ${{secrets.SITH_PATH}}
|
|
||||||
|
|
||||||
git pull
|
git fetch
|
||||||
poetry install
|
git reset --hard origin/taiste
|
||||||
poetry run ./manage.py migrate
|
uv sync --group prod
|
||||||
echo "yes" | poetry run ./manage.py collectstatic
|
npm install
|
||||||
poetry run ./manage.py compilestatic
|
uv run ./manage.py install_xapian
|
||||||
poetry run ./manage.py compilemessages
|
uv run ./manage.py migrate
|
||||||
|
uv run ./manage.py collectstatic --clear --noinput
|
||||||
|
uv run ./manage.py compilemessages
|
||||||
|
|
||||||
sudo systemctl restart uwsgi
|
sudo systemctl restart uwsgi
|
||||||
|
|
||||||
sentry:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment: taiste
|
|
||||||
timeout-minutes: 30
|
|
||||||
needs: deployment
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Sentry Release
|
|
||||||
uses: getsentry/action-release@v1.2.0
|
|
||||||
env:
|
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
||||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
|
||||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
|
||||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
|
||||||
with:
|
|
||||||
environment: taiste
|
|
||||||
|
17
.gitignore
vendored
17
.gitignore
vendored
@ -1,4 +1,4 @@
|
|||||||
db.sqlite3
|
*.sqlite3
|
||||||
*.log
|
*.log
|
||||||
*.pyc
|
*.pyc
|
||||||
*.mo
|
*.mo
|
||||||
@ -8,7 +8,7 @@ pyrightconfig.json
|
|||||||
dist/
|
dist/
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
env/
|
.venv/
|
||||||
doc/html
|
doc/html
|
||||||
data/
|
data/
|
||||||
galaxy/test_galaxy_state.json
|
galaxy/test_galaxy_state.json
|
||||||
@ -17,4 +17,15 @@ sith/settings_custom.py
|
|||||||
sith/search_indexes/
|
sith/search_indexes/
|
||||||
.coverage
|
.coverage
|
||||||
coverage_report/
|
coverage_report/
|
||||||
doc/_build
|
node_modules/
|
||||||
|
.env
|
||||||
|
*.pid
|
||||||
|
|
||||||
|
# compiled documentation
|
||||||
|
site/
|
||||||
|
|
||||||
|
### Redis ###
|
||||||
|
|
||||||
|
# Ignore redis binary dump (dump.rdb) files
|
||||||
|
|
||||||
|
*.rdb
|
||||||
|
26
.pre-commit-config.yaml
Normal file
26
.pre-commit-config.yaml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
# Ruff version.
|
||||||
|
rev: v0.8.3
|
||||||
|
hooks:
|
||||||
|
- id: ruff # just check the code, and print the errors
|
||||||
|
- id: ruff # actually fix the fixable errors, but print nothing
|
||||||
|
args: ["--fix", "--silent"]
|
||||||
|
# Run the formatter.
|
||||||
|
- id: ruff-format
|
||||||
|
- repo: https://github.com/biomejs/pre-commit
|
||||||
|
rev: "v0.1.0" # Use the sha / tag you want to point at
|
||||||
|
hooks:
|
||||||
|
- id: biome-check
|
||||||
|
additional_dependencies: ["@biomejs/biome@1.9.4"]
|
||||||
|
- repo: https://github.com/rtts/djhtml
|
||||||
|
rev: 3.0.7
|
||||||
|
hooks:
|
||||||
|
- id: djhtml
|
||||||
|
name: format templates
|
||||||
|
entry: djhtml --tabwidth 2
|
||||||
|
types: ["jinja"]
|
||||||
|
- id: djcss
|
||||||
|
name: format scss files
|
||||||
|
entry: djcss --tabwidth 2
|
||||||
|
types: ["scss"]
|
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
3.12
|
@ -1,26 +0,0 @@
|
|||||||
# Read the Docs configuration file
|
|
||||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
|
||||||
|
|
||||||
# Required
|
|
||||||
version: 2
|
|
||||||
|
|
||||||
# Allow installing xapian-bindings in pip
|
|
||||||
build:
|
|
||||||
apt_packages:
|
|
||||||
- libxapian-dev
|
|
||||||
|
|
||||||
# Build documentation in the doc/ directory with Sphinx
|
|
||||||
sphinx:
|
|
||||||
configuration: doc/conf.py
|
|
||||||
|
|
||||||
# Optionally build your docs in additional formats such as PDF and ePub
|
|
||||||
formats: all
|
|
||||||
|
|
||||||
# Optionally set the version of Python and requirements required to build your docs
|
|
||||||
python:
|
|
||||||
version: "3.8"
|
|
||||||
install:
|
|
||||||
- method: pip
|
|
||||||
path: .
|
|
||||||
extra_requirements:
|
|
||||||
- docs
|
|
1
Procfile.service
Normal file
1
Procfile.service
Normal file
@ -0,0 +1 @@
|
|||||||
|
redis: redis-server --port $REDIS_PORT
|
1
Procfile.static
Normal file
1
Procfile.static
Normal file
@ -0,0 +1 @@
|
|||||||
|
bundler: npm run serve
|
49
README.md
49
README.md
@ -1,40 +1,21 @@
|
|||||||
<p align="center">
|
# Sith
|
||||||
<a href="#">
|
|
||||||
<img src="https://img.shields.io/badge/Code%20Style-Black-000000?style=for-the-badge">
|
|
||||||
</a>
|
|
||||||
<a href="#">
|
|
||||||
<img src="https://img.shields.io/github/checks-status/ae-utbm/sith3/master?logo=github&style=for-the-badge&label=BUILD">
|
|
||||||
</a>
|
|
||||||
<a href="https://sith-ae.readthedocs.io/">
|
|
||||||
<img src="https://img.shields.io/readthedocs/sith-ae?logo=readthedocs&style=for-the-badge">
|
|
||||||
</a>
|
|
||||||
<a href="https://discord.gg/XK9WfPsUFm">
|
|
||||||
<img src="https://img.shields.io/discord/971448179075731476?label=Discord&logo=discord&style=for-the-badge">
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3 align="center">This is the source code of the UTBM's student association available at https://ae.utbm.fr/.</h3>
|
[](#)
|
||||||
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
|
[](#)
|
||||||
|
[](https://ae-utbm.github.io/sith)
|
||||||
|
[](https://squidfunk.github.io/mkdocs-material/)
|
||||||
|
[](https://biomejs.dev)
|
||||||
|
[](https://discord.gg/xk9wfpsufm)
|
||||||
|
|
||||||
<p align="justify">All documentation is in the <code>docs</code> directory and online at https://sith-ae.readthedocs.io/. This documentation is written in French because it targets a French audience and it's too much work to maintain two versions. The code and code comments are strictly written in English.</p>
|
### This is the source code of the UTBM's student association available at [https://ae.utbm.fr/](https://ae.utbm.fr/).
|
||||||
|
|
||||||
<h4>If you want to contribute, here's how we recommend to read the docs:</h4>
|
All documentation is in the `docs` directory and online at [https://ae-utbm.github.io/sith](https://ae-utbm.github.io/sith). This documentation is written in French because it targets a French audience and it's too much work to maintain two versions. The code and code comments are strictly written in English.
|
||||||
|
|
||||||
<ul>
|
#### If you want to contribute, here's how we recommend to read the docs:
|
||||||
<li>
|
|
||||||
<p align="justify">
|
* First, it's advised to read the about part of the project to understand the goals and the mindset of the current and previous maintainers and know what to expect to learn.
|
||||||
First, it's advised to read the about part of the project to understand the goals and the mindset of the current and previous maintainers and know what to expect to learn.
|
* If in the first part you realize that you need more background about what we use, we provide some links to tutorials and documentation at the end of our documentation. Feel free to use it and complete it with what you found helpful.
|
||||||
</p>
|
* Keep in mind that this documentation is thought to be read in order.
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<p align="justify">
|
|
||||||
If in the first part you realize that you need more background about what we use, we provide some links to tutorials and documentation at the end of our documentation. Feel free to use it and complete it with what you found helpful.
|
|
||||||
</p>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<p align="justify">
|
|
||||||
Keep in mind that this documentation is thought to be read in order.
|
|
||||||
</p>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
> This project is licensed under GNU GPL, see the LICENSE file at the top of the repository for more details.
|
> This project is licensed under GNU GPL, see the LICENSE file at the top of the repository for more details.
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
#
|
||||||
# Copyright 2023 © AE UTBM
|
# Copyright 2023 © AE UTBM
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
@ -6,10 +5,10 @@
|
|||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# https://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
# You can find the source code of the website at https://github.com/ae-utbm/sith
|
||||||
#
|
#
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
#
|
||||||
# Copyright 2023 © AE UTBM
|
# Copyright 2023 © AE UTBM
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
@ -6,18 +5,26 @@
|
|||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# https://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
# You can find the source code of the website at https://github.com/ae-utbm/sith
|
||||||
#
|
#
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from accounting.models import *
|
from accounting.models import (
|
||||||
|
AccountingType,
|
||||||
|
BankAccount,
|
||||||
|
ClubAccount,
|
||||||
|
Company,
|
||||||
|
GeneralJournal,
|
||||||
|
Label,
|
||||||
|
Operation,
|
||||||
|
SimplifiedAccountingType,
|
||||||
|
)
|
||||||
|
|
||||||
admin.site.register(BankAccount)
|
admin.site.register(BankAccount)
|
||||||
admin.site.register(ClubAccount)
|
admin.site.register(ClubAccount)
|
||||||
|
23
accounting/api.py
Normal file
23
accounting/api.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from annotated_types import MinLen
|
||||||
|
from ninja_extra import ControllerBase, api_controller, paginate, route
|
||||||
|
from ninja_extra.pagination import PageNumberPaginationExtra
|
||||||
|
from ninja_extra.schemas import PaginatedResponseSchema
|
||||||
|
|
||||||
|
from accounting.models import ClubAccount, Company
|
||||||
|
from accounting.schemas import ClubAccountSchema, CompanySchema
|
||||||
|
from core.auth.api_permissions import CanAccessLookup
|
||||||
|
|
||||||
|
|
||||||
|
@api_controller("/lookup", permissions=[CanAccessLookup])
|
||||||
|
class AccountingController(ControllerBase):
|
||||||
|
@route.get("/club-account", response=PaginatedResponseSchema[ClubAccountSchema])
|
||||||
|
@paginate(PageNumberPaginationExtra, page_size=50)
|
||||||
|
def search_club_account(self, search: Annotated[str, MinLen(1)]):
|
||||||
|
return ClubAccount.objects.filter(name__icontains=search).values()
|
||||||
|
|
||||||
|
@route.get("/company", response=PaginatedResponseSchema[CompanySchema])
|
||||||
|
@paginate(PageNumberPaginationExtra, page_size=50)
|
||||||
|
def search_company(self, search: Annotated[str, MinLen(1)]):
|
||||||
|
return Company.objects.filter(name__icontains=search).values()
|
@ -1,10 +1,10 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
import accounting.models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import accounting.models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@ -101,6 +100,6 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name="operation", unique_together=set([("number", "journal")])
|
name="operation", unique_together={("number", "journal")}
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import phonenumber_field.modelfields
|
import phonenumber_field.modelfields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@ -46,6 +45,6 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name="label", unique_together=set([("name", "club_account")])
|
name="label", unique_together={("name", "club_account")}
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
#
|
||||||
# Copyright 2023 © AE UTBM
|
# Copyright 2023 © AE UTBM
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
@ -6,46 +5,56 @@
|
|||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# https://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
# You can find the source code of the website at https://github.com/ae-utbm/sith
|
||||||
#
|
#
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
from django.urls import reverse
|
from decimal import Decimal
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.core import validators
|
|
||||||
from django.db import models
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.template import defaultfilters
|
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core import validators
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
from django.template import defaultfilters
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from phonenumber_field.modelfields import PhoneNumberField
|
from phonenumber_field.modelfields import PhoneNumberField
|
||||||
|
|
||||||
from decimal import Decimal
|
|
||||||
from core.models import User, SithFile
|
|
||||||
from club.models import Club
|
from club.models import Club
|
||||||
|
from core.models import SithFile, User
|
||||||
|
|
||||||
|
|
||||||
class CurrencyField(models.DecimalField):
|
class CurrencyField(models.DecimalField):
|
||||||
"""
|
"""Custom database field used for currency."""
|
||||||
This is a custom database field used for currency
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
kwargs["max_digits"] = 12
|
kwargs["max_digits"] = 12
|
||||||
kwargs["decimal_places"] = 2
|
kwargs["decimal_places"] = 2
|
||||||
super(CurrencyField, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
try:
|
try:
|
||||||
return super(CurrencyField, self).to_python(value).quantize(Decimal("0.01"))
|
return super().to_python(value).quantize(Decimal("0.01"))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
if settings.TESTING:
|
||||||
|
from model_bakery import baker
|
||||||
|
|
||||||
|
baker.generators.add(
|
||||||
|
CurrencyField,
|
||||||
|
lambda: baker.random_gen.gen_decimal(max_digits=8, decimal_places=2),
|
||||||
|
)
|
||||||
|
else: # pragma: no cover
|
||||||
|
# baker is only used in tests, so we don't need coverage for this part
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Accounting classes
|
# Accounting classes
|
||||||
|
|
||||||
|
|
||||||
@ -62,31 +71,8 @@ class Company(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("company")
|
verbose_name = _("company")
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def __str__(self):
|
||||||
"""
|
return self.name
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def can_be_edited_by(self, user):
|
|
||||||
"""
|
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
for club in user.memberships.filter(end_date=None).all():
|
|
||||||
if club and club.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def can_be_viewed_by(self, user):
|
|
||||||
"""
|
|
||||||
Method to see if that object can be viewed by the given user
|
|
||||||
"""
|
|
||||||
for club in user.memberships.filter(end_date=None).all():
|
|
||||||
if club and club.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("accounting:co_edit", kwargs={"co_id": self.id})
|
return reverse("accounting:co_edit", kwargs={"co_id": self.id})
|
||||||
@ -94,8 +80,21 @@ class Company(models.Model):
|
|||||||
def get_display_name(self):
|
def get_display_name(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def __str__(self):
|
def is_owned_by(self, user):
|
||||||
return self.name
|
"""Check if that object can be edited by the given user."""
|
||||||
|
return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
||||||
|
|
||||||
|
def can_be_edited_by(self, user):
|
||||||
|
"""Check if that object can be edited by the given user."""
|
||||||
|
return user.memberships.filter(
|
||||||
|
end_date=None, club__role=settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
def can_be_viewed_by(self, user):
|
||||||
|
"""Check if that object can be viewed by the given user."""
|
||||||
|
return user.memberships.filter(
|
||||||
|
end_date=None, club__role_gte=settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
class BankAccount(models.Model):
|
class BankAccount(models.Model):
|
||||||
@ -113,24 +112,20 @@ class BankAccount(models.Model):
|
|||||||
verbose_name = _("Bank account")
|
verbose_name = _("Bank account")
|
||||||
ordering = ["club", "name"]
|
ordering = ["club", "name"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("accounting:bank_details", kwargs={"b_account_id": self.id})
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
"""
|
"""Check if that object can be edited by the given user."""
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_anonymous:
|
if user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
return True
|
return True
|
||||||
m = self.club.get_membership_for(user)
|
m = self.club.get_membership_for(user)
|
||||||
if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
return m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse("accounting:bank_details", kwargs={"b_account_id": self.id})
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
class ClubAccount(models.Model):
|
class ClubAccount(models.Model):
|
||||||
@ -152,48 +147,33 @@ class ClubAccount(models.Model):
|
|||||||
verbose_name = _("Club account")
|
verbose_name = _("Club account")
|
||||||
ordering = ["bank_account", "name"]
|
ordering = ["bank_account", "name"]
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def __str__(self):
|
||||||
"""
|
return self.name
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_anonymous:
|
|
||||||
return False
|
|
||||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def can_be_edited_by(self, user):
|
|
||||||
"""
|
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
m = self.club.get_membership_for(user)
|
|
||||||
if m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def can_be_viewed_by(self, user):
|
|
||||||
"""
|
|
||||||
Method to see if that object can be viewed by the given user
|
|
||||||
"""
|
|
||||||
m = self.club.get_membership_for(user)
|
|
||||||
if m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def has_open_journal(self):
|
|
||||||
for j in self.journals.all():
|
|
||||||
if not j.closed:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_open_journal(self):
|
|
||||||
return self.journals.filter(closed=False).first()
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("accounting:club_details", kwargs={"c_account_id": self.id})
|
return reverse("accounting:club_details", kwargs={"c_account_id": self.id})
|
||||||
|
|
||||||
def __str__(self):
|
def is_owned_by(self, user):
|
||||||
return self.name
|
"""Check if that object can be edited by the given user."""
|
||||||
|
if user.is_anonymous:
|
||||||
|
return False
|
||||||
|
return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
||||||
|
|
||||||
|
def can_be_edited_by(self, user):
|
||||||
|
"""Check if that object can be edited by the given user."""
|
||||||
|
m = self.club.get_membership_for(user)
|
||||||
|
return m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||||
|
|
||||||
|
def can_be_viewed_by(self, user):
|
||||||
|
"""Check if that object can be viewed by the given user."""
|
||||||
|
m = self.club.get_membership_for(user)
|
||||||
|
return m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||||
|
|
||||||
|
def has_open_journal(self):
|
||||||
|
return self.journals.filter(closed=False).exists()
|
||||||
|
|
||||||
|
def get_open_journal(self):
|
||||||
|
return self.journals.filter(closed=False).first()
|
||||||
|
|
||||||
def get_display_name(self):
|
def get_display_name(self):
|
||||||
return _("%(club_account)s on %(bank_account)s") % {
|
return _("%(club_account)s on %(bank_account)s") % {
|
||||||
@ -203,9 +183,7 @@ class ClubAccount(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class GeneralJournal(models.Model):
|
class GeneralJournal(models.Model):
|
||||||
"""
|
"""Class storing all the operations for a period of time."""
|
||||||
Class storing all the operations for a period of time
|
|
||||||
"""
|
|
||||||
|
|
||||||
start_date = models.DateField(_("start date"))
|
start_date = models.DateField(_("start date"))
|
||||||
end_date = models.DateField(_("end date"), null=True, blank=True, default=None)
|
end_date = models.DateField(_("end date"), null=True, blank=True, default=None)
|
||||||
@ -225,36 +203,28 @@ class GeneralJournal(models.Model):
|
|||||||
verbose_name = _("General journal")
|
verbose_name = _("General journal")
|
||||||
ordering = ["-start_date"]
|
ordering = ["-start_date"]
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def __str__(self):
|
||||||
"""
|
return self.name
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_anonymous:
|
|
||||||
return False
|
|
||||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
|
||||||
return True
|
|
||||||
if self.club_account.can_be_edited_by(user):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def can_be_edited_by(self, user):
|
|
||||||
"""
|
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
|
||||||
return True
|
|
||||||
if self.club_account.can_be_edited_by(user):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def can_be_viewed_by(self, user):
|
|
||||||
return self.club_account.can_be_viewed_by(user)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("accounting:journal_details", kwargs={"j_id": self.id})
|
return reverse("accounting:journal_details", kwargs={"j_id": self.id})
|
||||||
|
|
||||||
def __str__(self):
|
def is_owned_by(self, user):
|
||||||
return self.name
|
"""Check if that object can be edited by the given user."""
|
||||||
|
if user.is_anonymous:
|
||||||
|
return False
|
||||||
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
|
return True
|
||||||
|
return self.club_account.can_be_edited_by(user)
|
||||||
|
|
||||||
|
def can_be_edited_by(self, user):
|
||||||
|
"""Check if that object can be edited by the given user."""
|
||||||
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
|
return True
|
||||||
|
return self.club_account.can_be_edited_by(user)
|
||||||
|
|
||||||
|
def can_be_viewed_by(self, user):
|
||||||
|
return self.club_account.can_be_viewed_by(user)
|
||||||
|
|
||||||
def update_amounts(self):
|
def update_amounts(self):
|
||||||
self.amount = 0
|
self.amount = 0
|
||||||
@ -272,9 +242,7 @@ class GeneralJournal(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Operation(models.Model):
|
class Operation(models.Model):
|
||||||
"""
|
"""An operation is a line in the journal, a debit or a credit."""
|
||||||
An operation is a line in the journal, a debit or a credit
|
|
||||||
"""
|
|
||||||
|
|
||||||
number = models.IntegerField(_("number"))
|
number = models.IntegerField(_("number"))
|
||||||
journal = models.ForeignKey(
|
journal = models.ForeignKey(
|
||||||
@ -357,6 +325,18 @@ class Operation(models.Model):
|
|||||||
unique_together = ("number", "journal")
|
unique_together = ("number", "journal")
|
||||||
ordering = ["-number"]
|
ordering = ["-number"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.amount} € | {self.date} | {self.accounting_type} | {self.done}"
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.number is None:
|
||||||
|
self.number = self.journal.operations.count() + 1
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
self.journal.update_amounts()
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("accounting:journal_details", kwargs={"j_id": self.journal.id})
|
||||||
|
|
||||||
def __getattribute__(self, attr):
|
def __getattribute__(self, attr):
|
||||||
if attr == "target":
|
if attr == "target":
|
||||||
return self.get_target()
|
return self.get_target()
|
||||||
@ -364,7 +344,7 @@ class Operation(models.Model):
|
|||||||
return object.__getattribute__(self, attr)
|
return object.__getattribute__(self, attr)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super(Operation, self).clean()
|
super().clean()
|
||||||
if self.date is None:
|
if self.date is None:
|
||||||
raise ValidationError(_("The date must be set."))
|
raise ValidationError(_("The date must be set."))
|
||||||
elif self.date < self.journal.start_date:
|
elif self.date < self.journal.start_date:
|
||||||
@ -410,16 +390,8 @@ class Operation(models.Model):
|
|||||||
tar = Company.objects.filter(id=self.target_id).first()
|
tar = Company.objects.filter(id=self.target_id).first()
|
||||||
return tar
|
return tar
|
||||||
|
|
||||||
def save(self):
|
|
||||||
if self.number is None:
|
|
||||||
self.number = self.journal.operations.count() + 1
|
|
||||||
super(Operation, self).save()
|
|
||||||
self.journal.update_amounts()
|
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
"""
|
"""Check if that object can be edited by the given user."""
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_anonymous:
|
if user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
@ -427,40 +399,22 @@ class Operation(models.Model):
|
|||||||
if self.journal.closed:
|
if self.journal.closed:
|
||||||
return False
|
return False
|
||||||
m = self.journal.club_account.club.get_membership_for(user)
|
m = self.journal.club_account.club.get_membership_for(user)
|
||||||
if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
return m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def can_be_edited_by(self, user):
|
def can_be_edited_by(self, user):
|
||||||
"""
|
"""Check if that object can be edited by the given user."""
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
||||||
return True
|
return True
|
||||||
if self.journal.closed:
|
if self.journal.closed:
|
||||||
return False
|
return False
|
||||||
m = self.journal.club_account.club.get_membership_for(user)
|
m = self.journal.club_account.club.get_membership_for(user)
|
||||||
if m is not None and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
|
return m is not None and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse("accounting:journal_details", kwargs={"j_id": self.journal.id})
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "%d € | %s | %s | %s" % (
|
|
||||||
self.amount,
|
|
||||||
self.date,
|
|
||||||
self.accounting_type,
|
|
||||||
self.done,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AccountingType(models.Model):
|
class AccountingType(models.Model):
|
||||||
"""
|
"""Accounting types.
|
||||||
Class describing the accounting types.
|
|
||||||
|
|
||||||
Thoses are numbers used in accounting to classify operations
|
Those are numbers used in accounting to classify operations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
code = models.CharField(
|
code = models.CharField(
|
||||||
@ -487,27 +441,21 @@ class AccountingType(models.Model):
|
|||||||
verbose_name = _("accounting type")
|
verbose_name = _("accounting type")
|
||||||
ordering = ["movement_type", "code"]
|
ordering = ["movement_type", "code"]
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def __str__(self):
|
||||||
"""
|
return self.code + " - " + self.get_movement_type_display() + " - " + self.label
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_anonymous:
|
|
||||||
return False
|
|
||||||
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("accounting:type_list")
|
return reverse("accounting:type_list")
|
||||||
|
|
||||||
def __str__(self):
|
def is_owned_by(self, user):
|
||||||
return self.code + " - " + self.get_movement_type_display() + " - " + self.label
|
"""Check if that object can be edited by the given user."""
|
||||||
|
if user.is_anonymous:
|
||||||
|
return False
|
||||||
|
return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
||||||
|
|
||||||
|
|
||||||
class SimplifiedAccountingType(models.Model):
|
class SimplifiedAccountingType(models.Model):
|
||||||
"""
|
"""Simplified version of `AccountingType`."""
|
||||||
Class describing the simplified accounting types.
|
|
||||||
"""
|
|
||||||
|
|
||||||
label = models.CharField(_("label"), max_length=128)
|
label = models.CharField(_("label"), max_length=128)
|
||||||
accounting_type = models.ForeignKey(
|
accounting_type = models.ForeignKey(
|
||||||
@ -521,6 +469,15 @@ class SimplifiedAccountingType(models.Model):
|
|||||||
verbose_name = _("simplified type")
|
verbose_name = _("simplified type")
|
||||||
ordering = ["accounting_type__movement_type", "accounting_type__code"]
|
ordering = ["accounting_type__movement_type", "accounting_type__code"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return (
|
||||||
|
f"{self.get_movement_type_display()} "
|
||||||
|
f"- {self.accounting_type.code} - {self.label}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("accounting:simple_type_list")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def movement_type(self):
|
def movement_type(self):
|
||||||
return self.accounting_type.movement_type
|
return self.accounting_type.movement_type
|
||||||
@ -528,21 +485,9 @@ class SimplifiedAccountingType(models.Model):
|
|||||||
def get_movement_type_display(self):
|
def get_movement_type_display(self):
|
||||||
return self.accounting_type.get_movement_type_display()
|
return self.accounting_type.get_movement_type_display()
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse("accounting:simple_type_list")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return (
|
|
||||||
self.get_movement_type_display()
|
|
||||||
+ " - "
|
|
||||||
+ self.accounting_type.code
|
|
||||||
+ " - "
|
|
||||||
+ self.label
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Label(models.Model):
|
class Label(models.Model):
|
||||||
"""Label allow a club to sort its operations"""
|
"""Label allow a club to sort its operations."""
|
||||||
|
|
||||||
name = models.CharField(_("label"), max_length=64)
|
name = models.CharField(_("label"), max_length=64)
|
||||||
club_account = models.ForeignKey(
|
club_account = models.ForeignKey(
|
||||||
|
15
accounting/schemas.py
Normal file
15
accounting/schemas.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from ninja import ModelSchema
|
||||||
|
|
||||||
|
from accounting.models import ClubAccount, Company
|
||||||
|
|
||||||
|
|
||||||
|
class ClubAccountSchema(ModelSchema):
|
||||||
|
class Meta:
|
||||||
|
model = ClubAccount
|
||||||
|
fields = ["id", "name"]
|
||||||
|
|
||||||
|
|
||||||
|
class CompanySchema(ModelSchema):
|
||||||
|
class Meta:
|
||||||
|
model = Company
|
||||||
|
fields = ["id", "name"]
|
@ -0,0 +1,60 @@
|
|||||||
|
import { AjaxSelect } from "#core:core/components/ajax-select-base";
|
||||||
|
import { registerComponent } from "#core:utils/web-components";
|
||||||
|
import type { TomOption } from "tom-select/dist/types/types";
|
||||||
|
import type { escape_html } from "tom-select/dist/types/utils";
|
||||||
|
import {
|
||||||
|
type ClubAccountSchema,
|
||||||
|
type CompanySchema,
|
||||||
|
accountingSearchClubAccount,
|
||||||
|
accountingSearchCompany,
|
||||||
|
} from "#openapi";
|
||||||
|
|
||||||
|
@registerComponent("club-account-ajax-select")
|
||||||
|
export class ClubAccountAjaxSelect extends AjaxSelect {
|
||||||
|
protected valueField = "id";
|
||||||
|
protected labelField = "name";
|
||||||
|
protected searchField = ["code", "name"];
|
||||||
|
|
||||||
|
protected async search(query: string): Promise<TomOption[]> {
|
||||||
|
const resp = await accountingSearchClubAccount({ query: { search: query } });
|
||||||
|
if (resp.data) {
|
||||||
|
return resp.data.results;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderOption(item: ClubAccountSchema, sanitize: typeof escape_html) {
|
||||||
|
return `<div class="select-item">
|
||||||
|
<span class="select-item-text">${sanitize(item.name)}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderItem(item: ClubAccountSchema, sanitize: typeof escape_html) {
|
||||||
|
return `<span>${sanitize(item.name)}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@registerComponent("company-ajax-select")
|
||||||
|
export class CompanyAjaxSelect extends AjaxSelect {
|
||||||
|
protected valueField = "id";
|
||||||
|
protected labelField = "name";
|
||||||
|
protected searchField = ["code", "name"];
|
||||||
|
|
||||||
|
protected async search(query: string): Promise<TomOption[]> {
|
||||||
|
const resp = await accountingSearchCompany({ query: { search: query } });
|
||||||
|
if (resp.data) {
|
||||||
|
return resp.data.results;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderOption(item: CompanySchema, sanitize: typeof escape_html) {
|
||||||
|
return `<div class="select-item">
|
||||||
|
<span class="select-item-text">${sanitize(item.name)}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderItem(item: CompanySchema, sanitize: typeof escape_html) {
|
||||||
|
return `<span>${sanitize(item.name)}</span>`;
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +1,27 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}Accounting type list{% endtrans %}
|
{% trans %}Accounting type list{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="accounting">
|
<div id="accounting">
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
|
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
|
||||||
{% trans %}Accounting types{% endtrans %}
|
{% trans %}Accounting types{% endtrans %}
|
||||||
</p>
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<p><a href="{{ url('accounting:type_new') }}">{% trans %}New accounting type{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:type_new') }}">{% trans %}New accounting type{% endtrans %}</a></p>
|
||||||
{% if accountingtype_list %}
|
{% if accountingtype_list %}
|
||||||
<h3>{% trans %}Accounting type list{% endtrans %}</h3>
|
<h3>{% trans %}Accounting type list{% endtrans %}</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{% for a in accountingtype_list %}
|
{% for a in accountingtype_list %}
|
||||||
<li><a href="{{ url('accounting:type_edit', type_id=a.id) }}">{{ a }}</a></li>
|
<li><a href="{{ url('accounting:type_edit', type_id=a.id) }}">{{ a }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans %}There is no types in this website.{% endtrans %}
|
{% trans %}There is no types in this website.{% endtrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -1,37 +1,37 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}Bank account: {% endtrans %}{{ object.name }}
|
{% trans %}Bank account: {% endtrans %}{{ object.name }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="accounting">
|
<div id="accounting">
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
|
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
|
||||||
{{ object.name }}
|
{{ object.name }}
|
||||||
</p>
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<h2>{% trans %}Bank account: {% endtrans %}{{ object.name }}</h2>
|
<h2>{% trans %}Bank account: {% endtrans %}{{ object.name }}</h2>
|
||||||
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and not object.club_accounts.exists() %}
|
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and not object.club_accounts.exists() %}
|
||||||
<a href="{{ url('accounting:bank_delete', b_account_id=object.id) }}">{% trans %}Delete{% endtrans %}</a>
|
<a href="{{ url('accounting:bank_delete', b_account_id=object.id) }}">{% trans %}Delete{% endtrans %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<h4>{% trans %}Infos{% endtrans %}</h4>
|
<h4>{% trans %}Infos{% endtrans %}</h4>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>{% trans %}IBAN: {% endtrans %}</strong>{{ object.iban }}</li>
|
<li><strong>{% trans %}IBAN: {% endtrans %}</strong>{{ object.iban }}</li>
|
||||||
<li><strong>{% trans %}Number: {% endtrans %}</strong>{{ object.number }}</li>
|
<li><strong>{% trans %}Number: {% endtrans %}</strong>{{ object.number }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p><a href="{{ url('accounting:club_new') }}?parent={{ object.id }}">{% trans %}New club account{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:club_new') }}?parent={{ object.id }}">{% trans %}New club account{% endtrans %}</a></p>
|
||||||
<ul>
|
<ul>
|
||||||
{% for c in object.club_accounts.all() %}
|
{% for c in object.club_accounts.all() %}
|
||||||
<li><a href="{{ url('accounting:club_details', c_account_id=c.id) }}">{{ c }}</a>
|
<li><a href="{{ url('accounting:club_details', c_account_id=c.id) }}">{{ c }}</a>
|
||||||
- <a href="{{ url('accounting:club_edit', c_account_id=c.id) }}">{% trans %}Edit{% endtrans %}</a>
|
- <a href="{{ url('accounting:club_edit', c_account_id=c.id) }}">{% trans %}Edit{% endtrans %}</a>
|
||||||
{% if c.journals.count() == 0 %}
|
{% if c.journals.count() == 0 %}
|
||||||
- <a href="{{ url('accounting:club_delete', c_account_id=c.id) }}">{% trans %}Delete{% endtrans %}</a>
|
- <a href="{{ url('accounting:club_delete', c_account_id=c.id) }}">{% trans %}Delete{% endtrans %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,32 +1,32 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}Bank account list{% endtrans %}
|
{% trans %}Bank account list{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="accounting">
|
<div id="accounting">
|
||||||
<h4>
|
<h4>
|
||||||
{% trans %}Accounting{% endtrans %}
|
{% trans %}Accounting{% endtrans %}
|
||||||
</h4>
|
</h4>
|
||||||
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
|
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
|
||||||
<p><a href="{{ url('accounting:simple_type_list') }}">{% trans %}Manage simplified types{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:simple_type_list') }}">{% trans %}Manage simplified types{% endtrans %}</a></p>
|
||||||
<p><a href="{{ url('accounting:type_list') }}">{% trans %}Manage accounting types{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:type_list') }}">{% trans %}Manage accounting types{% endtrans %}</a></p>
|
||||||
<p><a href="{{ url('accounting:bank_new') }}">{% trans %}New bank account{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:bank_new') }}">{% trans %}New bank account{% endtrans %}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if bankaccount_list %}
|
{% if bankaccount_list %}
|
||||||
<h3>{% trans %}Bank account list{% endtrans %}</h3>
|
<h3>{% trans %}Bank account list{% endtrans %}</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{% for a in object_list %}
|
{% for a in object_list %}
|
||||||
<li><a href="{{ url('accounting:bank_details', b_account_id=a.id) }}">{{ a }}</a>
|
<li><a href="{{ url('accounting:bank_details', b_account_id=a.id) }}">{{ a }}</a>
|
||||||
- <a href="{{ url('accounting:bank_edit', b_account_id=a.id) }}">{% trans %}Edit{% endtrans %}</a>
|
- <a href="{{ url('accounting:bank_edit', b_account_id=a.id) }}">{% trans %}Edit{% endtrans %}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans %}There is no accounts in this website.{% endtrans %}
|
{% trans %}There is no accounts in this website.{% endtrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,68 +1,68 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}Club account:{% endtrans %} {{ object.name }}
|
{% trans %}Club account:{% endtrans %} {{ object.name }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="accounting">
|
<div id="accounting">
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
|
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
|
||||||
<a href="{{ url('accounting:bank_details', b_account_id=object.bank_account.id) }}">{{object.bank_account }}</a> >
|
<a href="{{ url('accounting:bank_details', b_account_id=object.bank_account.id) }}">{{object.bank_account }}</a> >
|
||||||
{{ object }}
|
{{ object }}
|
||||||
</p>
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<h2>{% trans %}Club account:{% endtrans %} {{ object.name }}</h2>
|
<h2>{% trans %}Club account:{% endtrans %} {{ object.name }}</h2>
|
||||||
{% if user.is_root and not object.journals.exists() %}
|
{% if user.is_root and not object.journals.exists() %}
|
||||||
<a href="{{ url('accounting:club_delete', c_account_id=object.id) }}">{% trans %}Delete{% endtrans %}</a>
|
<a href="{{ url('accounting:club_delete', c_account_id=object.id) }}">{% trans %}Delete{% endtrans %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
|
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
|
||||||
<p><a href="{{ url('accounting:label_new') }}?parent={{ object.id }}">{% trans %}New label{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:label_new') }}?parent={{ object.id }}">{% trans %}New label{% endtrans %}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p><a href="{{ url('accounting:label_list', clubaccount_id=object.id) }}">{% trans %}Label list{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:label_list', clubaccount_id=object.id) }}">{% trans %}Label list{% endtrans %}</a></p>
|
||||||
{% if not object.has_open_journal() %}
|
{% if not object.has_open_journal() %}
|
||||||
<p><a href="{{ url('accounting:journal_new') }}?parent={{ object.id }}">{% trans %}New journal{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:journal_new') }}?parent={{ object.id }}">{% trans %}New journal{% endtrans %}</a></p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{% trans %}You can not create new journal while you still have one opened{% endtrans %}</p>
|
<p>{% trans %}You can not create new journal while you still have one opened{% endtrans %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans %}Name{% endtrans %}</td>
|
<td>{% trans %}Name{% endtrans %}</td>
|
||||||
<td>{% trans %}Start{% endtrans %}</td>
|
<td>{% trans %}Start{% endtrans %}</td>
|
||||||
<td>{% trans %}End{% endtrans %}</td>
|
<td>{% trans %}End{% endtrans %}</td>
|
||||||
<td>{% trans %}Amount{% endtrans %}</td>
|
<td>{% trans %}Amount{% endtrans %}</td>
|
||||||
<td>{% trans %}Effective amount{% endtrans %}</td>
|
<td>{% trans %}Effective amount{% endtrans %}</td>
|
||||||
<td>{% trans %}Closed{% endtrans %}</td>
|
<td>{% trans %}Closed{% endtrans %}</td>
|
||||||
<td>{% trans %}Actions{% endtrans %}</td>
|
<td>{% trans %}Actions{% endtrans %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for j in object.journals.all() %}
|
{% for j in object.journals.all() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ j.name }}</td>
|
<td>{{ j.name }}</td>
|
||||||
<td>{{ j.start_date }}</td>
|
<td>{{ j.start_date }}</td>
|
||||||
{% if j.end_date %}
|
{% if j.end_date %}
|
||||||
<td>{{ j.end_date }}</td>
|
<td>{{ j.end_date }}</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td> - </td>
|
<td> - </td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>{{ j.amount }} €</td>
|
<td>{{ j.amount }} €</td>
|
||||||
<td>{{ j.effective_amount }} €</td>
|
<td>{{ j.effective_amount }} €</td>
|
||||||
{% if j.closed %}
|
{% if j.closed %}
|
||||||
<td>{% trans %}Yes{% endtrans %}</td>
|
<td>{% trans %}Yes{% endtrans %}</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td>{% trans %}No{% endtrans %}</td>
|
<td>{% trans %}No{% endtrans %}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td> <a href="{{ url('accounting:journal_details', j_id=j.id) }}">{% trans %}View{% endtrans %}</a>
|
<td> <a href="{{ url('accounting:journal_details', j_id=j.id) }}">{% trans %}View{% endtrans %}</a>
|
||||||
<a href="{{ url('accounting:journal_edit', j_id=j.id) }}">{% trans %}Edit{% endtrans %}</a>
|
<a href="{{ url('accounting:journal_edit', j_id=j.id) }}">{% trans %}Edit{% endtrans %}</a>
|
||||||
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and j.operations.count() == 0 %}
|
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and j.operations.count() == 0 %}
|
||||||
<a href="{{ url('accounting:journal_delete', j_id=j.id) }}">{% trans %}Delete{% endtrans %}</a>
|
<a href="{{ url('accounting:journal_delete', j_id=j.id) }}">{% trans %}Delete{% endtrans %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,30 +1,30 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}Company list{% endtrans %}
|
{% trans %}Company list{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="accounting">
|
<div id="accounting">
|
||||||
{% if user.is_root
|
{% if user.is_root
|
||||||
or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
||||||
%}
|
%}
|
||||||
<p><a href="{{ url('accounting:co_new') }}">{% trans %}Create new company{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:co_new') }}">{% trans %}Create new company{% endtrans %}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br/>
|
<br/>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans %}Companies{% endtrans %}</td>
|
<td>{% trans %}Companies{% endtrans %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for o in object_list %}
|
{% for o in object_list %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{ url('accounting:co_edit', co_id=o.id) }}">{{ o.get_display_name() }}</a></td>
|
<td><a href="{{ url('accounting:co_edit', co_id=o.id) }}">{{ o.get_display_name() }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,103 +1,103 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}General journal:{% endtrans %} {{ object.name }}
|
{% trans %}General journal:{% endtrans %} {{ object.name }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="accounting">
|
<div id="accounting">
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
|
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
|
||||||
<a href="{{ url('accounting:bank_details', b_account_id=object.club_account.bank_account.id) }}">{{object.club_account.bank_account }}</a> >
|
<a href="{{ url('accounting:bank_details', b_account_id=object.club_account.bank_account.id) }}">{{object.club_account.bank_account }}</a> >
|
||||||
<a href="{{ url('accounting:club_details', c_account_id=object.club_account.id) }}">{{ object.club_account }}</a> >
|
<a href="{{ url('accounting:club_details', c_account_id=object.club_account.id) }}">{{ object.club_account }}</a> >
|
||||||
{{ object.name }}
|
{{ object.name }}
|
||||||
</p>
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<h2>{% trans %}General journal:{% endtrans %} {{ object.name }}</h2>
|
<h2>{% trans %}General journal:{% endtrans %} {{ object.name }}</h2>
|
||||||
<p><a href="{{ url('accounting:label_new') }}?parent={{ object.club_account.id }}">{% trans %}New label{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:label_new') }}?parent={{ object.club_account.id }}">{% trans %}New label{% endtrans %}</a></p>
|
||||||
<p><a href="{{ url('accounting:label_list', clubaccount_id=object.club_account.id) }}">{% trans %}Label list{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:label_list', clubaccount_id=object.club_account.id) }}">{% trans %}Label list{% endtrans %}</a></p>
|
||||||
<p><a href="{{ url('accounting:co_list') }}">{% trans %}Company list{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:co_list') }}">{% trans %}Company list{% endtrans %}</a></p>
|
||||||
<p><strong>{% trans %}Amount: {% endtrans %}</strong>{{ object.amount }} € -
|
<p><strong>{% trans %}Amount: {% endtrans %}</strong>{{ object.amount }} € -
|
||||||
<strong>{% trans %}Effective amount: {% endtrans %}</strong>{{ object.effective_amount }} €</p>
|
<strong>{% trans %}Effective amount: {% endtrans %}</strong>{{ object.effective_amount }} €</p>
|
||||||
{% if object.closed %}
|
{% if object.closed %}
|
||||||
<p>{% trans %}Journal is closed, you can not create operation{% endtrans %}</p>
|
<p>{% trans %}Journal is closed, you can not create operation{% endtrans %}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p><a href="{{ url('accounting:op_new', j_id=object.id) }}">{% trans %}New operation{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:op_new', j_id=object.id) }}">{% trans %}New operation{% endtrans %}</a></p>
|
||||||
</br>
|
</br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="journal-table">
|
<div class="journal-table">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>{% trans %}Nb{% endtrans %}</td>
|
||||||
|
<td>{% trans %}Date{% endtrans %}</td>
|
||||||
|
<td>{% trans %}Label{% endtrans %}</td>
|
||||||
|
<td>{% trans %}Amount{% endtrans %}</td>
|
||||||
|
<td>{% trans %}Payment mode{% endtrans %}</td>
|
||||||
|
<td>{% trans %}Target{% endtrans %}</td>
|
||||||
|
<td>{% trans %}Code{% endtrans %}</td>
|
||||||
|
<td>{% trans %}Nature{% endtrans %}</td>
|
||||||
|
<td>{% trans %}Done{% endtrans %}</td>
|
||||||
|
<td>{% trans %}Comment{% endtrans %}</td>
|
||||||
|
<td>{% trans %}File{% endtrans %}</td>
|
||||||
|
<td>{% trans %}Actions{% endtrans %}</td>
|
||||||
|
<td>{% trans %}PDF{% endtrans %}</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for o in object.operations.all() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans %}Nb{% endtrans %}</td>
|
<td>{{ o.number }}</td>
|
||||||
<td>{% trans %}Date{% endtrans %}</td>
|
<td>{{ o.date }}</td>
|
||||||
<td>{% trans %}Label{% endtrans %}</td>
|
<td>{{ o.label or "" }}</td>
|
||||||
<td>{% trans %}Amount{% endtrans %}</td>
|
{% if o.accounting_type.movement_type == "DEBIT" %}
|
||||||
<td>{% trans %}Payment mode{% endtrans %}</td>
|
<td class="neg-amount"> {{ o.amount }} €</td>
|
||||||
<td>{% trans %}Target{% endtrans %}</td>
|
{% else %}
|
||||||
<td>{% trans %}Code{% endtrans %}</td>
|
<td class="pos-amount"> {{ o.amount }} €</td>
|
||||||
<td>{% trans %}Nature{% endtrans %}</td>
|
{% endif %}
|
||||||
<td>{% trans %}Done{% endtrans %}</td>
|
<td>{{ o.get_mode_display() }}</td>
|
||||||
<td>{% trans %}Comment{% endtrans %}</td>
|
{% if o.target_type == "OTHER" %}
|
||||||
<td>{% trans %}File{% endtrans %}</td>
|
|
||||||
<td>{% trans %}Actions{% endtrans %}</td>
|
|
||||||
<td>{% trans %}PDF{% endtrans %}</td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for o in object.operations.all() %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ o.number }}</td>
|
|
||||||
<td>{{ o.date }}</td>
|
|
||||||
<td>{{ o.label or "" }}</td>
|
|
||||||
{% if o.accounting_type.movement_type == "DEBIT" %}
|
|
||||||
<td class="neg-amount"> {{ o.amount }} €</td>
|
|
||||||
{% else %}
|
|
||||||
<td class="pos-amount"> {{ o.amount }} €</td>
|
|
||||||
{% endif %}
|
|
||||||
<td>{{ o.get_mode_display() }}</td>
|
|
||||||
{% if o.target_type == "OTHER" %}
|
|
||||||
<td>{{ o.target_label }}</td>
|
<td>{{ o.target_label }}</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td><a href="{{ o.target.get_absolute_url() }}">{{ o.target.get_display_name() }}</a></td>
|
<td><a href="{{ o.target.get_absolute_url() }}">{{ o.target.get_display_name() }}</a></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>{{ o.accounting_type.code }}</td>
|
<td>{{ o.accounting_type.code }}</td>
|
||||||
<td>{{ o.accounting_type.label }}</td>
|
<td>{{ o.accounting_type.label }}</td>
|
||||||
{% if o.done %}
|
{% if o.done %}
|
||||||
<td>{% trans %}Yes{% endtrans %}</td>
|
<td>{% trans %}Yes{% endtrans %}</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td>{% trans %}No{% endtrans %}</td>
|
<td>{% trans %}No{% endtrans %}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>{{ o.remark }}
|
<td>{{ o.remark }}
|
||||||
{% if not o.linked_operation and o.target_type == "ACCOUNT" and not o.target.has_open_journal() %}
|
{% if not o.linked_operation and o.target_type == "ACCOUNT" and not o.target.has_open_journal() %}
|
||||||
<p><strong>
|
<p><strong>
|
||||||
{% trans %}Warning: this operation has no linked operation because the targeted club account has no opened journal.{% endtrans %}
|
{% trans %}Warning: this operation has no linked operation because the targeted club account has no opened journal.{% endtrans %}
|
||||||
</strong></p>
|
</strong></p>
|
||||||
<p><strong>
|
<p><strong>
|
||||||
{% trans url=o.target.get_absolute_url() %}Open a journal in <a href="{{ url }}">this club account</a>, then save this operation again to make the linked operation.{% endtrans %}
|
{% trans url=o.target.get_absolute_url() %}Open a journal in <a href="{{ url }}">this club account</a>, then save this operation again to make the linked operation.{% endtrans %}
|
||||||
</strong></p>
|
</strong></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% if o.invoice %}
|
{% if o.invoice %}
|
||||||
<td><a href="{{ url('core:download', file_id=o.invoice.id) }}">{{ o.invoice.name }}</a></td>
|
<td><a href="{{ url('core:download', file_id=o.invoice.id) }}">{{ o.invoice.name }}</a></td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td>-</td>
|
<td>-</td>
|
||||||
|
{% endif %}
|
||||||
|
<td>
|
||||||
|
{%
|
||||||
|
if o.journal.club_account.bank_account.name not in ["AE TI", "TI"]
|
||||||
|
or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
||||||
|
%}
|
||||||
|
{% if not o.journal.closed %}
|
||||||
|
<a href="{{ url('accounting:op_edit', op_id=o.id) }}">{% trans %}Edit{% endtrans %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>
|
{% endif %}
|
||||||
{%
|
</td>
|
||||||
if o.journal.club_account.bank_account.name not in ["AE TI", "TI"]
|
<td><a href="{{ url('accounting:op_pdf', op_id=o.id) }}">{% trans %}Generate{% endtrans %}</a></td>
|
||||||
or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
|
</tr>
|
||||||
%}
|
{% endfor %}
|
||||||
{% if not o.journal.closed %}
|
</tbody>
|
||||||
<a href="{{ url('accounting:op_edit', op_id=o.id) }}">{% trans %}Edit{% endtrans %}</a>
|
</table>
|
||||||
{% endif %}
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
</td>
|
|
||||||
<td><a href="{{ url('accounting:op_pdf', op_id=o.id) }}">{% trans %}Generate{% endtrans %}</a></td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,33 +1,33 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}General journal:{% endtrans %} {{ object.name }}
|
{% trans %}General journal:{% endtrans %} {{ object.name }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="accounting">
|
<div id="accounting">
|
||||||
<h3>{% trans %}Accounting statement: {% endtrans %} {{ object.name }}</h3>
|
<h3>{% trans %}Accounting statement: {% endtrans %} {{ object.name }}</h3>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans %}Operation type{% endtrans %}</td>
|
<td>{% trans %}Operation type{% endtrans %}</td>
|
||||||
<td>{% trans %}Sum{% endtrans %}</td>
|
<td>{% trans %}Sum{% endtrans %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for k,v in statement.items() %}
|
{% for k,v in statement.items() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ k }}</td>
|
<td>{{ k }}</td>
|
||||||
<td>{{ "%.2f" % v }}</td>
|
<td>{{ "%.2f" % v }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<p><strong>{% trans %}Amount: {% endtrans %}</strong>{{ "%.2f" % object.amount }} €</p>
|
<p><strong>{% trans %}Amount: {% endtrans %}</strong>{{ "%.2f" % object.amount }} €</p>
|
||||||
<p><strong>{% trans %}Effective amount: {% endtrans %}</strong>{{ "%.2f" %object.effective_amount }} €</p>
|
<p><strong>{% trans %}Effective amount: {% endtrans %}</strong>{{ "%.2f" %object.effective_amount }} €</p>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,57 +1,57 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}General journal:{% endtrans %} {{ object.name }}
|
{% trans %}General journal:{% endtrans %} {{ object.name }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% macro display_tables(dict) %}
|
{% macro display_tables(dict) %}
|
||||||
<div id="accounting">
|
<div id="accounting">
|
||||||
<h6>{% trans %}Credit{% endtrans %}</h6>
|
<h6>{% trans %}Credit{% endtrans %}</h6>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans %}Nature of operation{% endtrans %}</td>
|
<td>{% trans %}Nature of operation{% endtrans %}</td>
|
||||||
<td>{% trans %}Sum{% endtrans %}</td>
|
<td>{% trans %}Sum{% endtrans %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for k,v in dict['CREDIT'].items() %}
|
{% for k,v in dict['CREDIT'].items() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ k }}</td>
|
<td>{{ k }}</td>
|
||||||
<td>{{ "%.2f" % v }}</td>
|
<td>{{ "%.2f" % v }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% trans %}Total: {% endtrans %}{{ "%.2f" % dict['CREDIT_sum'] }}
|
{% trans %}Total: {% endtrans %}{{ "%.2f" % dict['CREDIT_sum'] }}
|
||||||
|
|
||||||
<h6>{% trans %}Debit{% endtrans %}</h6>
|
<h6>{% trans %}Debit{% endtrans %}</h6>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans %}Nature of operation{% endtrans %}</td>
|
<td>{% trans %}Nature of operation{% endtrans %}</td>
|
||||||
<td>{% trans %}Sum{% endtrans %}</td>
|
<td>{% trans %}Sum{% endtrans %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for k,v in dict['DEBIT'].items() %}
|
{% for k,v in dict['DEBIT'].items() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ k }}</td>
|
<td>{{ k }}</td>
|
||||||
<td>{{ "%.2f" % v }}</td>
|
<td>{{ "%.2f" % v }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% trans %}Total: {% endtrans %}{{ "%.2f" % dict['DEBIT_sum'] }}
|
{% trans %}Total: {% endtrans %}{{ "%.2f" % dict['DEBIT_sum'] }}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>{% trans %}Statement by nature: {% endtrans %} {{ object.name }}</h3>
|
<h3>{% trans %}Statement by nature: {% endtrans %} {{ object.name }}</h3>
|
||||||
|
|
||||||
{% for k,v in statement.items() %}
|
{% for k,v in statement.items() %}
|
||||||
<h4 style="background: lightblue; padding: 4px;">{{ k }} : {{ "%.2f" % (v['CREDIT_sum'] - v['DEBIT_sum']) }}</h4>
|
<h4 style="background: lightblue; padding: 4px;">{{ k }} : {{ "%.2f" % (v['CREDIT_sum'] - v['DEBIT_sum']) }}</h4>
|
||||||
{{ display_tables(v) }}
|
{{ display_tables(v) }}
|
||||||
<hr>
|
<hr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,37 +1,37 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}General journal:{% endtrans %} {{ object.name }}
|
{% trans %}General journal:{% endtrans %} {{ object.name }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="accounting">
|
<div id="accounting">
|
||||||
<h3>{% trans %}Statement by person: {% endtrans %} {{ object.name }}</h3>
|
<h3>{% trans %}Statement by person: {% endtrans %} {{ object.name }}</h3>
|
||||||
|
|
||||||
<h4>{% trans %}Credit{% endtrans %}</h4>
|
<h4>{% trans %}Credit{% endtrans %}</h4>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans %}Target of the operation{% endtrans %}</td>
|
<td>{% trans %}Target of the operation{% endtrans %}</td>
|
||||||
<td>{% trans %}Sum{% endtrans %}</td>
|
<td>{% trans %}Sum{% endtrans %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for key in credit_statement.keys() %}
|
{% for key in credit_statement.keys() %}
|
||||||
<tr>
|
<tr>
|
||||||
{% if key.target_type == "OTHER" %}
|
{% if key.target_type == "OTHER" %}
|
||||||
<td>{{ o.target_label }}</td>
|
<td>{{ o.target_label }}</td>
|
||||||
{% elif key %}
|
{% elif key %}
|
||||||
<td><a href="{{ key.get_absolute_url() }}">{{ key.get_display_name() }}</a></td>
|
<td><a href="{{ key.get_absolute_url() }}">{{ key.get_display_name() }}</a></td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td></td>
|
<td></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>{{ "%.2f" % credit_statement[key] }}</td>
|
<td>{{ "%.2f" % credit_statement[key] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@ -40,29 +40,29 @@
|
|||||||
<h4>{% trans %}Debit{% endtrans %}</h4>
|
<h4>{% trans %}Debit{% endtrans %}</h4>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans %}Target of the operation{% endtrans %}</td>
|
<td>{% trans %}Target of the operation{% endtrans %}</td>
|
||||||
<td>{% trans %}Sum{% endtrans %}</td>
|
<td>{% trans %}Sum{% endtrans %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for key in debit_statement.keys() %}
|
{% for key in debit_statement.keys() %}
|
||||||
<tr>
|
<tr>
|
||||||
{% if key.target_type == "OTHER" %}
|
{% if key.target_type == "OTHER" %}
|
||||||
<td>{{ o.target_label }}</td>
|
<td>{{ o.target_label }}</td>
|
||||||
{% elif key %}
|
{% elif key %}
|
||||||
<td><a href="{{ key.get_absolute_url() }}">{{ key.get_display_name() }}</a></td>
|
<td><a href="{{ key.get_absolute_url() }}">{{ key.get_display_name() }}</a></td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td></td>
|
<td></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>{{ "%.2f" % debit_statement[key] }}</td>
|
<td>{{ "%.2f" % debit_statement[key] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<p>Total : {{ "%.2f" % total_debit }}</p>
|
<p>Total : {{ "%.2f" % total_debit }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,36 +1,36 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}Label list{% endtrans %}
|
{% trans %}Label list{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="accounting">
|
<div id="accounting">
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
|
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
|
||||||
<a href="{{ url('accounting:bank_details', b_account_id=object.bank_account.id) }}">{{object.bank_account }}</a> >
|
<a href="{{ url('accounting:bank_details', b_account_id=object.bank_account.id) }}">{{object.bank_account }}</a> >
|
||||||
<a href="{{ url('accounting:club_details', c_account_id=object.id) }}">{{ object }}</a>
|
<a href="{{ url('accounting:club_details', c_account_id=object.id) }}">{{ object }}</a>
|
||||||
</p>
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<p><a href="{{ url('accounting:club_details', c_account_id=object.id) }}">{% trans %}Back to club account{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:club_details', c_account_id=object.id) }}">{% trans %}Back to club account{% endtrans %}</a></p>
|
||||||
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
|
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
|
||||||
<p><a href="{{ url('accounting:label_new') }}?parent={{ object.id }}">{% trans %}New label{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:label_new') }}?parent={{ object.id }}">{% trans %}New label{% endtrans %}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if object.labels.all() %}
|
{% if object.labels.all() %}
|
||||||
<h3>{% trans %}Label list{% endtrans %}</h3>
|
<h3>{% trans %}Label list{% endtrans %}</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{% for l in object.labels.all() %}
|
{% for l in object.labels.all() %}
|
||||||
<li><a href="{{ url('accounting:label_edit', label_id=l.id) }}">{{ l }}</a>
|
<li><a href="{{ url('accounting:label_edit', label_id=l.id) }}">{{ l }}</a>
|
||||||
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
|
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
|
||||||
-
|
-
|
||||||
<a href="{{ url('accounting:label_delete', label_id=l.id) }}">{% trans %}Delete{% endtrans %}</a>
|
<a href="{{ url('accounting:label_delete', label_id=l.id) }}">{% trans %}Delete{% endtrans %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans %}There is no label in this club account.{% endtrans %}
|
{% trans %}There is no label in this club account.{% endtrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -1,123 +1,123 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}Edit operation{% endtrans %}
|
{% trans %}Edit operation{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="accounting">
|
<div id="accounting">
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
|
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
|
||||||
<a href="{{ url('accounting:bank_details', b_account_id=object.club_account.bank_account.id) }}">{{object.club_account.bank_account }}</a> >
|
<a href="{{ url('accounting:bank_details', b_account_id=object.club_account.bank_account.id) }}">{{object.club_account.bank_account }}</a> >
|
||||||
<a href="{{ url('accounting:club_details', c_account_id=object.club_account.id) }}">{{ object.club_account }}</a> >
|
<a href="{{ url('accounting:club_details', c_account_id=object.club_account.id) }}">{{ object.club_account }}</a> >
|
||||||
<a href="{{ url('accounting:journal_details', j_id=object.id) }}">{{ object.name }}</a> >
|
<a href="{{ url('accounting:journal_details', j_id=object.id) }}">{{ object.name }}</a> >
|
||||||
{% trans %}Edit operation{% endtrans %}
|
{% trans %}Edit operation{% endtrans %}
|
||||||
</p>
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<h2>{% trans %}Edit operation{% endtrans %}</h2>
|
<h2>{% trans %}Edit operation{% endtrans %}</h2>
|
||||||
<form action="" method="post">
|
<form action="" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form.non_field_errors() }}
|
{{ form.non_field_errors() }}
|
||||||
{{ form.journal }}
|
{{ form.journal }}
|
||||||
{{ form.target_id }}
|
{{ form.target_id }}
|
||||||
<p>{{ form.amount.errors }}<label for="{{ form.amount.name }}">{{ form.amount.label }}</label> {{ form.amount }}</p>
|
<p>{{ form.amount.errors }}<label for="{{ form.amount.name }}">{{ form.amount.label }}</label> {{ form.amount }}</p>
|
||||||
<p>{{ form.remark.errors }}<label for="{{ form.remark.name }}">{{ form.remark.label }}</label> {{ form.remark }}</p>
|
<p>{{ form.remark.errors }}<label for="{{ form.remark.name }}">{{ form.remark.label }}</label> {{ form.remark }}</p>
|
||||||
<br />
|
<br />
|
||||||
<strong>{% trans %}Warning: if you select <em>Account</em>, the opposite operation will be created in the target account. If you don't want that, select <em>Club</em> instead of <em>Account</em>.{% endtrans %}</strong>
|
<strong>{% trans %}Warning: if you select <em>Account</em>, the opposite operation will be created in the target account. If you don't want that, select <em>Club</em> instead of <em>Account</em>.{% endtrans %}</strong>
|
||||||
<p>{{ form.target_type.errors }}<label for="{{ form.target_type.name }}">{{ form.target_type.label }}</label> {{ form.target_type }}</p>
|
<p>{{ form.target_type.errors }}<label for="{{ form.target_type.name }}">{{ form.target_type.label }}</label> {{ form.target_type }}</p>
|
||||||
{{ form.user }}
|
{{ form.user }}
|
||||||
{{ form.club }}
|
{{ form.club }}
|
||||||
{{ form.club_account }}
|
{{ form.club_account }}
|
||||||
{{ form.company }}
|
{{ form.company }}
|
||||||
{{ form.target_label }}
|
{{ form.target_label }}
|
||||||
<span id="id_need_link_full"><label>{{ form.need_link.label }}</label> {{ form.need_link }}</span>
|
<span id="id_need_link_full"><label>{{ form.need_link.label }}</label> {{ form.need_link }}</span>
|
||||||
<p>{{ form.date.errors }}<label for="{{ form.date.name }}">{{ form.date.label }}</label> {{ form.date }}</p>
|
<p>{{ form.date.errors }}<label for="{{ form.date.name }}">{{ form.date.label }}</label> {{ form.date }}</p>
|
||||||
<p>{{ form.mode.errors }}<label for="{{ form.mode.name }}">{{ form.mode.label }}</label> {{ form.mode }}</p>
|
<p>{{ form.mode.errors }}<label for="{{ form.mode.name }}">{{ form.mode.label }}</label> {{ form.mode }}</p>
|
||||||
<p>{{ form.cheque_number.errors }}<label for="{{ form.cheque_number.name }}">{{ form.cheque_number.label }}</label> {{
|
<p>{{ form.cheque_number.errors }}<label for="{{ form.cheque_number.name }}">{{ form.cheque_number.label }}</label> {{
|
||||||
form.cheque_number }}</p>
|
form.cheque_number }}</p>
|
||||||
<p>{{ form.invoice.errors }}<label for="{{ form.invoice.name }}">{{ form.invoice.label }}</label> {{ form.invoice }}</p>
|
<p>{{ form.invoice.errors }}<label for="{{ form.invoice.name }}">{{ form.invoice.label }}</label> {{ form.invoice }}</p>
|
||||||
<p>{{ form.simpleaccounting_type.errors }}<label for="{{ form.simpleaccounting_type.name }}">{{
|
<p>{{ form.simpleaccounting_type.errors }}<label for="{{ form.simpleaccounting_type.name }}">{{
|
||||||
form.simpleaccounting_type.label }}</label> {{ form.simpleaccounting_type }}</p>
|
form.simpleaccounting_type.label }}</label> {{ form.simpleaccounting_type }}</p>
|
||||||
<p>{{ form.accounting_type.errors }}<label for="{{ form.accounting_type.name }}">{{ form.accounting_type.label }}</label> {{
|
<p>{{ form.accounting_type.errors }}<label for="{{ form.accounting_type.name }}">{{ form.accounting_type.label }}</label> {{
|
||||||
form.accounting_type }}</p>
|
form.accounting_type }}</p>
|
||||||
<p>{{ form.label.errors }}<label for="{{ form.label.name }}">{{ form.label.label }}</label> {{ form.label }}</p>
|
<p>{{ form.label.errors }}<label for="{{ form.label.name }}">{{ form.label.label }}</label> {{ form.label }}</p>
|
||||||
<p>{{ form.done.errors }}<label for="{{ form.done.name }}">{{ form.done.label }}</label> {{ form.done }}</p>
|
<p>{{ form.done.errors }}<label for="{{ form.done.name }}">{{ form.done.label }}</label> {{ form.done }}</p>
|
||||||
{% if form.instance.linked_operation %}
|
{% if form.instance.linked_operation %}
|
||||||
{% set obj = form.instance.linked_operation %}
|
{% set obj = form.instance.linked_operation %}
|
||||||
<p><strong>{% trans %}Linked operation:{% endtrans %}</strong><br>
|
<p><strong>{% trans %}Linked operation:{% endtrans %}</strong><br>
|
||||||
<a href="{{ url('accounting:bank_details', b_account_id=obj.journal.club_account.bank_account.id) }}">
|
<a href="{{ url('accounting:bank_details', b_account_id=obj.journal.club_account.bank_account.id) }}">
|
||||||
{{obj.journal.club_account.bank_account }}</a> >
|
{{obj.journal.club_account.bank_account }}</a> >
|
||||||
<a href="{{ url('accounting:club_details', c_account_id=obj.journal.club_account.id) }}">{{ obj.journal.club_account }}</a> >
|
<a href="{{ url('accounting:club_details', c_account_id=obj.journal.club_account.id) }}">{{ obj.journal.club_account }}</a> >
|
||||||
<a href="{{ url('accounting:journal_details', j_id=obj.journal.id) }}">{{ obj.journal }}</a> >
|
<a href="{{ url('accounting:journal_details', j_id=obj.journal.id) }}">{{ obj.journal }}</a> >
|
||||||
n°{{ obj.number }}
|
n°{{ obj.number }}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
|
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script>
|
<script>
|
||||||
$( function() {
|
$( function() {
|
||||||
var target_type = $('#id_target_type');
|
var target_type = $('#id_target_type');
|
||||||
var user = $('#id_user_wrapper');
|
var user = $('user-ajax-select');
|
||||||
var club = $('#id_club_wrapper');
|
var club = $('club-ajax-select');
|
||||||
var club_account = $('#id_club_account_wrapper');
|
var club_account = $('club-account-ajax-select');
|
||||||
var company = $('#id_company_wrapper');
|
var company = $('company-ajax-select');
|
||||||
var other = $('#id_target_label');
|
var other = $('#id_target_label');
|
||||||
var need_link = $('#id_need_link_full');
|
var need_link = $('#id_need_link_full');
|
||||||
function update_targets () {
|
function update_targets () {
|
||||||
if (target_type.val() == "USER") {
|
if (target_type.val() == "USER") {
|
||||||
console.log(user);
|
console.log(user);
|
||||||
user.show();
|
user.show();
|
||||||
club.hide();
|
club.hide();
|
||||||
club_account.hide();
|
club_account.hide();
|
||||||
company.hide();
|
company.hide();
|
||||||
other.hide();
|
other.hide();
|
||||||
need_link.hide();
|
need_link.hide();
|
||||||
} else if (target_type.val() == "ACCOUNT") {
|
} else if (target_type.val() == "ACCOUNT") {
|
||||||
club_account.show();
|
club_account.show();
|
||||||
need_link.show();
|
need_link.show();
|
||||||
user.hide();
|
user.hide();
|
||||||
club.hide();
|
club.hide();
|
||||||
company.hide();
|
company.hide();
|
||||||
other.hide();
|
other.hide();
|
||||||
} else if (target_type.val() == "CLUB") {
|
} else if (target_type.val() == "CLUB") {
|
||||||
club.show();
|
club.show();
|
||||||
user.hide();
|
user.hide();
|
||||||
club_account.hide();
|
club_account.hide();
|
||||||
company.hide();
|
company.hide();
|
||||||
other.hide();
|
other.hide();
|
||||||
need_link.hide();
|
need_link.hide();
|
||||||
} else if (target_type.val() == "COMPANY") {
|
} else if (target_type.val() == "COMPANY") {
|
||||||
company.show();
|
company.show();
|
||||||
user.hide();
|
user.hide();
|
||||||
club_account.hide();
|
club_account.hide();
|
||||||
club.hide();
|
club.hide();
|
||||||
other.hide();
|
other.hide();
|
||||||
need_link.hide();
|
need_link.hide();
|
||||||
} else if (target_type.val() == "OTHER") {
|
} else if (target_type.val() == "OTHER") {
|
||||||
other.show();
|
other.show();
|
||||||
user.hide();
|
user.hide();
|
||||||
club.hide();
|
club.hide();
|
||||||
club_account.hide();
|
club_account.hide();
|
||||||
company.hide();
|
company.hide();
|
||||||
need_link.hide();
|
need_link.hide();
|
||||||
} else {
|
} else {
|
||||||
company.hide();
|
company.hide();
|
||||||
user.hide();
|
user.hide();
|
||||||
club_account.hide();
|
club_account.hide();
|
||||||
club.hide();
|
club.hide();
|
||||||
other.hide();
|
other.hide();
|
||||||
need_link.hide();
|
need_link.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
update_targets();
|
update_targets();
|
||||||
target_type.change(update_targets);
|
target_type.change(update_targets);
|
||||||
} );
|
} );
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}Refound account{% endtrans %}
|
{% trans %}Refound account{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="accounting">
|
<div id="accounting">
|
||||||
<h3>{% trans %}Refound account{% endtrans %}</h3>
|
<h3>{% trans %}Refound account{% endtrans %}</h3>
|
||||||
<form action="" method="post">
|
<form action="" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form.as_p() }}
|
{{ form.as_p() }}
|
||||||
<p><input type="submit" value="{% trans %}Refound{% endtrans %}" /></p>
|
<p><input type="submit" value="{% trans %}Refound{% endtrans %}" /></p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -1,27 +1,27 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}Simplified type list{% endtrans %}
|
{% trans %}Simplified type list{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="accounting">
|
<div id="accounting">
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
|
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
|
||||||
{% trans %}Simplified types{% endtrans %}
|
{% trans %}Simplified types{% endtrans %}
|
||||||
</p>
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<p><a href="{{ url('accounting:simple_type_new') }}">{% trans %}New simplified type{% endtrans %}</a></p>
|
<p><a href="{{ url('accounting:simple_type_new') }}">{% trans %}New simplified type{% endtrans %}</a></p>
|
||||||
{% if simplifiedaccountingtype_list %}
|
{% if simplifiedaccountingtype_list %}
|
||||||
<h3>{% trans %}Simplified type list{% endtrans %}</h3>
|
<h3>{% trans %}Simplified type list{% endtrans %}</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{% for a in simplifiedaccountingtype_list %}
|
{% for a in simplifiedaccountingtype_list %}
|
||||||
<li><a href="{{ url('accounting:simple_type_edit', type_id=a.id) }}">{{ a }}</a></li>
|
<li><a href="{{ url('accounting:simple_type_edit', type_id=a.id) }}">{{ a }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans %}There is no types in this website.{% endtrans %}
|
{% trans %}There is no types in this website.{% endtrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
#
|
||||||
# Copyright 2023 © AE UTBM
|
# Copyright 2023 © AE UTBM
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
@ -6,98 +5,93 @@
|
|||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# https://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
# You can find the source code of the website at https://github.com/ae-utbm/sith
|
||||||
#
|
#
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.core.management import call_command
|
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
|
|
||||||
from core.models import User
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from accounting.models import (
|
from accounting.models import (
|
||||||
GeneralJournal,
|
|
||||||
Operation,
|
|
||||||
Label,
|
|
||||||
AccountingType,
|
AccountingType,
|
||||||
|
GeneralJournal,
|
||||||
|
Label,
|
||||||
|
Operation,
|
||||||
SimplifiedAccountingType,
|
SimplifiedAccountingType,
|
||||||
)
|
)
|
||||||
|
from core.models import User
|
||||||
|
|
||||||
|
|
||||||
class RefoundAccountTest(TestCase):
|
class TestRefoundAccount(TestCase):
|
||||||
def setUp(self):
|
@classmethod
|
||||||
self.skia = User.objects.filter(username="skia").first()
|
def setUpTestData(cls):
|
||||||
|
cls.skia = User.objects.get(username="skia")
|
||||||
# reffil skia's account
|
# reffil skia's account
|
||||||
self.skia.customer.amount = 800
|
cls.skia.customer.amount = 800
|
||||||
self.skia.customer.save()
|
cls.skia.customer.save()
|
||||||
|
cls.refound_account_url = reverse("accounting:refound_account")
|
||||||
|
|
||||||
def test_permission_denied(self):
|
def test_permission_denied(self):
|
||||||
self.client.login(username="guy", password="plop")
|
self.client.force_login(User.objects.get(username="guy"))
|
||||||
response_post = self.client.post(
|
response_post = self.client.post(
|
||||||
reverse("accounting:refound_account"), {"user": self.skia.id}
|
self.refound_account_url, {"user": self.skia.id}
|
||||||
)
|
)
|
||||||
response_get = self.client.get(reverse("accounting:refound_account"))
|
response_get = self.client.get(self.refound_account_url)
|
||||||
self.assertTrue(response_get.status_code == 403)
|
assert response_get.status_code == 403
|
||||||
self.assertTrue(response_post.status_code == 403)
|
assert response_post.status_code == 403
|
||||||
|
|
||||||
def test_root_granteed(self):
|
def test_root_granteed(self):
|
||||||
self.client.login(username="root", password="plop")
|
self.client.force_login(User.objects.get(username="root"))
|
||||||
response_post = self.client.post(
|
response = self.client.post(self.refound_account_url, {"user": self.skia.id})
|
||||||
reverse("accounting:refound_account"), {"user": self.skia.id}
|
self.assertRedirects(response, self.refound_account_url)
|
||||||
)
|
self.skia.refresh_from_db()
|
||||||
self.skia = User.objects.filter(username="skia").first()
|
response = self.client.get(self.refound_account_url)
|
||||||
response_get = self.client.get(reverse("accounting:refound_account"))
|
assert response.status_code == 200
|
||||||
self.assertFalse(response_get.status_code == 403)
|
assert '<form action="" method="post">' in str(response.content)
|
||||||
self.assertTrue('<form action="" method="post">' in str(response_get.content))
|
assert self.skia.customer.amount == 0
|
||||||
self.assertFalse(response_post.status_code == 403)
|
|
||||||
self.assertTrue(self.skia.customer.amount == 0)
|
|
||||||
|
|
||||||
def test_comptable_granteed(self):
|
def test_comptable_granteed(self):
|
||||||
self.client.login(username="comptable", password="plop")
|
self.client.force_login(User.objects.get(username="comptable"))
|
||||||
response_post = self.client.post(
|
response = self.client.post(self.refound_account_url, {"user": self.skia.id})
|
||||||
reverse("accounting:refound_account"), {"user": self.skia.id}
|
self.assertRedirects(response, self.refound_account_url)
|
||||||
)
|
self.skia.refresh_from_db()
|
||||||
self.skia = User.objects.filter(username="skia").first()
|
response = self.client.get(self.refound_account_url)
|
||||||
response_get = self.client.get(reverse("accounting:refound_account"))
|
assert response.status_code == 200
|
||||||
self.assertFalse(response_get.status_code == 403)
|
assert '<form action="" method="post">' in str(response.content)
|
||||||
self.assertTrue('<form action="" method="post">' in str(response_get.content))
|
assert self.skia.customer.amount == 0
|
||||||
self.assertFalse(response_post.status_code == 403)
|
|
||||||
self.assertTrue(self.skia.customer.amount == 0)
|
|
||||||
|
|
||||||
|
|
||||||
class JournalTest(TestCase):
|
class TestJournal(TestCase):
|
||||||
def setUp(self):
|
@classmethod
|
||||||
self.journal = GeneralJournal.objects.filter(id=1).first()
|
def setUpTestData(cls):
|
||||||
|
cls.journal = GeneralJournal.objects.get(id=1)
|
||||||
|
|
||||||
def test_permission_granted(self):
|
def test_permission_granted(self):
|
||||||
self.client.login(username="comptable", password="plop")
|
self.client.force_login(User.objects.get(username="comptable"))
|
||||||
response_get = self.client.get(
|
response_get = self.client.get(
|
||||||
reverse("accounting:journal_details", args=[self.journal.id])
|
reverse("accounting:journal_details", args=[self.journal.id])
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(response_get.status_code == 200)
|
assert response_get.status_code == 200
|
||||||
self.assertTrue(
|
assert "<td>M\\xc3\\xa9thode de paiement</td>" in str(response_get.content)
|
||||||
"<td>M\\xc3\\xa9thode de paiement</td>" in str(response_get.content)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_permission_not_granted(self):
|
def test_permission_not_granted(self):
|
||||||
self.client.login(username="skia", password="plop")
|
self.client.force_login(User.objects.get(username="skia"))
|
||||||
response_get = self.client.get(
|
response_get = self.client.get(
|
||||||
reverse("accounting:journal_details", args=[self.journal.id])
|
reverse("accounting:journal_details", args=[self.journal.id])
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(response_get.status_code == 403)
|
assert response_get.status_code == 403
|
||||||
self.assertFalse(
|
assert "<td>M\xc3\xa9thode de paiement</td>" not in str(response_get.content)
|
||||||
"<td>M\xc3\xa9thode de paiement</td>" in str(response_get.content)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OperationTest(TestCase):
|
class TestOperation(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.tomorrow_formatted = (date.today() + timedelta(days=1)).strftime(
|
self.tomorrow_formatted = (date.today() + timedelta(days=1)).strftime(
|
||||||
"%d/%m/%Y"
|
"%d/%m/%Y"
|
||||||
@ -108,9 +102,8 @@ class OperationTest(TestCase):
|
|||||||
code="443", label="Ce code n'existe pas", movement_type="CREDIT"
|
code="443", label="Ce code n'existe pas", movement_type="CREDIT"
|
||||||
)
|
)
|
||||||
at.save()
|
at.save()
|
||||||
l = Label(club_account=self.journal.club_account, name="bob")
|
label = Label.objects.create(club_account=self.journal.club_account, name="bob")
|
||||||
l.save()
|
self.client.force_login(User.objects.get(username="comptable"))
|
||||||
self.client.login(username="comptable", password="plop")
|
|
||||||
self.op1 = Operation(
|
self.op1 = Operation(
|
||||||
journal=self.journal,
|
journal=self.journal,
|
||||||
date=date.today(),
|
date=date.today(),
|
||||||
@ -118,7 +111,7 @@ class OperationTest(TestCase):
|
|||||||
remark="Test bilan",
|
remark="Test bilan",
|
||||||
mode="CASH",
|
mode="CASH",
|
||||||
done=True,
|
done=True,
|
||||||
label=l,
|
label=label,
|
||||||
accounting_type=at,
|
accounting_type=at,
|
||||||
target_type="USER",
|
target_type="USER",
|
||||||
target_id=self.skia.id,
|
target_id=self.skia.id,
|
||||||
@ -131,7 +124,7 @@ class OperationTest(TestCase):
|
|||||||
remark="Test bilan",
|
remark="Test bilan",
|
||||||
mode="CASH",
|
mode="CASH",
|
||||||
done=True,
|
done=True,
|
||||||
label=l,
|
label=label,
|
||||||
accounting_type=at,
|
accounting_type=at,
|
||||||
target_type="USER",
|
target_type="USER",
|
||||||
target_id=self.skia.id,
|
target_id=self.skia.id,
|
||||||
@ -139,8 +132,7 @@ class OperationTest(TestCase):
|
|||||||
self.op2.save()
|
self.op2.save()
|
||||||
|
|
||||||
def test_new_operation(self):
|
def test_new_operation(self):
|
||||||
self.client.login(username="comptable", password="plop")
|
at = AccountingType.objects.get(code="604")
|
||||||
at = AccountingType.objects.filter(code="604").first()
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("accounting:op_new", args=[self.journal.id]),
|
reverse("accounting:op_new", args=[self.journal.id]),
|
||||||
{
|
{
|
||||||
@ -172,8 +164,7 @@ class OperationTest(TestCase):
|
|||||||
self.assertTrue("<td>Le fantome de la nuit</td>" in str(response_get.content))
|
self.assertTrue("<td>Le fantome de la nuit</td>" in str(response_get.content))
|
||||||
|
|
||||||
def test_bad_new_operation(self):
|
def test_bad_new_operation(self):
|
||||||
self.client.login(username="comptable", password="plop")
|
AccountingType.objects.get(code="604")
|
||||||
AccountingType.objects.filter(code="604").first()
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("accounting:op_new", args=[self.journal.id]),
|
reverse("accounting:op_new", args=[self.journal.id]),
|
||||||
{
|
{
|
||||||
@ -199,7 +190,7 @@ class OperationTest(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_new_operation_not_authorized(self):
|
def test_new_operation_not_authorized(self):
|
||||||
self.client.login(username="skia", password="plop")
|
self.client.force_login(self.skia)
|
||||||
at = AccountingType.objects.filter(code="604").first()
|
at = AccountingType.objects.filter(code="604").first()
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("accounting:op_new", args=[self.journal.id]),
|
reverse("accounting:op_new", args=[self.journal.id]),
|
||||||
@ -225,8 +216,7 @@ class OperationTest(TestCase):
|
|||||||
self.journal.operations.filter(target_label="Le fantome du jour").exists()
|
self.journal.operations.filter(target_label="Le fantome du jour").exists()
|
||||||
)
|
)
|
||||||
|
|
||||||
def test__operation_simple_accounting(self):
|
def test_operation_simple_accounting(self):
|
||||||
self.client.login(username="comptable", password="plop")
|
|
||||||
sat = SimplifiedAccountingType.objects.all().first()
|
sat = SimplifiedAccountingType.objects.all().first()
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("accounting:op_new", args=[self.journal.id]),
|
reverse("accounting:op_new", args=[self.journal.id]),
|
||||||
@ -247,15 +237,14 @@ class OperationTest(TestCase):
|
|||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertFalse(response.status_code == 403)
|
assert response.status_code != 403
|
||||||
self.assertTrue(self.journal.operations.filter(amount=23).exists())
|
assert self.journal.operations.filter(amount=23).exists()
|
||||||
response_get = self.client.get(
|
response_get = self.client.get(
|
||||||
reverse("accounting:journal_details", args=[self.journal.id])
|
reverse("accounting:journal_details", args=[self.journal.id])
|
||||||
)
|
)
|
||||||
self.assertTrue(
|
assert "<td>Le fantome de l'aurore</td>" in str(response_get.content)
|
||||||
"<td>Le fantome de l'aurore</td>" in str(response_get.content)
|
|
||||||
)
|
assert (
|
||||||
self.assertTrue(
|
|
||||||
self.journal.operations.filter(amount=23)
|
self.journal.operations.filter(amount=23)
|
||||||
.values("accounting_type")
|
.values("accounting_type")
|
||||||
.first()["accounting_type"]
|
.first()["accounting_type"]
|
||||||
@ -263,47 +252,37 @@ class OperationTest(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_nature_statement(self):
|
def test_nature_statement(self):
|
||||||
self.client.login(username="comptable", password="plop")
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("accounting:journal_nature_statement", args=[self.journal.id])
|
reverse("accounting:journal_nature_statement", args=[self.journal.id])
|
||||||
)
|
)
|
||||||
self.assertContains(response, "bob (Troll Penché) : 3.00", status_code=200)
|
self.assertContains(response, "bob (Troll Penché) : 3.00", status_code=200)
|
||||||
|
|
||||||
def test_person_statement(self):
|
def test_person_statement(self):
|
||||||
self.client.login(username="comptable", password="plop")
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("accounting:journal_person_statement", args=[self.journal.id])
|
reverse("accounting:journal_person_statement", args=[self.journal.id])
|
||||||
)
|
)
|
||||||
self.assertContains(response, "Total : 5575.72", status_code=200)
|
self.assertContains(response, "Total : 5575.72", status_code=200)
|
||||||
self.assertContains(response, "Total : 71.42")
|
self.assertContains(response, "Total : 71.42")
|
||||||
self.assertContains(
|
content = response.content.decode()
|
||||||
response,
|
self.assertInHTML(
|
||||||
"""
|
"""<td><a href="/user/1/">S' Kia</a></td><td>3.00</td>""", content
|
||||||
<td><a href="/user/1/">S' Kia</a></td>
|
|
||||||
|
|
||||||
<td>3.00</td>""",
|
|
||||||
)
|
)
|
||||||
self.assertContains(
|
self.assertInHTML(
|
||||||
response,
|
"""<td><a href="/user/1/">S' Kia</a></td><td>823.00</td>""", content
|
||||||
"""
|
|
||||||
<td><a href="/user/1/">S' Kia</a></td>
|
|
||||||
|
|
||||||
<td>823.00</td>""",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_accounting_statement(self):
|
def test_accounting_statement(self):
|
||||||
self.client.login(username="comptable", password="plop")
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("accounting:journal_accounting_statement", args=[self.journal.id])
|
reverse("accounting:journal_accounting_statement", args=[self.journal.id])
|
||||||
)
|
)
|
||||||
self.assertContains(
|
assert response.status_code == 200
|
||||||
response,
|
self.assertInHTML(
|
||||||
"""
|
"""
|
||||||
<tr>
|
<tr>
|
||||||
<td>443 - Crédit - Ce code n'existe pas</td>
|
<td>443 - Crédit - Ce code n'existe pas</td>
|
||||||
<td>3.00</td>
|
<td>3.00</td>
|
||||||
</tr>""",
|
</tr>""",
|
||||||
status_code=200,
|
response.content.decode(),
|
||||||
)
|
)
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
response,
|
response,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
#
|
||||||
# Copyright 2023 © AE UTBM
|
# Copyright 2023 © AE UTBM
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
@ -6,17 +5,51 @@
|
|||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# https://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
# You can find the source code of the website at https://github.com/ae-utbm/sith
|
||||||
#
|
#
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from accounting.views import *
|
from accounting.views import (
|
||||||
|
AccountingTypeCreateView,
|
||||||
|
AccountingTypeEditView,
|
||||||
|
AccountingTypeListView,
|
||||||
|
BankAccountCreateView,
|
||||||
|
BankAccountDeleteView,
|
||||||
|
BankAccountDetailView,
|
||||||
|
BankAccountEditView,
|
||||||
|
BankAccountListView,
|
||||||
|
ClubAccountCreateView,
|
||||||
|
ClubAccountDeleteView,
|
||||||
|
ClubAccountDetailView,
|
||||||
|
ClubAccountEditView,
|
||||||
|
CompanyCreateView,
|
||||||
|
CompanyEditView,
|
||||||
|
CompanyListView,
|
||||||
|
JournalAccountingStatementView,
|
||||||
|
JournalCreateView,
|
||||||
|
JournalDeleteView,
|
||||||
|
JournalDetailView,
|
||||||
|
JournalEditView,
|
||||||
|
JournalNatureStatementView,
|
||||||
|
JournalPersonStatementView,
|
||||||
|
LabelCreateView,
|
||||||
|
LabelDeleteView,
|
||||||
|
LabelEditView,
|
||||||
|
LabelListView,
|
||||||
|
OperationCreateView,
|
||||||
|
OperationEditView,
|
||||||
|
OperationPDFView,
|
||||||
|
RefoundAccountView,
|
||||||
|
SimplifiedAccountingTypeCreateView,
|
||||||
|
SimplifiedAccountingTypeEditView,
|
||||||
|
SimplifiedAccountingTypeListView,
|
||||||
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Accounting types
|
# Accounting types
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
#
|
||||||
# Copyright 2023 © AE UTBM
|
# Copyright 2023 © AE UTBM
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
@ -6,57 +5,63 @@
|
|||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# https://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
# You can find the source code of the website at https://github.com/ae-utbm/sith
|
||||||
#
|
#
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
from django.views.generic import ListView, DetailView
|
|
||||||
from django.views.generic.edit import UpdateView, CreateView, DeleteView, FormView
|
|
||||||
from django.urls import reverse_lazy, reverse
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.forms.models import modelform_factory
|
|
||||||
from django.core.exceptions import PermissionDenied, ValidationError
|
|
||||||
from django.forms import HiddenInput
|
|
||||||
from django.db import transaction
|
|
||||||
from django.db.models import Sum
|
|
||||||
from django.conf import settings
|
|
||||||
from django import forms
|
|
||||||
from django.http import HttpResponse
|
|
||||||
import collections
|
import collections
|
||||||
|
|
||||||
from ajax_select.fields import AutoCompleteSelectField
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
|
from django.core.exceptions import PermissionDenied, ValidationError
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.models import Sum
|
||||||
|
from django.forms import HiddenInput
|
||||||
|
from django.forms.models import modelform_factory
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.urls import reverse, reverse_lazy
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views.generic import DetailView, ListView
|
||||||
|
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
|
||||||
|
|
||||||
from core.views import (
|
|
||||||
CanViewMixin,
|
|
||||||
CanEditMixin,
|
|
||||||
CanEditPropMixin,
|
|
||||||
CanCreateMixin,
|
|
||||||
TabedViewMixin,
|
|
||||||
)
|
|
||||||
from core.views.forms import SelectFile, SelectDate
|
|
||||||
from accounting.models import (
|
from accounting.models import (
|
||||||
|
AccountingType,
|
||||||
BankAccount,
|
BankAccount,
|
||||||
ClubAccount,
|
ClubAccount,
|
||||||
GeneralJournal,
|
|
||||||
Operation,
|
|
||||||
AccountingType,
|
|
||||||
Company,
|
Company,
|
||||||
SimplifiedAccountingType,
|
GeneralJournal,
|
||||||
Label,
|
Label,
|
||||||
|
Operation,
|
||||||
|
SimplifiedAccountingType,
|
||||||
)
|
)
|
||||||
from counter.models import Counter, Selling, Product
|
from accounting.widgets.select import (
|
||||||
|
AutoCompleteSelectClubAccount,
|
||||||
|
AutoCompleteSelectCompany,
|
||||||
|
)
|
||||||
|
from club.models import Club
|
||||||
|
from club.widgets.select import AutoCompleteSelectClub
|
||||||
|
from core.auth.mixins import (
|
||||||
|
CanCreateMixin,
|
||||||
|
CanEditMixin,
|
||||||
|
CanEditPropMixin,
|
||||||
|
CanViewMixin,
|
||||||
|
)
|
||||||
|
from core.models import User
|
||||||
|
from core.views.forms import SelectDate, SelectFile
|
||||||
|
from core.views.mixins import TabedViewMixin
|
||||||
|
from core.views.widgets.select import AutoCompleteSelectUser
|
||||||
|
from counter.models import Counter, Product, Selling
|
||||||
|
|
||||||
# Main accounting view
|
# Main accounting view
|
||||||
|
|
||||||
|
|
||||||
class BankAccountListView(CanViewMixin, ListView):
|
class BankAccountListView(CanViewMixin, ListView):
|
||||||
"""
|
"""A list view for the admins."""
|
||||||
A list view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = BankAccount
|
model = BankAccount
|
||||||
template_name = "accounting/bank_account_list.jinja"
|
template_name = "accounting/bank_account_list.jinja"
|
||||||
@ -67,18 +72,14 @@ class BankAccountListView(CanViewMixin, ListView):
|
|||||||
|
|
||||||
|
|
||||||
class SimplifiedAccountingTypeListView(CanViewMixin, ListView):
|
class SimplifiedAccountingTypeListView(CanViewMixin, ListView):
|
||||||
"""
|
"""A list view for the admins."""
|
||||||
A list view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = SimplifiedAccountingType
|
model = SimplifiedAccountingType
|
||||||
template_name = "accounting/simplifiedaccountingtype_list.jinja"
|
template_name = "accounting/simplifiedaccountingtype_list.jinja"
|
||||||
|
|
||||||
|
|
||||||
class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView):
|
class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView):
|
||||||
"""
|
"""An edit view for the admins."""
|
||||||
An edit view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = SimplifiedAccountingType
|
model = SimplifiedAccountingType
|
||||||
pk_url_kwarg = "type_id"
|
pk_url_kwarg = "type_id"
|
||||||
@ -86,32 +87,27 @@ class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView):
|
|||||||
template_name = "core/edit.jinja"
|
template_name = "core/edit.jinja"
|
||||||
|
|
||||||
|
|
||||||
class SimplifiedAccountingTypeCreateView(CanCreateMixin, CreateView):
|
class SimplifiedAccountingTypeCreateView(PermissionRequiredMixin, CreateView):
|
||||||
"""
|
"""Create an accounting type (for the admins)."""
|
||||||
Create an accounting type (for the admins)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = SimplifiedAccountingType
|
model = SimplifiedAccountingType
|
||||||
fields = ["label", "accounting_type"]
|
fields = ["label", "accounting_type"]
|
||||||
template_name = "core/create.jinja"
|
template_name = "core/create.jinja"
|
||||||
|
permission_required = "accounting.add_simplifiedaccountingtype"
|
||||||
|
|
||||||
|
|
||||||
# Accounting types
|
# Accounting types
|
||||||
|
|
||||||
|
|
||||||
class AccountingTypeListView(CanViewMixin, ListView):
|
class AccountingTypeListView(CanViewMixin, ListView):
|
||||||
"""
|
"""A list view for the admins."""
|
||||||
A list view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = AccountingType
|
model = AccountingType
|
||||||
template_name = "accounting/accountingtype_list.jinja"
|
template_name = "accounting/accountingtype_list.jinja"
|
||||||
|
|
||||||
|
|
||||||
class AccountingTypeEditView(CanViewMixin, UpdateView):
|
class AccountingTypeEditView(CanViewMixin, UpdateView):
|
||||||
"""
|
"""An edit view for the admins."""
|
||||||
An edit view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = AccountingType
|
model = AccountingType
|
||||||
pk_url_kwarg = "type_id"
|
pk_url_kwarg = "type_id"
|
||||||
@ -119,23 +115,20 @@ class AccountingTypeEditView(CanViewMixin, UpdateView):
|
|||||||
template_name = "core/edit.jinja"
|
template_name = "core/edit.jinja"
|
||||||
|
|
||||||
|
|
||||||
class AccountingTypeCreateView(CanCreateMixin, CreateView):
|
class AccountingTypeCreateView(PermissionRequiredMixin, CreateView):
|
||||||
"""
|
"""Create an accounting type (for the admins)."""
|
||||||
Create an accounting type (for the admins)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = AccountingType
|
model = AccountingType
|
||||||
fields = ["code", "label", "movement_type"]
|
fields = ["code", "label", "movement_type"]
|
||||||
template_name = "core/create.jinja"
|
template_name = "core/create.jinja"
|
||||||
|
permission_required = "accounting.add_accountingtype"
|
||||||
|
|
||||||
|
|
||||||
# BankAccount views
|
# BankAccount views
|
||||||
|
|
||||||
|
|
||||||
class BankAccountEditView(CanViewMixin, UpdateView):
|
class BankAccountEditView(CanViewMixin, UpdateView):
|
||||||
"""
|
"""An edit view for the admins."""
|
||||||
An edit view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = BankAccount
|
model = BankAccount
|
||||||
pk_url_kwarg = "b_account_id"
|
pk_url_kwarg = "b_account_id"
|
||||||
@ -144,9 +137,7 @@ class BankAccountEditView(CanViewMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class BankAccountDetailView(CanViewMixin, DetailView):
|
class BankAccountDetailView(CanViewMixin, DetailView):
|
||||||
"""
|
"""A detail view, listing every club account."""
|
||||||
A detail view, listing every club account
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = BankAccount
|
model = BankAccount
|
||||||
pk_url_kwarg = "b_account_id"
|
pk_url_kwarg = "b_account_id"
|
||||||
@ -154,9 +145,7 @@ class BankAccountDetailView(CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class BankAccountCreateView(CanCreateMixin, CreateView):
|
class BankAccountCreateView(CanCreateMixin, CreateView):
|
||||||
"""
|
"""Create a bank account (for the admins)."""
|
||||||
Create a bank account (for the admins)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = BankAccount
|
model = BankAccount
|
||||||
fields = ["name", "club", "iban", "number"]
|
fields = ["name", "club", "iban", "number"]
|
||||||
@ -166,9 +155,7 @@ class BankAccountCreateView(CanCreateMixin, CreateView):
|
|||||||
class BankAccountDeleteView(
|
class BankAccountDeleteView(
|
||||||
CanEditPropMixin, DeleteView
|
CanEditPropMixin, DeleteView
|
||||||
): # TODO change Delete to Close
|
): # TODO change Delete to Close
|
||||||
"""
|
"""Delete a bank account (for the admins)."""
|
||||||
Delete a bank account (for the admins)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = BankAccount
|
model = BankAccount
|
||||||
pk_url_kwarg = "b_account_id"
|
pk_url_kwarg = "b_account_id"
|
||||||
@ -180,9 +167,7 @@ class BankAccountDeleteView(
|
|||||||
|
|
||||||
|
|
||||||
class ClubAccountEditView(CanViewMixin, UpdateView):
|
class ClubAccountEditView(CanViewMixin, UpdateView):
|
||||||
"""
|
"""An edit view for the admins."""
|
||||||
An edit view for the admins
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = ClubAccount
|
model = ClubAccount
|
||||||
pk_url_kwarg = "c_account_id"
|
pk_url_kwarg = "c_account_id"
|
||||||
@ -191,9 +176,7 @@ class ClubAccountEditView(CanViewMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class ClubAccountDetailView(CanViewMixin, DetailView):
|
class ClubAccountDetailView(CanViewMixin, DetailView):
|
||||||
"""
|
"""A detail view, listing every journal."""
|
||||||
A detail view, listing every journal
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = ClubAccount
|
model = ClubAccount
|
||||||
pk_url_kwarg = "c_account_id"
|
pk_url_kwarg = "c_account_id"
|
||||||
@ -201,17 +184,15 @@ class ClubAccountDetailView(CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class ClubAccountCreateView(CanCreateMixin, CreateView):
|
class ClubAccountCreateView(CanCreateMixin, CreateView):
|
||||||
"""
|
"""Create a club account (for the admins)."""
|
||||||
Create a club account (for the admins)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = ClubAccount
|
model = ClubAccount
|
||||||
fields = ["name", "club", "bank_account"]
|
fields = ["name", "club", "bank_account"]
|
||||||
template_name = "core/create.jinja"
|
template_name = "core/create.jinja"
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
ret = super(ClubAccountCreateView, self).get_initial()
|
ret = super().get_initial()
|
||||||
if "parent" in self.request.GET.keys():
|
if "parent" in self.request.GET:
|
||||||
obj = BankAccount.objects.filter(id=int(self.request.GET["parent"])).first()
|
obj = BankAccount.objects.filter(id=int(self.request.GET["parent"])).first()
|
||||||
if obj is not None:
|
if obj is not None:
|
||||||
ret["bank_account"] = obj.id
|
ret["bank_account"] = obj.id
|
||||||
@ -221,9 +202,7 @@ class ClubAccountCreateView(CanCreateMixin, CreateView):
|
|||||||
class ClubAccountDeleteView(
|
class ClubAccountDeleteView(
|
||||||
CanEditPropMixin, DeleteView
|
CanEditPropMixin, DeleteView
|
||||||
): # TODO change Delete to Close
|
): # TODO change Delete to Close
|
||||||
"""
|
"""Delete a club account (for the admins)."""
|
||||||
Delete a club account (for the admins)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = ClubAccount
|
model = ClubAccount
|
||||||
pk_url_kwarg = "c_account_id"
|
pk_url_kwarg = "c_account_id"
|
||||||
@ -239,17 +218,14 @@ class JournalTabsMixin(TabedViewMixin):
|
|||||||
return _("Journal")
|
return _("Journal")
|
||||||
|
|
||||||
def get_list_of_tabs(self):
|
def get_list_of_tabs(self):
|
||||||
tab_list = []
|
return [
|
||||||
tab_list.append(
|
|
||||||
{
|
{
|
||||||
"url": reverse(
|
"url": reverse(
|
||||||
"accounting:journal_details", kwargs={"j_id": self.object.id}
|
"accounting:journal_details", kwargs={"j_id": self.object.id}
|
||||||
),
|
),
|
||||||
"slug": "journal",
|
"slug": "journal",
|
||||||
"name": _("Journal"),
|
"name": _("Journal"),
|
||||||
}
|
},
|
||||||
)
|
|
||||||
tab_list.append(
|
|
||||||
{
|
{
|
||||||
"url": reverse(
|
"url": reverse(
|
||||||
"accounting:journal_nature_statement",
|
"accounting:journal_nature_statement",
|
||||||
@ -257,9 +233,7 @@ class JournalTabsMixin(TabedViewMixin):
|
|||||||
),
|
),
|
||||||
"slug": "nature_statement",
|
"slug": "nature_statement",
|
||||||
"name": _("Statement by nature"),
|
"name": _("Statement by nature"),
|
||||||
}
|
},
|
||||||
)
|
|
||||||
tab_list.append(
|
|
||||||
{
|
{
|
||||||
"url": reverse(
|
"url": reverse(
|
||||||
"accounting:journal_person_statement",
|
"accounting:journal_person_statement",
|
||||||
@ -267,9 +241,7 @@ class JournalTabsMixin(TabedViewMixin):
|
|||||||
),
|
),
|
||||||
"slug": "person_statement",
|
"slug": "person_statement",
|
||||||
"name": _("Statement by person"),
|
"name": _("Statement by person"),
|
||||||
}
|
},
|
||||||
)
|
|
||||||
tab_list.append(
|
|
||||||
{
|
{
|
||||||
"url": reverse(
|
"url": reverse(
|
||||||
"accounting:journal_accounting_statement",
|
"accounting:journal_accounting_statement",
|
||||||
@ -277,15 +249,12 @@ class JournalTabsMixin(TabedViewMixin):
|
|||||||
),
|
),
|
||||||
"slug": "accounting_statement",
|
"slug": "accounting_statement",
|
||||||
"name": _("Accounting statement"),
|
"name": _("Accounting statement"),
|
||||||
}
|
},
|
||||||
)
|
]
|
||||||
return tab_list
|
|
||||||
|
|
||||||
|
|
||||||
class JournalCreateView(CanCreateMixin, CreateView):
|
class JournalCreateView(CanCreateMixin, CreateView):
|
||||||
"""
|
"""Create a general journal."""
|
||||||
Create a general journal
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = GeneralJournal
|
model = GeneralJournal
|
||||||
form_class = modelform_factory(
|
form_class = modelform_factory(
|
||||||
@ -296,8 +265,8 @@ class JournalCreateView(CanCreateMixin, CreateView):
|
|||||||
template_name = "core/create.jinja"
|
template_name = "core/create.jinja"
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
ret = super(JournalCreateView, self).get_initial()
|
ret = super().get_initial()
|
||||||
if "parent" in self.request.GET.keys():
|
if "parent" in self.request.GET:
|
||||||
obj = ClubAccount.objects.filter(id=int(self.request.GET["parent"])).first()
|
obj = ClubAccount.objects.filter(id=int(self.request.GET["parent"])).first()
|
||||||
if obj is not None:
|
if obj is not None:
|
||||||
ret["club_account"] = obj.id
|
ret["club_account"] = obj.id
|
||||||
@ -305,9 +274,7 @@ class JournalCreateView(CanCreateMixin, CreateView):
|
|||||||
|
|
||||||
|
|
||||||
class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView):
|
class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||||
"""
|
"""A detail view, listing every operation."""
|
||||||
A detail view, listing every operation
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = GeneralJournal
|
model = GeneralJournal
|
||||||
pk_url_kwarg = "j_id"
|
pk_url_kwarg = "j_id"
|
||||||
@ -316,9 +283,7 @@ class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class JournalEditView(CanEditMixin, UpdateView):
|
class JournalEditView(CanEditMixin, UpdateView):
|
||||||
"""
|
"""Update a general journal."""
|
||||||
Update a general journal
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = GeneralJournal
|
model = GeneralJournal
|
||||||
pk_url_kwarg = "j_id"
|
pk_url_kwarg = "j_id"
|
||||||
@ -327,9 +292,7 @@ class JournalEditView(CanEditMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class JournalDeleteView(CanEditPropMixin, DeleteView):
|
class JournalDeleteView(CanEditPropMixin, DeleteView):
|
||||||
"""
|
"""Delete a club account (for the admins)."""
|
||||||
Delete a club account (for the admins)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = GeneralJournal
|
model = GeneralJournal
|
||||||
pk_url_kwarg = "j_id"
|
pk_url_kwarg = "j_id"
|
||||||
@ -339,7 +302,7 @@ class JournalDeleteView(CanEditPropMixin, DeleteView):
|
|||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
if self.object.operations.count() == 0:
|
if self.object.operations.count() == 0:
|
||||||
return super(JournalDeleteView, self).dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
else:
|
else:
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
|
||||||
@ -373,12 +336,30 @@ class OperationForm(forms.ModelForm):
|
|||||||
"invoice": SelectFile,
|
"invoice": SelectFile,
|
||||||
}
|
}
|
||||||
|
|
||||||
user = AutoCompleteSelectField("users", help_text=None, required=False)
|
user = forms.ModelChoiceField(
|
||||||
club_account = AutoCompleteSelectField(
|
help_text=None,
|
||||||
"club_accounts", help_text=None, required=False
|
required=False,
|
||||||
|
widget=AutoCompleteSelectUser,
|
||||||
|
queryset=User.objects.all(),
|
||||||
|
)
|
||||||
|
club_account = forms.ModelChoiceField(
|
||||||
|
help_text=None,
|
||||||
|
required=False,
|
||||||
|
widget=AutoCompleteSelectClubAccount,
|
||||||
|
queryset=ClubAccount.objects.all(),
|
||||||
|
)
|
||||||
|
club = forms.ModelChoiceField(
|
||||||
|
help_text=None,
|
||||||
|
required=False,
|
||||||
|
widget=AutoCompleteSelectClub,
|
||||||
|
queryset=Club.objects.all(),
|
||||||
|
)
|
||||||
|
company = forms.ModelChoiceField(
|
||||||
|
help_text=None,
|
||||||
|
required=False,
|
||||||
|
widget=AutoCompleteSelectCompany,
|
||||||
|
queryset=Company.objects.all(),
|
||||||
)
|
)
|
||||||
club = AutoCompleteSelectField("clubs", help_text=None, required=False)
|
|
||||||
company = AutoCompleteSelectField("companies", help_text=None, required=False)
|
|
||||||
need_link = forms.BooleanField(
|
need_link = forms.BooleanField(
|
||||||
label=_("Link this operation to the target account"),
|
label=_("Link this operation to the target account"),
|
||||||
required=False,
|
required=False,
|
||||||
@ -387,7 +368,7 @@ class OperationForm(forms.ModelForm):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
club_account = kwargs.pop("club_account", None)
|
club_account = kwargs.pop("club_account", None)
|
||||||
super(OperationForm, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if club_account:
|
if club_account:
|
||||||
self.fields["label"].queryset = club_account.labels.order_by("name").all()
|
self.fields["label"].queryset = club_account.labels.order_by("name").all()
|
||||||
if self.instance.target_type == "USER":
|
if self.instance.target_type == "USER":
|
||||||
@ -400,8 +381,8 @@ class OperationForm(forms.ModelForm):
|
|||||||
self.fields["company"].initial = self.instance.target_id
|
self.fields["company"].initial = self.instance.target_id
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
self.cleaned_data = super(OperationForm, self).clean()
|
self.cleaned_data = super().clean()
|
||||||
if "target_type" in self.cleaned_data.keys():
|
if "target_type" in self.cleaned_data:
|
||||||
if (
|
if (
|
||||||
self.cleaned_data.get("user") is None
|
self.cleaned_data.get("user") is None
|
||||||
and self.cleaned_data.get("club") is None
|
and self.cleaned_data.get("club") is None
|
||||||
@ -430,7 +411,7 @@ class OperationForm(forms.ModelForm):
|
|||||||
return self.cleaned_data
|
return self.cleaned_data
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
ret = super(OperationForm, self).save()
|
ret = super().save()
|
||||||
if (
|
if (
|
||||||
self.instance.target_type == "ACCOUNT"
|
self.instance.target_type == "ACCOUNT"
|
||||||
and not self.instance.linked_operation
|
and not self.instance.linked_operation
|
||||||
@ -468,9 +449,7 @@ class OperationForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class OperationCreateView(CanCreateMixin, CreateView):
|
class OperationCreateView(CanCreateMixin, CreateView):
|
||||||
"""
|
"""Create an operation."""
|
||||||
Create an operation
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Operation
|
model = Operation
|
||||||
form_class = OperationForm
|
form_class = OperationForm
|
||||||
@ -482,23 +461,21 @@ class OperationCreateView(CanCreateMixin, CreateView):
|
|||||||
return self.form_class(club_account=ca, **self.get_form_kwargs())
|
return self.form_class(club_account=ca, **self.get_form_kwargs())
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
ret = super(OperationCreateView, self).get_initial()
|
ret = super().get_initial()
|
||||||
if self.journal is not None:
|
if self.journal is not None:
|
||||||
ret["journal"] = self.journal.id
|
ret["journal"] = self.journal.id
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add journal to the context"""
|
"""Add journal to the context."""
|
||||||
kwargs = super(OperationCreateView, self).get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
if self.journal:
|
if self.journal:
|
||||||
kwargs["object"] = self.journal
|
kwargs["object"] = self.journal
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
class OperationEditView(CanEditMixin, UpdateView):
|
class OperationEditView(CanEditMixin, UpdateView):
|
||||||
"""
|
"""An edit view, working as detail for the moment."""
|
||||||
An edit view, working as detail for the moment
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Operation
|
model = Operation
|
||||||
pk_url_kwarg = "op_id"
|
pk_url_kwarg = "op_id"
|
||||||
@ -506,29 +483,27 @@ class OperationEditView(CanEditMixin, UpdateView):
|
|||||||
template_name = "accounting/operation_edit.jinja"
|
template_name = "accounting/operation_edit.jinja"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add journal to the context"""
|
"""Add journal to the context."""
|
||||||
kwargs = super(OperationEditView, self).get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
kwargs["object"] = self.object.journal
|
kwargs["object"] = self.object.journal
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
class OperationPDFView(CanViewMixin, DetailView):
|
class OperationPDFView(CanViewMixin, DetailView):
|
||||||
"""
|
"""Display the PDF of a given operation."""
|
||||||
Display the PDF of a given operation
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Operation
|
model = Operation
|
||||||
pk_url_kwarg = "op_id"
|
pk_url_kwarg = "op_id"
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
from reportlab.pdfgen import canvas
|
|
||||||
from reportlab.lib.units import cm
|
|
||||||
from reportlab.platypus import Table, TableStyle
|
|
||||||
from reportlab.lib import colors
|
from reportlab.lib import colors
|
||||||
from reportlab.lib.pagesizes import letter
|
from reportlab.lib.pagesizes import letter
|
||||||
|
from reportlab.lib.units import cm
|
||||||
from reportlab.lib.utils import ImageReader
|
from reportlab.lib.utils import ImageReader
|
||||||
from reportlab.pdfbase.ttfonts import TTFont
|
|
||||||
from reportlab.pdfbase import pdfmetrics
|
from reportlab.pdfbase import pdfmetrics
|
||||||
|
from reportlab.pdfbase.ttfonts import TTFont
|
||||||
|
from reportlab.pdfgen import canvas
|
||||||
|
from reportlab.platypus import Table, TableStyle
|
||||||
|
|
||||||
pdfmetrics.registerFont(TTFont("DejaVu", "DejaVuSerif.ttf"))
|
pdfmetrics.registerFont(TTFont("DejaVu", "DejaVuSerif.ttf"))
|
||||||
|
|
||||||
@ -599,7 +574,7 @@ class OperationPDFView(CanViewMixin, DetailView):
|
|||||||
payment_mode = ""
|
payment_mode = ""
|
||||||
for m in settings.SITH_ACCOUNTING_PAYMENT_METHOD:
|
for m in settings.SITH_ACCOUNTING_PAYMENT_METHOD:
|
||||||
if m[0] == mode:
|
if m[0] == mode:
|
||||||
payment_mode += "[\u00D7]"
|
payment_mode += "[\u00d7]"
|
||||||
else:
|
else:
|
||||||
payment_mode += "[ ]"
|
payment_mode += "[ ]"
|
||||||
payment_mode += " %s\n" % (m[1])
|
payment_mode += " %s\n" % (m[1])
|
||||||
@ -667,9 +642,7 @@ class OperationPDFView(CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||||
"""
|
"""Display a statement sorted by labels."""
|
||||||
Display a statement sorted by labels
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = GeneralJournal
|
model = GeneralJournal
|
||||||
pk_url_kwarg = "j_id"
|
pk_url_kwarg = "j_id"
|
||||||
@ -680,19 +653,17 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
|||||||
ret = collections.OrderedDict()
|
ret = collections.OrderedDict()
|
||||||
statement = collections.OrderedDict()
|
statement = collections.OrderedDict()
|
||||||
total_sum = 0
|
total_sum = 0
|
||||||
for sat in [None] + list(
|
for sat in [
|
||||||
SimplifiedAccountingType.objects.order_by("label").all()
|
None,
|
||||||
):
|
*list(SimplifiedAccountingType.objects.order_by("label")),
|
||||||
sum = queryset.filter(
|
]:
|
||||||
|
amount = queryset.filter(
|
||||||
accounting_type__movement_type=movement_type, simpleaccounting_type=sat
|
accounting_type__movement_type=movement_type, simpleaccounting_type=sat
|
||||||
).aggregate(amount_sum=Sum("amount"))["amount_sum"]
|
).aggregate(amount_sum=Sum("amount"))["amount_sum"]
|
||||||
if sat:
|
label = sat.label if sat is not None else ""
|
||||||
sat = sat.label
|
if amount:
|
||||||
else:
|
total_sum += amount
|
||||||
sat = ""
|
statement[label] = amount
|
||||||
if sum:
|
|
||||||
total_sum += sum
|
|
||||||
statement[sat] = sum
|
|
||||||
ret[movement_type] = statement
|
ret[movement_type] = statement
|
||||||
ret[movement_type + "_sum"] = total_sum
|
ret[movement_type + "_sum"] = total_sum
|
||||||
return ret
|
return ret
|
||||||
@ -715,28 +686,23 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
|||||||
self.statement(self.object.operations.filter(label=None).all(), "DEBIT")
|
self.statement(self.object.operations.filter(label=None).all(), "DEBIT")
|
||||||
)
|
)
|
||||||
statement[_("No label operations")] = no_label_statement
|
statement[_("No label operations")] = no_label_statement
|
||||||
for l in labels:
|
for label in labels:
|
||||||
l_stmt = collections.OrderedDict()
|
l_stmt = collections.OrderedDict()
|
||||||
l_stmt.update(
|
journals = self.object.operations.filter(label=label).all()
|
||||||
self.statement(self.object.operations.filter(label=l).all(), "CREDIT")
|
l_stmt.update(self.statement(journals, "CREDIT"))
|
||||||
)
|
l_stmt.update(self.statement(journals, "DEBIT"))
|
||||||
l_stmt.update(
|
statement[label] = l_stmt
|
||||||
self.statement(self.object.operations.filter(label=l).all(), "DEBIT")
|
|
||||||
)
|
|
||||||
statement[l] = l_stmt
|
|
||||||
return statement
|
return statement
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add infos to the context"""
|
"""Add infos to the context."""
|
||||||
kwargs = super(JournalNatureStatementView, self).get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
kwargs["statement"] = self.big_statement()
|
kwargs["statement"] = self.big_statement()
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||||
"""
|
"""Calculate a dictionary with operation target and sum of operations."""
|
||||||
Calculate a dictionary with operation target and sum of operations
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = GeneralJournal
|
model = GeneralJournal
|
||||||
pk_url_kwarg = "j_id"
|
pk_url_kwarg = "j_id"
|
||||||
@ -766,8 +732,8 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
|||||||
return sum(self.statement(movement_type).values())
|
return sum(self.statement(movement_type).values())
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add journal to the context"""
|
"""Add journal to the context."""
|
||||||
kwargs = super(JournalPersonStatementView, self).get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
kwargs["credit_statement"] = self.statement("CREDIT")
|
kwargs["credit_statement"] = self.statement("CREDIT")
|
||||||
kwargs["debit_statement"] = self.statement("DEBIT")
|
kwargs["debit_statement"] = self.statement("DEBIT")
|
||||||
kwargs["total_credit"] = self.total("CREDIT")
|
kwargs["total_credit"] = self.total("CREDIT")
|
||||||
@ -776,9 +742,7 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
||||||
"""
|
"""Calculate a dictionary with operation type and sum of operations."""
|
||||||
Calculate a dictionary with operation type and sum of operations
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = GeneralJournal
|
model = GeneralJournal
|
||||||
pk_url_kwarg = "j_id"
|
pk_url_kwarg = "j_id"
|
||||||
@ -796,8 +760,8 @@ class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView)
|
|||||||
return statement
|
return statement
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add journal to the context"""
|
"""Add journal to the context."""
|
||||||
kwargs = super(JournalAccountingStatementView, self).get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
kwargs["statement"] = self.statement()
|
kwargs["statement"] = self.statement()
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
@ -811,9 +775,7 @@ class CompanyListView(CanViewMixin, ListView):
|
|||||||
|
|
||||||
|
|
||||||
class CompanyCreateView(CanCreateMixin, CreateView):
|
class CompanyCreateView(CanCreateMixin, CreateView):
|
||||||
"""
|
"""Create a company."""
|
||||||
Create a company
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Company
|
model = Company
|
||||||
fields = ["name"]
|
fields = ["name"]
|
||||||
@ -822,9 +784,7 @@ class CompanyCreateView(CanCreateMixin, CreateView):
|
|||||||
|
|
||||||
|
|
||||||
class CompanyEditView(CanCreateMixin, UpdateView):
|
class CompanyEditView(CanCreateMixin, UpdateView):
|
||||||
"""
|
"""Edit a company."""
|
||||||
Edit a company
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Company
|
model = Company
|
||||||
pk_url_kwarg = "co_id"
|
pk_url_kwarg = "co_id"
|
||||||
@ -852,8 +812,8 @@ class LabelCreateView(
|
|||||||
template_name = "core/create.jinja"
|
template_name = "core/create.jinja"
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
ret = super(LabelCreateView, self).get_initial()
|
ret = super().get_initial()
|
||||||
if "parent" in self.request.GET.keys():
|
if "parent" in self.request.GET:
|
||||||
obj = ClubAccount.objects.filter(id=int(self.request.GET["parent"])).first()
|
obj = ClubAccount.objects.filter(id=int(self.request.GET["parent"])).first()
|
||||||
if obj is not None:
|
if obj is not None:
|
||||||
ret["club_account"] = obj.id
|
ret["club_account"] = obj.id
|
||||||
@ -877,15 +837,17 @@ class LabelDeleteView(CanEditMixin, DeleteView):
|
|||||||
|
|
||||||
|
|
||||||
class CloseCustomerAccountForm(forms.Form):
|
class CloseCustomerAccountForm(forms.Form):
|
||||||
user = AutoCompleteSelectField(
|
user = forms.ModelChoiceField(
|
||||||
"users", label=_("Refound this account"), help_text=None, required=True
|
label=_("Refound this account"),
|
||||||
|
help_text=None,
|
||||||
|
required=True,
|
||||||
|
widget=AutoCompleteSelectUser,
|
||||||
|
queryset=User.objects.all(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RefoundAccountView(FormView):
|
class RefoundAccountView(FormView):
|
||||||
"""
|
"""Create a selling with the same amount than the current user money."""
|
||||||
Create a selling with the same amount than the current user money
|
|
||||||
"""
|
|
||||||
|
|
||||||
template_name = "accounting/refound_account.jinja"
|
template_name = "accounting/refound_account.jinja"
|
||||||
form_class = CloseCustomerAccountForm
|
form_class = CloseCustomerAccountForm
|
||||||
@ -897,19 +859,19 @@ class RefoundAccountView(FormView):
|
|||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
|
||||||
def dispatch(self, request, *arg, **kwargs):
|
def dispatch(self, request, *arg, **kwargs):
|
||||||
res = super(RefoundAccountView, self).dispatch(request, *arg, **kwargs)
|
res = super().dispatch(request, *arg, **kwargs)
|
||||||
if self.permission(request.user):
|
if self.permission(request.user):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def post(self, request, *arg, **kwargs):
|
def post(self, request, *arg, **kwargs):
|
||||||
self.operator = request.user
|
self.operator = request.user
|
||||||
if self.permission(request.user):
|
if self.permission(request.user):
|
||||||
return super(RefoundAccountView, self).post(self, request, *arg, **kwargs)
|
return super().post(self, request, *arg, **kwargs)
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
self.customer = form.cleaned_data["user"]
|
self.customer = form.cleaned_data["user"]
|
||||||
self.create_selling()
|
self.create_selling()
|
||||||
return super(RefoundAccountView, self).form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse("accounting:refound_account")
|
return reverse("accounting:refound_account")
|
||||||
|
39
accounting/widgets/select.py
Normal file
39
accounting/widgets/select.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
from pydantic import TypeAdapter
|
||||||
|
|
||||||
|
from accounting.models import ClubAccount, Company
|
||||||
|
from accounting.schemas import ClubAccountSchema, CompanySchema
|
||||||
|
from core.views.widgets.select import AutoCompleteSelect, AutoCompleteSelectMultiple
|
||||||
|
|
||||||
|
_js = ["bundled/accounting/components/ajax-select-index.ts"]
|
||||||
|
|
||||||
|
|
||||||
|
class AutoCompleteSelectClubAccount(AutoCompleteSelect):
|
||||||
|
component_name = "club-account-ajax-select"
|
||||||
|
model = ClubAccount
|
||||||
|
adapter = TypeAdapter(list[ClubAccountSchema])
|
||||||
|
|
||||||
|
js = _js
|
||||||
|
|
||||||
|
|
||||||
|
class AutoCompleteSelectMultipleClubAccount(AutoCompleteSelectMultiple):
|
||||||
|
component_name = "club-account-ajax-select"
|
||||||
|
model = ClubAccount
|
||||||
|
adapter = TypeAdapter(list[ClubAccountSchema])
|
||||||
|
|
||||||
|
js = _js
|
||||||
|
|
||||||
|
|
||||||
|
class AutoCompleteSelectCompany(AutoCompleteSelect):
|
||||||
|
component_name = "company-ajax-select"
|
||||||
|
model = Company
|
||||||
|
adapter = TypeAdapter(list[CompanySchema])
|
||||||
|
|
||||||
|
js = _js
|
||||||
|
|
||||||
|
|
||||||
|
class AutoCompleteSelectMultipleCompany(AutoCompleteSelectMultiple):
|
||||||
|
component_name = "company-ajax-select"
|
||||||
|
model = Company
|
||||||
|
adapter = TypeAdapter(list[CompanySchema])
|
||||||
|
|
||||||
|
js = _js
|
10
antispam/admin.py
Normal file
10
antispam/admin.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from antispam.models import ToxicDomain
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ToxicDomain)
|
||||||
|
class ToxicDomainAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("domain", "is_externally_managed", "created")
|
||||||
|
search_fields = ("domain", "is_externally_managed", "created")
|
||||||
|
list_filter = ("is_externally_managed",)
|
7
antispam/apps.py
Normal file
7
antispam/apps.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AntispamConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
verbose_name = "antispam"
|
||||||
|
name = "antispam"
|
18
antispam/forms.py
Normal file
18
antispam/forms.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.core.validators import EmailValidator
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from antispam.models import ToxicDomain
|
||||||
|
|
||||||
|
|
||||||
|
class AntiSpamEmailField(forms.EmailField):
|
||||||
|
"""An email field that email addresses with a known toxic domain."""
|
||||||
|
|
||||||
|
def run_validators(self, value: str):
|
||||||
|
super().run_validators(value)
|
||||||
|
# Domain part should exist since email validation is guaranteed to run first
|
||||||
|
domain = re.search(EmailValidator.domain_regex, value)
|
||||||
|
if ToxicDomain.objects.filter(domain=domain[0]).exists():
|
||||||
|
raise forms.ValidationError(_("Email domain is not allowed."))
|
0
antispam/management/commands/__init__.py
Normal file
0
antispam/management/commands/__init__.py
Normal file
69
antispam/management/commands/update_spam_database.py
Normal file
69
antispam/management/commands/update_spam_database.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import requests
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
from django.db.models import Max
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from antispam.models import ToxicDomain
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""Update blocked ips/mails database"""
|
||||||
|
|
||||||
|
help = "Update blocked ips/mails database"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"--force", action="store_true", help="Force re-creation even if up to date"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _should_update(self, *, force: bool = False) -> bool:
|
||||||
|
if force:
|
||||||
|
return True
|
||||||
|
oldest = ToxicDomain.objects.filter(is_externally_managed=True).aggregate(
|
||||||
|
res=Max("created")
|
||||||
|
)["res"]
|
||||||
|
return not (oldest and timezone.now() < (oldest + timezone.timedelta(days=1)))
|
||||||
|
|
||||||
|
def _download_domains(self, providers: list[str]) -> set[str]:
|
||||||
|
domains = set()
|
||||||
|
for provider in providers:
|
||||||
|
res = requests.get(provider)
|
||||||
|
if not res.ok:
|
||||||
|
self.stderr.write(
|
||||||
|
f"Source {provider} responded with code {res.status_code}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
domains |= set(res.content.decode().splitlines())
|
||||||
|
return domains
|
||||||
|
|
||||||
|
def _update_domains(self, domains: set[str]):
|
||||||
|
# Cleanup database
|
||||||
|
ToxicDomain.objects.filter(is_externally_managed=True).delete()
|
||||||
|
|
||||||
|
# Create database
|
||||||
|
ToxicDomain.objects.bulk_create(
|
||||||
|
[
|
||||||
|
ToxicDomain(domain=domain, is_externally_managed=True)
|
||||||
|
for domain in domains
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
self.stdout.write("Domain database updated")
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
if not self._should_update(force=options["force"]):
|
||||||
|
self.stdout.write("Domain database is up to date")
|
||||||
|
return
|
||||||
|
self.stdout.write("Updating domain database")
|
||||||
|
|
||||||
|
domains = self._download_domains(settings.TOXIC_DOMAINS_PROVIDERS)
|
||||||
|
|
||||||
|
if not domains:
|
||||||
|
self.stderr.write(
|
||||||
|
"No domains could be fetched from settings.TOXIC_DOMAINS_PROVIDERS. "
|
||||||
|
"Please, have a look at your settings."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._update_domains(domains)
|
35
antispam/migrations/0001_initial.py
Normal file
35
antispam/migrations/0001_initial.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Generated by Django 4.2.14 on 2024-08-03 23:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ToxicDomain",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"domain",
|
||||||
|
models.URLField(
|
||||||
|
max_length=253,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="domain",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
|
(
|
||||||
|
"is_externally_managed",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="True if kept up-to-date using external toxic domain providers, else False",
|
||||||
|
verbose_name="is externally managed",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
0
antispam/migrations/__init__.py
Normal file
0
antispam/migrations/__init__.py
Normal file
19
antispam/models.py
Normal file
19
antispam/models.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class ToxicDomain(models.Model):
|
||||||
|
"""Domain marked as spam in public databases"""
|
||||||
|
|
||||||
|
domain = models.URLField(_("domain"), max_length=253, primary_key=True)
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
is_externally_managed = models.BooleanField(
|
||||||
|
_("is externally managed"),
|
||||||
|
default=False,
|
||||||
|
help_text=_(
|
||||||
|
"True if kept up-to-date using external toxic domain providers, else False"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.domain
|
@ -1,15 +0,0 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
19
api/admin.py
19
api/admin.py
@ -1,19 +0,0 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
@ -1,19 +0,0 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
# Create your models here.
|
|
19
api/tests.py
19
api/tests.py
@ -1,19 +0,0 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
50
api/urls.py
50
api/urls.py
@ -1,50 +0,0 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
from django.urls import re_path, path, include
|
|
||||||
|
|
||||||
from api.views import *
|
|
||||||
from rest_framework import routers
|
|
||||||
|
|
||||||
# Router config
|
|
||||||
router = routers.DefaultRouter()
|
|
||||||
router.register(r"counter", CounterViewSet, basename="api_counter")
|
|
||||||
router.register(r"user", UserViewSet, basename="api_user")
|
|
||||||
router.register(r"club", ClubViewSet, basename="api_club")
|
|
||||||
router.register(r"group", GroupViewSet, basename="api_group")
|
|
||||||
|
|
||||||
# Launderette
|
|
||||||
router.register(
|
|
||||||
r"launderette/place", LaunderettePlaceViewSet, basename="api_launderette_place"
|
|
||||||
)
|
|
||||||
router.register(
|
|
||||||
r"launderette/machine",
|
|
||||||
LaunderetteMachineViewSet,
|
|
||||||
basename="api_launderette_machine",
|
|
||||||
)
|
|
||||||
router.register(
|
|
||||||
r"launderette/token", LaunderetteTokenViewSet, basename="api_launderette_token"
|
|
||||||
)
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
# API
|
|
||||||
re_path(r"^", include(router.urls)),
|
|
||||||
re_path(r"^login/", include("rest_framework.urls", namespace="rest_framework")),
|
|
||||||
re_path(r"^markdown$", RenderMarkdown, name="api_markdown"),
|
|
||||||
re_path(r"^mailings$", FetchMailingLists, name="mailings_fetch"),
|
|
||||||
re_path(r"^uv$", uv_endpoint, name="uv_endpoint"),
|
|
||||||
path("sas/<int:user>", all_pictures_of_user_endpoint, name="all_pictures_of_user"),
|
|
||||||
]
|
|
@ -1,73 +0,0 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework import viewsets
|
|
||||||
from django.core.exceptions import PermissionDenied
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from django.db.models.query import QuerySet
|
|
||||||
|
|
||||||
from core.views import can_view, can_edit
|
|
||||||
|
|
||||||
|
|
||||||
def check_if(obj, user, test):
|
|
||||||
"""
|
|
||||||
Detect if it's a single object or a queryset
|
|
||||||
aply a given test on individual object and return global permission
|
|
||||||
"""
|
|
||||||
if isinstance(obj, QuerySet):
|
|
||||||
for o in obj:
|
|
||||||
if test(o, user) is False:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return test(obj, user)
|
|
||||||
|
|
||||||
|
|
||||||
class ManageModelMixin:
|
|
||||||
@action(detail=True)
|
|
||||||
def id(self, request, pk=None):
|
|
||||||
"""
|
|
||||||
Get by id (api/v1/router/{pk}/id/)
|
|
||||||
"""
|
|
||||||
self.queryset = get_object_or_404(self.queryset.filter(id=pk))
|
|
||||||
serializer = self.get_serializer(self.queryset)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
|
|
||||||
class RightModelViewSet(ManageModelMixin, viewsets.ModelViewSet):
|
|
||||||
def dispatch(self, request, *arg, **kwargs):
|
|
||||||
res = super(RightModelViewSet, self).dispatch(request, *arg, **kwargs)
|
|
||||||
obj = self.queryset
|
|
||||||
user = self.request.user
|
|
||||||
try:
|
|
||||||
if request.method == "GET" and check_if(obj, user, can_view):
|
|
||||||
return res
|
|
||||||
if request.method != "GET" and check_if(obj, user, can_edit):
|
|
||||||
return res
|
|
||||||
except:
|
|
||||||
pass # To prevent bug with Anonymous user
|
|
||||||
raise PermissionDenied
|
|
||||||
|
|
||||||
|
|
||||||
from .api import *
|
|
||||||
from .counter import *
|
|
||||||
from .user import *
|
|
||||||
from .club import *
|
|
||||||
from .group import *
|
|
||||||
from .launderette import *
|
|
||||||
from .uv import *
|
|
||||||
from .sas import *
|
|
@ -1,34 +0,0 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.decorators import api_view, renderer_classes
|
|
||||||
from rest_framework.renderers import StaticHTMLRenderer
|
|
||||||
|
|
||||||
from core.templatetags.renderer import markdown
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(["POST"])
|
|
||||||
@renderer_classes((StaticHTMLRenderer,))
|
|
||||||
def RenderMarkdown(request):
|
|
||||||
"""
|
|
||||||
Render Markdown
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
data = markdown(request.POST["text"])
|
|
||||||
except:
|
|
||||||
data = "Error"
|
|
||||||
return Response(data)
|
|
@ -1,56 +0,0 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework import serializers
|
|
||||||
from rest_framework.decorators import api_view, renderer_classes
|
|
||||||
from rest_framework.renderers import StaticHTMLRenderer
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import PermissionDenied
|
|
||||||
|
|
||||||
from club.models import Club, Mailing
|
|
||||||
|
|
||||||
from api.views import RightModelViewSet
|
|
||||||
|
|
||||||
|
|
||||||
class ClubSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Club
|
|
||||||
fields = ("id", "name", "unix_name", "address", "members")
|
|
||||||
|
|
||||||
|
|
||||||
class ClubViewSet(RightModelViewSet):
|
|
||||||
"""
|
|
||||||
Manage Clubs (api/v1/club/)
|
|
||||||
"""
|
|
||||||
|
|
||||||
serializer_class = ClubSerializer
|
|
||||||
queryset = Club.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
|
||||||
@renderer_classes((StaticHTMLRenderer,))
|
|
||||||
def FetchMailingLists(request):
|
|
||||||
key = request.GET.get("key", "")
|
|
||||||
if key != settings.SITH_MAILING_FETCH_KEY:
|
|
||||||
raise PermissionDenied
|
|
||||||
data = ""
|
|
||||||
for mailing in Mailing.objects.filter(
|
|
||||||
is_moderated=True, club__is_active=True
|
|
||||||
).all():
|
|
||||||
data += mailing.fetch_format() + "\n"
|
|
||||||
return Response(data)
|
|
@ -1,52 +0,0 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
|
|
||||||
from counter.models import Counter
|
|
||||||
|
|
||||||
from api.views import RightModelViewSet
|
|
||||||
|
|
||||||
|
|
||||||
class CounterSerializer(serializers.ModelSerializer):
|
|
||||||
is_open = serializers.BooleanField(read_only=True)
|
|
||||||
barman_list = serializers.ListField(
|
|
||||||
child=serializers.IntegerField(), read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Counter
|
|
||||||
fields = ("id", "name", "type", "club", "products", "is_open", "barman_list")
|
|
||||||
|
|
||||||
|
|
||||||
class CounterViewSet(RightModelViewSet):
|
|
||||||
"""
|
|
||||||
Manage Counters (api/v1/counter/)
|
|
||||||
"""
|
|
||||||
|
|
||||||
serializer_class = CounterSerializer
|
|
||||||
queryset = Counter.objects.all()
|
|
||||||
|
|
||||||
@action(detail=False)
|
|
||||||
def bar(self, request):
|
|
||||||
"""
|
|
||||||
Return all bars (api/v1/counter/bar/)
|
|
||||||
"""
|
|
||||||
self.queryset = self.queryset.filter(type="BAR")
|
|
||||||
serializer = self.get_serializer(self.queryset, many=True)
|
|
||||||
return Response(serializer.data)
|
|
@ -1,35 +0,0 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from core.models import RealGroup
|
|
||||||
|
|
||||||
from api.views import RightModelViewSet
|
|
||||||
|
|
||||||
|
|
||||||
class GroupSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = RealGroup
|
|
||||||
|
|
||||||
|
|
||||||
class GroupViewSet(RightModelViewSet):
|
|
||||||
"""
|
|
||||||
Manage Groups (api/v1/group/)
|
|
||||||
"""
|
|
||||||
|
|
||||||
serializer_class = GroupSerializer
|
|
||||||
queryset = RealGroup.objects.all()
|
|
@ -1,128 +0,0 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
|
|
||||||
from launderette.models import Launderette, Machine, Token
|
|
||||||
|
|
||||||
from api.views import RightModelViewSet
|
|
||||||
|
|
||||||
|
|
||||||
class LaunderettePlaceSerializer(serializers.ModelSerializer):
|
|
||||||
machine_list = serializers.ListField(
|
|
||||||
child=serializers.IntegerField(), read_only=True
|
|
||||||
)
|
|
||||||
token_list = serializers.ListField(child=serializers.IntegerField(), read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Launderette
|
|
||||||
fields = (
|
|
||||||
"id",
|
|
||||||
"name",
|
|
||||||
"counter",
|
|
||||||
"machine_list",
|
|
||||||
"token_list",
|
|
||||||
"get_absolute_url",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class LaunderetteMachineSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Machine
|
|
||||||
fields = ("id", "name", "type", "is_working", "launderette")
|
|
||||||
|
|
||||||
|
|
||||||
class LaunderetteTokenSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Token
|
|
||||||
fields = (
|
|
||||||
"id",
|
|
||||||
"name",
|
|
||||||
"type",
|
|
||||||
"launderette",
|
|
||||||
"borrow_date",
|
|
||||||
"user",
|
|
||||||
"is_avaliable",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class LaunderettePlaceViewSet(RightModelViewSet):
|
|
||||||
"""
|
|
||||||
Manage Launderette (api/v1/launderette/place/)
|
|
||||||
"""
|
|
||||||
|
|
||||||
serializer_class = LaunderettePlaceSerializer
|
|
||||||
queryset = Launderette.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class LaunderetteMachineViewSet(RightModelViewSet):
|
|
||||||
"""
|
|
||||||
Manage Washing Machines (api/v1/launderette/machine/)
|
|
||||||
"""
|
|
||||||
|
|
||||||
serializer_class = LaunderetteMachineSerializer
|
|
||||||
queryset = Machine.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class LaunderetteTokenViewSet(RightModelViewSet):
|
|
||||||
"""
|
|
||||||
Manage Launderette's tokens (api/v1/launderette/token/)
|
|
||||||
"""
|
|
||||||
|
|
||||||
serializer_class = LaunderetteTokenSerializer
|
|
||||||
queryset = Token.objects.all()
|
|
||||||
|
|
||||||
@action(detail=False)
|
|
||||||
def washing(self, request):
|
|
||||||
"""
|
|
||||||
Return all washing tokens (api/v1/launderette/token/washing)
|
|
||||||
"""
|
|
||||||
self.queryset = self.queryset.filter(type="WASHING")
|
|
||||||
serializer = self.get_serializer(self.queryset, many=True)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
@action(detail=False)
|
|
||||||
def drying(self, request):
|
|
||||||
"""
|
|
||||||
Return all drying tokens (api/v1/launderette/token/drying)
|
|
||||||
"""
|
|
||||||
self.queryset = self.queryset.filter(type="DRYING")
|
|
||||||
serializer = self.get_serializer(self.queryset, many=True)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
@action(detail=False)
|
|
||||||
def avaliable(self, request):
|
|
||||||
"""
|
|
||||||
Return all avaliable tokens (api/v1/launderette/token/avaliable)
|
|
||||||
"""
|
|
||||||
self.queryset = self.queryset.filter(
|
|
||||||
borrow_date__isnull=True, user__isnull=True
|
|
||||||
)
|
|
||||||
serializer = self.get_serializer(self.queryset, many=True)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
@action(detail=False)
|
|
||||||
def unavaliable(self, request):
|
|
||||||
"""
|
|
||||||
Return all unavaliable tokens (api/v1/launderette/token/unavaliable)
|
|
||||||
"""
|
|
||||||
self.queryset = self.queryset.filter(
|
|
||||||
borrow_date__isnull=False, user__isnull=False
|
|
||||||
)
|
|
||||||
serializer = self.get_serializer(self.queryset, many=True)
|
|
||||||
return Response(serializer.data)
|
|
@ -1,42 +0,0 @@
|
|||||||
from typing import List
|
|
||||||
from rest_framework.decorators import api_view, renderer_classes
|
|
||||||
from rest_framework.exceptions import PermissionDenied
|
|
||||||
from rest_framework.generics import get_object_or_404
|
|
||||||
from rest_framework.renderers import JSONRenderer
|
|
||||||
from rest_framework.request import Request
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from core.views import can_edit
|
|
||||||
from core.models import User
|
|
||||||
from sas.models import Picture
|
|
||||||
|
|
||||||
|
|
||||||
def all_pictures_of_user(user: User) -> List[Picture]:
|
|
||||||
return [
|
|
||||||
relation.picture
|
|
||||||
for relation in user.pictures.exclude(picture=None)
|
|
||||||
.order_by("-picture__parent__date", "id")
|
|
||||||
.select_related("picture__parent")
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
|
||||||
@renderer_classes((JSONRenderer,))
|
|
||||||
def all_pictures_of_user_endpoint(request: Request, user: int):
|
|
||||||
requested_user: User = get_object_or_404(User, pk=user)
|
|
||||||
if not can_edit(requested_user, request.user):
|
|
||||||
raise PermissionDenied
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"name": f"{picture.parent.name} - {picture.name}",
|
|
||||||
"date": picture.date,
|
|
||||||
"author": str(picture.owner),
|
|
||||||
"full_size_url": picture.get_download_url(),
|
|
||||||
"compressed_url": picture.get_download_compressed_url(),
|
|
||||||
"thumb_url": picture.get_download_thumb_url(),
|
|
||||||
}
|
|
||||||
for picture in all_pictures_of_user(requested_user)
|
|
||||||
]
|
|
||||||
)
|
|
@ -1,60 +0,0 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
|
||||||
# Copyright 2023 © AE UTBM
|
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
|
||||||
#
|
|
||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
||||||
# https://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
|
||||||
#
|
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
|
|
||||||
from core.models import User
|
|
||||||
|
|
||||||
from api.views import RightModelViewSet
|
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = User
|
|
||||||
fields = (
|
|
||||||
"id",
|
|
||||||
"first_name",
|
|
||||||
"last_name",
|
|
||||||
"email",
|
|
||||||
"date_of_birth",
|
|
||||||
"nick_name",
|
|
||||||
"is_active",
|
|
||||||
"date_joined",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(RightModelViewSet):
|
|
||||||
"""
|
|
||||||
Manage Users (api/v1/user/)
|
|
||||||
Only show active users
|
|
||||||
"""
|
|
||||||
|
|
||||||
serializer_class = UserSerializer
|
|
||||||
queryset = User.objects.filter(is_active=True)
|
|
||||||
|
|
||||||
@action(detail=False)
|
|
||||||
def birthday(self, request):
|
|
||||||
"""
|
|
||||||
Return all users born today (api/v1/user/birstdays)
|
|
||||||
"""
|
|
||||||
date = datetime.datetime.today()
|
|
||||||
self.queryset = self.queryset.filter(date_of_birth=date)
|
|
||||||
serializer = self.get_serializer(self.queryset, many=True)
|
|
||||||
return Response(serializer.data)
|
|
127
api/views/uv.py
127
api/views/uv.py
@ -1,127 +0,0 @@
|
|||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.decorators import api_view, renderer_classes
|
|
||||||
from rest_framework.renderers import JSONRenderer
|
|
||||||
from django.core.exceptions import PermissionDenied
|
|
||||||
from django.conf import settings
|
|
||||||
from rest_framework import serializers
|
|
||||||
import urllib.request
|
|
||||||
import json
|
|
||||||
|
|
||||||
from pedagogy.views import CanCreateUVFunctionMixin
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
|
||||||
@renderer_classes((JSONRenderer,))
|
|
||||||
def uv_endpoint(request):
|
|
||||||
if not CanCreateUVFunctionMixin.can_create_uv(request.user):
|
|
||||||
raise PermissionDenied
|
|
||||||
|
|
||||||
params = request.query_params
|
|
||||||
if "year" not in params or "code" not in params:
|
|
||||||
raise serializers.ValidationError("Missing query parameter")
|
|
||||||
|
|
||||||
short_uv, full_uv = find_uv("fr", params["year"], params["code"])
|
|
||||||
if short_uv is None or full_uv is None:
|
|
||||||
return Response(status=204)
|
|
||||||
|
|
||||||
return Response(make_clean_uv(short_uv, full_uv))
|
|
||||||
|
|
||||||
|
|
||||||
def find_uv(lang, year, code):
|
|
||||||
"""
|
|
||||||
Uses the UTBM API to find an UV.
|
|
||||||
short_uv is the UV entry in the UV list. It is returned as it contains
|
|
||||||
information which are not in full_uv.
|
|
||||||
full_uv is the detailed representation of an UV.
|
|
||||||
"""
|
|
||||||
# query the UV list
|
|
||||||
uvs_url = settings.SITH_PEDAGOGY_UTBM_API + "/uvs/{}/{}".format(lang, year)
|
|
||||||
response = urllib.request.urlopen(uvs_url)
|
|
||||||
uvs = json.loads(response.read().decode("utf-8"))
|
|
||||||
|
|
||||||
try:
|
|
||||||
# find the first UV which matches the code
|
|
||||||
short_uv = next(uv for uv in uvs if uv["code"] == code)
|
|
||||||
except StopIteration:
|
|
||||||
return (None, None)
|
|
||||||
|
|
||||||
# get detailed information about the UV
|
|
||||||
uv_url = settings.SITH_PEDAGOGY_UTBM_API + "/uv/{}/{}/{}/{}".format(
|
|
||||||
lang, year, code, short_uv["codeFormation"]
|
|
||||||
)
|
|
||||||
response = urllib.request.urlopen(uv_url)
|
|
||||||
full_uv = json.loads(response.read().decode("utf-8"))
|
|
||||||
|
|
||||||
return (short_uv, full_uv)
|
|
||||||
|
|
||||||
|
|
||||||
def make_clean_uv(short_uv, full_uv):
|
|
||||||
"""
|
|
||||||
Cleans the data up so that it corresponds to our data representation.
|
|
||||||
"""
|
|
||||||
res = {}
|
|
||||||
|
|
||||||
res["credit_type"] = short_uv["codeCategorie"]
|
|
||||||
|
|
||||||
# probably wrong on a few UVs as we pick the first UV we find but
|
|
||||||
# availability depends on the formation
|
|
||||||
semesters = {
|
|
||||||
(True, True): "AUTUMN_AND_SPRING",
|
|
||||||
(True, False): "AUTUMN",
|
|
||||||
(False, True): "SPRING",
|
|
||||||
}
|
|
||||||
res["semester"] = semesters.get(
|
|
||||||
(short_uv["ouvertAutomne"], short_uv["ouvertPrintemps"]), "CLOSED"
|
|
||||||
)
|
|
||||||
|
|
||||||
langs = {"es": "SP", "en": "EN", "de": "DE"}
|
|
||||||
res["language"] = langs.get(full_uv["codeLangue"], "FR")
|
|
||||||
|
|
||||||
if full_uv["departement"] == "Pôle Humanités":
|
|
||||||
res["department"] = "HUMA"
|
|
||||||
else:
|
|
||||||
departments = {
|
|
||||||
"AL": "IMSI",
|
|
||||||
"AE": "EE",
|
|
||||||
"GI": "GI",
|
|
||||||
"GC": "EE",
|
|
||||||
"GM": "MC",
|
|
||||||
"TC": "TC",
|
|
||||||
"GP": "IMSI",
|
|
||||||
"ED": "EDIM",
|
|
||||||
"AI": "GI",
|
|
||||||
"AM": "MC",
|
|
||||||
}
|
|
||||||
res["department"] = departments.get(full_uv["codeFormation"], "NA")
|
|
||||||
|
|
||||||
res["credits"] = full_uv["creditsEcts"]
|
|
||||||
|
|
||||||
activities = ("CM", "TD", "TP", "THE", "TE")
|
|
||||||
for activity in activities:
|
|
||||||
res["hours_{}".format(activity)] = 0
|
|
||||||
for activity in full_uv["activites"]:
|
|
||||||
if activity["code"] in activities:
|
|
||||||
res["hours_{}".format(activity["code"])] += activity["nbh"] // 60
|
|
||||||
|
|
||||||
# wrong if the manager changes depending on the semester
|
|
||||||
semester = full_uv.get("automne", None)
|
|
||||||
if not semester:
|
|
||||||
semester = full_uv.get("printemps", {})
|
|
||||||
res["manager"] = semester.get("responsable", "")
|
|
||||||
|
|
||||||
res["title"] = full_uv["libelle"]
|
|
||||||
|
|
||||||
descriptions = {
|
|
||||||
"objectives": "objectifs",
|
|
||||||
"program": "programme",
|
|
||||||
"skills": "acquisitionCompetences",
|
|
||||||
"key_concepts": "acquisitionNotions",
|
|
||||||
}
|
|
||||||
|
|
||||||
for res_key, full_uv_key in descriptions.items():
|
|
||||||
res[res_key] = full_uv[full_uv_key]
|
|
||||||
# if not found or the API did not return a string
|
|
||||||
if type(res[res_key]) != str:
|
|
||||||
res[res_key] = ""
|
|
||||||
|
|
||||||
return res
|
|
29
biome.json
Normal file
29
biome.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||||
|
"vcs": {
|
||||||
|
"enabled": true,
|
||||||
|
"clientKind": "git",
|
||||||
|
"useIgnoreFile": true
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"ignoreUnknown": false,
|
||||||
|
"ignore": ["*.min.*", "staticfiles/generated"]
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"indentStyle": "space",
|
||||||
|
"lineWidth": 88
|
||||||
|
},
|
||||||
|
"organizeImports": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"all": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"globals": ["Alpine", "$", "jQuery", "gettext", "interpolate"]
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,3 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
#
|
||||||
# Copyright 2023 © AE UTBM
|
# Copyright 2023 © AE UTBM
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
@ -6,10 +5,10 @@
|
|||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# https://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
# You can find the source code of the website at https://github.com/ae-utbm/sith
|
||||||
#
|
#
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
#
|
||||||
# Copyright 2023 © AE UTBM
|
# Copyright 2023 © AE UTBM
|
||||||
# ae@utbm.fr / ae.info@utbm.fr
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
@ -6,14 +5,13 @@
|
|||||||
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
# https://ae.utbm.fr.
|
# https://ae.utbm.fr.
|
||||||
#
|
#
|
||||||
# You can find the source code of the website at https://github.com/ae-utbm/sith3
|
# You can find the source code of the website at https://github.com/ae-utbm/sith
|
||||||
#
|
#
|
||||||
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
|
||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
from ajax_select import make_ajax_form
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from club.models import Club, Membership
|
from club.models import Club, Membership
|
||||||
@ -22,6 +20,14 @@ from club.models import Club, Membership
|
|||||||
@admin.register(Club)
|
@admin.register(Club)
|
||||||
class ClubAdmin(admin.ModelAdmin):
|
class ClubAdmin(admin.ModelAdmin):
|
||||||
list_display = ("name", "unix_name", "parent", "is_active")
|
list_display = ("name", "unix_name", "parent", "is_active")
|
||||||
|
search_fields = ("name", "unix_name")
|
||||||
|
autocomplete_fields = (
|
||||||
|
"parent",
|
||||||
|
"board_group",
|
||||||
|
"members_group",
|
||||||
|
"home",
|
||||||
|
"page",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Membership)
|
@admin.register(Membership)
|
||||||
@ -33,4 +39,4 @@ class MembershipAdmin(admin.ModelAdmin):
|
|||||||
"user__last_name",
|
"user__last_name",
|
||||||
"club__name",
|
"club__name",
|
||||||
)
|
)
|
||||||
form = make_ajax_form(Membership, {"user": "users"})
|
autocomplete_fields = ("user",)
|
||||||
|
22
club/api.py
Normal file
22
club/api.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from annotated_types import MinLen
|
||||||
|
from ninja_extra import ControllerBase, api_controller, paginate, route
|
||||||
|
from ninja_extra.pagination import PageNumberPaginationExtra
|
||||||
|
from ninja_extra.schemas import PaginatedResponseSchema
|
||||||
|
|
||||||
|
from club.models import Club
|
||||||
|
from club.schemas import ClubSchema
|
||||||
|
from core.auth.api_permissions import CanAccessLookup
|
||||||
|
|
||||||
|
|
||||||
|
@api_controller("/club")
|
||||||
|
class ClubController(ControllerBase):
|
||||||
|
@route.get(
|
||||||
|
"/search",
|
||||||
|
response=PaginatedResponseSchema[ClubSchema],
|
||||||
|
permissions=[CanAccessLookup],
|
||||||
|
)
|
||||||
|
@paginate(PageNumberPaginationExtra, page_size=50)
|
||||||
|
def search_club(self, search: Annotated[str, MinLen(1)]):
|
||||||
|
return Club.objects.filter(name__icontains=search).values()
|
@ -1,4 +1,3 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2016,2017
|
||||||
# - Skia <skia@libskia.so>
|
# - Skia <skia@libskia.so>
|
||||||
@ -23,18 +22,15 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
|
from club.models import Club, Mailing, MailingSubscription, Membership
|
||||||
|
|
||||||
from club.models import Mailing, MailingSubscription, Club, Membership
|
|
||||||
|
|
||||||
from core.models import User
|
from core.models import User
|
||||||
from core.views.forms import SelectDate, SelectDateTime
|
from core.views.forms import SelectDate, SelectDateTime
|
||||||
|
from core.views.widgets.select import AutoCompleteSelectMultipleUser
|
||||||
from counter.models import Counter
|
from counter.models import Counter
|
||||||
from core.views.forms import TzAwareDateTimeField
|
|
||||||
|
|
||||||
|
|
||||||
class ClubEditForm(forms.ModelForm):
|
class ClubEditForm(forms.ModelForm):
|
||||||
@ -43,28 +39,27 @@ class ClubEditForm(forms.ModelForm):
|
|||||||
fields = ["address", "logo", "short_description"]
|
fields = ["address", "logo", "short_description"]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(ClubEditForm, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields["short_description"].widget = forms.Textarea()
|
self.fields["short_description"].widget = forms.Textarea()
|
||||||
|
|
||||||
|
|
||||||
class MailingForm(forms.Form):
|
class MailingForm(forms.Form):
|
||||||
"""
|
"""Form handling mailing lists right."""
|
||||||
Form handling mailing lists right
|
|
||||||
"""
|
|
||||||
|
|
||||||
ACTION_NEW_MAILING = 1
|
ACTION_NEW_MAILING = 1
|
||||||
ACTION_NEW_SUBSCRIPTION = 2
|
ACTION_NEW_SUBSCRIPTION = 2
|
||||||
ACTION_REMOVE_SUBSCRIPTION = 3
|
ACTION_REMOVE_SUBSCRIPTION = 3
|
||||||
|
|
||||||
subscription_users = AutoCompleteSelectMultipleField(
|
subscription_users = forms.ModelMultipleChoiceField(
|
||||||
"users",
|
|
||||||
label=_("Users to add"),
|
label=_("Users to add"),
|
||||||
help_text=_("Search users to add (one or more)."),
|
help_text=_("Search users to add (one or more)."),
|
||||||
required=False,
|
required=False,
|
||||||
|
widget=AutoCompleteSelectMultipleUser,
|
||||||
|
queryset=User.objects.all(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, club_id, user_id, mailings, *args, **kwargs):
|
def __init__(self, club_id, user_id, mailings, *args, **kwargs):
|
||||||
super(MailingForm, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields["action"] = forms.TypedChoiceField(
|
self.fields["action"] = forms.TypedChoiceField(
|
||||||
choices=(
|
choices=(
|
||||||
@ -109,24 +104,15 @@ class MailingForm(forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def check_required(self, cleaned_data, field):
|
def check_required(self, cleaned_data, field):
|
||||||
"""
|
"""If the given field doesn't exist or has no value, add a required error on it."""
|
||||||
If the given field doesn't exist or has no value, add a required error on it
|
|
||||||
"""
|
|
||||||
if not cleaned_data.get(field, None):
|
if not cleaned_data.get(field, None):
|
||||||
self.add_error(field, _("This field is required"))
|
self.add_error(field, _("This field is required"))
|
||||||
|
|
||||||
def clean_subscription_users(self):
|
def clean_subscription_users(self):
|
||||||
"""
|
"""Convert given users into real users and check their validity."""
|
||||||
Convert given users into real users and check their validity
|
cleaned_data = super().clean()
|
||||||
"""
|
|
||||||
cleaned_data = super(MailingForm, self).clean()
|
|
||||||
users = []
|
users = []
|
||||||
for user in cleaned_data["subscription_users"]:
|
for user in cleaned_data["subscription_users"]:
|
||||||
user = User.objects.filter(id=user).first()
|
|
||||||
if not user:
|
|
||||||
raise forms.ValidationError(
|
|
||||||
_("One of the selected users doesn't exist"), code="invalid"
|
|
||||||
)
|
|
||||||
if not user.email:
|
if not user.email:
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
_("One of the selected users doesn't have an email address"),
|
_("One of the selected users doesn't have an email address"),
|
||||||
@ -136,9 +122,9 @@ class MailingForm(forms.Form):
|
|||||||
return users
|
return users
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super(MailingForm, self).clean()
|
cleaned_data = super().clean()
|
||||||
|
|
||||||
if not "action" in cleaned_data:
|
if "action" not in cleaned_data:
|
||||||
# If there is no action provided, we can stop here
|
# If there is no action provided, we can stop here
|
||||||
raise forms.ValidationError(_("An action is required"), code="invalid")
|
raise forms.ValidationError(_("An action is required"), code="invalid")
|
||||||
|
|
||||||
@ -159,15 +145,19 @@ class MailingForm(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class SellingsForm(forms.Form):
|
class SellingsForm(forms.Form):
|
||||||
begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
|
begin_date = forms.DateTimeField(
|
||||||
end_date = TzAwareDateTimeField(label=_("End date"), required=False)
|
label=_("Begin date"), widget=SelectDateTime, required=False
|
||||||
|
)
|
||||||
|
end_date = forms.DateTimeField(
|
||||||
|
label=_("End date"), widget=SelectDateTime, required=False
|
||||||
|
)
|
||||||
|
|
||||||
counters = forms.ModelMultipleChoiceField(
|
counters = forms.ModelMultipleChoiceField(
|
||||||
Counter.objects.order_by("name").all(), label=_("Counter"), required=False
|
Counter.objects.order_by("name").all(), label=_("Counter"), required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, club, *args, **kwargs):
|
def __init__(self, club, *args, **kwargs):
|
||||||
super(SellingsForm, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields["products"] = forms.ModelMultipleChoiceField(
|
self.fields["products"] = forms.ModelMultipleChoiceField(
|
||||||
club.products.order_by("name").filter(archived=False).all(),
|
club.products.order_by("name").filter(archived=False).all(),
|
||||||
label=_("Products"),
|
label=_("Products"),
|
||||||
@ -181,18 +171,17 @@ class SellingsForm(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class ClubMemberForm(forms.Form):
|
class ClubMemberForm(forms.Form):
|
||||||
"""
|
"""Form handling the members of a club."""
|
||||||
Form handling the members of a club
|
|
||||||
"""
|
|
||||||
|
|
||||||
error_css_class = "error"
|
error_css_class = "error"
|
||||||
required_css_class = "required"
|
required_css_class = "required"
|
||||||
|
|
||||||
users = AutoCompleteSelectMultipleField(
|
users = forms.ModelMultipleChoiceField(
|
||||||
"users",
|
|
||||||
label=_("Users to add"),
|
label=_("Users to add"),
|
||||||
help_text=_("Search users to add (one or more)."),
|
help_text=_("Search users to add (one or more)."),
|
||||||
required=False,
|
required=False,
|
||||||
|
widget=AutoCompleteSelectMultipleUser,
|
||||||
|
queryset=User.objects.all(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -204,7 +193,7 @@ class ClubMemberForm(forms.Form):
|
|||||||
self.club.members.filter(end_date=None).order_by("-role").all()
|
self.club.members.filter(end_date=None).order_by("-role").all()
|
||||||
)
|
)
|
||||||
self.request_user_membership = self.club.get_membership_for(self.request_user)
|
self.request_user_membership = self.club.get_membership_for(self.request_user)
|
||||||
super(ClubMemberForm, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Using a ModelForm binds too much the form with the model and we don't want that
|
# Using a ModelForm binds too much the form with the model and we don't want that
|
||||||
# We want the view to process the model creation since they are multiple users
|
# We want the view to process the model creation since they are multiple users
|
||||||
@ -240,18 +229,13 @@ class ClubMemberForm(forms.Form):
|
|||||||
self.fields.pop("start_date")
|
self.fields.pop("start_date")
|
||||||
|
|
||||||
def clean_users(self):
|
def clean_users(self):
|
||||||
|
"""Check that the user is not trying to add an user already in the club.
|
||||||
|
|
||||||
|
Also check that the user is valid and has a valid subscription.
|
||||||
"""
|
"""
|
||||||
Check that the user is not trying to add an user already in the club
|
cleaned_data = super().clean()
|
||||||
Also check that the user is valid and has a valid subscription
|
|
||||||
"""
|
|
||||||
cleaned_data = super(ClubMemberForm, self).clean()
|
|
||||||
users = []
|
users = []
|
||||||
for user_id in cleaned_data["users"]:
|
for user in cleaned_data["users"]:
|
||||||
user = User.objects.filter(id=user_id).first()
|
|
||||||
if not user:
|
|
||||||
raise forms.ValidationError(
|
|
||||||
_("One of the selected users doesn't exist"), code="invalid"
|
|
||||||
)
|
|
||||||
if not user.is_subscribed:
|
if not user.is_subscribed:
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
_("User must be subscriber to take part to a club"), code="invalid"
|
_("User must be subscriber to take part to a club"), code="invalid"
|
||||||
@ -264,10 +248,8 @@ class ClubMemberForm(forms.Form):
|
|||||||
return users
|
return users
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""
|
"""Check user rights for adding an user."""
|
||||||
Check user rights for adding an user
|
cleaned_data = super().clean()
|
||||||
"""
|
|
||||||
cleaned_data = super(ClubMemberForm, self).clean()
|
|
||||||
|
|
||||||
if "start_date" in cleaned_data and not cleaned_data["start_date"]:
|
if "start_date" in cleaned_data and not cleaned_data["start_date"]:
|
||||||
# Drop start_date if allowed to edition but not specified
|
# Drop start_date if allowed to edition but not specified
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
from django.conf import settings
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
from django.conf import settings
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
from django.conf import settings
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@ -109,6 +109,6 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name="mailingsubscription",
|
name="mailingsubscription",
|
||||||
unique_together=set([("user", "email", "mailing")]),
|
unique_together={("user", "email", "mailing")},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -1,21 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
from club.models import Club
|
|
||||||
from core.operations import PsqlRunOnly
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
def generate_club_pages(apps, schema_editor):
|
|
||||||
def recursive_generate_club_page(club):
|
|
||||||
club.make_page()
|
|
||||||
for child in Club.objects.filter(parent=club).all():
|
|
||||||
recursive_generate_club_page(child)
|
|
||||||
|
|
||||||
for club in Club.objects.filter(parent=None).all():
|
|
||||||
recursive_generate_club_page(club)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@ -49,11 +35,4 @@ class Migration(migrations.Migration):
|
|||||||
null=True,
|
null=True,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PsqlRunOnly(
|
|
||||||
"SET CONSTRAINTS ALL IMMEDIATE", reverse_sql=migrations.RunSQL.noop
|
|
||||||
),
|
|
||||||
migrations.RunPython(generate_club_pages),
|
|
||||||
PsqlRunOnly(
|
|
||||||
migrations.RunSQL.noop, reverse_sql="SET CONSTRAINTS ALL IMMEDIATE"
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import club.models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import club.models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@ -15,7 +15,7 @@ class Migration(migrations.Migration):
|
|||||||
name="owner_group",
|
name="owner_group",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
default=club.models.Club.get_default_owner_group,
|
default=club.models.get_default_owner_group,
|
||||||
related_name="owned_club",
|
related_name="owned_club",
|
||||||
to="core.Group",
|
to="core.Group",
|
||||||
),
|
),
|
||||||
|
106
club/migrations/0012_club_board_group_club_members_group.py
Normal file
106
club/migrations/0012_club_board_group_club_members_group.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
# Generated by Django 4.2.16 on 2024-11-20 17:08
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.db.models.functions.datetime
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.migrations.state import StateApps
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.utils.timezone import localdate
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_meta_groups(apps: StateApps, schema_editor):
|
||||||
|
"""Attach the existing meta groups to the clubs.
|
||||||
|
|
||||||
|
Until now, the meta groups were not attached to the clubs,
|
||||||
|
nor to the users.
|
||||||
|
This creates actual foreign relationships between the clubs
|
||||||
|
and theirs groups and the users and theirs groups.
|
||||||
|
|
||||||
|
Warnings:
|
||||||
|
When the meta groups associated with the clubs aren't found,
|
||||||
|
they are created.
|
||||||
|
Thus the migration shouldn't fail, and all the clubs will
|
||||||
|
have their groups.
|
||||||
|
However, there will probably be some groups that have
|
||||||
|
not been found but exist nonetheless,
|
||||||
|
so there will be duplicates and dangling groups.
|
||||||
|
There must be a manual cleanup after this migration.
|
||||||
|
"""
|
||||||
|
Group = apps.get_model("core", "Group")
|
||||||
|
Club = apps.get_model("club", "Club")
|
||||||
|
|
||||||
|
meta_groups = Group.objects.filter(is_meta=True)
|
||||||
|
clubs = list(Club.objects.all())
|
||||||
|
for club in clubs:
|
||||||
|
club.board_group = meta_groups.get_or_create(
|
||||||
|
name=club.unix_name + settings.SITH_BOARD_SUFFIX,
|
||||||
|
defaults={"is_meta": True},
|
||||||
|
)[0]
|
||||||
|
club.members_group = meta_groups.get_or_create(
|
||||||
|
name=club.unix_name + settings.SITH_MEMBER_SUFFIX,
|
||||||
|
defaults={"is_meta": True},
|
||||||
|
)[0]
|
||||||
|
club.save()
|
||||||
|
club.refresh_from_db()
|
||||||
|
memberships = club.members.filter(
|
||||||
|
Q(end_date=None) | Q(end_date__gt=localdate())
|
||||||
|
).select_related("user")
|
||||||
|
club.members_group.users.set([m.user for m in memberships])
|
||||||
|
club.board_group.users.set(
|
||||||
|
[
|
||||||
|
m.user
|
||||||
|
for m in memberships.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# steps of the migration :
|
||||||
|
# - Create a nullable field for the board group and the member group
|
||||||
|
# - Edit those new fields to make them point to currently existing meta groups
|
||||||
|
# - When this data migration is done, make the fields non-nullable
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("core", "0040_alter_user_options_user_user_permissions_and_more"),
|
||||||
|
("club", "0011_auto_20180426_2013"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="club",
|
||||||
|
name="edit_groups",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="club",
|
||||||
|
name="owner_group",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="club",
|
||||||
|
name="view_groups",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="club",
|
||||||
|
name="board_group",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="club_board",
|
||||||
|
to="core.group",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="club",
|
||||||
|
name="members_group",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="club",
|
||||||
|
to="core.group",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
migrate_meta_groups, reverse_code=migrations.RunPython.noop, elidable=True
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,36 @@
|
|||||||
|
# Generated by Django 4.2.17 on 2025-01-04 16:46
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [("club", "0012_club_board_group_club_members_group")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="club",
|
||||||
|
name="board_group",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="club_board",
|
||||||
|
to="core.group",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="club",
|
||||||
|
name="members_group",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="club",
|
||||||
|
to="core.group",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="membership",
|
||||||
|
constraint=models.CheckConstraint(
|
||||||
|
check=models.Q(("end_date__gte", models.F("start_date"))),
|
||||||
|
name="end_after_start",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
521
club/models.py
521
club/models.py
@ -1,4 +1,3 @@
|
|||||||
# -*- coding:utf-8 -*
|
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2016,2017
|
||||||
# - Skia <skia@libskia.so>
|
# - Skia <skia@libskia.so>
|
||||||
@ -22,31 +21,35 @@
|
|||||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
from typing import Optional
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Iterable, Self
|
||||||
|
|
||||||
from django.core.cache import cache
|
|
||||||
from django.db import models
|
|
||||||
from django.core import validators
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import Q
|
from django.core import validators
|
||||||
from django.utils.timezone import now
|
from django.core.cache import cache
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
from django.core.validators import RegexValidator, validate_email
|
||||||
from django.db import transaction
|
from django.db import models, transaction
|
||||||
|
from django.db.models import Exists, F, OuterRef, Q
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.core.validators import RegexValidator, validate_email
|
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.timezone import localdate
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from core.models import User, MetaGroup, Group, SithFile, RealGroup, Notification, Page
|
from core.models import Group, Notification, Page, SithFile, User
|
||||||
|
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
|
|
||||||
|
|
||||||
|
# This function prevents generating migration upon settings change
|
||||||
|
def get_default_owner_group():
|
||||||
|
return settings.SITH_GROUP_ROOT_ID
|
||||||
|
|
||||||
|
|
||||||
class Club(models.Model):
|
class Club(models.Model):
|
||||||
"""
|
"""The Club class, made as a tree to allow nice tidy organization."""
|
||||||
The Club class, made as a tree to allow nice tidy organization
|
|
||||||
"""
|
|
||||||
|
|
||||||
id = models.AutoField(primary_key=True, db_index=True)
|
id = models.AutoField(primary_key=True, db_index=True)
|
||||||
name = models.CharField(_("name"), max_length=64)
|
name = models.CharField(_("name"), max_length=64)
|
||||||
@ -76,23 +79,6 @@ class Club(models.Model):
|
|||||||
_("short description"), max_length=1000, default="", blank=True, null=True
|
_("short description"), max_length=1000, default="", blank=True, null=True
|
||||||
)
|
)
|
||||||
address = models.CharField(_("address"), max_length=254)
|
address = models.CharField(_("address"), max_length=254)
|
||||||
|
|
||||||
# This function prevents generating migration upon settings change
|
|
||||||
def get_default_owner_group():
|
|
||||||
return settings.SITH_GROUP_ROOT_ID
|
|
||||||
|
|
||||||
owner_group = models.ForeignKey(
|
|
||||||
Group,
|
|
||||||
related_name="owned_club",
|
|
||||||
default=get_default_owner_group,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
)
|
|
||||||
edit_groups = models.ManyToManyField(
|
|
||||||
Group, related_name="editable_club", blank=True
|
|
||||||
)
|
|
||||||
view_groups = models.ManyToManyField(
|
|
||||||
Group, related_name="viewable_club", blank=True
|
|
||||||
)
|
|
||||||
home = models.OneToOneField(
|
home = models.OneToOneField(
|
||||||
SithFile,
|
SithFile,
|
||||||
related_name="home_of_club",
|
related_name="home_of_club",
|
||||||
@ -104,18 +90,57 @@ class Club(models.Model):
|
|||||||
page = models.OneToOneField(
|
page = models.OneToOneField(
|
||||||
Page, related_name="club", blank=True, null=True, on_delete=models.CASCADE
|
Page, related_name="club", blank=True, null=True, on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
|
members_group = models.OneToOneField(
|
||||||
|
Group, related_name="club", on_delete=models.PROTECT
|
||||||
|
)
|
||||||
|
board_group = models.OneToOneField(
|
||||||
|
Group, related_name="club_board", on_delete=models.PROTECT
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["name", "unix_name"]
|
ordering = ["name", "unix_name"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
creation = self._state.adding
|
||||||
|
if not creation:
|
||||||
|
db_club = Club.objects.get(id=self.id)
|
||||||
|
if self.unix_name != db_club.unix_name:
|
||||||
|
self.home.name = self.unix_name
|
||||||
|
self.home.save()
|
||||||
|
if self.name != db_club.name:
|
||||||
|
self.board_group.name = f"{self.name} - Bureau"
|
||||||
|
self.board_group.save()
|
||||||
|
self.members_group.name = f"{self.name} - Membres"
|
||||||
|
self.members_group.save()
|
||||||
|
if creation:
|
||||||
|
self.board_group = Group.objects.create(
|
||||||
|
name=f"{self.name} - Bureau", is_manually_manageable=False
|
||||||
|
)
|
||||||
|
self.members_group = Group.objects.create(
|
||||||
|
name=f"{self.name} - Membres", is_manually_manageable=False
|
||||||
|
)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
if creation:
|
||||||
|
self.make_home()
|
||||||
|
self.make_page()
|
||||||
|
cache.set(f"sith_club_{self.unix_name}", self)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("club:club_view", kwargs={"club_id": self.id})
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def president(self):
|
def president(self) -> Membership | None:
|
||||||
|
"""Fetch the membership of the current president of this club."""
|
||||||
return self.members.filter(
|
return self.members.filter(
|
||||||
role=settings.SITH_CLUB_ROLES_ID["President"], end_date=None
|
role=settings.SITH_CLUB_ROLES_ID["President"], end_date=None
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
def check_loop(self):
|
def check_loop(self):
|
||||||
"""Raise a validation error when a loop is found within the parent list"""
|
"""Raise a validation error when a loop is found within the parent list."""
|
||||||
objs = []
|
objs = []
|
||||||
cur = self
|
cur = self
|
||||||
while cur.parent is not None:
|
while cur.parent is not None:
|
||||||
@ -127,36 +152,18 @@ class Club(models.Model):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
self.check_loop()
|
self.check_loop()
|
||||||
|
|
||||||
def _change_unixname(self, old_name, new_name):
|
def make_home(self) -> None:
|
||||||
c = Club.objects.filter(unix_name=new_name).first()
|
if self.home:
|
||||||
if c is None:
|
return
|
||||||
# Update all the groups names
|
home_root = SithFile.objects.filter(parent=None, name="clubs").first()
|
||||||
Group.objects.filter(name=old_name).update(name=new_name)
|
root = User.objects.filter(username="root").first()
|
||||||
Group.objects.filter(name=old_name + settings.SITH_BOARD_SUFFIX).update(
|
if home_root and root:
|
||||||
name=new_name + settings.SITH_BOARD_SUFFIX
|
home = SithFile(parent=home_root, name=self.unix_name, owner=root)
|
||||||
)
|
home.save()
|
||||||
Group.objects.filter(name=old_name + settings.SITH_MEMBER_SUFFIX).update(
|
self.home = home
|
||||||
name=new_name + settings.SITH_MEMBER_SUFFIX
|
self.save()
|
||||||
)
|
|
||||||
|
|
||||||
if self.home:
|
def make_page(self) -> None:
|
||||||
self.home.name = new_name
|
|
||||||
self.home.save()
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ValidationError(_("A club with that unix_name already exists"))
|
|
||||||
|
|
||||||
def make_home(self):
|
|
||||||
if not self.home:
|
|
||||||
home_root = SithFile.objects.filter(parent=None, name="clubs").first()
|
|
||||||
root = User.objects.filter(username="root").first()
|
|
||||||
if home_root and root:
|
|
||||||
home = SithFile(parent=home_root, name=self.unix_name, owner=root)
|
|
||||||
home.save()
|
|
||||||
self.home = home
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
def make_page(self):
|
|
||||||
root = User.objects.filter(username="root").first()
|
root = User.objects.filter(username="root").first()
|
||||||
if not self.page:
|
if not self.page:
|
||||||
club_root = Page.objects.filter(name=settings.SITH_CLUB_ROOT_PAGE).first()
|
club_root = Page.objects.filter(name=settings.SITH_CLUB_ROOT_PAGE).first()
|
||||||
@ -186,74 +193,40 @@ class Club(models.Model):
|
|||||||
self.page.parent = self.parent.page
|
self.page.parent = self.parent.page
|
||||||
self.page.save(force_lock=True)
|
self.page.save(force_lock=True)
|
||||||
|
|
||||||
@transaction.atomic()
|
def delete(self, *args, **kwargs) -> tuple[int, dict[str, int]]:
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
old = Club.objects.filter(id=self.id).first()
|
|
||||||
creation = old is None
|
|
||||||
if not creation and old.unix_name != self.unix_name:
|
|
||||||
self._change_unixname(self.unix_name)
|
|
||||||
super(Club, self).save(*args, **kwargs)
|
|
||||||
if creation:
|
|
||||||
board = MetaGroup(name=self.unix_name + settings.SITH_BOARD_SUFFIX)
|
|
||||||
board.save()
|
|
||||||
member = MetaGroup(name=self.unix_name + settings.SITH_MEMBER_SUFFIX)
|
|
||||||
member.save()
|
|
||||||
subscribers = Group.objects.filter(
|
|
||||||
name=settings.SITH_MAIN_MEMBERS_GROUP
|
|
||||||
).first()
|
|
||||||
self.make_home()
|
|
||||||
self.home.edit_groups.set([board])
|
|
||||||
self.home.view_groups.set([member, subscribers])
|
|
||||||
self.home.save()
|
|
||||||
self.make_page()
|
|
||||||
cache.set(f"sith_club_{self.unix_name}", self)
|
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
|
||||||
super().delete(*args, **kwargs)
|
|
||||||
# Invalidate the cache of this club and of its memberships
|
# Invalidate the cache of this club and of its memberships
|
||||||
for membership in self.members.ongoing().select_related("user"):
|
for membership in self.members.ongoing().select_related("user"):
|
||||||
cache.delete(f"membership_{self.id}_{membership.user.id}")
|
cache.delete(f"membership_{self.id}_{membership.user.id}")
|
||||||
cache.delete(f"sith_club_{self.unix_name}")
|
cache.delete(f"sith_club_{self.unix_name}")
|
||||||
|
self.board_group.delete()
|
||||||
|
self.members_group.delete()
|
||||||
|
return super().delete(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def get_display_name(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def is_owned_by(self, user: User) -> bool:
|
||||||
return reverse("club:club_view", kwargs={"club_id": self.id})
|
"""Method to see if that object can be super edited by the given user."""
|
||||||
|
|
||||||
def get_display_name(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
|
||||||
"""
|
|
||||||
Method to see if that object can be super edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_anonymous:
|
if user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
return user.is_board_member
|
return user.is_root or user.is_board_member
|
||||||
|
|
||||||
def get_full_logo_url(self):
|
def get_full_logo_url(self) -> str:
|
||||||
return "https://%s%s" % (settings.SITH_URL, self.logo.url)
|
return f"https://{settings.SITH_URL}{self.logo.url}"
|
||||||
|
|
||||||
def can_be_edited_by(self, user):
|
def can_be_edited_by(self, user: User) -> bool:
|
||||||
"""
|
"""Method to see if that object can be edited by the given user."""
|
||||||
Method to see if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
return self.has_rights_in_club(user)
|
return self.has_rights_in_club(user)
|
||||||
|
|
||||||
def can_be_viewed_by(self, user):
|
def can_be_viewed_by(self, user: User) -> bool:
|
||||||
"""
|
"""Method to see if that object can be seen by the given user."""
|
||||||
Method to see if that object can be seen by the given user
|
return user.was_subscribed
|
||||||
"""
|
|
||||||
sub = User.objects.filter(pk=user.pk).first()
|
|
||||||
if sub is None:
|
|
||||||
return False
|
|
||||||
return sub.was_subscribed
|
|
||||||
|
|
||||||
def get_membership_for(self, user: User) -> Optional["Membership"]:
|
def get_membership_for(self, user: User) -> Membership | None:
|
||||||
"""
|
"""Return the current membership the given user.
|
||||||
Return the current membership the given user.
|
|
||||||
The result is cached.
|
Note:
|
||||||
|
The result is cached.
|
||||||
"""
|
"""
|
||||||
if user.is_anonymous:
|
if user.is_anonymous:
|
||||||
return None
|
return None
|
||||||
@ -268,22 +241,17 @@ class Club(models.Model):
|
|||||||
cache.set(f"membership_{self.id}_{user.id}", membership)
|
cache.set(f"membership_{self.id}_{user.id}", membership)
|
||||||
return membership
|
return membership
|
||||||
|
|
||||||
def has_rights_in_club(self, user):
|
def has_rights_in_club(self, user: User) -> bool:
|
||||||
m = self.get_membership_for(user)
|
return user.is_in_group(pk=self.board_group_id)
|
||||||
return m is not None and m.role > settings.SITH_MAXIMUM_FREE_ROLE
|
|
||||||
|
|
||||||
|
|
||||||
class MembershipQuerySet(models.QuerySet):
|
class MembershipQuerySet(models.QuerySet):
|
||||||
def ongoing(self) -> "MembershipQuerySet":
|
def ongoing(self) -> Self:
|
||||||
"""
|
"""Filter all memberships which are not finished yet."""
|
||||||
Filter all memberships which are not finished yet
|
return self.filter(Q(end_date=None) | Q(end_date__gt=localdate()))
|
||||||
"""
|
|
||||||
# noinspection PyTypeChecker
|
|
||||||
return self.filter(Q(end_date=None) | Q(end_date__gte=timezone.now()))
|
|
||||||
|
|
||||||
def board(self) -> "MembershipQuerySet":
|
def board(self) -> Self:
|
||||||
"""
|
"""Filter all memberships where the user is/was in the board.
|
||||||
Filter all memberships where the user is/was in the board.
|
|
||||||
|
|
||||||
Be aware that users who were in the board in the past
|
Be aware that users who were in the board in the past
|
||||||
are included, even if there are no more members.
|
are included, even if there are no more members.
|
||||||
@ -291,51 +259,71 @@ class MembershipQuerySet(models.QuerySet):
|
|||||||
If you want to get the users who are currently in the board,
|
If you want to get the users who are currently in the board,
|
||||||
mind combining this with the :meth:`ongoing` queryset method
|
mind combining this with the :meth:`ongoing` queryset method
|
||||||
"""
|
"""
|
||||||
# noinspection PyTypeChecker
|
|
||||||
return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE)
|
return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE)
|
||||||
|
|
||||||
def update(self, **kwargs):
|
def update(self, **kwargs) -> int:
|
||||||
"""
|
"""Refresh the cache and edit group ownership.
|
||||||
Work just like the default Django's update() method,
|
|
||||||
but add a cache refresh for the elements of the queryset.
|
|
||||||
|
|
||||||
Be aware that this adds a db query to retrieve the updated objects
|
Update the cache, when necessary, remove
|
||||||
|
users from club groups they are no more in
|
||||||
|
and add them in the club groups they should be in.
|
||||||
|
|
||||||
|
Be aware that this adds three db queries :
|
||||||
|
one to retrieve the updated memberships,
|
||||||
|
one to perform group removal and one to perform
|
||||||
|
group attribution.
|
||||||
"""
|
"""
|
||||||
nb_rows = super().update(**kwargs)
|
nb_rows = super().update(**kwargs)
|
||||||
if nb_rows > 0:
|
if nb_rows == 0:
|
||||||
# if at least a row was affected, refresh the cache
|
# if no row was affected, no need to refresh the cache
|
||||||
for membership in self.all():
|
return 0
|
||||||
if membership.end_date is not None:
|
|
||||||
cache.set(
|
|
||||||
f"membership_{membership.club_id}_{membership.user_id}",
|
|
||||||
"not_member",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
cache.set(
|
|
||||||
f"membership_{membership.club_id}_{membership.user_id}",
|
|
||||||
membership,
|
|
||||||
)
|
|
||||||
|
|
||||||
def delete(self):
|
cache_memberships = {}
|
||||||
"""
|
memberships = set(self.select_related("club"))
|
||||||
Work just like the default Django's delete() method,
|
# delete all User-Group relations and recreate the necessary ones
|
||||||
|
# It's more concise to write and more reliable
|
||||||
|
Membership._remove_club_groups(memberships)
|
||||||
|
Membership._add_club_groups(memberships)
|
||||||
|
for member in memberships:
|
||||||
|
cache_key = f"membership_{member.club_id}_{member.user_id}"
|
||||||
|
if member.end_date is None:
|
||||||
|
cache_memberships[cache_key] = member
|
||||||
|
else:
|
||||||
|
cache_memberships[cache_key] = "not_member"
|
||||||
|
cache.set_many(cache_memberships)
|
||||||
|
return nb_rows
|
||||||
|
|
||||||
|
def delete(self) -> tuple[int, dict[str, int]]:
|
||||||
|
"""Work just like the default Django's delete() method,
|
||||||
but add a cache invalidation for the elements of the queryset
|
but add a cache invalidation for the elements of the queryset
|
||||||
before the deletion.
|
before the deletion,
|
||||||
|
and a removal of the user from the club groups.
|
||||||
|
|
||||||
Be aware that this adds a db query to retrieve the deleted element.
|
Be aware that this adds some db queries :
|
||||||
As this first query take place before the deletion operation,
|
|
||||||
it will be performed even if the deletion fails.
|
- 1 to retrieve the deleted elements in order to perform
|
||||||
|
post-delete operations.
|
||||||
|
As we can't know if a delete will affect rows or not,
|
||||||
|
this query will always happen
|
||||||
|
- 1 query to remove the users from the club groups.
|
||||||
|
If the delete operation affected no row,
|
||||||
|
this query won't happen.
|
||||||
"""
|
"""
|
||||||
ids = list(self.values_list("club_id", "user_id"))
|
memberships = set(self.all())
|
||||||
nb_rows, _ = super().delete()
|
nb_rows, rows_counts = super().delete()
|
||||||
if nb_rows > 0:
|
if nb_rows > 0:
|
||||||
for club_id, user_id in ids:
|
Membership._remove_club_groups(memberships)
|
||||||
cache.set(f"membership_{club_id}_{user_id}", "not_member")
|
cache.set_many(
|
||||||
|
{
|
||||||
|
f"membership_{m.club_id}_{m.user_id}": "not_member"
|
||||||
|
for m in memberships
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return nb_rows, rows_counts
|
||||||
|
|
||||||
|
|
||||||
class Membership(models.Model):
|
class Membership(models.Model):
|
||||||
"""
|
"""The Membership class makes the connection between User and Clubs.
|
||||||
The Membership class makes the connection between User and Clubs
|
|
||||||
|
|
||||||
Both Users and Clubs can have many Membership objects:
|
Both Users and Clubs can have many Membership objects:
|
||||||
- a user can be a member of many clubs at a time
|
- a user can be a member of many clubs at a time
|
||||||
@ -374,54 +362,142 @@ class Membership(models.Model):
|
|||||||
|
|
||||||
objects = MembershipQuerySet.as_manager()
|
objects = MembershipQuerySet.as_manager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.CheckConstraint(
|
||||||
|
check=Q(end_date__gte=F("start_date")), name="end_after_start"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return (
|
return (
|
||||||
self.club.name
|
f"{self.club.name} - {self.user.username} "
|
||||||
+ " - "
|
f"- {settings.SITH_CLUB_ROLES[self.role]} "
|
||||||
+ self.user.username
|
f"- {str(_('past member')) if self.end_date is not None else ''}"
|
||||||
+ " - "
|
|
||||||
+ str(settings.SITH_CLUB_ROLES[self.role])
|
|
||||||
+ str(" - " + str(_("past member")) if self.end_date is not None else "")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
|
||||||
"""
|
|
||||||
Method to see if that object can be super edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_anonymous:
|
|
||||||
return False
|
|
||||||
return user.is_board_member
|
|
||||||
|
|
||||||
def can_be_edited_by(self, user: User) -> bool:
|
|
||||||
"""
|
|
||||||
Check if that object can be edited by the given user
|
|
||||||
"""
|
|
||||||
if user.is_root or user.is_board_member:
|
|
||||||
return True
|
|
||||||
membership = self.club.get_membership_for(user)
|
|
||||||
if membership is not None and membership.role >= self.role:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse("club:club_members", kwargs={"club_id": self.club_id})
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
# a save may either be an update or a creation
|
||||||
|
# and may result in either an ongoing or an ended membership.
|
||||||
|
# It could also be a retrogradation from the board to being a simple member.
|
||||||
|
# To avoid problems, the user is removed from the club groups beforehand ;
|
||||||
|
# he will be added back if necessary
|
||||||
|
self._remove_club_groups([self])
|
||||||
if self.end_date is None:
|
if self.end_date is None:
|
||||||
|
self._add_club_groups([self])
|
||||||
cache.set(f"membership_{self.club_id}_{self.user_id}", self)
|
cache.set(f"membership_{self.club_id}_{self.user_id}", self)
|
||||||
else:
|
else:
|
||||||
cache.set(f"membership_{self.club_id}_{self.user_id}", "not_member")
|
cache.set(f"membership_{self.club_id}_{self.user_id}", "not_member")
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("club:club_members", kwargs={"club_id": self.club_id})
|
||||||
|
|
||||||
|
def is_owned_by(self, user: User) -> bool:
|
||||||
|
"""Method to see if that object can be super edited by the given user."""
|
||||||
|
if user.is_anonymous:
|
||||||
|
return False
|
||||||
|
return user.is_root or user.is_board_member
|
||||||
|
|
||||||
|
def can_be_edited_by(self, user: User) -> bool:
|
||||||
|
"""Check if that object can be edited by the given user."""
|
||||||
|
if user.is_root or user.is_board_member:
|
||||||
|
return True
|
||||||
|
membership = self.club.get_membership_for(user)
|
||||||
|
return membership is not None and membership.role >= self.role
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
|
self._remove_club_groups([self])
|
||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
cache.delete(f"membership_{self.club_id}_{self.user_id}")
|
cache.delete(f"membership_{self.club_id}_{self.user_id}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _remove_club_groups(
|
||||||
|
memberships: Iterable[Membership],
|
||||||
|
) -> tuple[int, dict[str, int]]:
|
||||||
|
"""Remove users of those memberships from the club groups.
|
||||||
|
|
||||||
|
For example, if a user is in the Troll club board,
|
||||||
|
he is in the board group and the members group of the Troll.
|
||||||
|
After calling this function, he will be in neither.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The result of the deletion queryset.
|
||||||
|
|
||||||
|
Warnings:
|
||||||
|
If this function isn't used in combination
|
||||||
|
with an actual deletion of the memberships,
|
||||||
|
it will result in an inconsistent state,
|
||||||
|
where users will be in the clubs, without
|
||||||
|
having the associated rights.
|
||||||
|
"""
|
||||||
|
clubs = {m.club_id for m in memberships}
|
||||||
|
users = {m.user_id for m in memberships}
|
||||||
|
groups = Group.objects.filter(Q(club__in=clubs) | Q(club_board__in=clubs))
|
||||||
|
return User.groups.through.objects.filter(
|
||||||
|
Q(group__in=groups) & Q(user__in=users)
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _add_club_groups(
|
||||||
|
memberships: Iterable[Membership],
|
||||||
|
) -> list[User.groups.through]:
|
||||||
|
"""Add users of those memberships to the club groups.
|
||||||
|
|
||||||
|
For example, if a user just joined the Troll club board,
|
||||||
|
he will be added in both the members group and the board group
|
||||||
|
of the club.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created User-Group relations.
|
||||||
|
|
||||||
|
Warnings:
|
||||||
|
If this function isn't used in combination
|
||||||
|
with an actual update/creation of the memberships,
|
||||||
|
it will result in an inconsistent state,
|
||||||
|
where users will have the rights associated to the
|
||||||
|
club, without actually being part of it.
|
||||||
|
"""
|
||||||
|
# only active membership (i.e. `end_date=None`)
|
||||||
|
# grant the attribution of club groups.
|
||||||
|
memberships = [m for m in memberships if m.end_date is None]
|
||||||
|
if not memberships:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if sum(1 for m in memberships if not hasattr(m, "club")) > 1:
|
||||||
|
# if more than one membership hasn't its `club` attribute set
|
||||||
|
# it's less expensive to reload the whole query with
|
||||||
|
# a select_related than perform a distinct query
|
||||||
|
# to fetch each club.
|
||||||
|
ids = {m.id for m in memberships}
|
||||||
|
memberships = list(
|
||||||
|
Membership.objects.filter(id__in=ids).select_related("club")
|
||||||
|
)
|
||||||
|
club_groups = []
|
||||||
|
for membership in memberships:
|
||||||
|
club_groups.append(
|
||||||
|
User.groups.through(
|
||||||
|
user_id=membership.user_id,
|
||||||
|
group_id=membership.club.members_group_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if membership.role > settings.SITH_MAXIMUM_FREE_ROLE:
|
||||||
|
club_groups.append(
|
||||||
|
User.groups.through(
|
||||||
|
user_id=membership.user_id,
|
||||||
|
group_id=membership.club.board_group_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return User.groups.through.objects.bulk_create(
|
||||||
|
club_groups, ignore_conflicts=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Mailing(models.Model):
|
class Mailing(models.Model):
|
||||||
"""
|
"""A Mailing list for a club.
|
||||||
This class correspond to a mailing list
|
|
||||||
Remember that mailing lists should be validated by UTBM
|
Warning:
|
||||||
|
Remember that mailing lists should be validated by UTBM.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
club = models.ForeignKey(
|
club = models.ForeignKey(
|
||||||
@ -454,6 +530,25 @@ class Mailing(models.Model):
|
|||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s - %s" % (self.club, self.email_full)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.is_moderated:
|
||||||
|
unread_notif_subquery = Notification.objects.filter(
|
||||||
|
user=OuterRef("pk"), type="MAILING_MODERATION", viewed=False
|
||||||
|
)
|
||||||
|
for user in User.objects.filter(
|
||||||
|
~Exists(unread_notif_subquery),
|
||||||
|
groups__id__in=[settings.SITH_GROUP_COM_ADMIN_ID],
|
||||||
|
):
|
||||||
|
Notification(
|
||||||
|
user=user,
|
||||||
|
url=reverse("com:mailing_admin"),
|
||||||
|
type="MAILING_MODERATION",
|
||||||
|
).save(*args, **kwargs)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if Mailing.objects.filter(email=self.email).exists():
|
if Mailing.objects.filter(email=self.email).exists():
|
||||||
raise ValidationError(_("This mailing list already exists."))
|
raise ValidationError(_("This mailing list already exists."))
|
||||||
@ -461,7 +556,7 @@ class Mailing(models.Model):
|
|||||||
self.is_moderated = True
|
self.is_moderated = True
|
||||||
else:
|
else:
|
||||||
self.moderator = None
|
self.moderator = None
|
||||||
super(Mailing, self).clean()
|
super().clean()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def email_full(self):
|
def email_full(self):
|
||||||
@ -483,39 +578,15 @@ class Mailing(models.Model):
|
|||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
self.subscriptions.all().delete()
|
self.subscriptions.all().delete()
|
||||||
super(Mailing, self).delete()
|
super().delete()
|
||||||
|
|
||||||
def fetch_format(self):
|
def fetch_format(self):
|
||||||
resp = self.email + ": "
|
destination = "".join(s.fetch_format() for s in self.subscriptions.all())
|
||||||
for sub in self.subscriptions.all():
|
return f"{self.email}: {destination}"
|
||||||
resp += sub.fetch_format()
|
|
||||||
return resp
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
if not self.is_moderated:
|
|
||||||
for user in (
|
|
||||||
RealGroup.objects.filter(id=settings.SITH_GROUP_COM_ADMIN_ID)
|
|
||||||
.first()
|
|
||||||
.users.all()
|
|
||||||
):
|
|
||||||
if not user.notifications.filter(
|
|
||||||
type="MAILING_MODERATION", viewed=False
|
|
||||||
).exists():
|
|
||||||
Notification(
|
|
||||||
user=user,
|
|
||||||
url=reverse("com:mailing_admin"),
|
|
||||||
type="MAILING_MODERATION",
|
|
||||||
).save()
|
|
||||||
super(Mailing, self).save()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "%s - %s" % (self.club, self.email_full)
|
|
||||||
|
|
||||||
|
|
||||||
class MailingSubscription(models.Model):
|
class MailingSubscription(models.Model):
|
||||||
"""
|
"""Link between user and mailing list."""
|
||||||
This class makes the link between user and mailing list
|
|
||||||
"""
|
|
||||||
|
|
||||||
mailing = models.ForeignKey(
|
mailing = models.ForeignKey(
|
||||||
Mailing,
|
Mailing,
|
||||||
@ -538,6 +609,9 @@ class MailingSubscription(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
unique_together = (("user", "email", "mailing"),)
|
unique_together = (("user", "email", "mailing"),)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "(%s) - %s : %s" % (self.mailing, self.get_username, self.email)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if not self.user and not self.email:
|
if not self.user and not self.email:
|
||||||
raise ValidationError(_("At least user or email is required"))
|
raise ValidationError(_("At least user or email is required"))
|
||||||
@ -552,7 +626,7 @@ class MailingSubscription(models.Model):
|
|||||||
)
|
)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
pass
|
pass
|
||||||
super(MailingSubscription, self).clean()
|
super().clean()
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
if user.is_anonymous:
|
if user.is_anonymous:
|
||||||
@ -580,6 +654,3 @@ class MailingSubscription(models.Model):
|
|||||||
|
|
||||||
def fetch_format(self):
|
def fetch_format(self):
|
||||||
return self.get_email + " "
|
return self.get_email + " "
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "(%s) - %s : %s" % (self.mailing, self.get_username, self.email)
|
|
||||||
|
23
club/schemas.py
Normal file
23
club/schemas.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from ninja import ModelSchema
|
||||||
|
|
||||||
|
from club.models import Club
|
||||||
|
|
||||||
|
|
||||||
|
class ClubSchema(ModelSchema):
|
||||||
|
class Meta:
|
||||||
|
model = Club
|
||||||
|
fields = ["id", "name"]
|
||||||
|
|
||||||
|
|
||||||
|
class ClubProfileSchema(ModelSchema):
|
||||||
|
"""The infos needed to display a simple club profile."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Club
|
||||||
|
fields = ["id", "name", "logo"]
|
||||||
|
|
||||||
|
url: str
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_url(obj: Club) -> str:
|
||||||
|
return obj.get_absolute_url()
|
30
club/static/bundled/club/components/ajax-select-index.ts
Normal file
30
club/static/bundled/club/components/ajax-select-index.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { AjaxSelect } from "#core:core/components/ajax-select-base";
|
||||||
|
import { registerComponent } from "#core:utils/web-components";
|
||||||
|
import type { TomOption } from "tom-select/dist/types/types";
|
||||||
|
import type { escape_html } from "tom-select/dist/types/utils";
|
||||||
|
import { type ClubSchema, clubSearchClub } from "#openapi";
|
||||||
|
|
||||||
|
@registerComponent("club-ajax-select")
|
||||||
|
export class ClubAjaxSelect extends AjaxSelect {
|
||||||
|
protected valueField = "id";
|
||||||
|
protected labelField = "name";
|
||||||
|
protected searchField = ["code", "name"];
|
||||||
|
|
||||||
|
protected async search(query: string): Promise<TomOption[]> {
|
||||||
|
const resp = await clubSearchClub({ query: { search: query } });
|
||||||
|
if (resp.data) {
|
||||||
|
return resp.data.results;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderOption(item: ClubSchema, sanitize: typeof escape_html) {
|
||||||
|
return `<div class="select-item">
|
||||||
|
<span class="select-item-text">${sanitize(item.name)}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderItem(item: ClubSchema, sanitize: typeof escape_html) {
|
||||||
|
return `<span>${sanitize(item.name)}</span>`;
|
||||||
|
}
|
||||||
|
}
|
@ -2,16 +2,16 @@
|
|||||||
{% from 'core/macros.jinja' import user_profile_link %}
|
{% from 'core/macros.jinja' import user_profile_link %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="club_detail">
|
<div id="club_detail">
|
||||||
{% if club.logo %}
|
{% if club.logo %}
|
||||||
<div class="club_logo"><img src="{{ club.logo.url }}" alt="{{ club.unix_name }}"></div>
|
<div class="club_logo"><img src="{{ club.logo.url }}" alt="{{ club.unix_name }}"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if page_revision %}
|
{% if page_revision %}
|
||||||
{{ page_revision|markdown }}
|
{{ page_revision|markdown }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<h3>{% trans %}Club{% endtrans %}</h3>
|
<h3>{% trans %}Club{% endtrans %}</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,48 +1,48 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}Club list{% endtrans %}
|
{% trans %}Club list{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% macro display_club(club) -%}
|
{% macro display_club(club) -%}
|
||||||
|
|
||||||
{% if club.is_active or user.is_root %}
|
{% if club.is_active or user.is_root %}
|
||||||
|
|
||||||
<li><a href="{{ url('club:club_view', club_id=club.id) }}">{{ club.name }}</a>
|
|
||||||
|
|
||||||
{% if not club.is_active %}
|
|
||||||
({% trans %}inactive{% endtrans %})
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if club.president %} - <a href="{{ url('core:user_profile', user_id=club.president.user.id) }}">{{ club.president.user }}</a>{% endif %}
|
<li><a href="{{ url('club:club_view', club_id=club.id) }}">{{ club.name }}</a>
|
||||||
{% if club.short_description %}<p>{{ club.short_description|markdown }}</p>{% endif %}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{%- if club.children.all()|length != 0 %}
|
{% if not club.is_active %}
|
||||||
<ul>
|
({% trans %}inactive{% endtrans %})
|
||||||
{%- for c in club.children.order_by('name') %}
|
{% endif %}
|
||||||
{{ display_club(c) }}
|
|
||||||
{%- endfor %}
|
{% if club.president %} - <a href="{{ url('core:user_profile', user_id=club.president.user.id) }}">{{ club.president.user }}</a>{% endif %}
|
||||||
</ul>
|
{% if club.short_description %}<p>{{ club.short_description|markdown }}</p>{% endif %}
|
||||||
{%- endif -%}
|
|
||||||
</li>
|
{% endif %}
|
||||||
|
|
||||||
|
{%- if club.children.all()|length != 0 %}
|
||||||
|
<ul>
|
||||||
|
{%- for c in club.children.order_by('name') %}
|
||||||
|
{{ display_club(c) }}
|
||||||
|
{%- endfor %}
|
||||||
|
</ul>
|
||||||
|
{%- endif -%}
|
||||||
|
</li>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if user.is_root %}
|
{% if user.is_root %}
|
||||||
<p><a href="{{ url('club:club_new') }}">{% trans %}New club{% endtrans %}</a></p>
|
<p><a href="{{ url('club:club_new') }}">{% trans %}New club{% endtrans %}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if club_list %}
|
{% if club_list %}
|
||||||
<h3>{% trans %}Club list{% endtrans %}</h3>
|
<h3>{% trans %}Club list{% endtrans %}</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{%- for c in club_list.all().order_by('name') if c.parent is none %}
|
{%- for c in club_list.all().order_by('name') if c.parent is none %}
|
||||||
{{ display_club(c) }}
|
{{ display_club(c) }}
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans %}There is no club in this website.{% endtrans %}
|
{% trans %}There is no club in this website.{% endtrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,81 +2,81 @@
|
|||||||
{% from 'core/macros.jinja' import user_profile_link, select_all_checkbox %}
|
{% from 'core/macros.jinja' import user_profile_link, select_all_checkbox %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>{% trans %}Club members{% endtrans %}</h2>
|
<h2>{% trans %}Club members{% endtrans %}</h2>
|
||||||
{% if members %}
|
{% if members %}
|
||||||
<form action="{{ url('club:club_members', club_id=club.id) }}" id="users_old" method="post">
|
<form action="{{ url('club:club_members', club_id=club.id) }}" id="users_old" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% set users_old = dict(form.users_old | groupby("choice_label")) %}
|
{% set users_old = dict(form.users_old | groupby("choice_label")) %}
|
||||||
{% if users_old %}
|
{% if users_old %}
|
||||||
{{ select_all_checkbox("users_old") }}
|
{{ select_all_checkbox("users_old") }}
|
||||||
<p></p>
|
<p></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans %}User{% endtrans %}</td>
|
<td>{% trans %}User{% endtrans %}</td>
|
||||||
<td>{% trans %}Role{% endtrans %}</td>
|
<td>{% trans %}Role{% endtrans %}</td>
|
||||||
<td>{% trans %}Description{% endtrans %}</td>
|
<td>{% trans %}Description{% endtrans %}</td>
|
||||||
<td>{% trans %}Since{% endtrans %}</td>
|
<td>{% trans %}Since{% endtrans %}</td>
|
||||||
{% if users_old %}
|
{% if users_old %}
|
||||||
<td>{% trans %}Mark as old{% endtrans %}</td>
|
<td>{% trans %}Mark as old{% endtrans %}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for m in members %}
|
{% for m in members %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ user_profile_link(m.user) }}</td>
|
<td>{{ user_profile_link(m.user) }}</td>
|
||||||
<td>{{ settings.SITH_CLUB_ROLES[m.role] }}</td>
|
<td>{{ settings.SITH_CLUB_ROLES[m.role] }}</td>
|
||||||
<td>{{ m.description }}</td>
|
<td>{{ m.description }}</td>
|
||||||
<td>{{ m.start_date }}</td>
|
<td>{{ m.start_date }}</td>
|
||||||
{% if users_old %}
|
{% if users_old %}
|
||||||
<td>
|
<td>
|
||||||
{% set user_old = users_old[m.user.get_display_name()] %}
|
{% set user_old = users_old[m.user.get_display_name()] %}
|
||||||
{% if user_old %}
|
{% if user_old %}
|
||||||
{{ user_old[0].tag() }}
|
{{ user_old[0].tag() }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{{ form.users_old.errors }}
|
{{ form.users_old.errors }}
|
||||||
{% if users_old %}
|
{% if users_old %}
|
||||||
<p></p>
|
<p></p>
|
||||||
<input type="submit" name="submit" value="{% trans %}Mark as old{% endtrans %}">
|
<input type="submit" name="submit" value="{% trans %}Mark as old{% endtrans %}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{% trans %}There are no members in this club.{% endtrans %}</p>
|
<p>{% trans %}There are no members in this club.{% endtrans %}</p>
|
||||||
|
{% endif %}
|
||||||
|
<form action="{{ url('club:club_members', club_id=club.id) }}" id="add_users" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.non_field_errors() }}
|
||||||
|
<p>
|
||||||
|
{{ form.users.errors }}
|
||||||
|
<label for="{{ form.users.id_for_label }}">{{ form.users.label }} :</label>
|
||||||
|
{{ form.users }}
|
||||||
|
<span class="helptext">{{ form.users.help_text }}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ form.role.errors }}
|
||||||
|
<label for="{{ form.role.id_for_label }}">{{ form.role.label }} :</label>
|
||||||
|
{{ form.role }}
|
||||||
|
</p>
|
||||||
|
{% if form.start_date %}
|
||||||
|
<p>
|
||||||
|
{{ form.start_date.errors }}
|
||||||
|
<label for="{{ form.start_date.id_for_label }}">{{ form.start_date.label }} :</label>
|
||||||
|
{{ form.start_date }}
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form action="{{ url('club:club_members', club_id=club.id) }}" id="add_users" method="post">
|
<p>
|
||||||
{% csrf_token %}
|
{{ form.description.errors }}
|
||||||
{{ form.non_field_errors() }}
|
<label for="{{ form.description.id_for_label }}">{{ form.description.label }} :</label>
|
||||||
<p>
|
{{ form.description }}
|
||||||
{{ form.users.errors }}
|
</p>
|
||||||
<label for="{{ form.users.id_for_label }}">{{ form.users.label }} :</label>
|
<p><input type="submit" value="{% trans %}Add{% endtrans %}" /></p>
|
||||||
{{ form.users }}
|
</form>
|
||||||
<span class="helptext">{{ form.users.help_text }}</span>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{{ form.role.errors }}
|
|
||||||
<label for="{{ form.role.id_for_label }}">{{ form.role.label }} :</label>
|
|
||||||
{{ form.role }}
|
|
||||||
</p>
|
|
||||||
{% if form.start_date %}
|
|
||||||
<p>
|
|
||||||
{{ form.start_date.errors }}
|
|
||||||
<label for="{{ form.start_date.id_for_label }}">{{ form.start_date.label }} :</label>
|
|
||||||
{{ form.start_date }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
<p>
|
|
||||||
{{ form.description.errors }}
|
|
||||||
<label for="{{ form.description.id_for_label }}">{{ form.description.label }} :</label>
|
|
||||||
{{ form.description }}
|
|
||||||
</p>
|
|
||||||
<p><input type="submit" value="{% trans %}Add{% endtrans %}" /></p>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -2,27 +2,27 @@
|
|||||||
{% from 'core/macros.jinja' import user_profile_link %}
|
{% from 'core/macros.jinja' import user_profile_link %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>{% trans %}Club old members{% endtrans %}</h2>
|
<h2>{% trans %}Club old members{% endtrans %}</h2>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<td>{% trans %}User{% endtrans %}</td>
|
<td>{% trans %}User{% endtrans %}</td>
|
||||||
<td>{% trans %}Role{% endtrans %}</td>
|
<td>{% trans %}Role{% endtrans %}</td>
|
||||||
<td>{% trans %}Description{% endtrans %}</td>
|
<td>{% trans %}Description{% endtrans %}</td>
|
||||||
<td>{% trans %}From{% endtrans %}</td>
|
<td>{% trans %}From{% endtrans %}</td>
|
||||||
<td>{% trans %}To{% endtrans %}</td>
|
<td>{% trans %}To{% endtrans %}</td>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for m in club.members.exclude(end_date=None).order_by('-role', 'description', '-end_date').all() %}
|
{% for m in club.members.exclude(end_date=None).order_by('-role', 'description', '-end_date').all() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ user_profile_link(m.user) }}</td>
|
<td>{{ user_profile_link(m.user) }}</td>
|
||||||
<td>{{ settings.SITH_CLUB_ROLES[m.role] }}</td>
|
<td>{{ settings.SITH_CLUB_ROLES[m.role] }}</td>
|
||||||
<td>{{ m.description }}</td>
|
<td>{{ m.description }}</td>
|
||||||
<td>{{ m.start_date }}</td>
|
<td>{{ m.start_date }}</td>
|
||||||
<td>{{ m.end_date }}</td>
|
<td>{{ m.end_date }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,66 +1,94 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
{% from 'core/macros.jinja' import user_profile_link, paginate %}
|
{% from 'core/macros.jinja' import user_profile_link %}
|
||||||
|
|
||||||
|
{# This page uses a custom macro instead of the core `paginate_jinja` and `paginate_alpine`
|
||||||
|
because it works with a somewhat dynamic form,
|
||||||
|
but was written before Alpine was introduced in the project.
|
||||||
|
TODO : rewrite the pagination used in this template an Alpine one
|
||||||
|
#}
|
||||||
|
{% macro paginate(page_obj, paginator, js_action) %}
|
||||||
|
{% set js = js_action|default('') %}
|
||||||
|
{% if page_obj.has_previous() or page_obj.has_next() %}
|
||||||
|
{% if page_obj.has_previous() %}
|
||||||
|
<a {% if js %} type="submit" onclick="{{ js }}" {% endif %} href="?page={{ page_obj.previous_page_number() }}">{% trans %}Previous{% endtrans %}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="disabled">{% trans %}Previous{% endtrans %}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% for i in paginator.page_range %}
|
||||||
|
{% if page_obj.number == i %}
|
||||||
|
<span class="active">{{ i }} <span class="sr-only">({% trans %}current{% endtrans %})</span></span>
|
||||||
|
{% else %}
|
||||||
|
<a {% if js %} type="submit" onclick="{{ js }}" {% endif %} href="?page={{ i }}">{{ i }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if page_obj.has_next() %}
|
||||||
|
<a {% if js %} type="submit" onclick="{{ js }}" {% endif %} href="?page={{ page_obj.next_page_number() }}">{% trans %}Next{% endtrans %}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="disabled">{% trans %}Next{% endtrans %}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>{% trans %}Sellings{% endtrans %}</h3>
|
<h3>{% trans %}Sales{% endtrans %}</h3>
|
||||||
<form id="form" action="?page=1" method="post">
|
<form id="form" action="?page=1" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form }}
|
{{ form }}
|
||||||
<p><input type="submit" value="{% trans %}Show{% endtrans %}" /></p>
|
<p><input type="submit" value="{% trans %}Show{% endtrans %}" /></p>
|
||||||
<p><input type="submit" value="{% trans %}Download as cvs{% endtrans %}" formaction="{{ url('club:sellings_csv', club_id=object.id) }}"/></p>
|
<p><input type="submit" value="{% trans %}Download as cvs{% endtrans %}" formaction="{{ url('club:sellings_csv', club_id=object.id) }}"/></p>
|
||||||
</form>
|
</form>
|
||||||
<p>
|
<p>
|
||||||
{% trans %}Quantity: {% endtrans %}{{ total_quantity }} {% trans %}units{% endtrans %}<br/>
|
{% trans %}Quantity: {% endtrans %}{{ total_quantity }} {% trans %}units{% endtrans %}<br/>
|
||||||
{% trans %}Total: {% endtrans %}{{ total }} €<br/>
|
{% trans %}Total: {% endtrans %}{{ total }} €<br/>
|
||||||
{% trans %}Benefit: {% endtrans %}{{ benefit }} €
|
{% trans %}Benefit: {% endtrans %}{{ benefit }} €
|
||||||
</p>
|
</p>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans %}Date{% endtrans %}</td>
|
<td>{% trans %}Date{% endtrans %}</td>
|
||||||
<td>{% trans %}Counter{% endtrans %}</td>
|
<td>{% trans %}Counter{% endtrans %}</td>
|
||||||
<td>{% trans %}Barman{% endtrans %}</td>
|
<td>{% trans %}Barman{% endtrans %}</td>
|
||||||
<td>{% trans %}Customer{% endtrans %}</td>
|
<td>{% trans %}Customer{% endtrans %}</td>
|
||||||
<td>{% trans %}Label{% endtrans %}</td>
|
<td>{% trans %}Label{% endtrans %}</td>
|
||||||
<td>{% trans %}Quantity{% endtrans %}</td>
|
<td>{% trans %}Quantity{% endtrans %}</td>
|
||||||
<td>{% trans %}Total{% endtrans %}</td>
|
<td>{% trans %}Total{% endtrans %}</td>
|
||||||
<td>{% trans %}Payment method{% endtrans %}</td>
|
<td>{% trans %}Payment method{% endtrans %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for s in paginated_result %}
|
{% for s in paginated_result %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ s.date|localtime|date(DATETIME_FORMAT) }} {{ s.date|localtime|time(DATETIME_FORMAT) }}</td>
|
<td>{{ s.date|localtime|date(DATETIME_FORMAT) }} {{ s.date|localtime|time(DATETIME_FORMAT) }}</td>
|
||||||
<td>{{ s.counter }}</td>
|
<td>{{ s.counter }}</td>
|
||||||
{% if s.seller %}
|
{% if s.seller %}
|
||||||
<td><a href="{{ s.seller.get_absolute_url() }}">{{ s.seller.get_display_name() }}</a></td>
|
<td><a href="{{ s.seller.get_absolute_url() }}">{{ s.seller.get_display_name() }}</a></td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td></td>
|
<td></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if s.customer %}
|
{% if s.customer %}
|
||||||
<td><a href="{{ s.customer.user.get_absolute_url() }}">{{ s.customer.user.get_display_name() }}</a></td>
|
<td><a href="{{ s.customer.user.get_absolute_url() }}">{{ s.customer.user.get_display_name() }}</a></td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td></td>
|
<td></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>{{ s.label }}</td>
|
<td>{{ s.label }}</td>
|
||||||
<td>{{ s.quantity }}</td>
|
<td>{{ s.quantity }}</td>
|
||||||
<td>{{ s.quantity * s.unit_price }} €</td>
|
<td>{{ s.quantity * s.unit_price }} €</td>
|
||||||
<td>{{ s.get_payment_method_display() }}</td>
|
<td>{{ s.get_payment_method_display() }}</td>
|
||||||
{% if s.is_owned_by(user) %}
|
{% if s.is_owned_by(user) %}
|
||||||
<td><a href="{{ url('counter:selling_delete', selling_id=s.id) }}">{% trans %}Delete{% endtrans %}</a></td>
|
<td><a href="{{ url('counter:selling_delete', selling_id=s.id) }}">{% trans %}Delete{% endtrans %}</a></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
function formPagination(link){
|
function formPagination(link){
|
||||||
$("form").attr("action", link.href);
|
$("form").attr("action", link.href);
|
||||||
link.href = "javascript:void(0)"; // block link action
|
link.href = "javascript:void(0)"; // block link action
|
||||||
$("form").submit();
|
$("form").submit();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{{ paginate(paginated_result, paginator, "formPagination(this)") }}
|
{{ paginate(paginated_result, paginator, "formPagination(this)") }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,46 +1,46 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>{% trans %}Club tools{% endtrans %}</h3>
|
<h3>{% trans %}Club tools{% endtrans %}</h3>
|
||||||
<div>
|
<div>
|
||||||
<h4>{% trans %}Communication:{% endtrans %}</h4>
|
<h4>{% trans %}Communication:{% endtrans %}</h4>
|
||||||
<ul>
|
<ul>
|
||||||
<li> <a href="{{ url('com:news_new') }}?club={{ object.id }}">{% trans %}Create a news{% endtrans %}</a></li>
|
<li> <a href="{{ url('com:news_new') }}?club={{ object.id }}">{% trans %}Create a news{% endtrans %}</a></li>
|
||||||
<li> <a href="{{ url('com:weekmail_article') }}?club={{ object.id }}">{% trans %}Post in the Weekmail{% endtrans %}</a></li>
|
<li> <a href="{{ url('com:weekmail_article') }}?club={{ object.id }}">{% trans %}Post in the Weekmail{% endtrans %}</a></li>
|
||||||
{% if object.trombi %}
|
{% if object.trombi %}
|
||||||
<li> <a href="{{ url('trombi:detail', trombi_id=object.trombi.id) }}">{% trans %}Edit Trombi{% endtrans %}</a></li>
|
<li> <a href="{{ url('trombi:detail', trombi_id=object.trombi.id) }}">{% trans %}Edit Trombi{% endtrans %}</a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li> <a href="{{ url('trombi:create', club_id=object.id) }}">{% trans %}New Trombi{% endtrans %}</a></li>
|
<li> <a href="{{ url('trombi:create', club_id=object.id) }}">{% trans %}New Trombi{% endtrans %}</a></li>
|
||||||
<li> <a href="{{ url('club:poster_list', club_id=object.id) }}">{% trans %}Posters{% endtrans %}</a></li>
|
<li> <a href="{{ url('club:poster_list', club_id=object.id) }}">{% trans %}Posters{% endtrans %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
<h4>{% trans %}Counters:{% endtrans %}</h4>
|
<h4>{% trans %}Counters:{% endtrans %}</h4>
|
||||||
<ul>
|
<ul>
|
||||||
{% if object.unix_name == settings.SITH_LAUNDERETTE_MANAGER['unix_name'] %}
|
{% if object.unix_name == settings.SITH_LAUNDERETTE_MANAGER['unix_name'] %}
|
||||||
{% for l in Launderette.objects.all() %}
|
{% for l in Launderette.objects.all() %}
|
||||||
<li><a href="{{ url('launderette:main_click', launderette_id=l.id) }}">{{ l }}</a></li>
|
<li><a href="{{ url('launderette:main_click', launderette_id=l.id) }}">{{ l }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% elif object.counters.filter(type="OFFICE")|count > 0 %}
|
{% elif object.counters.filter(type="OFFICE")|count > 0 %}
|
||||||
{% for c in object.counters.filter(type="OFFICE") %}
|
{% for c in object.counters.filter(type="OFFICE") %}
|
||||||
<li>{{ c }}:
|
<li>{{ c }}:
|
||||||
<a href="{{ url('counter:details', counter_id=c.id) }}">View</a>
|
<a href="{{ url('counter:details', counter_id=c.id) }}">View</a>
|
||||||
<a href="{{ url('counter:admin', counter_id=c.id) }}">Edit</a>
|
<a href="{{ url('counter:admin', counter_id=c.id) }}">Edit</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
{% if object.club_account.exists() %}
|
{% if object.club_account.exists() %}
|
||||||
<h4>{% trans %}Accouting: {% endtrans %}</h4>
|
<h4>{% trans %}Accounting: {% endtrans %}</h4>
|
||||||
<ul>
|
<ul>
|
||||||
{% for ca in object.club_account.all() %}
|
{% for ca in object.club_account.all() %}
|
||||||
<li><a href="{{ url('accounting:club_details', c_account_id=ca.id) }}">{{ ca.get_display_name() }}</a></li>
|
<li><a href="{{ url('accounting:club_details', c_account_id=ca.id) }}">{{ ca.get_display_name() }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if object.unix_name == settings.SITH_LAUNDERETTE_MANAGER['unix_name'] %}
|
{% if object.unix_name == settings.SITH_LAUNDERETTE_MANAGER['unix_name'] %}
|
||||||
<li><a href="{{ url('launderette:launderette_list') }}">{% trans %}Manage launderettes{% endtrans %}</a></li>
|
<li><a href="{{ url('launderette:launderette_list') }}">{% trans %}Manage launderettes{% endtrans %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,107 +2,107 @@
|
|||||||
{% from 'core/macros.jinja' import select_all_checkbox %}
|
{% from 'core/macros.jinja' import select_all_checkbox %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}Mailing lists{% endtrans %}
|
{% trans %}Mailing lists{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<b>{% trans %}Remember : mailing lists need to be moderated, if your new created list is not shown wait until moderation takes action{% endtrans %}</b>
|
<b>{% trans %}Remember : mailing lists need to be moderated, if your new created list is not shown wait until moderation takes action{% endtrans %}</b>
|
||||||
|
|
||||||
{% if mailings_not_moderated %}
|
{% if mailings_not_moderated %}
|
||||||
<p>{% trans %}Mailing lists waiting for moderation{% endtrans %}</p>
|
<p>{% trans %}Mailing lists waiting for moderation{% endtrans %}</p>
|
||||||
<ul>
|
<ul>
|
||||||
{% for mailing in mailings_not_moderated %}
|
{% for mailing in mailings_not_moderated %}
|
||||||
<li>{{ mailing.email_full }}<a href="{{ url('club:mailing_delete', mailing_id=mailing.id) }}"> - {% trans %}Delete{% endtrans %}</a></li>
|
<li>{{ mailing.email_full }}<a href="{{ url('club:mailing_delete', mailing_id=mailing.id) }}"> - {% trans %}Delete{% endtrans %}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if mailings_moderated %}
|
{% if mailings_moderated %}
|
||||||
{% for mailing in mailings_moderated %}
|
{% for mailing in mailings_moderated %}
|
||||||
<h2>{% trans %}Mailing{% endtrans %} {{ mailing.email_full }}
|
<h2>{% trans %}Mailing{% endtrans %} {{ mailing.email_full }}
|
||||||
{%- if user.is_owner(mailing) -%}
|
{%- if user.is_owner(mailing) -%}
|
||||||
<a href="{{ url('club:mailing_delete', mailing_id=mailing.id) }}"> - {% trans %}Delete{% endtrans %}</a>
|
<a href="{{ url('club:mailing_delete', mailing_id=mailing.id) }}"> - {% trans %}Delete{% endtrans %}</a>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</h2>
|
</h2>
|
||||||
<form method="GET" action="{{ url('club:mailing_generate', mailing_id=mailing.id) }}" style="display:inline-block;">
|
<form method="GET" action="{{ url('club:mailing_generate', mailing_id=mailing.id) }}" style="display:inline-block;">
|
||||||
<input type="submit" name="generateMalingList" value="{% trans %}Generate mailing list{% endtrans %}">
|
<input type="submit" name="generateMalingList" value="{% trans %}Generate mailing list{% endtrans %}">
|
||||||
</form>
|
</form>
|
||||||
{% set form_mailing_removal = form["removal_" + mailing.id|string] %}
|
{% set form_mailing_removal = form["removal_" + mailing.id|string] %}
|
||||||
{% if form_mailing_removal.field.choices %}
|
{% if form_mailing_removal.field.choices %}
|
||||||
{% set ms = dict(mailing.subscriptions.all() | groupby('id')) %}
|
{% set ms = dict(mailing.subscriptions.all() | groupby('id')) %}
|
||||||
<form action="{{ url('club:mailing', club_id=club.id) }}" id="{{ form_mailing_removal.auto_id }}" method="post" enctype="multipart/form-data">
|
<form action="{{ url('club:mailing', club_id=club.id) }}" id="{{ form_mailing_removal.auto_id }}" method="post" enctype="multipart/form-data">
|
||||||
<p style="margin-bottom: 1em;">{{ select_all_checkbox(form_mailing_removal.auto_id) }}</p>
|
<p style="margin-bottom: 1em;">{{ select_all_checkbox(form_mailing_removal.auto_id) }}</p>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input hidden type="number" name="{{ form.action.name }}" value="{{ form_actions.REMOVE_SUBSCRIPTION }}" />
|
<input hidden type="number" name="{{ form.action.name }}" value="{{ form_actions.REMOVE_SUBSCRIPTION }}" />
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% trans %}User{% endtrans %}</td>
|
<td>{% trans %}User{% endtrans %}</td>
|
||||||
<td>{% trans %}Email{% endtrans %}</td>
|
<td>{% trans %}Email{% endtrans %}</td>
|
||||||
<td>{% trans %}Delete{% endtrans %}</td>
|
<td>{% trans %}Delete{% endtrans %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for widget in form_mailing_removal.subwidgets %}
|
{% for widget in form_mailing_removal.subwidgets %}
|
||||||
{% set user = ms[widget.data.value.value][0] %}
|
{% set user = ms[widget.data.value.value][0] %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ user.get_username }}</td>
|
<td>{{ user.get_username }}</td>
|
||||||
<td>{{ user.get_email }}</td>
|
<td>{{ user.get_email }}</td>
|
||||||
<td>{{ widget.tag() }}</td>
|
<td>{{ widget.tag() }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{{ form_mailing_removal.errors }}
|
{{ form_mailing_removal.errors }}
|
||||||
<p><input type="submit" value="{% trans %}Remove from mailing list{% endtrans %}" /></p>
|
<p><input type="submit" value="{% trans %}Remove from mailing list{% endtrans %}" /></p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p><b>{% trans %}There is no subscriber for this mailing list{% endtrans %}</b></p>
|
<p><b>{% trans %}There is no subscriber for this mailing list{% endtrans %}</b></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{% trans %}No mailing list existing for this club{% endtrans %}</p>
|
<p>{% trans %}No mailing list existing for this club{% endtrans %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p>{{ form.non_field_errors() }}</p>
|
<p>{{ form.non_field_errors() }}</p>
|
||||||
{% if mailings_moderated %}
|
{% if mailings_moderated %}
|
||||||
<h2>{% trans %}New member{% endtrans %}</h2>
|
<h2>{% trans %}New member{% endtrans %}</h2>
|
||||||
<form action="{{ url('club:mailing', club_id=club.id) }}" method="post" enctype="multipart/form-data">
|
|
||||||
{% csrf_token %}
|
|
||||||
<p>
|
|
||||||
{{ form.subscription_mailing.errors }}
|
|
||||||
<label for="{{ form.subscription_mailing.id_for_label }}">{{ form.subscription_mailing.label }}</label>
|
|
||||||
{{ form.subscription_mailing }}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{{ form.subscription_users.errors }}
|
|
||||||
<label for="{{ form.subscription_users.id_for_label }}">{{ form.subscription_users.label }}</label>
|
|
||||||
{{ form.subscription_users }}
|
|
||||||
<span class="helptext">{{ form.subscription_users.help_text }}</span>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{{ form.subscription_email.errors }}
|
|
||||||
<label for="{{ form.subscription_email.id_for_label }}">{{ form.subscription_email.label }}</label>
|
|
||||||
{{ form.subscription_email }}
|
|
||||||
</p>
|
|
||||||
<input hidden type="number" name="{{ form.action.name }}" value="{{ form_actions.NEW_SUBSCRIPTION }}" />
|
|
||||||
<p><input type="submit" value="{% trans %}Add to mailing list{% endtrans %}" /></p>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h2>{% trans %}New mailing{% endtrans %}</h2>
|
|
||||||
<form action="{{ url('club:mailing', club_id=club.id) }}" method="post" enctype="multipart/form-data">
|
<form action="{{ url('club:mailing', club_id=club.id) }}" method="post" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<p>
|
<p>
|
||||||
{{ form.mailing_email.errors }}
|
{{ form.subscription_mailing.errors }}
|
||||||
<label for="{{ form.mailing_email.id_for_label }}">{{ form.mailing_email.label }}</label>
|
<label for="{{ form.subscription_mailing.id_for_label }}">{{ form.subscription_mailing.label }}</label>
|
||||||
{{ form.mailing_email }}
|
{{ form.subscription_mailing }}
|
||||||
</p>
|
</p>
|
||||||
<input hidden type="number" name="{{ form.action.name }}" value="{{ form_actions.NEW_MALING }}" />
|
<p>
|
||||||
<p><input type="submit" value="{% trans %}Create mailing list{% endtrans %}" /></p>
|
{{ form.subscription_users.errors }}
|
||||||
|
<label for="{{ form.subscription_users.id_for_label }}">{{ form.subscription_users.label }}</label>
|
||||||
|
{{ form.subscription_users }}
|
||||||
|
<span class="helptext">{{ form.subscription_users.help_text }}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ form.subscription_email.errors }}
|
||||||
|
<label for="{{ form.subscription_email.id_for_label }}">{{ form.subscription_email.label }}</label>
|
||||||
|
{{ form.subscription_email }}
|
||||||
|
</p>
|
||||||
|
<input hidden type="number" name="{{ form.action.name }}" value="{{ form_actions.NEW_SUBSCRIPTION }}" />
|
||||||
|
<p><input type="submit" value="{% trans %}Add to mailing list{% endtrans %}" /></p>
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h2>{% trans %}New mailing{% endtrans %}</h2>
|
||||||
|
<form action="{{ url('club:mailing', club_id=club.id) }}" method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>
|
||||||
|
{{ form.mailing_email.errors }}
|
||||||
|
<label for="{{ form.mailing_email.id_for_label }}">{{ form.mailing_email.label }}</label>
|
||||||
|
{{ form.mailing_email }}
|
||||||
|
</p>
|
||||||
|
<input hidden type="number" name="{{ form.action.name }}" value="{{ form_actions.NEW_MALING }}" />
|
||||||
|
<p><input type="submit" value="{% trans %}Create mailing list{% endtrans %}" /></p>
|
||||||
|
</form>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
{% from 'core/macros_pages.jinja' import page_history %}
|
{% from 'core/macros_pages.jinja' import page_history %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if club.page %}
|
{% if club.page %}
|
||||||
{{ page_history(club.page) }}
|
{{ page_history(club.page) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans %}No page existing for this club{% endtrans %}
|
{% trans %}No page existing for this club{% endtrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
{% from 'core/macros_pages.jinja' import page_edit_form %}
|
{% from 'core/macros_pages.jinja' import page_edit_form %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ page_edit_form(page, form, url('club:club_edit_page', club_id=page.club.id), csrf_token) }}
|
{{ page_edit_form(page, form, url('club:club_edit_page', club_id=page.club.id), csrf_token) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user