mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2026-02-05 06:36:19 +01:00
Compare commits
652 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
113977b19f | ||
|
|
8f39b50b6d | ||
|
|
95b309c358 | ||
|
|
cf3df3b715 | ||
|
|
d815028c4d | ||
|
|
ac5570abe2 | ||
|
|
84ad571aa4 | ||
|
|
38e7705ae7 | ||
|
|
0b6af014fa | ||
|
|
2401f2299d | ||
|
|
919768c8bb | ||
|
|
24db21463b | ||
|
|
1e029af83a | ||
|
|
2b31be789d | ||
|
|
bf1b4bb549 | ||
|
|
dd42b807f0 | ||
|
|
542fbafd98 | ||
|
|
37b9f5c882 | ||
|
|
5bde9a6952 | ||
|
|
23ad9d02d3 | ||
|
|
f99878cc29 | ||
|
|
e64431b06c | ||
|
|
0b2993c1c3 | ||
|
|
75bccf1b0f | ||
|
|
945bc92898 | ||
|
|
ec7d14a839 | ||
|
|
dd1a368ff6 | ||
|
|
54085617dc | ||
|
|
8cdc5af453 | ||
|
|
da93940e13 | ||
|
|
f53b43d9dc | ||
|
|
497a167ca7 | ||
|
|
852c5a3037 | ||
|
|
90f6777a7a | ||
|
|
a8d890abaf | ||
|
|
79379b444c | ||
|
|
ace1de5c68 | ||
|
|
5d6128e9ea | ||
|
|
131cc5ed0a | ||
|
|
9297bed43f | ||
|
|
b2fddc683a | ||
|
|
9af634d16a | ||
|
|
a68163caa3 | ||
|
|
00770fd034 | ||
|
|
01164777ed | ||
|
|
00f5e3e1e0 | ||
|
|
8b2527f408 | ||
|
|
b7500e4e4e | ||
|
|
4f4bd0c419 | ||
|
|
8ae4e02012 | ||
|
|
cc9a07197d | ||
|
|
f18dd1029b | ||
|
|
fd8d43571a | ||
|
|
13e88492f1 | ||
|
|
38df580a56 | ||
|
|
ba39318313 | ||
|
|
d8c6035405 | ||
|
|
2ef3da916b | ||
|
|
d32d8b26ce | ||
|
|
f348b1a34c | ||
|
|
86aaa3edda | ||
|
|
26017056c7 | ||
|
|
e39a3c072b | ||
|
|
827291dda4 | ||
|
|
ea8958ccc3 | ||
|
|
20554df857 | ||
|
|
750f43eaf0 | ||
|
|
09cf28ec9f | ||
|
|
b61746b3cb | ||
|
|
22c22fafeb | ||
|
|
577c4395c4 | ||
|
|
d241f476f7 | ||
|
|
5832ed0c30 | ||
|
|
bd9ea225be | ||
|
|
4a575dd70c | ||
|
|
b80ee16a7c | ||
|
|
c888371e6c | ||
|
|
8de2c3bfcb | ||
|
|
6688f73565 | ||
|
|
7d929cb6e2 | ||
|
|
72740b9e4d | ||
|
|
f7d279fa16 | ||
|
|
ff7c9c48f3 | ||
|
|
d11832913d | ||
|
|
724e0e83f2 | ||
|
|
333f091f1a | ||
|
|
cfbb0b993a | ||
|
|
582b6754a4 | ||
|
|
7767c46bf4 | ||
|
|
bf34cef896 | ||
|
|
c085ec6860 | ||
|
|
5f5d0316b2 | ||
|
|
f9ec64c3ad | ||
|
|
0dfd0ad4b0 | ||
|
|
e88e11b9ba | ||
|
|
7a2a79ca7b | ||
|
|
4c0683c484 | ||
|
|
dfe62db8ee | ||
|
|
025c824fbb | ||
|
|
930c5d7c7a | ||
|
|
8b8dcc0127 | ||
|
|
4ad8e88bd8 | ||
|
|
89d4640e92 | ||
|
|
60b12bad61 | ||
|
|
2595fa5c51 | ||
|
|
3e487e5f13 | ||
|
|
b6d6c68e54 | ||
|
|
49548d6f9f | ||
|
|
8faadc23b0 | ||
|
|
20da1ebfab | ||
|
|
03305c72c7 | ||
|
|
a636fd1cf0 | ||
|
|
d8797a8dc6 | ||
|
|
363e18e15d | ||
|
|
982cac8c43 | ||
|
|
fabf64b838 | ||
|
|
e3e6ebe953 | ||
|
|
7ad9b52546 | ||
|
|
abb5090d63 | ||
|
|
52ae05d057 | ||
|
|
3cd216d119 | ||
|
|
581edc0a38 | ||
|
|
f17ebbede6 | ||
|
|
2bd2c09c23 | ||
|
|
c1cf859ec9 | ||
|
|
1ebf864998 | ||
|
|
0948e34e48 | ||
|
|
f9a1ea9c83 | ||
|
|
d02a8ebc1b | ||
|
|
5c2625b648 | ||
|
|
13174d006e | ||
|
|
2c59cc4cc3 | ||
|
|
41c81d3226 | ||
|
|
2ec7d3b4d9 | ||
|
|
b607b73598 | ||
|
|
f52791cd1f | ||
|
|
d829facbd4 | ||
|
|
44ac3a9ff2 | ||
|
|
2054a76353 | ||
|
|
4b073c3161 | ||
|
|
527c016c72 | ||
|
|
23849b9f42 | ||
|
|
5d4277adf0 | ||
|
|
0f479d525e | ||
|
|
4aae85a70f | ||
|
|
f0ec16be4e | ||
|
|
92f6d8b98d | ||
|
|
0a48436937 | ||
|
|
f4c5dc77d8 | ||
|
|
8656d1c972 | ||
|
|
1b81f5cfe8 | ||
|
|
a4723c5942 | ||
|
|
b31521c758 | ||
|
|
6dc96bb348 | ||
|
|
2da78f7793 | ||
|
|
e8d54a1d11 | ||
|
|
f79e764439 | ||
|
|
803b659fce | ||
|
|
537987398d | ||
|
|
cdcf65cb52 | ||
|
|
5fcb092151 | ||
|
|
1fd708e765 | ||
|
|
a19302babc | ||
|
|
18a627b01e | ||
|
|
a6b340c179 | ||
|
|
2fe1de1c97 | ||
|
|
0072983b4f | ||
|
|
e1e87edc74 | ||
|
|
eddb5480e9 | ||
|
|
f3c28ea933 | ||
|
|
829f8ba0a4 | ||
|
|
f49ca2bccb | ||
|
|
9c71a8d9a3 | ||
|
|
0032f91525 | ||
|
|
7f3492f978 | ||
|
|
2b9110e417 | ||
|
|
98619a0eb8 | ||
|
|
7a4aa05c39 | ||
|
|
5b26757662 | ||
|
|
ba597cd2ad | ||
|
|
5992ddd48e | ||
|
|
8486b95917 | ||
|
|
564f4fb5f9 | ||
|
|
688c11ff18 | ||
|
|
4a9a2a670c | ||
|
|
5b25637de5 | ||
|
|
bb15de6d1a | ||
|
|
cf2e368978 | ||
|
|
0d67673542 | ||
|
|
d7e58fb557 | ||
|
|
f8cffb64a1 | ||
|
|
dc97fb1cc5 | ||
|
|
392a0c4dcb | ||
|
|
970a111386 | ||
|
|
cafa7cbf17 | ||
|
|
0f0c0441a9 | ||
|
|
a0db8e8e2c | ||
|
|
641356f31d | ||
|
|
191b238a04 | ||
|
|
f6936c5f33 | ||
|
|
e8f8cb8aa3 | ||
|
|
96170a668f | ||
|
|
4a3e807066 | ||
|
|
ab369d9aac | ||
|
|
742864fe6c | ||
|
|
c3df1c4d1f | ||
|
|
63d7fb80e6 | ||
|
|
a7f468efd1 | ||
|
|
9f4ab9540b | ||
|
|
1e133b7c5d | ||
|
|
4aa7530bbc | ||
|
|
2e0ddf2e7a | ||
|
|
e24bc2a05d | ||
|
|
a8c0db3fd7 | ||
|
|
7b77a6cd40 | ||
|
|
b8b8e470f2 | ||
|
|
ad92ea243d | ||
|
|
489a8456f7 | ||
|
|
122e389c38 | ||
|
|
8318add6d5 | ||
|
|
6c3650d9f2 | ||
|
|
37005b1c68 | ||
|
|
0897383e41 | ||
|
|
15db817382 | ||
|
|
eaa1cde01a | ||
|
|
7c1d1074f9 | ||
|
|
0f0f9b6062 | ||
|
|
839232dc98 | ||
|
|
356df45583 | ||
|
|
6b78992f07 | ||
|
|
5af33facb6 | ||
|
|
a7b9bcf3e8 | ||
|
|
eafc6074c1 | ||
|
|
99383a482b | ||
|
|
6f2807cba2 | ||
|
|
39a40a8c43 | ||
|
|
31123f7e97 | ||
|
|
96300a012a | ||
|
|
5f98b5350e | ||
|
|
8874eceb5f | ||
|
|
9de4d557e3 | ||
|
|
1d5f2634f1 | ||
|
|
710d26d36d | ||
|
|
47793e6198 | ||
|
|
3c799eb40b | ||
|
|
0d7b8e315f | ||
|
|
ae7cfbff35 | ||
|
|
a0a497ab33 | ||
|
|
fd86471a0f | ||
|
|
5fcb56a087 | ||
|
|
808080d5ee | ||
|
|
e6037f1680 | ||
|
|
5c3ded6b07 | ||
|
|
0c14e35d15 | ||
|
|
c13be5d39a | ||
|
|
e4b515c1d5 | ||
|
|
65e2c87e8f | ||
|
|
68ce25854a | ||
|
|
1f80a02be9 | ||
|
|
3df6643513 | ||
|
|
e6a4cea4de | ||
|
|
bad36a69e8 | ||
|
|
2697fb5317 | ||
|
|
b47392ba7f | ||
|
|
a99a375375 | ||
|
|
8c3df89d52 | ||
|
|
6ea0ebc9f9 | ||
|
|
26236f5886 | ||
|
|
1420c71ec5 | ||
|
|
10bd77d761 | ||
|
|
192d286cf2 | ||
|
|
8ab9d2d5ad | ||
|
|
3630812b92 | ||
|
|
2f320bc256 | ||
|
|
45b8d42b8e | ||
|
|
bd2d19f867 | ||
|
|
0be404baca | ||
|
|
e6cee9ac83 | ||
|
|
5a2c9243c4 | ||
|
|
fecd748198 | ||
|
|
85351b2c66 | ||
|
|
8b3e5b6462 | ||
|
|
93af920b8f | ||
|
|
b1248d9845 | ||
|
|
c86abef07d | ||
|
|
17ad5dfe33 | ||
|
|
e1843fe1f1 | ||
|
|
256a21f058 | ||
|
|
1473259e41 | ||
|
|
e935b91e93 | ||
|
|
07258a6914 | ||
|
|
cb35808508 | ||
|
|
937d656227 | ||
|
|
d173a59441 | ||
|
|
75a3adb2c9 | ||
|
|
148e208476 | ||
|
|
8f59f2549a | ||
|
|
630400fee4 | ||
|
|
1ec6929e91 | ||
|
|
8c6bdd8ae2 | ||
|
|
e04138bced | ||
|
|
db5ad85811 | ||
|
|
5b44fd376d | ||
|
|
0036e8b280 | ||
|
|
ea2b5bfecf | ||
|
|
aa7495fa60 | ||
|
|
162ec1bd86 | ||
|
|
2668884008 | ||
|
|
abdc3f3485 | ||
|
|
911f21ee7c | ||
|
|
2387c40254 | ||
|
|
76e18a79b3 | ||
|
|
9725c9c947 | ||
|
|
b1b79d1245 | ||
|
|
247ed7cc64 | ||
|
|
2fd2af793e | ||
|
|
3fc36b9ce1 | ||
|
|
c12fd2d7bc | ||
|
|
7fe1ba2fb2 | ||
|
|
ab7eb3e5df | ||
|
|
3452c3acd1 | ||
|
|
3c305fbf37 | ||
|
|
a5e0721ec1 | ||
|
|
164a0d5376 | ||
|
|
2bcf6ec39a | ||
|
|
40824156bf | ||
|
|
cec4495034 | ||
|
|
74eb8621d9 | ||
|
|
4394d25961 | ||
|
|
8113327d31 | ||
|
|
aeeb35bc60 | ||
|
|
630aa3f320 | ||
|
|
3487a945c2 | ||
|
|
1936ae44a3 | ||
|
|
3d7a84e786 | ||
|
|
a4d6730cb0 | ||
|
|
b724227a29 | ||
|
|
d72964fd7c | ||
|
|
d4a41cfb60 | ||
|
|
05859453df | ||
|
|
240d910c9b | ||
|
|
0c31bce7d0 | ||
|
|
f4c4ae36ed | ||
|
|
b9931b2ceb | ||
|
|
4fd6f06c0b | ||
|
|
09573ba7ef | ||
|
|
08e9676760 | ||
|
|
15ae737522 | ||
|
|
50ec9e563e | ||
|
|
cb40649f8b | ||
|
|
bccead0881 | ||
|
|
35ae710624 | ||
|
|
75de4518f2 | ||
|
|
bfcdfea6c8 | ||
|
|
471553fa88 | ||
|
|
db59f5f69b | ||
|
|
1b5413646e | ||
|
|
0337d2517c | ||
|
|
87e6eb9688 | ||
|
|
7b8c246ef8 | ||
|
|
a268a8980a | ||
|
|
3b516c338e | ||
|
|
9952685805 | ||
|
|
2f59c8df22 | ||
|
|
6cd0a42791 | ||
|
|
4c42153bfd | ||
|
|
603bd9c37c | ||
|
|
87c0c3ac73 | ||
|
|
8a91e7f6ac | ||
|
|
281dbdbb01 | ||
|
|
8980d8d32f | ||
|
|
9d6cf9a62e | ||
|
|
0fabb2b368 | ||
|
|
9801ca0314 | ||
|
|
fd86b26b39 | ||
|
|
b0dbef1587 | ||
|
|
a6e60bc23b | ||
|
|
137e8a876d | ||
|
|
fe9538253f | ||
|
|
edda2c248e | ||
|
|
62275639e3 | ||
|
|
d0c68b82f4 | ||
|
|
78b5953bdf | ||
|
|
25e565b099 | ||
|
|
1fe0f78ad7 | ||
|
|
fc8b68156f | ||
|
|
b47cd197ce | ||
|
|
3943426c4c | ||
|
|
08a9bd42a3 | ||
|
|
b2ff339efe | ||
|
|
c604131e04 | ||
|
|
f17607f126 | ||
|
|
94e455a57b | ||
|
|
3c9149db4a | ||
|
|
96cc615c07 | ||
|
|
cd1f4a1c2b | ||
|
|
e0f99a42db | ||
|
|
3506e417d4 | ||
|
|
36197e2212 | ||
|
|
fc5f42d01e | ||
|
|
e26d3767e0 | ||
|
|
046b37c76a | ||
|
|
925ff3e116 | ||
|
|
e5ede4f7b6 | ||
|
|
ded9301527 | ||
|
|
bd1ed6ff73 | ||
|
|
feb65980d4 | ||
|
|
e81d75a782 | ||
|
|
ff305d13ae | ||
|
|
8bcbc1a779 | ||
|
|
3874aa6fee | ||
|
|
103e9f3a11 | ||
|
|
d02c25f421 | ||
|
|
228af38a4a | ||
|
|
051a48885c | ||
|
|
6bcdc6052f | ||
|
|
af3527e64f | ||
|
|
17ef3dd07a | ||
|
|
1f165ecd2a | ||
|
|
70d1d450a9 | ||
|
|
b667892698 | ||
|
|
dc11add0e9 | ||
|
|
cb429a0b88 | ||
|
|
b51039cfc0 | ||
|
|
eadd959d95 | ||
|
|
1856e03d88 | ||
|
|
7dcfa622a3 | ||
|
|
64251b9b3c | ||
|
|
6b073dd5fc | ||
|
|
0911fabfb2 | ||
|
|
050d3f5e63 | ||
|
|
bbe3f78ad1 | ||
|
|
8204c18895 | ||
|
|
b91c788897 | ||
|
|
1d20a3029f | ||
|
|
9cfebc9ae3 | ||
|
|
ddabb4539b | ||
|
|
ada35e221b | ||
|
|
6fef9d904e | ||
|
|
67cf2b5904 | ||
|
|
3bebe792f6 | ||
|
|
00b4d89181 | ||
|
|
f729c6b650 | ||
|
|
df95f8c3f3 | ||
|
|
fe36e57d72 | ||
|
|
31197812b6 | ||
|
|
bd3fe01a12 | ||
|
|
39f7f32b7d | ||
|
|
b4522a1277 | ||
|
|
bb6a7e8327 | ||
|
|
9bd42a7579 | ||
|
|
b41430e5a3 | ||
|
|
595353e838 | ||
|
|
f1a21bb856 | ||
|
|
e44c2935f9 | ||
|
|
4d546f948d | ||
|
|
3bab349d7b | ||
|
|
eef6126ef8 | ||
|
|
5c7478fa39 | ||
|
|
64b72d0b06 | ||
|
|
b266a98b25 | ||
|
|
8a27de5df8 | ||
|
|
f9b5310fce | ||
|
|
fdce173969 | ||
|
|
7b9ddf90c1 | ||
|
|
580c8c19de | ||
|
|
55cc77140e | ||
|
|
93c89dd7cc | ||
|
|
c970cbbd2d | ||
|
|
9ea55fa51f | ||
|
|
5775a11b4e | ||
|
|
1a666b6584 | ||
|
|
35407a2108 | ||
|
|
71fb19aa22 | ||
|
|
b7d7f7b8ce | ||
|
|
59b983edcc | ||
|
|
1734d034e1 | ||
|
|
7f7500ff0c | ||
|
|
ce77c24e5c | ||
|
|
5469a591c0 | ||
|
|
a4befc5e59 | ||
|
|
1ee8065592 | ||
|
|
e4e3bd44fc | ||
|
|
c75de07c2e | ||
|
|
e928131809 | ||
|
|
4f802e82a9 | ||
|
|
0c90bd462e | ||
|
|
bbb70c93d9 | ||
|
|
f6e6ba775c | ||
|
|
06646be907 | ||
|
|
1b4c1a4b9e | ||
|
|
ae3f5a0f62 | ||
|
|
3a984e8a4d | ||
|
|
7d711a54bc | ||
|
|
d92d629c25 | ||
|
|
21e630209a | ||
|
|
e3933998ef | ||
|
|
667afe9051 | ||
|
|
26dc2881eb | ||
|
|
250cb33285 | ||
|
|
db51abec1f | ||
|
|
530716d458 | ||
|
|
f3065d79b3 | ||
|
|
bca5f0472e | ||
|
|
8e54c43917 | ||
|
|
946df1d7a0 | ||
|
|
55f00f742c | ||
|
|
97b2cb71b7 | ||
|
|
ba3a5ba53c | ||
|
|
953c09c999 | ||
|
|
b4cc325b07 | ||
|
|
28c1343f3e | ||
|
|
c16fd94c4a | ||
|
|
bb2cc20838 | ||
|
|
7b815fd010 | ||
|
|
18584974df | ||
|
|
15823b7785 | ||
|
|
6c275d4cd2 | ||
|
|
2d64ee5e2a | ||
|
|
3ae5ffa3f6 | ||
|
|
57d9ddc2c6 | ||
|
|
8b84def494 | ||
|
|
546f01ceb2 | ||
|
|
be720d0e0f | ||
|
|
72bed03244 | ||
|
|
38083ed284 | ||
|
|
53f1b94475 | ||
|
|
ed4270a0e3 | ||
|
|
f1d5cc8903 | ||
|
|
80efdec5d9 | ||
|
|
d49687400a | ||
|
|
e6e03b50da | ||
|
|
543fa3cfa9 | ||
|
|
899988c7c2 | ||
|
|
2f48dd449b | ||
|
|
f70fbbdfee | ||
|
|
2b09ca240d | ||
|
|
0626ff84ad | ||
|
|
62ec746ee3 | ||
|
|
d0f12d7d56 | ||
|
|
b806a69604 | ||
|
|
a609d6360b | ||
|
|
dafbfc8644 | ||
|
|
55413eea19 | ||
|
|
5247c181af | ||
|
|
321af5ec87 | ||
|
|
9ccf340b3d | ||
|
|
d7dcacb899 | ||
|
|
8addd483c2 | ||
|
|
4d27e5ac9b | ||
|
|
31290f6e80 | ||
|
|
c31cc4dbee | ||
|
|
cc1f94cf61 | ||
|
|
a9132b8d50 | ||
|
|
7b4a9891aa | ||
|
|
dcaaf38ecc | ||
|
|
653a8aa850 | ||
|
|
274af11385 | ||
|
|
170b246901 | ||
|
|
5250432ce3 | ||
|
|
53d6e973eb | ||
|
|
c9bdd62d53 | ||
|
|
7eb98af528 | ||
|
|
385e3e21b3 | ||
|
|
127ec63d76 | ||
|
|
4988b5f260 | ||
|
|
f28a50f92c | ||
|
|
e8efe8e609 | ||
|
|
d7e7457bc5 | ||
|
|
daff927811 | ||
|
|
8861ec0a61 | ||
|
|
bd4321f61a | ||
|
|
d831482fe0 | ||
|
|
9ea79ea389 | ||
|
|
b6fdf840ef | ||
|
|
73f262ce4b | ||
|
|
f63434adc3 | ||
|
|
42948386ec | ||
|
|
1ce0dbde0e | ||
|
|
32e0621b0a | ||
|
|
78e05b84e9 | ||
|
|
76ebd21163 | ||
|
|
38aaf545c6 | ||
|
|
527d7ef671 | ||
|
|
e54b80e061 | ||
|
|
27f95a8b2c | ||
|
|
a1e8903128 | ||
|
|
b00ac2aef4 | ||
|
|
8865d15ed9 | ||
|
|
fc3d4b7f33 | ||
|
|
934cc44540 | ||
|
|
106de3dd4c | ||
|
|
9b55cfcbe3 | ||
|
|
8137f1023a | ||
|
|
d670e33b6f | ||
|
|
3d3bb8fc94 | ||
|
|
9c880eae8a | ||
|
|
54a71630f1 | ||
|
|
923a8453cc | ||
|
|
00447ca819 | ||
|
|
ad4ee9d822 | ||
|
|
40e9dbfda2 | ||
|
|
b9da6911e6 | ||
|
|
81f9211098 | ||
|
|
8290081365 | ||
|
|
81af610c11 | ||
|
|
cfa2cf58f3 | ||
|
|
01c17d28f6 | ||
|
|
efd2a5e8c5 | ||
|
|
c437b00727 | ||
|
|
5df0d1ddc6 | ||
|
|
c3521b0d87 | ||
|
|
148916d35e | ||
|
|
06c7da944c | ||
|
|
f2ba741499 | ||
|
|
0f9927686b | ||
|
|
59855a71ef | ||
|
|
fffb21bb4f | ||
|
|
30bb6cdfab | ||
|
|
8771477884 | ||
|
|
55a5070691 | ||
|
|
1182b51e4b | ||
|
|
9976ecc2aa | ||
|
|
3bd8107fcf | ||
|
|
a48c67de5c | ||
|
|
bb0a7c014e | ||
|
|
80729b6b06 | ||
|
|
ff168d1c9e | ||
|
|
331100370c | ||
|
|
47babf2ed7 | ||
|
|
c1388bf23f | ||
|
|
3f4dfe9b0b | ||
|
|
0caac20d77 | ||
|
|
9d0a65a516 | ||
|
|
ab061ba7a6 | ||
|
|
2d24d064d5 | ||
|
|
458685026b | ||
|
|
b0448a4565 | ||
|
|
f902f59b31 | ||
|
|
2b8bfbe544 | ||
|
|
564a25e578 | ||
|
|
f50b08d301 | ||
|
|
26566d9ce0 | ||
|
|
f42d438be2 | ||
|
|
1fbc39b614 | ||
|
|
36af471c3c | ||
|
|
a5e86c9a36 | ||
|
|
4b27dd40b9 | ||
|
|
ff0aac9d8a | ||
|
|
d3fbc133a2 | ||
|
|
ce7a8e7a3d | ||
|
|
80157a032a |
28
.editorconfig
Normal file
28
.editorconfig
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
tab_width = 4
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.{yaml,yml,less}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# Makefiles always use tabs for indentation
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[*.bat]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[{Dockerfile,*.dockerfile}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -38,7 +38,6 @@ htmlcov/
|
|||||||
.tox/
|
.tox/
|
||||||
.coverage
|
.coverage
|
||||||
.cache
|
.cache
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
coverage.xml
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
@@ -64,5 +63,17 @@ celerybeat-schedule
|
|||||||
.idea/*
|
.idea/*
|
||||||
/nbproject/
|
/nbproject/
|
||||||
|
|
||||||
|
#VSCode
|
||||||
|
.vscode/
|
||||||
|
|
||||||
#gitlab configs
|
#gitlab configs
|
||||||
.gitlab/
|
.gitlab/
|
||||||
|
|
||||||
|
#transifex
|
||||||
|
.tx/
|
||||||
|
|
||||||
|
#other
|
||||||
|
.flake8
|
||||||
|
.pylintrc
|
||||||
|
Makefile
|
||||||
|
.isort.cfg
|
||||||
|
|||||||
251
.gitlab-ci.yml
251
.gitlab-ci.yml
@@ -1,36 +1,251 @@
|
|||||||
|
.only-default: &only-default
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
- branches
|
||||||
|
- merge_requests
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- "test"
|
- pre-commit
|
||||||
|
- gitlab
|
||||||
|
- test
|
||||||
- deploy
|
- deploy
|
||||||
|
- docker
|
||||||
|
|
||||||
|
include:
|
||||||
|
- template: Dependency-Scanning.gitlab-ci.yml
|
||||||
|
- template: Security/SAST.gitlab-ci.yml
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- python -V
|
- apt-get update && apt-get install redis-server -y
|
||||||
- pip install wheel tox
|
- redis-server --daemonize yes
|
||||||
|
- python -V
|
||||||
|
- pip install wheel tox
|
||||||
|
|
||||||
test-3.5:
|
pre-commit-check:
|
||||||
image: python:3.5-stretch
|
<<: *only-default
|
||||||
|
stage: pre-commit
|
||||||
|
image: python:3.6-buster
|
||||||
|
variables:
|
||||||
|
PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- ${PRE_COMMIT_HOME}
|
||||||
script:
|
script:
|
||||||
- tox -e py35
|
- pip install pre-commit
|
||||||
|
- pre-commit run --all-files
|
||||||
|
|
||||||
test-3.6:
|
sast:
|
||||||
image: python:3.6-stretch
|
stage: gitlab
|
||||||
script:
|
before_script: []
|
||||||
- tox -e py36
|
|
||||||
|
|
||||||
test-3.7:
|
dependency_scanning:
|
||||||
image: python:3.7-stretch
|
stage: gitlab
|
||||||
|
before_script:
|
||||||
|
- apt-get update && apt-get install redis-server libmariadb-dev -y
|
||||||
|
- redis-server --daemonize yes
|
||||||
|
- python -V
|
||||||
|
- pip install wheel tox
|
||||||
|
|
||||||
|
test-3.7-core:
|
||||||
|
<<: *only-default
|
||||||
|
image: python:3.7-bullseye
|
||||||
script:
|
script:
|
||||||
- tox -e py37
|
- tox -e py37-core
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
reports:
|
||||||
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
|
path: coverage.xml
|
||||||
|
|
||||||
|
test-3.8-core:
|
||||||
|
<<: *only-default
|
||||||
|
image: python:3.8-bullseye
|
||||||
|
script:
|
||||||
|
- tox -e py38-core
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
reports:
|
||||||
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
|
path: coverage.xml
|
||||||
|
|
||||||
|
test-3.9-core:
|
||||||
|
<<: *only-default
|
||||||
|
image: python:3.9-bullseye
|
||||||
|
script:
|
||||||
|
- tox -e py39-core
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
reports:
|
||||||
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
|
path: coverage.xml
|
||||||
|
|
||||||
|
test-3.10-core:
|
||||||
|
<<: *only-default
|
||||||
|
image: python:3.10-bullseye
|
||||||
|
script:
|
||||||
|
- tox -e py310-core
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
reports:
|
||||||
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
|
path: coverage.xml
|
||||||
|
|
||||||
|
test-3.11-core:
|
||||||
|
<<: *only-default
|
||||||
|
image: python:3.11-rc-bullseye
|
||||||
|
script:
|
||||||
|
- tox -e py311-core
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
reports:
|
||||||
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
|
path: coverage.xml
|
||||||
|
allow_failure: true
|
||||||
|
|
||||||
|
test-3.7-all:
|
||||||
|
<<: *only-default
|
||||||
|
image: python:3.7-bullseye
|
||||||
|
script:
|
||||||
|
- tox -e py37-all
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
reports:
|
||||||
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
|
path: coverage.xml
|
||||||
|
|
||||||
|
test-3.8-all:
|
||||||
|
<<: *only-default
|
||||||
|
image: python:3.8-bullseye
|
||||||
|
script:
|
||||||
|
- tox -e py38-all
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
reports:
|
||||||
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
|
path: coverage.xml
|
||||||
|
|
||||||
|
test-3.9-all:
|
||||||
|
<<: *only-default
|
||||||
|
image: python:3.9-bullseye
|
||||||
|
script:
|
||||||
|
- tox -e py39-all
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
reports:
|
||||||
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
|
path: coverage.xml
|
||||||
|
|
||||||
|
test-3.10-all:
|
||||||
|
<<: *only-default
|
||||||
|
image: python:3.10-bullseye
|
||||||
|
script:
|
||||||
|
- tox -e py310-all
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
reports:
|
||||||
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
|
path: coverage.xml
|
||||||
|
|
||||||
|
test-3.11-all:
|
||||||
|
<<: *only-default
|
||||||
|
image: python:3.11-rc-bullseye
|
||||||
|
script:
|
||||||
|
- tox -e py311-all
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
reports:
|
||||||
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
|
path: coverage.xml
|
||||||
|
allow_failure: true
|
||||||
|
|
||||||
|
test-docs:
|
||||||
|
<<: *only-default
|
||||||
|
image: python:3.9-bullseye
|
||||||
|
script:
|
||||||
|
- tox -e docs
|
||||||
|
|
||||||
deploy_production:
|
deploy_production:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image: python:3.6-stretch
|
image: python:3.10-bullseye
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- pip install twine
|
- pip install twine wheel
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- python setup.py sdist
|
- python setup.py sdist bdist_wheel
|
||||||
- twine upload dist/*
|
- twine upload dist/*
|
||||||
|
|
||||||
only:
|
rules:
|
||||||
- tags
|
- if: $CI_COMMIT_TAG
|
||||||
|
|
||||||
|
build-image:
|
||||||
|
before_script: []
|
||||||
|
image: docker:20.10.10
|
||||||
|
stage: docker
|
||||||
|
services:
|
||||||
|
- docker:20.10.10-dind
|
||||||
|
script: |
|
||||||
|
CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -)
|
||||||
|
IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_COMMIT_SHORT_SHA
|
||||||
|
CURRENT_TAG=$CI_REGISTRY_IMAGE/auth:$CI_COMMIT_TAG
|
||||||
|
MINOR_TAG=$CI_REGISTRY_IMAGE/auth:$(echo $CI_COMMIT_TAG | cut -d '.' -f 1-2)
|
||||||
|
MAJOR_TAG=$CI_REGISTRY_IMAGE/auth:$(echo $CI_COMMIT_TAG | cut -d '.' -f 1)
|
||||||
|
LATEST_TAG=$CI_REGISTRY_IMAGE/auth:latest
|
||||||
|
|
||||||
|
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||||
|
docker build . -t $IMAGE_TAG -f docker/Dockerfile --build-arg AUTH_VERSION=$(echo $CI_COMMIT_TAG | cut -c 2-)
|
||||||
|
docker tag $IMAGE_TAG $CURRENT_TAG
|
||||||
|
docker tag $IMAGE_TAG $MINOR_TAG
|
||||||
|
docker tag $IMAGE_TAG $MAJOR_TAG
|
||||||
|
docker tag $IMAGE_TAG $LATEST_TAG
|
||||||
|
docker image push --all-tags $CI_REGISTRY_IMAGE/auth
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_TAG
|
||||||
|
|
||||||
|
build-image-dev:
|
||||||
|
before_script: []
|
||||||
|
image: docker:20.10.10
|
||||||
|
stage: docker
|
||||||
|
services:
|
||||||
|
- docker:20.10.10-dind
|
||||||
|
script: |
|
||||||
|
CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -)
|
||||||
|
IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA
|
||||||
|
|
||||||
|
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||||
|
docker build . -t $IMAGE_TAG -f docker/Dockerfile --build-arg AUTH_PACKAGE=git+https://gitlab.com/allianceauth/allianceauth@$CI_COMMIT_BRANCH
|
||||||
|
docker push $IMAGE_TAG
|
||||||
|
rules:
|
||||||
|
- if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == ""'
|
||||||
|
when: manual
|
||||||
|
- if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME != ""'
|
||||||
|
when: never
|
||||||
|
|
||||||
|
build-image-mr:
|
||||||
|
before_script: []
|
||||||
|
image: docker:20.10.10
|
||||||
|
stage: docker
|
||||||
|
services:
|
||||||
|
- docker:20.10.10-dind
|
||||||
|
script: |
|
||||||
|
CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -)
|
||||||
|
IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME-$CI_COMMIT_SHORT_SHA
|
||||||
|
|
||||||
|
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||||
|
docker build . -t $IMAGE_TAG -f docker/Dockerfile --build-arg AUTH_PACKAGE=git+$CI_MERGE_REQUEST_SOURCE_PROJECT_URL@$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
|
||||||
|
docker push $IMAGE_TAG
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
when: manual
|
||||||
|
- if: '$CI_PIPELINE_SOURCE != "merge_request_event"'
|
||||||
|
when: never
|
||||||
|
|||||||
34
.pre-commit-config.yaml
Normal file
34
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Apply to all files without committing:
|
||||||
|
# pre-commit run --all-files
|
||||||
|
# Update this file:
|
||||||
|
# pre-commit autoupdate
|
||||||
|
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.0.1
|
||||||
|
hooks:
|
||||||
|
- id: check-case-conflict
|
||||||
|
- id: check-json
|
||||||
|
- id: check-xml
|
||||||
|
- id: check-yaml
|
||||||
|
- id: fix-byte-order-marker
|
||||||
|
- id: trailing-whitespace
|
||||||
|
exclude: (\.min\.css|\.min\.js|\.mo|\.po|swagger\.json)$
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
exclude: (\.min\.css|\.min\.js|\.mo|\.po|swagger\.json)$
|
||||||
|
- id: mixed-line-ending
|
||||||
|
args: [ '--fix=lf' ]
|
||||||
|
- id: fix-encoding-pragma
|
||||||
|
args: [ '--remove' ]
|
||||||
|
|
||||||
|
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
|
||||||
|
rev: 2.3.54
|
||||||
|
hooks:
|
||||||
|
- id: editorconfig-checker
|
||||||
|
exclude: ^(LICENSE|allianceauth\/static\/css\/themes\/bootstrap-locals.less|allianceauth\/eveonline\/swagger.json|(.*.po)|(.*.mo))
|
||||||
|
|
||||||
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
|
rev: v2.29.0
|
||||||
|
hooks:
|
||||||
|
- id: pyupgrade
|
||||||
|
args: [ --py37-plus ]
|
||||||
23
.readthedocs.yml
Normal file
23
.readthedocs.yml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# .readthedocs.yml
|
||||||
|
# Read the Docs configuration file
|
||||||
|
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||||
|
|
||||||
|
# Required
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
# Build documentation in the docs/ directory with Sphinx
|
||||||
|
sphinx:
|
||||||
|
configuration: docs/conf.py
|
||||||
|
|
||||||
|
# Build documentation with MkDocs
|
||||||
|
#mkdocs:
|
||||||
|
# configuration: mkdocs.yml
|
||||||
|
|
||||||
|
# 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.7
|
||||||
|
install:
|
||||||
|
- requirements: docs/requirements.txt
|
||||||
25
.travis.yml
25
.travis.yml
@@ -1,25 +0,0 @@
|
|||||||
language: python
|
|
||||||
sudo: false
|
|
||||||
cache: pip
|
|
||||||
dist: trusty
|
|
||||||
install:
|
|
||||||
- pip install coveralls>=1.1 tox
|
|
||||||
# command to run tests
|
|
||||||
script:
|
|
||||||
- tox
|
|
||||||
after_success:
|
|
||||||
coveralls
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- env: TOXENV=py35-dj111
|
|
||||||
python: '3.5'
|
|
||||||
- env: TOXENV=py36-dj111
|
|
||||||
python: '3.6'
|
|
||||||
- env: TOXENV=py35-dj20
|
|
||||||
python: '3.5'
|
|
||||||
- env: TOXENV=py36-dj20
|
|
||||||
python: '3.6'
|
|
||||||
- env: TOXENV=py37-dj20
|
|
||||||
python: '3.7-dev'
|
|
||||||
allow_failures:
|
|
||||||
- env: TOXENV=py37-dj20
|
|
||||||
1
LICENSE
1
LICENSE
@@ -337,4 +337,3 @@ proprietary programs. If your program is a subroutine library, you may
|
|||||||
consider it more useful to permit linking proprietary applications with the
|
consider it more useful to permit linking proprietary applications with the
|
||||||
library. If this is what you want to do, use the GNU Lesser General
|
library. If this is what you want to do, use the GNU Lesser General
|
||||||
Public License instead of this License.
|
Public License instead of this License.
|
||||||
|
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -11,32 +11,34 @@
|
|||||||
|
|
||||||
An auth system for EVE Online to help in-game organizations manage online service access.
|
An auth system for EVE Online to help in-game organizations manage online service access.
|
||||||
|
|
||||||
## Contens
|
## Content
|
||||||
|
|
||||||
- [Overview](#overview)
|
- [Overview](#overview)
|
||||||
- [Documentation](http://allianceauth.rtfd.io)
|
- [Documentation](http://allianceauth.rtfd.io)
|
||||||
- [Support](#support)
|
- [Support](#support)
|
||||||
- [Release Notes](https://gitlab.com/allianceauth/allianceauth/-/releases)
|
- [Release Notes](https://gitlab.com/allianceauth/allianceauth/-/releases)
|
||||||
- [Devloper Team](#developer-team)
|
- [Developer Team](#developer-team)
|
||||||
- [Contributing](#contributing)
|
- [Contributing](#contributing)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Alliance Auth (AA) is a web application that helps Eve Online organizations efficiently manage access to their applications and services.
|
Alliance Auth (AA) is a web site that helps Eve Online organizations efficiently manage access to applications and services.
|
||||||
|
|
||||||
Main features:
|
Main features:
|
||||||
|
|
||||||
- Automatically grants or revokes user access to external applications / services (e.g. Discord, Mumble) and web apps (e.g. SRP requests) based on the user's current membership to [in-game organizations](https://allianceauth.readthedocs.io/en/latest/features/states/) and [groups](https://allianceauth.readthedocs.io/en/latest/features/groups/)
|
- Automatically grants or revokes user access to external services (e.g. Discord, Mumble) and web apps (e.g. SRP requests) based on the user's current membership to [in-game organizations](https://allianceauth.readthedocs.io/en/latest/features/core/states/) and [groups](https://allianceauth.readthedocs.io/en/latest/features/core/groups/)
|
||||||
|
|
||||||
- Provides a central web site where users can directly access web apps (e.g. SRP requests, Fleet Schedule) and manage their access to external services and groups.
|
- Provides a central web site where users can directly access web apps (e.g. SRP requests, Fleet Schedule) and manage their access to external services and groups.
|
||||||
|
|
||||||
- Includes a set of connectors (called ["services"](https://allianceauth.readthedocs.io/en/latest/installation/services/)) for integrating access management with many popular external applications / services like Discord, Mumble, Teamspeak 3, SMF and others
|
- Includes a set of connectors (called ["services"](https://allianceauth.readthedocs.io/en/latest/features/services/)) for integrating access management with many popular external applications / services like Discord, Mumble, Teamspeak 3, SMF and others
|
||||||
|
|
||||||
- Includes a set of web apps called ["plug-in apps"](https://allianceauth.readthedocs.io/en/latest/features/) which add many useful functions: fleet schedule, timer board, SRP request management, fleet activity tracker and character application management
|
- Includes a set of web [apps](https://allianceauth.readthedocs.io/en/latest/features/apps/) which add many useful functions, e.g.: fleet schedule, timer board, SRP request management, fleet activity tracker
|
||||||
|
|
||||||
- Can be easily extended with new services and plugin-apps. Many additional services and plugin-apps are provided by the community and can be found here: [Community Creations](https://gitlab.com/allianceauth/community-creations)
|
- Can be easily extended with additional services and apps. Many are provided by the community and can be found here: [Community Creations](https://gitlab.com/allianceauth/community-creations)
|
||||||
|
|
||||||
For further details about AA - including an installation guide and a full list of included services and plugin apps - please see the [offical documentation](http://allianceauth.rtfd.io).
|
- English :flag_gb:, Chinese :flag_cn:, German :flag_de:, Spanish :flag_es:, Korean :flag_kr: and Russian :flag_ru: localization
|
||||||
|
|
||||||
|
For further details about AA - including an installation guide and a full list of included services and plugin apps - please see the [official documentation](http://allianceauth.rtfd.io).
|
||||||
|
|
||||||
## Screenshot
|
## Screenshot
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# This will make sure the app is always imported when
|
# This will make sure the app is always imported when
|
||||||
# Django starts so that shared_task will use this app.
|
# Django starts so that shared_task will use this app.
|
||||||
|
|
||||||
__version__ = '2.5.0'
|
__version__ = '2.13.0'
|
||||||
NAME = 'Alliance Auth v%s' % __version__
|
__title__ = 'Alliance Auth'
|
||||||
|
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
|
||||||
|
NAME = f'{__title__} v{__version__}'
|
||||||
default_app_config = 'allianceauth.apps.AllianceAuthConfig'
|
default_app_config = 'allianceauth.apps.AllianceAuthConfig'
|
||||||
|
|||||||
1
allianceauth/analytics/__init__.py
Normal file
1
allianceauth/analytics/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
default_app_config = 'allianceauth.analytics.apps.AnalyticsConfig'
|
||||||
21
allianceauth/analytics/admin.py
Normal file
21
allianceauth/analytics/admin.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import AnalyticsIdentifier, AnalyticsPath, AnalyticsTokens
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(AnalyticsIdentifier)
|
||||||
|
class AnalyticsIdentifierAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ['identifier', ]
|
||||||
|
list_display = ('identifier',)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(AnalyticsTokens)
|
||||||
|
class AnalyticsTokensAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ['name', ]
|
||||||
|
list_display = ('name', 'type',)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(AnalyticsPath)
|
||||||
|
class AnalyticsPathAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ['ignore_path', ]
|
||||||
|
list_display = ('ignore_path',)
|
||||||
9
allianceauth/analytics/apps.py
Normal file
9
allianceauth/analytics/apps.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AnalyticsConfig(AppConfig):
|
||||||
|
name = 'allianceauth.analytics'
|
||||||
|
label = 'analytics'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
import allianceauth.analytics.signals
|
||||||
21
allianceauth/analytics/fixtures/disable_analytics.json
Normal file
21
allianceauth/analytics/fixtures/disable_analytics.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"model": "analytics.AnalyticsTokens",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"name": "AA Team Public Google Analytics (Universal)",
|
||||||
|
"type": "GA-V4",
|
||||||
|
"token": "UA-186249766-2",
|
||||||
|
"send_page_views": "False",
|
||||||
|
"send_celery_tasks": "False",
|
||||||
|
"send_stats": "False"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "analytics.AnalyticsIdentifier",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"identifier": "ab33e241fbf042b6aa77c7655a768af7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
52
allianceauth/analytics/middleware.py
Normal file
52
allianceauth/analytics/middleware.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
|
from .models import AnalyticsTokens, AnalyticsIdentifier
|
||||||
|
from .tasks import send_ga_tracking_web_view
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class AnalyticsMiddleware(MiddlewareMixin):
|
||||||
|
def process_response(self, request, response):
|
||||||
|
"""Django Middleware: Process Page Views and creates Analytics Celery Tasks"""
|
||||||
|
if getattr(settings, "ANALYTICS_DISABLED", False):
|
||||||
|
return response
|
||||||
|
analyticstokens = AnalyticsTokens.objects.all()
|
||||||
|
client_id = AnalyticsIdentifier.objects.get(id=1).identifier.hex
|
||||||
|
try:
|
||||||
|
title = BeautifulSoup(
|
||||||
|
response.content, "html.parser").html.head.title.text
|
||||||
|
except AttributeError:
|
||||||
|
title = ''
|
||||||
|
for token in analyticstokens:
|
||||||
|
# Check if Page View Sending is Disabled
|
||||||
|
if token.send_page_views is False:
|
||||||
|
continue
|
||||||
|
# Check Exclusions
|
||||||
|
ignore = False
|
||||||
|
for ignore_path in token.ignore_paths.values():
|
||||||
|
ignore_path_regex = re.compile(ignore_path["ignore_path"])
|
||||||
|
if re.search(ignore_path_regex, request.path) is not None:
|
||||||
|
ignore = True
|
||||||
|
|
||||||
|
if ignore is True:
|
||||||
|
continue
|
||||||
|
|
||||||
|
tracking_id = token.token
|
||||||
|
locale = request.LANGUAGE_CODE
|
||||||
|
path = request.path
|
||||||
|
try:
|
||||||
|
useragent = request.headers["User-Agent"]
|
||||||
|
except KeyError:
|
||||||
|
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
||||||
|
|
||||||
|
send_ga_tracking_web_view.s(tracking_id=tracking_id,
|
||||||
|
client_id=client_id,
|
||||||
|
page=path,
|
||||||
|
title=title,
|
||||||
|
locale=locale,
|
||||||
|
useragent=useragent).\
|
||||||
|
apply_async(priority=9)
|
||||||
|
return response
|
||||||
42
allianceauth/analytics/migrations/0001_initial.py
Normal file
42
allianceauth/analytics/migrations/0001_initial.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Generated by Django 3.1.4 on 2020-12-30 13:11
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AnalyticsIdentifier',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('identifier', models.UUIDField(default=uuid.uuid4, editable=False)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AnalyticsPath',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('ignore_path', models.CharField(default='/example/', max_length=254)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AnalyticsTokens',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=254)),
|
||||||
|
('type', models.CharField(choices=[('GA-U', 'Google Analytics Universal'), ('GA-V4', 'Google Analytics V4')], max_length=254)),
|
||||||
|
('token', models.CharField(max_length=254)),
|
||||||
|
('send_page_views', models.BooleanField(default=False)),
|
||||||
|
('send_celery_tasks', models.BooleanField(default=False)),
|
||||||
|
('send_stats', models.BooleanField(default=False)),
|
||||||
|
('ignore_paths', models.ManyToManyField(blank=True, to='analytics.AnalyticsPath')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
34
allianceauth/analytics/migrations/0002_add_AA_Team_Token.py
Normal file
34
allianceauth/analytics/migrations/0002_add_AA_Team_Token.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Generated by Django 3.1.4 on 2020-12-30 08:53
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def add_aa_team_token(apps, schema_editor):
|
||||||
|
# We can't import the Person model directly as it may be a newer
|
||||||
|
# version than this migration expects. We use the historical version.
|
||||||
|
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
|
||||||
|
token = Tokens()
|
||||||
|
token.type = 'GA-U'
|
||||||
|
token.token = 'UA-186249766-2'
|
||||||
|
token.send_page_views = True
|
||||||
|
token.send_celery_tasks = True
|
||||||
|
token.send_stats = True
|
||||||
|
token.name = 'AA Team Public Google Analytics (Universal)'
|
||||||
|
token.save()
|
||||||
|
|
||||||
|
|
||||||
|
def remove_aa_team_token(apps, schema_editor):
|
||||||
|
# Have to define some code to remove this identifier
|
||||||
|
# In case of migration rollback?
|
||||||
|
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
|
||||||
|
token = Tokens.objects.filter(token="UA-186249766-2").delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('analytics', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [migrations.RunPython(add_aa_team_token, remove_aa_team_token)
|
||||||
|
]
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 3.1.4 on 2020-12-30 08:53
|
||||||
|
|
||||||
|
from uuid import uuid4
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def generate_identifier(apps, schema_editor):
|
||||||
|
# We can't import the Person model directly as it may be a newer
|
||||||
|
# version than this migration expects. We use the historical version.
|
||||||
|
Identifier = apps.get_model('analytics', 'AnalyticsIdentifier')
|
||||||
|
identifier = Identifier()
|
||||||
|
identifier.id = 1
|
||||||
|
identifier.save()
|
||||||
|
|
||||||
|
|
||||||
|
def zero_identifier(apps, schema_editor):
|
||||||
|
# Have to define some code to remove this identifier
|
||||||
|
# In case of migration rollback?
|
||||||
|
Identifier = apps.get_model('analytics', 'AnalyticsIdentifier')
|
||||||
|
Identifier.objects.filter(id=1).delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('analytics', '0002_add_AA_Team_Token'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [migrations.RunPython(generate_identifier, zero_identifier)
|
||||||
|
]
|
||||||
42
allianceauth/analytics/migrations/0004_auto_20211015_0502.py
Normal file
42
allianceauth/analytics/migrations/0004_auto_20211015_0502.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Generated by Django 3.1.13 on 2021-10-15 05:02
|
||||||
|
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
|
||||||
|
# Add /admin/ and /user_notifications_count/ path to ignore
|
||||||
|
|
||||||
|
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
|
||||||
|
admin = AnalyticsPath.objects.create(ignore_path=r"^\/admin\/.*")
|
||||||
|
user_notifications_count = AnalyticsPath.objects.create(ignore_path=r"^\/user_notifications_count\/.*")
|
||||||
|
|
||||||
|
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
|
||||||
|
token = Tokens.objects.get(token="UA-186249766-2")
|
||||||
|
token.ignore_paths.add(admin, user_notifications_count)
|
||||||
|
|
||||||
|
|
||||||
|
def undo_modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
|
||||||
|
#
|
||||||
|
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
|
||||||
|
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
|
||||||
|
|
||||||
|
token = Tokens.objects.get(token="UA-186249766-2")
|
||||||
|
try:
|
||||||
|
admin = AnalyticsPath.objects.get(ignore_path=r"^\/admin\/.*", analyticstokens=token)
|
||||||
|
user_notifications_count = AnalyticsPath.objects.get(ignore_path=r"^\/user_notifications_count\/.*", analyticstokens=token)
|
||||||
|
admin.delete()
|
||||||
|
user_notifications_count.delete()
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
# Its fine if it doesnt exist, we just dont want them building up when re-migrating
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('analytics', '0003_Generate_Identifier'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [migrations.RunPython(modify_aa_team_token_add_page_ignore_paths, undo_modify_aa_team_token_add_page_ignore_paths)
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-10-17 16:26
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('analytics', '0004_auto_20211015_0502'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='analyticspath',
|
||||||
|
name='ignore_path',
|
||||||
|
field=models.CharField(default='/example/', help_text='Regex Expression, If matched no Analytics Page View is sent', max_length=254),
|
||||||
|
),
|
||||||
|
]
|
||||||
40
allianceauth/analytics/migrations/0006_more_ignore_paths.py
Normal file
40
allianceauth/analytics/migrations/0006_more_ignore_paths.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-10-19 01:47
|
||||||
|
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
|
||||||
|
# Add the /account/activate path to ignore
|
||||||
|
|
||||||
|
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
|
||||||
|
account_activate = AnalyticsPath.objects.create(ignore_path=r"^\/account\/activate\/.*")
|
||||||
|
|
||||||
|
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
|
||||||
|
token = Tokens.objects.get(token="UA-186249766-2")
|
||||||
|
token.ignore_paths.add(account_activate)
|
||||||
|
|
||||||
|
|
||||||
|
def undo_modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
|
||||||
|
#
|
||||||
|
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
|
||||||
|
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
|
||||||
|
|
||||||
|
token = Tokens.objects.get(token="UA-186249766-2")
|
||||||
|
|
||||||
|
try:
|
||||||
|
account_activate = AnalyticsPath.objects.get(ignore_path=r"^\/account\/activate\/.*", analyticstokens=token)
|
||||||
|
account_activate.delete()
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
# Its fine if it doesnt exist, we just dont want them building up when re-migrating
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('analytics', '0005_alter_analyticspath_ignore_path'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(modify_aa_team_token_add_page_ignore_paths, undo_modify_aa_team_token_add_page_ignore_paths)
|
||||||
|
]
|
||||||
38
allianceauth/analytics/models.py
Normal file
38
allianceauth/analytics/models.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
|
||||||
|
class AnalyticsIdentifier(models.Model):
|
||||||
|
|
||||||
|
identifier = models.UUIDField(default=uuid4,
|
||||||
|
editable=False)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.pk and AnalyticsIdentifier.objects.exists():
|
||||||
|
# Force a single object
|
||||||
|
raise ValidationError('There is can be only one \
|
||||||
|
AnalyticsIdentifier instance')
|
||||||
|
self.pk = self.id = 1 # If this happens to be deleted and recreated, force it to be 1
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class AnalyticsPath(models.Model):
|
||||||
|
ignore_path = models.CharField(max_length=254, default="/example/", help_text="Regex Expression, If matched no Analytics Page View is sent")
|
||||||
|
|
||||||
|
|
||||||
|
class AnalyticsTokens(models.Model):
|
||||||
|
|
||||||
|
class Analytics_Type(models.TextChoices):
|
||||||
|
GA_U = 'GA-U', _('Google Analytics Universal')
|
||||||
|
GA_V4 = 'GA-V4', _('Google Analytics V4')
|
||||||
|
|
||||||
|
name = models.CharField(max_length=254)
|
||||||
|
type = models.CharField(max_length=254, choices=Analytics_Type.choices)
|
||||||
|
token = models.CharField(max_length=254, blank=False)
|
||||||
|
send_page_views = models.BooleanField(default=False)
|
||||||
|
send_celery_tasks = models.BooleanField(default=False)
|
||||||
|
send_stats = models.BooleanField(default=False)
|
||||||
|
ignore_paths = models.ManyToManyField(AnalyticsPath, blank=True)
|
||||||
55
allianceauth/analytics/signals.py
Normal file
55
allianceauth/analytics/signals.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import logging
|
||||||
|
from celery.signals import task_failure, task_success
|
||||||
|
from django.conf import settings
|
||||||
|
from allianceauth.analytics.tasks import analytics_event
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@task_failure.connect
|
||||||
|
def process_failure_signal(
|
||||||
|
exception, traceback,
|
||||||
|
sender, task_id, signal,
|
||||||
|
args, kwargs, einfo, **kw):
|
||||||
|
logger.debug("Celery task_failure signal %s" % sender.__class__.__name__)
|
||||||
|
if getattr(settings, "ANALYTICS_DISABLED", False):
|
||||||
|
return
|
||||||
|
|
||||||
|
category = sender.__module__
|
||||||
|
|
||||||
|
if 'allianceauth.analytics' not in category:
|
||||||
|
if category.endswith(".tasks"):
|
||||||
|
category = category[:-6]
|
||||||
|
|
||||||
|
action = sender.__name__
|
||||||
|
|
||||||
|
label = f"{exception.__class__.__name__}"
|
||||||
|
|
||||||
|
analytics_event(category=category,
|
||||||
|
action=action,
|
||||||
|
label=label)
|
||||||
|
|
||||||
|
|
||||||
|
@task_success.connect
|
||||||
|
def celery_success_signal(sender, result=None, **kw):
|
||||||
|
logger.debug("Celery task_success signal %s" % sender.__class__.__name__)
|
||||||
|
if getattr(settings, "ANALYTICS_DISABLED", False):
|
||||||
|
return
|
||||||
|
|
||||||
|
category = sender.__module__
|
||||||
|
|
||||||
|
if 'allianceauth.analytics' not in category:
|
||||||
|
if category.endswith(".tasks"):
|
||||||
|
category = category[:-6]
|
||||||
|
|
||||||
|
action = sender.__name__
|
||||||
|
label = "Success"
|
||||||
|
|
||||||
|
value = 0
|
||||||
|
if isinstance(result, int):
|
||||||
|
value = result
|
||||||
|
|
||||||
|
analytics_event(category=category,
|
||||||
|
action=action,
|
||||||
|
label=label,
|
||||||
|
value=value)
|
||||||
207
allianceauth/analytics/tasks.py
Normal file
207
allianceauth/analytics/tasks.py
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
from django.conf import settings
|
||||||
|
from django.apps import apps
|
||||||
|
from celery import shared_task
|
||||||
|
from allianceauth import __version__
|
||||||
|
from .models import AnalyticsTokens, AnalyticsIdentifier
|
||||||
|
from .utils import (
|
||||||
|
install_stat_addons,
|
||||||
|
install_stat_tokens,
|
||||||
|
install_stat_users)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
BASE_URL = "https://www.google-analytics.com/"
|
||||||
|
|
||||||
|
DEBUG_URL = f"{BASE_URL}debug/collect"
|
||||||
|
COLLECTION_URL = f"{BASE_URL}collect"
|
||||||
|
|
||||||
|
if getattr(settings, "ANALYTICS_ENABLE_DEBUG", False) and settings.DEBUG:
|
||||||
|
# Force sending of analytics data during in a debug/test environemt
|
||||||
|
# Usefull for developers working on this feature.
|
||||||
|
logger.warning(
|
||||||
|
"You have 'ANALYTICS_ENABLE_DEBUG' Enabled! "
|
||||||
|
"This debug instance will send analytics data!")
|
||||||
|
DEBUG_URL = COLLECTION_URL
|
||||||
|
|
||||||
|
ANALYTICS_URL = COLLECTION_URL
|
||||||
|
|
||||||
|
if settings.DEBUG is True:
|
||||||
|
ANALYTICS_URL = DEBUG_URL
|
||||||
|
|
||||||
|
|
||||||
|
def analytics_event(category: str,
|
||||||
|
action: str,
|
||||||
|
label: str,
|
||||||
|
value: int = 0,
|
||||||
|
event_type: str = 'Celery'):
|
||||||
|
"""
|
||||||
|
Send a Google Analytics Event for each token stored
|
||||||
|
Includes check for if its enabled/disabled
|
||||||
|
|
||||||
|
Args:
|
||||||
|
`category` (str): Celery Namespace
|
||||||
|
`action` (str): Task Name
|
||||||
|
`label` (str): Optional, Task Success/Exception
|
||||||
|
`value` (int): Optional, If bulk, Query size, can be a binary True/False
|
||||||
|
`event_type` (str): Optional, Celery or Stats only, Default to Celery
|
||||||
|
"""
|
||||||
|
analyticstokens = AnalyticsTokens.objects.all()
|
||||||
|
client_id = AnalyticsIdentifier.objects.get(id=1).identifier.hex
|
||||||
|
for token in analyticstokens:
|
||||||
|
if event_type == 'Celery':
|
||||||
|
allowed = token.send_celery_tasks
|
||||||
|
elif event_type == 'Stats':
|
||||||
|
allowed = token.send_stats
|
||||||
|
else:
|
||||||
|
allowed = False
|
||||||
|
|
||||||
|
if allowed is True:
|
||||||
|
tracking_id = token.token
|
||||||
|
send_ga_tracking_celery_event.s(
|
||||||
|
tracking_id=tracking_id,
|
||||||
|
client_id=client_id,
|
||||||
|
category=category,
|
||||||
|
action=action,
|
||||||
|
label=label,
|
||||||
|
value=value).apply_async(priority=9)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task()
|
||||||
|
def analytics_daily_stats():
|
||||||
|
"""Celery Task: Do not call directly
|
||||||
|
|
||||||
|
Gathers a series of daily statistics and sends analytics events containing them
|
||||||
|
"""
|
||||||
|
users = install_stat_users()
|
||||||
|
tokens = install_stat_tokens()
|
||||||
|
addons = install_stat_addons()
|
||||||
|
logger.debug("Running Daily Analytics Upload")
|
||||||
|
|
||||||
|
analytics_event(category='allianceauth.analytics',
|
||||||
|
action='send_install_stats',
|
||||||
|
label='existence',
|
||||||
|
value=1,
|
||||||
|
event_type='Stats')
|
||||||
|
analytics_event(category='allianceauth.analytics',
|
||||||
|
action='send_install_stats',
|
||||||
|
label='users',
|
||||||
|
value=users,
|
||||||
|
event_type='Stats')
|
||||||
|
analytics_event(category='allianceauth.analytics',
|
||||||
|
action='send_install_stats',
|
||||||
|
label='tokens',
|
||||||
|
value=tokens,
|
||||||
|
event_type='Stats')
|
||||||
|
analytics_event(category='allianceauth.analytics',
|
||||||
|
action='send_install_stats',
|
||||||
|
label='addons',
|
||||||
|
value=addons,
|
||||||
|
event_type='Stats')
|
||||||
|
|
||||||
|
for appconfig in apps.get_app_configs():
|
||||||
|
analytics_event(category='allianceauth.analytics',
|
||||||
|
action='send_extension_stats',
|
||||||
|
label=appconfig.label,
|
||||||
|
value=1,
|
||||||
|
event_type='Stats')
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task()
|
||||||
|
def send_ga_tracking_web_view(
|
||||||
|
tracking_id: str,
|
||||||
|
client_id: str,
|
||||||
|
page: str,
|
||||||
|
title: str,
|
||||||
|
locale: str,
|
||||||
|
useragent: str) -> requests.Response:
|
||||||
|
|
||||||
|
"""Celery Task: Do not call directly
|
||||||
|
|
||||||
|
Sends Page View events to GA, Called only via analytics.middleware
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
`tracking_id` (str): Unique Server Identifier
|
||||||
|
`client_id` (str): GA Token
|
||||||
|
`page` (str): Page Path
|
||||||
|
`title` (str): Page Title
|
||||||
|
`locale` (str): Browser Language
|
||||||
|
`useragent` (str): Browser UserAgent
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
requests.Reponse Object
|
||||||
|
"""
|
||||||
|
headers = {"User-Agent": useragent}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'v': '1',
|
||||||
|
'tid': tracking_id,
|
||||||
|
'cid': client_id,
|
||||||
|
't': 'pageview',
|
||||||
|
'dp': page,
|
||||||
|
'dt': title,
|
||||||
|
'ul': locale,
|
||||||
|
'ua': useragent,
|
||||||
|
'aip': 1,
|
||||||
|
'an': "allianceauth",
|
||||||
|
'av': __version__
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
ANALYTICS_URL, data=payload,
|
||||||
|
timeout=5, headers=headers)
|
||||||
|
logger.debug(f"Analytics Page View HTTP{response.status_code}")
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task()
|
||||||
|
def send_ga_tracking_celery_event(
|
||||||
|
tracking_id: str,
|
||||||
|
client_id: str,
|
||||||
|
category: str,
|
||||||
|
action: str,
|
||||||
|
label: str,
|
||||||
|
value: int) -> requests.Response:
|
||||||
|
"""Celery Task: Do not call directly
|
||||||
|
|
||||||
|
Sends Page View events to GA, Called only via analytics.middleware
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
`tracking_id` (str): Unique Server Identifier
|
||||||
|
`client_id` (str): GA Token
|
||||||
|
`category` (str): Celery Namespace
|
||||||
|
`action` (str): Task Name
|
||||||
|
`label` (str): Optional, Task Success/Exception
|
||||||
|
`value` (int): Optional, If bulk, Query size, can be a binary True/False
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
requests.Reponse Object
|
||||||
|
"""
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'v': '1',
|
||||||
|
'tid': tracking_id,
|
||||||
|
'cid': client_id,
|
||||||
|
't': 'event',
|
||||||
|
'ec': category,
|
||||||
|
'ea': action,
|
||||||
|
'el': label,
|
||||||
|
'ev': value,
|
||||||
|
'aip': 1,
|
||||||
|
'an': "allianceauth",
|
||||||
|
'av': __version__
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
ANALYTICS_URL, data=payload,
|
||||||
|
timeout=5, headers=headers)
|
||||||
|
logger.debug(f"Analytics Celery/Stats Event HTTP{response.status_code}")
|
||||||
|
return response
|
||||||
108
allianceauth/analytics/tests/test_integration.py
Normal file
108
allianceauth/analytics/tests/test_integration.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
from unittest.mock import patch
|
||||||
|
from urllib.parse import parse_qs
|
||||||
|
|
||||||
|
import requests_mock
|
||||||
|
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
|
from allianceauth.analytics.tasks import ANALYTICS_URL
|
||||||
|
from allianceauth.eveonline.tasks import update_character
|
||||||
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||||
|
@requests_mock.mock()
|
||||||
|
class TestAnalyticsForViews(TestCase):
|
||||||
|
@override_settings(ANALYTICS_DISABLED=False)
|
||||||
|
def test_should_run_analytics(self, requests_mocker):
|
||||||
|
# given
|
||||||
|
requests_mocker.post(ANALYTICS_URL)
|
||||||
|
user = AuthUtils.create_user("Bruce Wayne")
|
||||||
|
self.client.force_login(user)
|
||||||
|
# when
|
||||||
|
response = self.client.get("/dashboard/")
|
||||||
|
# then
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(requests_mocker.called)
|
||||||
|
|
||||||
|
@override_settings(ANALYTICS_DISABLED=True)
|
||||||
|
def test_should_not_run_analytics(self, requests_mocker):
|
||||||
|
# given
|
||||||
|
requests_mocker.post(ANALYTICS_URL)
|
||||||
|
user = AuthUtils.create_user("Bruce Wayne")
|
||||||
|
self.client.force_login(user)
|
||||||
|
# when
|
||||||
|
response = self.client.get("/dashboard/")
|
||||||
|
# then
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertFalse(requests_mocker.called)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||||
|
@requests_mock.mock()
|
||||||
|
class TestAnalyticsForTasks(TestCase):
|
||||||
|
@override_settings(ANALYTICS_DISABLED=False)
|
||||||
|
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
|
||||||
|
def test_should_run_analytics_for_successful_task(
|
||||||
|
self, requests_mocker, mock_update_character
|
||||||
|
):
|
||||||
|
# given
|
||||||
|
requests_mocker.post(ANALYTICS_URL)
|
||||||
|
user = AuthUtils.create_user("Bruce Wayne")
|
||||||
|
character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
|
||||||
|
# when
|
||||||
|
update_character.delay(character.character_id)
|
||||||
|
# then
|
||||||
|
self.assertTrue(mock_update_character.called)
|
||||||
|
self.assertTrue(requests_mocker.called)
|
||||||
|
payload = parse_qs(requests_mocker.last_request.text)
|
||||||
|
self.assertListEqual(payload["el"], ["Success"])
|
||||||
|
|
||||||
|
@override_settings(ANALYTICS_DISABLED=True)
|
||||||
|
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
|
||||||
|
def test_should_not_run_analytics_for_successful_task(
|
||||||
|
self, requests_mocker, mock_update_character
|
||||||
|
):
|
||||||
|
# given
|
||||||
|
requests_mocker.post(ANALYTICS_URL)
|
||||||
|
user = AuthUtils.create_user("Bruce Wayne")
|
||||||
|
character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
|
||||||
|
# when
|
||||||
|
update_character.delay(character.character_id)
|
||||||
|
# then
|
||||||
|
self.assertTrue(mock_update_character.called)
|
||||||
|
self.assertFalse(requests_mocker.called)
|
||||||
|
|
||||||
|
@override_settings(ANALYTICS_DISABLED=False)
|
||||||
|
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
|
||||||
|
def test_should_run_analytics_for_failed_task(
|
||||||
|
self, requests_mocker, mock_update_character
|
||||||
|
):
|
||||||
|
# given
|
||||||
|
requests_mocker.post(ANALYTICS_URL)
|
||||||
|
mock_update_character.side_effect = RuntimeError
|
||||||
|
user = AuthUtils.create_user("Bruce Wayne")
|
||||||
|
character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
|
||||||
|
# when
|
||||||
|
update_character.delay(character.character_id)
|
||||||
|
# then
|
||||||
|
self.assertTrue(mock_update_character.called)
|
||||||
|
self.assertTrue(requests_mocker.called)
|
||||||
|
payload = parse_qs(requests_mocker.last_request.text)
|
||||||
|
self.assertNotEqual(payload["el"], ["Success"])
|
||||||
|
|
||||||
|
@override_settings(ANALYTICS_DISABLED=True)
|
||||||
|
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
|
||||||
|
def test_should_not_run_analytics_for_failed_task(
|
||||||
|
self, requests_mocker, mock_update_character
|
||||||
|
):
|
||||||
|
# given
|
||||||
|
requests_mocker.post(ANALYTICS_URL)
|
||||||
|
mock_update_character.side_effect = RuntimeError
|
||||||
|
user = AuthUtils.create_user("Bruce Wayne")
|
||||||
|
character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
|
||||||
|
# when
|
||||||
|
update_character.delay(character.character_id)
|
||||||
|
# then
|
||||||
|
self.assertTrue(mock_update_character.called)
|
||||||
|
self.assertFalse(requests_mocker.called)
|
||||||
23
allianceauth/analytics/tests/test_middleware.py
Normal file
23
allianceauth/analytics/tests/test_middleware.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from allianceauth.analytics.middleware import AnalyticsMiddleware
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
from django.test.testcases import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestAnalyticsMiddleware(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.middleware = AnalyticsMiddleware()
|
||||||
|
self.request = Mock()
|
||||||
|
self.request.headers = {
|
||||||
|
"User-Agent": "AUTOMATED TEST"
|
||||||
|
}
|
||||||
|
self.request.path = '/testURL/'
|
||||||
|
self.request.session = {}
|
||||||
|
self.request.LANGUAGE_CODE = 'en'
|
||||||
|
self.response = Mock()
|
||||||
|
self.response.content = 'hello world'
|
||||||
|
|
||||||
|
def test_middleware(self):
|
||||||
|
response = self.middleware.process_response(self.request, self.response)
|
||||||
|
self.assertEqual(self.response, response)
|
||||||
26
allianceauth/analytics/tests/test_models.py
Normal file
26
allianceauth/analytics/tests/test_models.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from allianceauth.analytics.models import AnalyticsIdentifier
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
from django.test.testcases import TestCase
|
||||||
|
|
||||||
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
|
|
||||||
|
# Identifiers
|
||||||
|
uuid_1 = "ab33e241fbf042b6aa77c7655a768af7"
|
||||||
|
uuid_2 = "7aa6bd70701f44729af5e3095ff4b55c"
|
||||||
|
|
||||||
|
|
||||||
|
class TestAnalyticsIdentifier(TestCase):
|
||||||
|
|
||||||
|
def test_identifier_random(self):
|
||||||
|
self.assertNotEqual(AnalyticsIdentifier.objects.get(), uuid4)
|
||||||
|
|
||||||
|
def test_identifier_singular(self):
|
||||||
|
AnalyticsIdentifier.objects.all().delete()
|
||||||
|
AnalyticsIdentifier.objects.create(identifier=uuid_1)
|
||||||
|
# Yeah i have multiple asserts here, they all do the same thing
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
AnalyticsIdentifier.objects.create(identifier=uuid_2)
|
||||||
|
self.assertEqual(AnalyticsIdentifier.objects.count(), 1)
|
||||||
|
self.assertEqual(AnalyticsIdentifier.objects.get(pk=1).identifier, UUID(uuid_1))
|
||||||
119
allianceauth/analytics/tests/test_tasks.py
Normal file
119
allianceauth/analytics/tests/test_tasks.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
from allianceauth.analytics.tasks import (
|
||||||
|
analytics_event,
|
||||||
|
send_ga_tracking_celery_event,
|
||||||
|
send_ga_tracking_web_view)
|
||||||
|
from django.test.testcases import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestAnalyticsTasks(TestCase):
|
||||||
|
def test_analytics_event(self):
|
||||||
|
analytics_event(
|
||||||
|
category='allianceauth.analytics',
|
||||||
|
action='send_tests',
|
||||||
|
label='test',
|
||||||
|
value=1,
|
||||||
|
event_type='Stats')
|
||||||
|
|
||||||
|
def test_send_ga_tracking_web_view_sent(self):
|
||||||
|
# This test sends if the event SENDS to google
|
||||||
|
# Not if it was successful
|
||||||
|
tracking_id = 'UA-186249766-2'
|
||||||
|
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||||
|
page = '/index/'
|
||||||
|
title = 'Hello World'
|
||||||
|
locale = 'en'
|
||||||
|
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
||||||
|
response = send_ga_tracking_web_view(
|
||||||
|
tracking_id,
|
||||||
|
client_id,
|
||||||
|
page,
|
||||||
|
title,
|
||||||
|
locale,
|
||||||
|
useragent)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_send_ga_tracking_web_view_success(self):
|
||||||
|
tracking_id = 'UA-186249766-2'
|
||||||
|
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||||
|
page = '/index/'
|
||||||
|
title = 'Hello World'
|
||||||
|
locale = 'en'
|
||||||
|
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
||||||
|
json_response = send_ga_tracking_web_view(
|
||||||
|
tracking_id,
|
||||||
|
client_id,
|
||||||
|
page,
|
||||||
|
title,
|
||||||
|
locale,
|
||||||
|
useragent).json()
|
||||||
|
self.assertTrue(json_response["hitParsingResult"][0]["valid"])
|
||||||
|
|
||||||
|
def test_send_ga_tracking_web_view_invalid_token(self):
|
||||||
|
tracking_id = 'UA-IntentionallyBadTrackingID-2'
|
||||||
|
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||||
|
page = '/index/'
|
||||||
|
title = 'Hello World'
|
||||||
|
locale = 'en'
|
||||||
|
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
||||||
|
json_response = send_ga_tracking_web_view(
|
||||||
|
tracking_id,
|
||||||
|
client_id,
|
||||||
|
page,
|
||||||
|
title,
|
||||||
|
locale,
|
||||||
|
useragent).json()
|
||||||
|
self.assertFalse(json_response["hitParsingResult"][0]["valid"])
|
||||||
|
self.assertEqual(json_response["hitParsingResult"][0]["parserMessage"][1]["description"], "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.")
|
||||||
|
|
||||||
|
# [{'valid': False, 'parserMessage': [{'messageType': 'INFO', 'description': 'IP Address from this hit was anonymized to 1.132.110.0.', 'messageCode': 'VALUE_MODIFIED'}, {'messageType': 'ERROR', 'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.", 'messageCode': 'VALUE_INVALID', 'parameter': 'tid'}], 'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'}]
|
||||||
|
|
||||||
|
def test_send_ga_tracking_celery_event_sent(self):
|
||||||
|
tracking_id = 'UA-186249766-2'
|
||||||
|
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||||
|
category = 'test'
|
||||||
|
action = 'test'
|
||||||
|
label = 'test'
|
||||||
|
value = '1'
|
||||||
|
response = send_ga_tracking_celery_event(
|
||||||
|
tracking_id,
|
||||||
|
client_id,
|
||||||
|
category,
|
||||||
|
action,
|
||||||
|
label,
|
||||||
|
value)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_send_ga_tracking_celery_event_success(self):
|
||||||
|
tracking_id = 'UA-186249766-2'
|
||||||
|
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||||
|
category = 'test'
|
||||||
|
action = 'test'
|
||||||
|
label = 'test'
|
||||||
|
value = '1'
|
||||||
|
json_response = send_ga_tracking_celery_event(
|
||||||
|
tracking_id,
|
||||||
|
client_id,
|
||||||
|
category,
|
||||||
|
action,
|
||||||
|
label,
|
||||||
|
value).json()
|
||||||
|
self.assertTrue(json_response["hitParsingResult"][0]["valid"])
|
||||||
|
|
||||||
|
def test_send_ga_tracking_celery_event_invalid_token(self):
|
||||||
|
tracking_id = 'UA-IntentionallyBadTrackingID-2'
|
||||||
|
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||||
|
category = 'test'
|
||||||
|
action = 'test'
|
||||||
|
label = 'test'
|
||||||
|
value = '1'
|
||||||
|
json_response = send_ga_tracking_celery_event(
|
||||||
|
tracking_id,
|
||||||
|
client_id,
|
||||||
|
category,
|
||||||
|
action,
|
||||||
|
label,
|
||||||
|
value).json()
|
||||||
|
self.assertFalse(json_response["hitParsingResult"][0]["valid"])
|
||||||
|
self.assertEqual(json_response["hitParsingResult"][0]["parserMessage"][1]["description"], "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.")
|
||||||
|
|
||||||
|
# [{'valid': False, 'parserMessage': [{'messageType': 'INFO', 'description': 'IP Address from this hit was anonymized to 1.132.110.0.', 'messageCode': 'VALUE_MODIFIED'}, {'messageType': 'ERROR', 'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.", 'messageCode': 'VALUE_INVALID', 'parameter': 'tid'}], 'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'}]
|
||||||
55
allianceauth/analytics/tests/test_utils.py
Normal file
55
allianceauth/analytics/tests/test_utils.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from django.apps import apps
|
||||||
|
from allianceauth.authentication.models import User
|
||||||
|
from esi.models import Token
|
||||||
|
from allianceauth.analytics.utils import install_stat_users, install_stat_tokens, install_stat_addons
|
||||||
|
|
||||||
|
from django.test.testcases import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
def create_testdata():
|
||||||
|
User.objects.all().delete()
|
||||||
|
User.objects.create_user(
|
||||||
|
'user_1'
|
||||||
|
'abc@example.com',
|
||||||
|
'password'
|
||||||
|
)
|
||||||
|
User.objects.create_user(
|
||||||
|
'user_2'
|
||||||
|
'abc@example.com',
|
||||||
|
'password'
|
||||||
|
)
|
||||||
|
#Token.objects.all().delete()
|
||||||
|
#Token.objects.create(
|
||||||
|
# character_id=101,
|
||||||
|
# character_name='character1',
|
||||||
|
# access_token='my_access_token'
|
||||||
|
#)
|
||||||
|
#Token.objects.create(
|
||||||
|
# character_id=102,
|
||||||
|
# character_name='character2',
|
||||||
|
# access_token='my_access_token'
|
||||||
|
#)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAnalyticsUtils(TestCase):
|
||||||
|
|
||||||
|
def test_install_stat_users(self):
|
||||||
|
create_testdata()
|
||||||
|
expected = 2
|
||||||
|
|
||||||
|
users = install_stat_users()
|
||||||
|
self.assertEqual(users, expected)
|
||||||
|
|
||||||
|
#def test_install_stat_tokens(self):
|
||||||
|
# create_testdata()
|
||||||
|
# expected = 2
|
||||||
|
#
|
||||||
|
# tokens = install_stat_tokens()
|
||||||
|
# self.assertEqual(tokens, expected)
|
||||||
|
|
||||||
|
def test_install_stat_addons(self):
|
||||||
|
# this test does what its testing...
|
||||||
|
# but helpful for existing as a sanity check
|
||||||
|
expected = len(list(apps.get_app_configs()))
|
||||||
|
addons = install_stat_addons()
|
||||||
|
self.assertEqual(addons, expected)
|
||||||
36
allianceauth/analytics/utils.py
Normal file
36
allianceauth/analytics/utils.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from django.apps import apps
|
||||||
|
from allianceauth.authentication.models import User
|
||||||
|
from esi.models import Token
|
||||||
|
|
||||||
|
|
||||||
|
def install_stat_users() -> int:
|
||||||
|
"""Count and Return the number of User accounts
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int
|
||||||
|
The Number of User objects"""
|
||||||
|
users = User.objects.count()
|
||||||
|
return users
|
||||||
|
|
||||||
|
|
||||||
|
def install_stat_tokens() -> int:
|
||||||
|
"""Count and Return the number of ESI Tokens Stored
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int
|
||||||
|
The Number of Token Objects"""
|
||||||
|
tokens = Token.objects.count()
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
|
def install_stat_addons() -> int:
|
||||||
|
"""Count and Return the number of Django Applications Installed
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int
|
||||||
|
The Number of Installed Apps"""
|
||||||
|
addons = len(list(apps.get_app_configs()))
|
||||||
|
return addons
|
||||||
@@ -1,15 +1,44 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||||
from django.contrib.auth.models import User as BaseUser, Permission as BasePermission
|
from django.contrib.auth.models import Group
|
||||||
from django.utils.text import slugify
|
from django.contrib.auth.models import Permission as BasePermission
|
||||||
from django.db.models import Q
|
from django.contrib.auth.models import User as BaseUser
|
||||||
from allianceauth.services.hooks import ServicesHook
|
from django.db.models import Count, Q
|
||||||
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
|
from django.db.models.functions import Lower
|
||||||
|
from django.db.models.signals import (
|
||||||
|
m2m_changed,
|
||||||
|
post_delete,
|
||||||
|
post_save,
|
||||||
|
pre_delete,
|
||||||
|
pre_save
|
||||||
|
)
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from allianceauth.authentication.models import State, get_guest_state, CharacterOwnership, UserProfile, OwnershipRecord
|
from django.urls import reverse
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.text import slugify
|
||||||
|
|
||||||
|
from allianceauth.authentication.models import (
|
||||||
|
CharacterOwnership,
|
||||||
|
OwnershipRecord,
|
||||||
|
State,
|
||||||
|
UserProfile,
|
||||||
|
get_guest_state
|
||||||
|
)
|
||||||
|
from allianceauth.eveonline.models import (
|
||||||
|
EveAllianceInfo,
|
||||||
|
EveCharacter,
|
||||||
|
EveCorporationInfo,
|
||||||
|
EveFactionInfo
|
||||||
|
)
|
||||||
|
from allianceauth.eveonline.tasks import update_character
|
||||||
from allianceauth.hooks import get_hooks
|
from allianceauth.hooks import get_hooks
|
||||||
from allianceauth.eveonline.models import EveCharacter
|
from allianceauth.services.hooks import ServicesHook
|
||||||
from django.forms import ModelForm
|
|
||||||
|
from .app_settings import (
|
||||||
|
AUTHENTICATION_ADMIN_USERS_MAX_CHARS,
|
||||||
|
AUTHENTICATION_ADMIN_USERS_MAX_GROUPS
|
||||||
|
)
|
||||||
|
from .forms import UserChangeForm, UserProfileForm
|
||||||
|
|
||||||
|
|
||||||
def make_service_hooks_update_groups_action(service):
|
def make_service_hooks_update_groups_action(service):
|
||||||
@@ -19,11 +48,14 @@ def make_service_hooks_update_groups_action(service):
|
|||||||
:return: fn to update services groups for the selected users
|
:return: fn to update services groups for the selected users
|
||||||
"""
|
"""
|
||||||
def update_service_groups(modeladmin, request, queryset):
|
def update_service_groups(modeladmin, request, queryset):
|
||||||
for user in queryset: # queryset filtering doesn't work here?
|
if hasattr(service, 'update_groups_bulk'):
|
||||||
service.update_groups(user)
|
service.update_groups_bulk(queryset)
|
||||||
|
else:
|
||||||
|
for user in queryset: # queryset filtering doesn't work here?
|
||||||
|
service.update_groups(user)
|
||||||
|
|
||||||
update_service_groups.__name__ = str('update_{}_groups'.format(slugify(service.name)))
|
update_service_groups.__name__ = str(f'update_{slugify(service.name)}_groups')
|
||||||
update_service_groups.short_description = "Sync groups for selected {} accounts".format(service.title)
|
update_service_groups.short_description = f"Sync groups for selected {service.title} accounts"
|
||||||
return update_service_groups
|
return update_service_groups
|
||||||
|
|
||||||
|
|
||||||
@@ -34,27 +66,21 @@ def make_service_hooks_sync_nickname_action(service):
|
|||||||
:return: fn to sync nickname for the selected users
|
:return: fn to sync nickname for the selected users
|
||||||
"""
|
"""
|
||||||
def sync_nickname(modeladmin, request, queryset):
|
def sync_nickname(modeladmin, request, queryset):
|
||||||
for user in queryset: # queryset filtering doesn't work here?
|
if hasattr(service, 'sync_nicknames_bulk'):
|
||||||
service.sync_nickname(user)
|
service.sync_nicknames_bulk(queryset)
|
||||||
|
else:
|
||||||
|
for user in queryset: # queryset filtering doesn't work here?
|
||||||
|
service.sync_nickname(user)
|
||||||
|
|
||||||
sync_nickname.__name__ = str('sync_{}_nickname'.format(slugify(service.name)))
|
sync_nickname.__name__ = str(f'sync_{slugify(service.name)}_nickname')
|
||||||
sync_nickname.short_description = "Sync nicknames for selected {} accounts".format(service.title)
|
sync_nickname.short_description = f"Sync nicknames for selected {service.title} accounts"
|
||||||
return sync_nickname
|
return sync_nickname
|
||||||
|
|
||||||
|
|
||||||
class QuerysetModelForm(ModelForm):
|
|
||||||
# allows specifying FK querysets through kwarg
|
|
||||||
def __init__(self, querysets=None, *args, **kwargs):
|
|
||||||
querysets = querysets or {}
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
for field, qs in querysets.items():
|
|
||||||
self.fields[field].queryset = qs
|
|
||||||
|
|
||||||
|
|
||||||
class UserProfileInline(admin.StackedInline):
|
class UserProfileInline(admin.StackedInline):
|
||||||
model = UserProfile
|
model = UserProfile
|
||||||
readonly_fields = ('state',)
|
readonly_fields = ('state',)
|
||||||
form = QuerysetModelForm
|
form = UserProfileForm
|
||||||
verbose_name = ''
|
verbose_name = ''
|
||||||
verbose_name_plural = 'Profile'
|
verbose_name_plural = 'Profile'
|
||||||
|
|
||||||
@@ -68,7 +94,6 @@ class UserProfileInline(admin.StackedInline):
|
|||||||
query |= Q(userprofile__isnull=True)
|
query |= Q(userprofile__isnull=True)
|
||||||
else:
|
else:
|
||||||
query |= Q(character_ownership__user=obj)
|
query |= Q(character_ownership__user=obj)
|
||||||
qs = EveCharacter.objects.filter(query)
|
|
||||||
formset = super().get_formset(request, obj=obj, **kwargs)
|
formset = super().get_formset(request, obj=obj, **kwargs)
|
||||||
|
|
||||||
def get_kwargs(self, index):
|
def get_kwargs(self, index):
|
||||||
@@ -76,47 +101,318 @@ class UserProfileInline(admin.StackedInline):
|
|||||||
formset.get_form_kwargs = get_kwargs
|
formset.get_form_kwargs = get_kwargs
|
||||||
return formset
|
return formset
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
def has_add_permission(self, request, obj=None):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def has_delete_permission(self, request, obj=None):
|
def has_delete_permission(self, request, obj=None):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@admin.display(description="")
|
||||||
|
def user_profile_pic(obj):
|
||||||
|
"""profile pic column data for user objects
|
||||||
|
|
||||||
|
works for both User objects and objects with `user` as FK to User
|
||||||
|
To be used for all user based admin lists (requires CSS)
|
||||||
|
"""
|
||||||
|
user_obj = obj.user if hasattr(obj, 'user') else obj
|
||||||
|
if user_obj.profile.main_character:
|
||||||
|
return format_html(
|
||||||
|
'<img src="{}" class="img-circle">',
|
||||||
|
user_obj.profile.main_character.portrait_url(size=32)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@admin.display(description="user / main", ordering="username")
|
||||||
|
def user_username(obj):
|
||||||
|
"""user column data for user objects
|
||||||
|
|
||||||
|
works for both User objects and objects with `user` as FK to User
|
||||||
|
To be used for all user based admin lists
|
||||||
|
"""
|
||||||
|
link = reverse(
|
||||||
|
'admin:{}_{}_change'.format(
|
||||||
|
obj._meta.app_label,
|
||||||
|
type(obj).__name__.lower()
|
||||||
|
),
|
||||||
|
args=(obj.pk,)
|
||||||
|
)
|
||||||
|
user_obj = obj.user if hasattr(obj, 'user') else obj
|
||||||
|
if user_obj.profile.main_character:
|
||||||
|
return format_html(
|
||||||
|
'<strong><a href="{}">{}</a></strong><br>{}',
|
||||||
|
link,
|
||||||
|
user_obj.username,
|
||||||
|
user_obj.profile.main_character.character_name
|
||||||
|
)
|
||||||
|
return format_html(
|
||||||
|
'<strong><a href="{}">{}</a></strong>',
|
||||||
|
link,
|
||||||
|
user_obj.username,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.display(
|
||||||
|
description="Corporation / Alliance (Main)",
|
||||||
|
ordering="profile__main_character__corporation_name"
|
||||||
|
)
|
||||||
|
def user_main_organization(obj):
|
||||||
|
"""main organization column data for user objects
|
||||||
|
|
||||||
|
works for both User objects and objects with `user` as FK to User
|
||||||
|
To be used for all user based admin lists
|
||||||
|
"""
|
||||||
|
user_obj = obj.user if hasattr(obj, 'user') else obj
|
||||||
|
if not user_obj.profile.main_character:
|
||||||
|
return ''
|
||||||
|
result = user_obj.profile.main_character.corporation_name
|
||||||
|
if user_obj.profile.main_character.alliance_id:
|
||||||
|
result += f'<br>{user_obj.profile.main_character.alliance_name}'
|
||||||
|
elif user_obj.profile.main_character.faction_name:
|
||||||
|
result += f'<br>{user_obj.profile.main_character.faction_name}'
|
||||||
|
return format_html(result)
|
||||||
|
|
||||||
|
|
||||||
|
class MainCorporationsFilter(admin.SimpleListFilter):
|
||||||
|
"""Custom filter to filter on corporations from mains only
|
||||||
|
|
||||||
|
works for both User objects and objects with `user` as FK to User
|
||||||
|
To be used for all user based admin lists
|
||||||
|
"""
|
||||||
|
title = 'corporation'
|
||||||
|
parameter_name = 'main_corporation_id__exact'
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
qs = EveCharacter.objects\
|
||||||
|
.exclude(userprofile=None)\
|
||||||
|
.values('corporation_id', 'corporation_name')\
|
||||||
|
.distinct()\
|
||||||
|
.order_by(Lower('corporation_name'))
|
||||||
|
return tuple(
|
||||||
|
(x['corporation_id'], x['corporation_name']) for x in qs
|
||||||
|
)
|
||||||
|
|
||||||
|
def queryset(self, request, qs):
|
||||||
|
if self.value() is None:
|
||||||
|
return qs.all()
|
||||||
|
if qs.model == User:
|
||||||
|
return qs.filter(
|
||||||
|
profile__main_character__corporation_id=self.value()
|
||||||
|
)
|
||||||
|
return qs.filter(
|
||||||
|
user__profile__main_character__corporation_id=self.value()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MainAllianceFilter(admin.SimpleListFilter):
|
||||||
|
"""Custom filter to filter on alliances from mains only
|
||||||
|
|
||||||
|
works for both User objects and objects with `user` as FK to User
|
||||||
|
To be used for all user based admin lists
|
||||||
|
"""
|
||||||
|
title = 'alliance'
|
||||||
|
parameter_name = 'main_alliance_id__exact'
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
qs = (
|
||||||
|
EveCharacter.objects
|
||||||
|
.exclude(alliance_id=None)
|
||||||
|
.exclude(userprofile=None)
|
||||||
|
.values('alliance_id', 'alliance_name')
|
||||||
|
.distinct()
|
||||||
|
.order_by(Lower('alliance_name'))
|
||||||
|
)
|
||||||
|
return tuple(
|
||||||
|
(x['alliance_id'], x['alliance_name']) for x in qs
|
||||||
|
)
|
||||||
|
|
||||||
|
def queryset(self, request, qs):
|
||||||
|
if self.value() is None:
|
||||||
|
return qs.all()
|
||||||
|
if qs.model == User:
|
||||||
|
return qs.filter(profile__main_character__alliance_id=self.value())
|
||||||
|
return qs.filter(
|
||||||
|
user__profile__main_character__alliance_id=self.value()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MainFactionFilter(admin.SimpleListFilter):
|
||||||
|
"""Custom filter to filter on factions from mains only
|
||||||
|
|
||||||
|
works for both User objects and objects with `user` as FK to User
|
||||||
|
To be used for all user based admin lists
|
||||||
|
"""
|
||||||
|
title = 'faction'
|
||||||
|
parameter_name = 'main_faction_id__exact'
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
qs = (
|
||||||
|
EveCharacter.objects
|
||||||
|
.exclude(faction_id=None)
|
||||||
|
.exclude(userprofile=None)
|
||||||
|
.values('faction_id', 'faction_name')
|
||||||
|
.distinct()
|
||||||
|
.order_by(Lower('faction_name'))
|
||||||
|
)
|
||||||
|
return tuple(
|
||||||
|
(x['faction_id'], x['faction_name']) for x in qs
|
||||||
|
)
|
||||||
|
|
||||||
|
def queryset(self, request, qs):
|
||||||
|
if self.value() is None:
|
||||||
|
return qs.all()
|
||||||
|
if qs.model == User:
|
||||||
|
return qs.filter(profile__main_character__faction_id=self.value())
|
||||||
|
return qs.filter(
|
||||||
|
user__profile__main_character__faction_id=self.value()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.display(description="Update main character model from ESI")
|
||||||
|
def update_main_character_model(modeladmin, request, queryset):
|
||||||
|
tasks_count = 0
|
||||||
|
for obj in queryset:
|
||||||
|
if obj.profile.main_character:
|
||||||
|
update_character.delay(obj.profile.main_character.character_id)
|
||||||
|
tasks_count += 1
|
||||||
|
|
||||||
|
modeladmin.message_user(
|
||||||
|
request, f'Update from ESI started for {tasks_count} characters'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserAdmin(BaseUserAdmin):
|
class UserAdmin(BaseUserAdmin):
|
||||||
|
"""Extending Django's UserAdmin model
|
||||||
|
|
||||||
|
Behavior of groups and characters columns can be configured via settings
|
||||||
"""
|
"""
|
||||||
Extending Django's UserAdmin model
|
|
||||||
"""
|
inlines = BaseUserAdmin.inlines + [UserProfileInline]
|
||||||
|
ordering = ('username', )
|
||||||
|
list_select_related = ('profile__state', 'profile__main_character')
|
||||||
|
show_full_result_count = True
|
||||||
|
list_display = (
|
||||||
|
user_profile_pic,
|
||||||
|
user_username,
|
||||||
|
'_state',
|
||||||
|
'_groups',
|
||||||
|
user_main_organization,
|
||||||
|
'_characters',
|
||||||
|
'is_active',
|
||||||
|
'date_joined',
|
||||||
|
'_role'
|
||||||
|
)
|
||||||
|
list_display_links = None
|
||||||
|
list_filter = (
|
||||||
|
'profile__state',
|
||||||
|
'groups',
|
||||||
|
MainCorporationsFilter,
|
||||||
|
MainAllianceFilter,
|
||||||
|
MainFactionFilter,
|
||||||
|
'is_active',
|
||||||
|
'date_joined',
|
||||||
|
'is_staff',
|
||||||
|
'is_superuser'
|
||||||
|
)
|
||||||
|
search_fields = ('username', 'character_ownerships__character__character_name')
|
||||||
|
readonly_fields = ('date_joined', 'last_login')
|
||||||
|
filter_horizontal = ('groups', 'user_permissions',)
|
||||||
|
form = UserChangeForm
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = {
|
||||||
|
"all": ("authentication/css/admin.css",)
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
qs = super().get_queryset(request)
|
||||||
|
return qs.prefetch_related("character_ownerships__character", "groups")
|
||||||
|
|
||||||
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
|
"""Inject current request into change form object."""
|
||||||
|
|
||||||
|
MyForm = super().get_form(request, obj, **kwargs)
|
||||||
|
if obj:
|
||||||
|
class MyFormInjected(MyForm):
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
kwargs['request'] = request
|
||||||
|
return MyForm(*args, **kwargs)
|
||||||
|
|
||||||
|
return MyFormInjected
|
||||||
|
return MyForm
|
||||||
|
|
||||||
def get_actions(self, request):
|
def get_actions(self, request):
|
||||||
actions = super(BaseUserAdmin, self).get_actions(request)
|
actions = super().get_actions(request)
|
||||||
|
actions[update_main_character_model.__name__] = (
|
||||||
|
update_main_character_model,
|
||||||
|
update_main_character_model.__name__,
|
||||||
|
update_main_character_model.short_description
|
||||||
|
)
|
||||||
|
|
||||||
for hook in get_hooks('services_hook'):
|
for hook in get_hooks('services_hook'):
|
||||||
svc = hook()
|
svc = hook()
|
||||||
# Check update_groups is redefined/overloaded
|
# Check update_groups is redefined/overloaded
|
||||||
if svc.update_groups.__module__ != ServicesHook.update_groups.__module__:
|
if svc.update_groups.__module__ != ServicesHook.update_groups.__module__:
|
||||||
action = make_service_hooks_update_groups_action(svc)
|
action = make_service_hooks_update_groups_action(svc)
|
||||||
actions[action.__name__] = (action,
|
actions[action.__name__] = (
|
||||||
action.__name__,
|
action,
|
||||||
action.short_description)
|
action.__name__,
|
||||||
|
action.short_description
|
||||||
|
)
|
||||||
|
|
||||||
# Create sync nickname action if service implements it
|
# Create sync nickname action if service implements it
|
||||||
if svc.sync_nickname.__module__ != ServicesHook.sync_nickname.__module__:
|
if svc.sync_nickname.__module__ != ServicesHook.sync_nickname.__module__:
|
||||||
action = make_service_hooks_sync_nickname_action(svc)
|
action = make_service_hooks_sync_nickname_action(svc)
|
||||||
actions[action.__name__] = (action,
|
actions[action.__name__] = (
|
||||||
action.__name__,
|
action, action.__name__,
|
||||||
action.short_description)
|
action.short_description
|
||||||
|
)
|
||||||
return actions
|
return actions
|
||||||
list_filter = BaseUserAdmin.list_filter + ('profile__state',)
|
|
||||||
inlines = BaseUserAdmin.inlines + [UserProfileInline]
|
|
||||||
list_display = ('username', 'email', 'get_main_character', 'get_state', 'is_active')
|
|
||||||
|
|
||||||
def get_main_character(self, obj):
|
def _list_2_html_w_tooltips(self, my_items: list, max_items: int) -> str:
|
||||||
return obj.profile.main_character
|
"""converts list of strings into HTML with cutoff and tooltip"""
|
||||||
get_main_character.short_description = "Main Character"
|
items_truncated_str = ', '.join(my_items[:max_items])
|
||||||
|
if not my_items:
|
||||||
|
result = None
|
||||||
|
elif len(my_items) <= max_items:
|
||||||
|
result = items_truncated_str
|
||||||
|
else:
|
||||||
|
items_truncated_str += ', (...)'
|
||||||
|
items_all_str = ', '.join(my_items)
|
||||||
|
result = format_html(
|
||||||
|
'<span data-tooltip="{}" class="tooltip">{}</span>',
|
||||||
|
items_all_str,
|
||||||
|
items_truncated_str
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
def get_state(self, obj):
|
def _characters(self, obj):
|
||||||
return obj.profile.state
|
character_ownerships = list(obj.character_ownerships.all())
|
||||||
get_state.short_description = "State"
|
characters = [obj.character.character_name for obj in character_ownerships]
|
||||||
|
return self._list_2_html_w_tooltips(
|
||||||
|
sorted(characters),
|
||||||
|
AUTHENTICATION_ADMIN_USERS_MAX_CHARS
|
||||||
|
)
|
||||||
|
|
||||||
|
@admin.display(ordering="profile__state")
|
||||||
|
def _state(self, obj):
|
||||||
|
return obj.profile.state.name
|
||||||
|
|
||||||
|
def _groups(self, obj):
|
||||||
|
my_groups = sorted(group.name for group in list(obj.groups.all()))
|
||||||
|
return self._list_2_html_w_tooltips(
|
||||||
|
my_groups, AUTHENTICATION_ADMIN_USERS_MAX_GROUPS
|
||||||
|
)
|
||||||
|
|
||||||
|
def _role(self, obj):
|
||||||
|
if obj.is_superuser:
|
||||||
|
role = 'Superuser'
|
||||||
|
elif obj.is_staff:
|
||||||
|
role = 'Staff'
|
||||||
|
else:
|
||||||
|
role = 'User'
|
||||||
|
return role
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
def has_change_permission(self, request, obj=None):
|
||||||
return request.user.has_perm('auth.change_user')
|
return request.user.has_perm('auth.change_user')
|
||||||
@@ -127,24 +423,86 @@ class UserAdmin(BaseUserAdmin):
|
|||||||
def has_delete_permission(self, request, obj=None):
|
def has_delete_permission(self, request, obj=None):
|
||||||
return request.user.has_perm('auth.delete_user')
|
return request.user.has_perm('auth.delete_user')
|
||||||
|
|
||||||
|
def get_object(self, *args , **kwargs):
|
||||||
|
obj = super().get_object(*args , **kwargs)
|
||||||
|
self.obj = obj # storing current object for use in formfield_for_manytomany
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
||||||
|
if db_field.name == "groups":
|
||||||
|
groups_qs = Group.objects.filter(authgroup__states__isnull=True)
|
||||||
|
obj_state = self.obj.profile.state
|
||||||
|
if obj_state:
|
||||||
|
matching_groups_qs = Group.objects.filter(authgroup__states=obj_state)
|
||||||
|
groups_qs = groups_qs | matching_groups_qs
|
||||||
|
kwargs["queryset"] = groups_qs.order_by(Lower("name"))
|
||||||
|
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
||||||
|
|
||||||
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
if obj and not request.user.is_superuser:
|
||||||
|
return self.readonly_fields + (
|
||||||
|
"is_staff", "is_superuser", "user_permissions"
|
||||||
|
)
|
||||||
|
return self.readonly_fields
|
||||||
|
|
||||||
|
|
||||||
@admin.register(State)
|
@admin.register(State)
|
||||||
class StateAdmin(admin.ModelAdmin):
|
class StateAdmin(admin.ModelAdmin):
|
||||||
|
list_select_related = True
|
||||||
|
list_display = ('name', 'priority', '_user_count')
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
qs = super().get_queryset(request)
|
||||||
|
return qs.annotate(user_count=Count("userprofile__id"))
|
||||||
|
|
||||||
|
@admin.display(description="Users", ordering="user_count")
|
||||||
|
def _user_count(self, obj):
|
||||||
|
return obj.user_count
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {
|
(None, {
|
||||||
'fields': ('name', 'permissions', 'priority'),
|
'fields': ('name', 'permissions', 'priority'),
|
||||||
}),
|
}),
|
||||||
('Membership', {
|
('Membership', {
|
||||||
'fields': ('public', 'member_characters', 'member_corporations', 'member_alliances'),
|
'fields': (
|
||||||
|
'public',
|
||||||
|
'member_characters',
|
||||||
|
'member_corporations',
|
||||||
|
'member_alliances',
|
||||||
|
'member_factions'
|
||||||
|
),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
filter_horizontal = ['member_characters', 'member_corporations', 'member_alliances', 'permissions']
|
filter_horizontal = [
|
||||||
list_display = ('name', 'priority', 'user_count')
|
'member_characters',
|
||||||
|
'member_corporations',
|
||||||
|
'member_alliances',
|
||||||
|
'member_factions',
|
||||||
|
'permissions'
|
||||||
|
]
|
||||||
|
|
||||||
|
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
||||||
|
"""overriding this formfield to have sorted lists in the form"""
|
||||||
|
if db_field.name == "member_characters":
|
||||||
|
kwargs["queryset"] = EveCharacter.objects.all()\
|
||||||
|
.order_by(Lower('character_name'))
|
||||||
|
elif db_field.name == "member_corporations":
|
||||||
|
kwargs["queryset"] = EveCorporationInfo.objects.all()\
|
||||||
|
.order_by(Lower('corporation_name'))
|
||||||
|
elif db_field.name == "member_alliances":
|
||||||
|
kwargs["queryset"] = EveAllianceInfo.objects.all()\
|
||||||
|
.order_by(Lower('alliance_name'))
|
||||||
|
elif db_field.name == "member_factions":
|
||||||
|
kwargs["queryset"] = EveFactionInfo.objects.all()\
|
||||||
|
.order_by(Lower('faction_name'))
|
||||||
|
elif db_field.name == "permissions":
|
||||||
|
kwargs["queryset"] = Permission.objects.select_related("content_type").all()
|
||||||
|
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
||||||
|
|
||||||
def has_delete_permission(self, request, obj=None):
|
def has_delete_permission(self, request, obj=None):
|
||||||
if obj == get_guest_state():
|
if obj == get_guest_state():
|
||||||
return False
|
return False
|
||||||
return super(StateAdmin, self).has_delete_permission(request, obj=obj)
|
return super().has_delete_permission(request, obj=obj)
|
||||||
|
|
||||||
def get_fieldsets(self, request, obj=None):
|
def get_fieldsets(self, request, obj=None):
|
||||||
if obj == get_guest_state():
|
if obj == get_guest_state():
|
||||||
@@ -153,16 +511,39 @@ class StateAdmin(admin.ModelAdmin):
|
|||||||
'fields': ('permissions', 'priority'),
|
'fields': ('permissions', 'priority'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
return super(StateAdmin, self).get_fieldsets(request, obj=obj)
|
return super().get_fieldsets(request, obj=obj)
|
||||||
|
|
||||||
@staticmethod
|
def get_readonly_fields(self, request, obj=None):
|
||||||
def user_count(obj):
|
if not request.user.is_superuser:
|
||||||
return obj.userprofile_set.all().count()
|
return self.readonly_fields + ("permissions",)
|
||||||
|
return self.readonly_fields
|
||||||
|
|
||||||
|
|
||||||
class BaseOwnershipAdmin(admin.ModelAdmin):
|
class BaseOwnershipAdmin(admin.ModelAdmin):
|
||||||
list_display = ('user', 'character')
|
list_select_related = (
|
||||||
search_fields = ('user__username', 'character__character_name', 'character__corporation_name', 'character__alliance_name')
|
'user__profile__state', 'user__profile__main_character', 'character')
|
||||||
|
list_display = (
|
||||||
|
user_profile_pic,
|
||||||
|
user_username,
|
||||||
|
user_main_organization,
|
||||||
|
'character',
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
'user__username',
|
||||||
|
'character__character_name',
|
||||||
|
'character__corporation_name',
|
||||||
|
'character__alliance_name',
|
||||||
|
'character__faction_name'
|
||||||
|
)
|
||||||
|
list_filter = (
|
||||||
|
MainCorporationsFilter,
|
||||||
|
MainAllianceFilter,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = {
|
||||||
|
"all": ("authentication/css/admin.css",)
|
||||||
|
}
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
if obj and obj.pk:
|
if obj and obj.pk:
|
||||||
@@ -184,6 +565,7 @@ class CharacterOwnershipAdmin(BaseOwnershipAdmin):
|
|||||||
class PermissionAdmin(admin.ModelAdmin):
|
class PermissionAdmin(admin.ModelAdmin):
|
||||||
actions = None
|
actions = None
|
||||||
readonly_fields = [field.name for field in BasePermission._meta.fields]
|
readonly_fields = [field.name for field in BasePermission._meta.fields]
|
||||||
|
search_fields = ('codename', )
|
||||||
list_display = ('admin_name', 'name', 'codename', 'content_type')
|
list_display = ('admin_name', 'name', 'codename', 'content_type')
|
||||||
list_filter = ('content_type__app_label',)
|
list_filter = ('content_type__app_label',)
|
||||||
|
|
||||||
@@ -191,7 +573,7 @@ class PermissionAdmin(admin.ModelAdmin):
|
|||||||
def admin_name(obj):
|
def admin_name(obj):
|
||||||
return str(obj)
|
return str(obj)
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
def has_add_permission(self, request, obj=None):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def has_delete_permission(self, request, obj=None):
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
|||||||
45
allianceauth/authentication/app_settings.py
Normal file
45
allianceauth/authentication/app_settings.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_setting(
|
||||||
|
name: str,
|
||||||
|
default_value: object,
|
||||||
|
min_value: int = None,
|
||||||
|
max_value: int = None,
|
||||||
|
required_type: type = None
|
||||||
|
):
|
||||||
|
"""cleans the input for a custom setting
|
||||||
|
|
||||||
|
Will use `default_value` if settings does not exit or has the wrong type
|
||||||
|
or is outside define boundaries (for int only)
|
||||||
|
|
||||||
|
Need to define `required_type` if `default_value` is `None`
|
||||||
|
|
||||||
|
Will assume `min_value` of 0 for int (can be overriden)
|
||||||
|
|
||||||
|
Returns cleaned value for setting
|
||||||
|
"""
|
||||||
|
if default_value is None and not required_type:
|
||||||
|
raise ValueError('You must specify a required_type for None defaults')
|
||||||
|
|
||||||
|
if not required_type:
|
||||||
|
required_type = type(default_value)
|
||||||
|
|
||||||
|
if min_value is None and required_type == int:
|
||||||
|
min_value = 0
|
||||||
|
|
||||||
|
if (hasattr(settings, name)
|
||||||
|
and isinstance(getattr(settings, name), required_type)
|
||||||
|
and (min_value is None or getattr(settings, name) >= min_value)
|
||||||
|
and (max_value is None or getattr(settings, name) <= max_value)
|
||||||
|
):
|
||||||
|
return getattr(settings, name)
|
||||||
|
else:
|
||||||
|
return default_value
|
||||||
|
|
||||||
|
|
||||||
|
AUTHENTICATION_ADMIN_USERS_MAX_GROUPS = \
|
||||||
|
_clean_setting('AUTHENTICATION_ADMIN_USERS_MAX_GROUPS', 10)
|
||||||
|
|
||||||
|
AUTHENTICATION_ADMIN_USERS_MAX_CHARS = \
|
||||||
|
_clean_setting('AUTHENTICATION_ADMIN_USERS_MAX_CHARS', 5)
|
||||||
@@ -3,10 +3,14 @@ from django.core.checks import register, Tags
|
|||||||
|
|
||||||
|
|
||||||
class AuthenticationConfig(AppConfig):
|
class AuthenticationConfig(AppConfig):
|
||||||
name = 'allianceauth.authentication'
|
name = "allianceauth.authentication"
|
||||||
label = 'authentication'
|
label = "authentication"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
super(AuthenticationConfig, self).ready()
|
from allianceauth.authentication import checks, signals # noqa: F401
|
||||||
from allianceauth.authentication import checks, signals
|
from allianceauth.authentication.task_statistics import (
|
||||||
|
signals as celery_signals,
|
||||||
|
)
|
||||||
|
|
||||||
register(Tags.security)(checks.check_login_scopes_setting)
|
register(Tags.security)(checks.check_login_scopes_setting)
|
||||||
|
celery_signals.reset_counters()
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from django.contrib.auth.backends import ModelBackend
|
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from django.contrib.auth.backends import ModelBackend
|
||||||
|
from django.contrib.auth.models import User, Permission
|
||||||
|
|
||||||
from .models import UserProfile, CharacterOwnership, OwnershipRecord
|
from .models import UserProfile, CharacterOwnership, OwnershipRecord
|
||||||
|
|
||||||
|
|
||||||
@@ -11,9 +12,11 @@ logger = logging.getLogger(__name__)
|
|||||||
class StateBackend(ModelBackend):
|
class StateBackend(ModelBackend):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_state_permissions(user_obj):
|
def _get_state_permissions(user_obj):
|
||||||
profile_state_field = UserProfile._meta.get_field('state')
|
"""returns permissions for state of given user object"""
|
||||||
user_state_query = 'state__%s__user' % profile_state_field.related_query_name()
|
if hasattr(user_obj, "profile") and user_obj.profile:
|
||||||
return Permission.objects.filter(**{user_state_query: user_obj})
|
return Permission.objects.filter(state=user_obj.profile.state)
|
||||||
|
else:
|
||||||
|
return Permission.objects.none()
|
||||||
|
|
||||||
def get_state_permissions(self, user_obj, obj=None):
|
def get_state_permissions(self, user_obj, obj=None):
|
||||||
return self._get_permissions(user_obj, obj, 'state')
|
return self._get_permissions(user_obj, obj, 'state')
|
||||||
@@ -33,17 +36,17 @@ class StateBackend(ModelBackend):
|
|||||||
try:
|
try:
|
||||||
ownership = CharacterOwnership.objects.get(character__character_id=token.character_id)
|
ownership = CharacterOwnership.objects.get(character__character_id=token.character_id)
|
||||||
if ownership.owner_hash == token.character_owner_hash:
|
if ownership.owner_hash == token.character_owner_hash:
|
||||||
logger.debug('Authenticating {0} by ownership of character {1}'.format(ownership.user, token.character_name))
|
logger.debug(f'Authenticating {ownership.user} by ownership of character {token.character_name}')
|
||||||
return ownership.user
|
return ownership.user
|
||||||
else:
|
else:
|
||||||
logger.debug('{0} has changed ownership. Creating new user account.'.format(token.character_name))
|
logger.debug(f'{token.character_name} has changed ownership. Creating new user account.')
|
||||||
ownership.delete()
|
ownership.delete()
|
||||||
return self.create_user(token)
|
return self.create_user(token)
|
||||||
except CharacterOwnership.DoesNotExist:
|
except CharacterOwnership.DoesNotExist:
|
||||||
try:
|
try:
|
||||||
# insecure legacy main check for pre-sso registration auth installs
|
# insecure legacy main check for pre-sso registration auth installs
|
||||||
profile = UserProfile.objects.get(main_character__character_id=token.character_id)
|
profile = UserProfile.objects.get(main_character__character_id=token.character_id)
|
||||||
logger.debug('Authenticating {0} by their main character {1} without active ownership.'.format(profile.user, profile.main_character))
|
logger.debug(f'Authenticating {profile.user} by their main character {profile.main_character} without active ownership.')
|
||||||
# attach an ownership
|
# attach an ownership
|
||||||
token.user = profile.user
|
token.user = profile.user
|
||||||
CharacterOwnership.objects.create_by_token(token)
|
CharacterOwnership.objects.create_by_token(token)
|
||||||
@@ -56,13 +59,13 @@ class StateBackend(ModelBackend):
|
|||||||
user = records[0].user
|
user = records[0].user
|
||||||
token.user = user
|
token.user = user
|
||||||
co = CharacterOwnership.objects.create_by_token(token)
|
co = CharacterOwnership.objects.create_by_token(token)
|
||||||
logger.debug('Authenticating {0} by matching owner hash record of character {1}'.format(user, co.character))
|
logger.debug(f'Authenticating {user} by matching owner hash record of character {co.character}')
|
||||||
if not user.profile.main_character:
|
if not user.profile.main_character:
|
||||||
# set this as their main by default if they have none
|
# set this as their main by default if they have none
|
||||||
user.profile.main_character = co.character
|
user.profile.main_character = co.character
|
||||||
user.profile.save()
|
user.profile.save()
|
||||||
return user
|
return user
|
||||||
logger.debug('Unable to authenticate character {0}. Creating new user.'.format(token.character_name))
|
logger.debug(f'Unable to authenticate character {token.character_name}. Creating new user.')
|
||||||
return self.create_user(token)
|
return self.create_user(token)
|
||||||
|
|
||||||
def create_user(self, token):
|
def create_user(self, token):
|
||||||
@@ -74,7 +77,7 @@ class StateBackend(ModelBackend):
|
|||||||
co = CharacterOwnership.objects.create_by_token(token) # assign ownership to this user
|
co = CharacterOwnership.objects.create_by_token(token) # assign ownership to this user
|
||||||
user.profile.main_character = co.character # assign main character as token character
|
user.profile.main_character = co.character # assign main character as token character
|
||||||
user.profile.save()
|
user.profile.save()
|
||||||
logger.debug('Created new user {0}'.format(user))
|
logger.debug(f'Created new user {user}')
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -84,10 +87,10 @@ class StateBackend(ModelBackend):
|
|||||||
if User.objects.filter(username__startswith=name).exists():
|
if User.objects.filter(username__startswith=name).exists():
|
||||||
u = User.objects.filter(username__startswith=name)
|
u = User.objects.filter(username__startswith=name)
|
||||||
num = len(u)
|
num = len(u)
|
||||||
username = "%s_%s" % (name, num)
|
username = f"{name}_{num}"
|
||||||
while u.filter(username=username).exists():
|
while u.filter(username=username).exists():
|
||||||
num += 1
|
num += 1
|
||||||
username = "%s_%s" % (name, num)
|
username = f"{name}_{num}"
|
||||||
else:
|
else:
|
||||||
username = name
|
username = name
|
||||||
return username
|
return username
|
||||||
|
|||||||
@@ -1,6 +1,66 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.contrib.auth.forms import UserChangeForm as BaseUserChangeForm
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.forms import ModelForm
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from allianceauth.authentication.models import User
|
||||||
|
|
||||||
|
|
||||||
class RegistrationForm(forms.Form):
|
class RegistrationForm(forms.Form):
|
||||||
email = forms.EmailField(label=_('Email'), max_length=254, required=True)
|
email = forms.EmailField(label=_('Email'), max_length=254, required=True)
|
||||||
|
|
||||||
|
class _meta:
|
||||||
|
model = User
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfileForm(ModelForm):
|
||||||
|
"""Allows specifying FK querysets through kwarg"""
|
||||||
|
|
||||||
|
def __init__(self, querysets=None, *args, **kwargs):
|
||||||
|
querysets = querysets or {}
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
for field, qs in querysets.items():
|
||||||
|
self.fields[field].queryset = qs
|
||||||
|
|
||||||
|
|
||||||
|
class UserChangeForm(BaseUserChangeForm):
|
||||||
|
"""Add custom cleaning to UserChangeForm"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.request = kwargs.pop("request") # Inject current request into form object
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
if not self.request.user.is_superuser:
|
||||||
|
if self.instance:
|
||||||
|
current_restricted = set(
|
||||||
|
self.instance.groups.filter(
|
||||||
|
authgroup__restricted=True
|
||||||
|
).values_list("pk", flat=True)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
current_restricted = set()
|
||||||
|
new_restricted = set(
|
||||||
|
cleaned_data["groups"].filter(
|
||||||
|
authgroup__restricted=True
|
||||||
|
).values_list("pk", flat=True)
|
||||||
|
)
|
||||||
|
if current_restricted != new_restricted:
|
||||||
|
restricted_removed = current_restricted - new_restricted
|
||||||
|
restricted_added = new_restricted - current_restricted
|
||||||
|
restricted_changed = restricted_removed | restricted_added
|
||||||
|
restricted_names_qs = Group.objects.filter(
|
||||||
|
pk__in=restricted_changed
|
||||||
|
).values_list("name", flat=True)
|
||||||
|
restricted_names = ",".join(list(restricted_names_qs))
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"groups": _(
|
||||||
|
"You are not allowed to add or remove these "
|
||||||
|
"restricted groups: %s" % restricted_names
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ urlpatterns = [
|
|||||||
url(r'^register/$', views.RegistrationView.as_view(), name='registration_register'),
|
url(r'^register/$', views.RegistrationView.as_view(), name='registration_register'),
|
||||||
url(r'^register/complete/$', views.registration_complete, name='registration_complete'),
|
url(r'^register/complete/$', views.registration_complete, name='registration_complete'),
|
||||||
url(r'^register/closed/$', views.registration_closed, name='registration_disallowed'),
|
url(r'^register/closed/$', views.registration_closed, name='registration_disallowed'),
|
||||||
url(r'', include('registration.auth_urls')),
|
url(r'', include('django.contrib.auth.urls')),
|
||||||
]
|
]
|
||||||
@@ -11,10 +11,10 @@ class Command(BaseCommand):
|
|||||||
if profiles.exists():
|
if profiles.exists():
|
||||||
for profile in profiles:
|
for profile in profiles:
|
||||||
self.stdout.write(self.style.ERROR(
|
self.stdout.write(self.style.ERROR(
|
||||||
'{0} does not have an ownership. Resetting user {1} main character.'.format(profile.main_character,
|
'{} does not have an ownership. Resetting user {} main character.'.format(profile.main_character,
|
||||||
profile.user)))
|
profile.user)))
|
||||||
profile.main_character = None
|
profile.main_character = None
|
||||||
profile.save()
|
profile.save()
|
||||||
self.stdout.write(self.style.WARNING('Reset {0} main characters.'.format(profiles.count())))
|
self.stdout.write(self.style.WARNING(f'Reset {profiles.count()} main characters.'))
|
||||||
else:
|
else:
|
||||||
self.stdout.write(self.style.SUCCESS('All main characters have active ownership.'))
|
self.stdout.write(self.style.SUCCESS('All main characters have active ownership.'))
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ def available_states_query(character):
|
|||||||
query |= Q(member_corporations__corporation_id=character.corporation_id)
|
query |= Q(member_corporations__corporation_id=character.corporation_id)
|
||||||
if character.alliance_id:
|
if character.alliance_id:
|
||||||
query |= Q(member_alliances__alliance_id=character.alliance_id)
|
query |= Q(member_alliances__alliance_id=character.alliance_id)
|
||||||
|
if character.faction_id:
|
||||||
|
query |= Q(member_factions__faction_id=character.faction_id)
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
@@ -23,8 +25,7 @@ class CharacterOwnershipManager(Manager):
|
|||||||
def create_by_token(self, token):
|
def create_by_token(self, token):
|
||||||
if not EveCharacter.objects.filter(character_id=token.character_id).exists():
|
if not EveCharacter.objects.filter(character_id=token.character_id).exists():
|
||||||
EveCharacter.objects.create_character(token.character_id)
|
EveCharacter.objects.create_character(token.character_id)
|
||||||
return self.create(character=EveCharacter.objects.get(character_id=token.character_id), user=token.user,
|
return self.create(character=EveCharacter.objects.get(character_id=token.character_id), user=token.user, owner_hash=token.character_owner_hash)
|
||||||
owner_hash=token.character_owner_hash)
|
|
||||||
|
|
||||||
|
|
||||||
class StateQuerySet(QuerySet):
|
class StateQuerySet(QuerySet):
|
||||||
@@ -50,7 +51,7 @@ class StateQuerySet(QuerySet):
|
|||||||
for state in self:
|
for state in self:
|
||||||
for profile in state.userprofile_set.all():
|
for profile in state.userprofile_set.all():
|
||||||
profile.assign_state(state=self.model.objects.exclude(pk=state.pk).get_for_user(profile.user))
|
profile.assign_state(state=self.model.objects.exclude(pk=state.pk).get_for_user(profile.user))
|
||||||
super(StateQuerySet, self).delete()
|
super().delete()
|
||||||
|
|
||||||
|
|
||||||
class StateManager(Manager):
|
class StateManager(Manager):
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.10.1 on 2016-09-05 21:38
|
# Generated by Django 1.10.1 on 2016-09-05 21:38
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.10.1 on 2016-09-07 19:14
|
# Generated by Django 1.10.1 on 2016-09-07 19:14
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.10.1 on 2016-09-09 20:29
|
# Generated by Django 1.10.1 on 2016-09-09 20:29
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.10.1 on 2016-09-09 23:19
|
# Generated by Django 1.10.1 on 2016-09-09 23:19
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.10.1 on 2016-09-09 23:11
|
# Generated by Django 1.10.1 on 2016-09-09 23:11
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.10.1 on 2016-09-10 05:42
|
# Generated by Django 1.10.1 on 2016-09-10 05:42
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.10.1 on 2016-09-10 21:50
|
# Generated by Django 1.10.1 on 2016-09-10 21:50
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.10.1 on 2016-09-12 13:04
|
# Generated by Django 1.10.1 on 2016-09-12 13:04
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.10.2 on 2016-10-21 02:28
|
# Generated by Django 1.10.2 on 2016-10-21 02:28
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.10.1 on 2017-01-07 06:47
|
# Generated by Django 1.10.1 on 2017-01-07 06:47
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
@@ -10,7 +8,7 @@ def count_completed_fields(model):
|
|||||||
def forward(apps, schema_editor):
|
def forward(apps, schema_editor):
|
||||||
# this ensures only one model exists per user
|
# this ensures only one model exists per user
|
||||||
AuthServicesInfo = apps.get_model('authentication', 'AuthServicesInfo')
|
AuthServicesInfo = apps.get_model('authentication', 'AuthServicesInfo')
|
||||||
users = set([a.user for a in AuthServicesInfo.objects.all()])
|
users = {a.user for a in AuthServicesInfo.objects.all()}
|
||||||
for u in users:
|
for u in users:
|
||||||
auths = AuthServicesInfo.objects.filter(user=u)
|
auths = AuthServicesInfo.objects.filter(user=u)
|
||||||
if auths.count() > 1:
|
if auths.count() > 1:
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.10.1 on 2017-01-07 07:11
|
# Generated by Django 1.10.1 on 2017-01-07 07:11
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.10.5 on 2017-01-12 00:59
|
# Generated by Django 1.10.5 on 2017-01-12 00:59
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.10.2 on 2016-12-11 23:14
|
# Generated by Django 1.10.2 on 2016-12-11 23:14
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.10.1 on 2016-09-09 23:19
|
# Generated by Django 1.10.1 on 2016-09-09 23:19
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.10.5 on 2017-03-22 23:09
|
# Generated by Django 1.10.5 on 2017-03-22 23:09
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import allianceauth.authentication.models
|
import allianceauth.authentication.models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@@ -107,8 +105,8 @@ def populate_ownerships(apps, schema_editor):
|
|||||||
EveCharacter = apps.get_model('eveonline', 'EveCharacter')
|
EveCharacter = apps.get_model('eveonline', 'EveCharacter')
|
||||||
|
|
||||||
unique_character_owners = [t['character_id'] for t in
|
unique_character_owners = [t['character_id'] for t in
|
||||||
Token.objects.all().values('character_id').annotate(n=models.Count('user')) if
|
Token.objects.all().values('character_id').annotate(n=models.Count('user')) if
|
||||||
t['n'] == 1 and EveCharacter.objects.filter(character_id=t['character_id']).exists()]
|
t['n'] == 1 and EveCharacter.objects.filter(character_id=t['character_id']).exists()]
|
||||||
|
|
||||||
tokens = Token.objects.filter(character_id__in=unique_character_owners)
|
tokens = Token.objects.filter(character_id__in=unique_character_owners)
|
||||||
for c_id in unique_character_owners:
|
for c_id in unique_character_owners:
|
||||||
@@ -171,8 +169,7 @@ def recreate_authservicesinfo(apps, schema_editor):
|
|||||||
|
|
||||||
# repopulate main characters
|
# repopulate main characters
|
||||||
for profile in UserProfile.objects.exclude(main_character__isnull=True).select_related('user', 'main_character'):
|
for profile in UserProfile.objects.exclude(main_character__isnull=True).select_related('user', 'main_character'):
|
||||||
AuthServicesInfo.objects.update_or_create(user=profile.user,
|
AuthServicesInfo.objects.update_or_create(user=profile.user, defaults={'main_char_id': profile.main_character.character_id})
|
||||||
defaults={'main_char_id': profile.main_character.character_id})
|
|
||||||
|
|
||||||
# repopulate states we understand
|
# repopulate states we understand
|
||||||
for profile in UserProfile.objects.exclude(state__name='Guest').filter(
|
for profile in UserProfile.objects.exclude(state__name='Guest').filter(
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-10-20 05:22
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('authentication', '0017_remove_fleetup_permission'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='state',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=32, unique=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 3.1.13 on 2021-10-12 20:21
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('eveonline', '0015_factions'),
|
||||||
|
('authentication', '0017_remove_fleetup_permission'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='state',
|
||||||
|
name='member_factions',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='Factions to whose members this state is available.', to='eveonline.EveFactionInfo'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-10-26 09:19
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('authentication', '0018_alter_state_name_length'),
|
||||||
|
('authentication', '0018_state_member_factions'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
||||||
@@ -3,7 +3,7 @@ import logging
|
|||||||
from django.contrib.auth.models import User, Permission
|
from django.contrib.auth.models import User, Permission
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo
|
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo, EveFactionInfo
|
||||||
from allianceauth.notifications import notify
|
from allianceauth.notifications import notify
|
||||||
|
|
||||||
from .managers import CharacterOwnershipManager, StateManager
|
from .managers import CharacterOwnershipManager, StateManager
|
||||||
@@ -12,10 +12,9 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class State(models.Model):
|
class State(models.Model):
|
||||||
name = models.CharField(max_length=20, unique=True)
|
name = models.CharField(max_length=32, unique=True)
|
||||||
permissions = models.ManyToManyField(Permission, blank=True)
|
permissions = models.ManyToManyField(Permission, blank=True)
|
||||||
priority = models.IntegerField(unique=True,
|
priority = models.IntegerField(unique=True, help_text="Users get assigned the state with the highest priority available to them.")
|
||||||
help_text="Users get assigned the state with the highest priority available to them.")
|
|
||||||
|
|
||||||
member_characters = models.ManyToManyField(EveCharacter, blank=True,
|
member_characters = models.ManyToManyField(EveCharacter, blank=True,
|
||||||
help_text="Characters to which this state is available.")
|
help_text="Characters to which this state is available.")
|
||||||
@@ -23,6 +22,8 @@ class State(models.Model):
|
|||||||
help_text="Corporations to whose members this state is available.")
|
help_text="Corporations to whose members this state is available.")
|
||||||
member_alliances = models.ManyToManyField(EveAllianceInfo, blank=True,
|
member_alliances = models.ManyToManyField(EveAllianceInfo, blank=True,
|
||||||
help_text="Alliances to whose members this state is available.")
|
help_text="Alliances to whose members this state is available.")
|
||||||
|
member_factions = models.ManyToManyField(EveFactionInfo, blank=True,
|
||||||
|
help_text="Factions to whose members this state is available.")
|
||||||
public = models.BooleanField(default=False, help_text="Make this state available to any character.")
|
public = models.BooleanField(default=False, help_text="Make this state available to any character.")
|
||||||
|
|
||||||
objects = StateManager()
|
objects = StateManager()
|
||||||
@@ -43,7 +44,7 @@ class State(models.Model):
|
|||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
for profile in self.userprofile_set.all():
|
for profile in self.userprofile_set.all():
|
||||||
profile.assign_state(state=State.objects.exclude(pk=self.pk).get_for_user(profile.user))
|
profile.assign_state(state=State.objects.exclude(pk=self.pk).get_for_user(profile.user))
|
||||||
super(State, self).delete(**kwargs)
|
super().delete(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
def get_guest_state():
|
def get_guest_state():
|
||||||
@@ -71,13 +72,24 @@ class UserProfile(models.Model):
|
|||||||
if self.state != state:
|
if self.state != state:
|
||||||
self.state = state
|
self.state = state
|
||||||
if commit:
|
if commit:
|
||||||
logger.info('Updating {} state to {}'.format(self.user, self.state))
|
logger.info(f'Updating {self.user} state to {self.state}')
|
||||||
self.save(update_fields=['state'])
|
self.save(update_fields=['state'])
|
||||||
notify(self.user, _('State Changed'),
|
notify(
|
||||||
_('Your user state has been changed to %(state)s') % ({'state': state}),
|
self.user,
|
||||||
'info')
|
_('State changed to: %s' % state),
|
||||||
|
_('Your user\'s state is now: %(state)s')
|
||||||
|
% ({'state': state}),
|
||||||
|
'info'
|
||||||
|
)
|
||||||
from allianceauth.authentication.signals import state_changed
|
from allianceauth.authentication.signals import state_changed
|
||||||
state_changed.send(sender=self.__class__, user=self.user, state=self.state)
|
|
||||||
|
# We need to ensure we get up to date perms here as they will have just changed.
|
||||||
|
# Clear all attribute caches and reload the model that will get passed to the signals!
|
||||||
|
self.refresh_from_db()
|
||||||
|
|
||||||
|
state_changed.send(
|
||||||
|
sender=self.__class__, user=self.user, state=self.state
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.user)
|
return str(self.user)
|
||||||
@@ -95,7 +107,7 @@ class CharacterOwnership(models.Model):
|
|||||||
objects = CharacterOwnershipManager()
|
objects = CharacterOwnershipManager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s: %s" % (self.user, self.character)
|
return f"{self.user}: {self.character}"
|
||||||
|
|
||||||
|
|
||||||
class OwnershipRecord(models.Model):
|
class OwnershipRecord(models.Model):
|
||||||
@@ -108,4 +120,4 @@ class OwnershipRecord(models.Model):
|
|||||||
ordering = ['-created']
|
ordering = ['-created']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s: %s on %s" % (self.user, self.character, self.created)
|
return f"{self.user}: {self.character} on {self.created}"
|
||||||
|
|||||||
@@ -23,35 +23,38 @@ def trigger_state_check(state):
|
|||||||
check_states = State.objects.filter(priority__lt=state.priority)
|
check_states = State.objects.filter(priority__lt=state.priority)
|
||||||
for profile in UserProfile.objects.filter(state__in=check_states):
|
for profile in UserProfile.objects.filter(state__in=check_states):
|
||||||
if state.available_to_user(profile.user):
|
if state.available_to_user(profile.user):
|
||||||
profile.state = state
|
profile.assign_state(state)
|
||||||
profile.save(update_fields=['state'])
|
|
||||||
state_changed.send(sender=state.__class__, user=profile.user, state=state)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(m2m_changed, sender=State.member_characters.through)
|
@receiver(m2m_changed, sender=State.member_characters.through)
|
||||||
def state_member_characters_changed(sender, instance, action, *args, **kwargs):
|
def state_member_characters_changed(sender, instance, action, *args, **kwargs):
|
||||||
if action.startswith('post_'):
|
if action.startswith('post_'):
|
||||||
logger.debug('State {} member characters changed. Re-evaluating membership.'.format(instance))
|
logger.debug(f'State {instance} member characters changed. Re-evaluating membership.')
|
||||||
trigger_state_check(instance)
|
trigger_state_check(instance)
|
||||||
|
|
||||||
|
|
||||||
@receiver(m2m_changed, sender=State.member_corporations.through)
|
@receiver(m2m_changed, sender=State.member_corporations.through)
|
||||||
def state_member_corporations_changed(sender, instance, action, *args, **kwargs):
|
def state_member_corporations_changed(sender, instance, action, *args, **kwargs):
|
||||||
if action.startswith('post_'):
|
if action.startswith('post_'):
|
||||||
logger.debug('State {} member corporations changed. Re-evaluating membership.'.format(instance))
|
logger.debug(f'State {instance} member corporations changed. Re-evaluating membership.')
|
||||||
trigger_state_check(instance)
|
trigger_state_check(instance)
|
||||||
|
|
||||||
|
|
||||||
@receiver(m2m_changed, sender=State.member_alliances.through)
|
@receiver(m2m_changed, sender=State.member_alliances.through)
|
||||||
def state_member_alliances_changed(sender, instance, action, *args, **kwargs):
|
def state_member_alliances_changed(sender, instance, action, *args, **kwargs):
|
||||||
if action.startswith('post_'):
|
if action.startswith('post_'):
|
||||||
logger.debug('State {} member alliances changed. Re-evaluating membership.'.format(instance))
|
logger.debug(f'State {instance} member alliances changed. Re-evaluating membership.')
|
||||||
trigger_state_check(instance)
|
trigger_state_check(instance)
|
||||||
|
|
||||||
|
@receiver(m2m_changed, sender=State.member_factions.through)
|
||||||
|
def state_member_factions_changed(sender, instance, action, *args, **kwargs):
|
||||||
|
if action.startswith('post_'):
|
||||||
|
logger.debug(f'State {instance} member factions changed. Re-evaluating membership.')
|
||||||
|
trigger_state_check(instance)
|
||||||
|
|
||||||
@receiver(post_save, sender=State)
|
@receiver(post_save, sender=State)
|
||||||
def state_saved(sender, instance, *args, **kwargs):
|
def state_saved(sender, instance, *args, **kwargs):
|
||||||
logger.debug('State {} saved. Re-evaluating membership.'.format(instance))
|
logger.debug(f'State {instance} saved. Re-evaluating membership.')
|
||||||
trigger_state_check(instance)
|
trigger_state_check(instance)
|
||||||
|
|
||||||
|
|
||||||
@@ -62,7 +65,7 @@ def reassess_on_profile_save(sender, instance, created, *args, **kwargs):
|
|||||||
if not created:
|
if not created:
|
||||||
update_fields = kwargs.pop('update_fields', []) or []
|
update_fields = kwargs.pop('update_fields', []) or []
|
||||||
if 'state' not in update_fields:
|
if 'state' not in update_fields:
|
||||||
logger.debug('Profile for {} saved without state change. Re-evaluating state.'.format(instance.user))
|
logger.debug(f'Profile for {instance.user} saved without state change. Re-evaluating state.')
|
||||||
instance.assign_state()
|
instance.assign_state()
|
||||||
|
|
||||||
|
|
||||||
@@ -70,15 +73,14 @@ def reassess_on_profile_save(sender, instance, created, *args, **kwargs):
|
|||||||
def create_required_models(sender, instance, created, *args, **kwargs):
|
def create_required_models(sender, instance, created, *args, **kwargs):
|
||||||
# ensure all users have a model
|
# ensure all users have a model
|
||||||
if created:
|
if created:
|
||||||
logger.debug('User {} created. Creating default UserProfile.'.format(instance))
|
logger.debug(f'User {instance} created. Creating default UserProfile.')
|
||||||
UserProfile.objects.get_or_create(user=instance)
|
UserProfile.objects.get_or_create(user=instance)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Token)
|
@receiver(post_save, sender=Token)
|
||||||
def record_character_ownership(sender, instance, created, *args, **kwargs):
|
def record_character_ownership(sender, instance, created, *args, **kwargs):
|
||||||
if created:
|
if created:
|
||||||
logger.debug('New token for {0} character {1} saved. Evaluating ownership.'.format(instance.user,
|
logger.debug(f'New token for {instance.user} character {instance.character_name} saved. Evaluating ownership.')
|
||||||
instance.character_name))
|
|
||||||
if instance.user:
|
if instance.user:
|
||||||
query = Q(owner_hash=instance.character_owner_hash) & Q(user=instance.user)
|
query = Q(owner_hash=instance.character_owner_hash) & Q(user=instance.user)
|
||||||
else:
|
else:
|
||||||
@@ -87,25 +89,21 @@ def record_character_ownership(sender, instance, created, *args, **kwargs):
|
|||||||
CharacterOwnership.objects.filter(character__character_id=instance.character_id).exclude(query).delete()
|
CharacterOwnership.objects.filter(character__character_id=instance.character_id).exclude(query).delete()
|
||||||
# create character if needed
|
# create character if needed
|
||||||
if EveCharacter.objects.filter(character_id=instance.character_id).exists() is False:
|
if EveCharacter.objects.filter(character_id=instance.character_id).exists() is False:
|
||||||
logger.debug('Token is for a new character. Creating model for {0} ({1})'.format(instance.character_name,
|
logger.debug(f'Token is for a new character. Creating model for {instance.character_name} ({instance.character_id})')
|
||||||
instance.character_id))
|
|
||||||
EveCharacter.objects.create_character(instance.character_id)
|
EveCharacter.objects.create_character(instance.character_id)
|
||||||
char = EveCharacter.objects.get(character_id=instance.character_id)
|
char = EveCharacter.objects.get(character_id=instance.character_id)
|
||||||
# check if we need to create ownership
|
# check if we need to create ownership
|
||||||
if instance.user and not CharacterOwnership.objects.filter(
|
if instance.user and not CharacterOwnership.objects.filter(
|
||||||
character__character_id=instance.character_id).exists():
|
character__character_id=instance.character_id).exists():
|
||||||
logger.debug("Character {0} is not yet owned. Assigning ownership to {1}".format(instance.character_name,
|
logger.debug(f"Character {instance.character_name} is not yet owned. Assigning ownership to {instance.user}")
|
||||||
instance.user))
|
CharacterOwnership.objects.update_or_create(character=char, defaults={'owner_hash': instance.character_owner_hash, 'user': instance.user})
|
||||||
CharacterOwnership.objects.update_or_create(character=char,
|
|
||||||
defaults={'owner_hash': instance.character_owner_hash,
|
|
||||||
'user': instance.user})
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=CharacterOwnership)
|
@receiver(pre_delete, sender=CharacterOwnership)
|
||||||
def validate_main_character(sender, instance, *args, **kwargs):
|
def validate_main_character(sender, instance, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
if instance.user.profile.main_character == instance.character:
|
if instance.user.profile.main_character == instance.character:
|
||||||
logger.info("Ownership of a main character {0} has been revoked. Resetting {1} main character.".format(
|
logger.info("Ownership of a main character {} has been revoked. Resetting {} main character.".format(
|
||||||
instance.character, instance.user))
|
instance.character, instance.user))
|
||||||
# clear main character as user no longer owns them
|
# clear main character as user no longer owns them
|
||||||
instance.user.profile.main_character = None
|
instance.user.profile.main_character = None
|
||||||
@@ -118,7 +116,7 @@ def validate_main_character(sender, instance, *args, **kwargs):
|
|||||||
@receiver(post_delete, sender=Token)
|
@receiver(post_delete, sender=Token)
|
||||||
def validate_ownership(sender, instance, *args, **kwargs):
|
def validate_ownership(sender, instance, *args, **kwargs):
|
||||||
if not Token.objects.filter(character_owner_hash=instance.character_owner_hash).filter(refresh_token__isnull=False).exists():
|
if not Token.objects.filter(character_owner_hash=instance.character_owner_hash).filter(refresh_token__isnull=False).exists():
|
||||||
logger.info("No remaining tokens to validate ownership of character {0}. Revoking ownership.".format(instance.character_name))
|
logger.info(f"No remaining tokens to validate ownership of character {instance.character_name}. Revoking ownership.")
|
||||||
CharacterOwnership.objects.filter(owner_hash=instance.character_owner_hash).delete()
|
CharacterOwnership.objects.filter(owner_hash=instance.character_owner_hash).delete()
|
||||||
|
|
||||||
|
|
||||||
@@ -129,11 +127,11 @@ def assign_state_on_active_change(sender, instance, *args, **kwargs):
|
|||||||
old_instance = User.objects.get(pk=instance.pk)
|
old_instance = User.objects.get(pk=instance.pk)
|
||||||
if old_instance.is_active != instance.is_active:
|
if old_instance.is_active != instance.is_active:
|
||||||
if instance.is_active:
|
if instance.is_active:
|
||||||
logger.debug("User {0} has been activated. Assigning state.".format(instance))
|
logger.debug(f"User {instance} has been activated. Assigning state.")
|
||||||
instance.profile.assign_state()
|
instance.profile.assign_state()
|
||||||
else:
|
else:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"User {0} has been deactivated. Revoking state and assigning to guest state.".format(instance))
|
f"User {instance} has been deactivated. Revoking state and assigning to guest state.")
|
||||||
instance.profile.state = get_guest_state()
|
instance.profile.state = get_guest_state()
|
||||||
instance.profile.save(update_fields=['state'])
|
instance.profile.save(update_fields=['state'])
|
||||||
|
|
||||||
@@ -142,10 +140,10 @@ def assign_state_on_active_change(sender, instance, *args, **kwargs):
|
|||||||
def check_state_on_character_update(sender, instance, *args, **kwargs):
|
def check_state_on_character_update(sender, instance, *args, **kwargs):
|
||||||
# if this is a main character updating, check that user's state
|
# if this is a main character updating, check that user's state
|
||||||
try:
|
try:
|
||||||
logger.debug("Character {0} has been saved. Assessing owner's state for changes.".format(instance))
|
logger.debug(f"Character {instance} has been saved. Assessing owner's state for changes.")
|
||||||
instance.userprofile.assign_state()
|
instance.userprofile.assign_state()
|
||||||
except UserProfile.DoesNotExist:
|
except UserProfile.DoesNotExist:
|
||||||
logger.debug("Character {0} is not a main character. No state assessment required.".format(instance))
|
logger.debug(f"Character {instance} is not a main character. No state assessment required.")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -155,7 +153,7 @@ def ownership_record_creation(sender, instance, created, *args, **kwargs):
|
|||||||
records = OwnershipRecord.objects.filter(owner_hash=instance.owner_hash).filter(character=instance.character)
|
records = OwnershipRecord.objects.filter(owner_hash=instance.owner_hash).filter(character=instance.character)
|
||||||
if records.exists():
|
if records.exists():
|
||||||
if records[0].user == instance.user: # most recent record is sorted first
|
if records[0].user == instance.user: # most recent record is sorted first
|
||||||
logger.debug("Already have ownership record of {0} by user {1}".format(instance.character, instance.user))
|
logger.debug(f"Already have ownership record of {instance.character} by user {instance.user}")
|
||||||
return
|
return
|
||||||
logger.info("Character {0} has a new owner {1}. Creating ownership record.".format(instance.character, instance.user))
|
logger.info(f"Character {instance.character} has a new owner {instance.user}. Creating ownership record.")
|
||||||
OwnershipRecord.objects.create(user=instance.user, character=instance.character, owner_hash=instance.owner_hash)
|
OwnershipRecord.objects.create(user=instance.user, character=instance.character, owner_hash=instance.owner_hash)
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
CSS for allianceauth admin site
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* styling for profile pic */
|
||||||
|
.img-circle {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.column-user_profile_pic {
|
||||||
|
width: 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tooltip */
|
||||||
|
.tooltip {
|
||||||
|
position: relative ;
|
||||||
|
}
|
||||||
|
.tooltip:hover::after {
|
||||||
|
content: attr(data-tooltip) ;
|
||||||
|
position: absolute ;
|
||||||
|
top: 1.1em ;
|
||||||
|
left: 1em ;
|
||||||
|
min-width: 200px ;
|
||||||
|
border: 1px #808080 solid ;
|
||||||
|
padding: 8px ;
|
||||||
|
color: black ;
|
||||||
|
background-color: rgb(255, 255, 204) ;
|
||||||
|
z-index: 1 ;
|
||||||
|
}
|
||||||
40
allianceauth/authentication/task_statistics/counters.py
Normal file
40
allianceauth/authentication/task_statistics/counters.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from collections import namedtuple
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
from .event_series import EventSeries
|
||||||
|
|
||||||
|
|
||||||
|
"""Global series for counting task events."""
|
||||||
|
succeeded_tasks = EventSeries("SUCCEEDED_TASKS")
|
||||||
|
retried_tasks = EventSeries("RETRIED_TASKS")
|
||||||
|
failed_tasks = EventSeries("FAILED_TASKS")
|
||||||
|
|
||||||
|
|
||||||
|
_TaskCounts = namedtuple(
|
||||||
|
"_TaskCounts", ["succeeded", "retried", "failed", "total", "earliest_task", "hours"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def dashboard_results(hours: int) -> _TaskCounts:
|
||||||
|
"""Counts of all task events within the given timeframe."""
|
||||||
|
|
||||||
|
def earliest_if_exists(events: EventSeries, earliest: dt.datetime) -> list:
|
||||||
|
my_earliest = events.first_event(earliest=earliest)
|
||||||
|
return [my_earliest] if my_earliest else []
|
||||||
|
|
||||||
|
earliest = dt.datetime.utcnow() - dt.timedelta(hours=hours)
|
||||||
|
earliest_events = list()
|
||||||
|
succeeded_count = succeeded_tasks.count(earliest=earliest)
|
||||||
|
earliest_events += earliest_if_exists(succeeded_tasks, earliest)
|
||||||
|
retried_count = retried_tasks.count(earliest=earliest)
|
||||||
|
earliest_events += earliest_if_exists(retried_tasks, earliest)
|
||||||
|
failed_count = failed_tasks.count(earliest=earliest)
|
||||||
|
earliest_events += earliest_if_exists(failed_tasks, earliest)
|
||||||
|
return _TaskCounts(
|
||||||
|
succeeded=succeeded_count,
|
||||||
|
retried=retried_count,
|
||||||
|
failed=failed_count,
|
||||||
|
total=succeeded_count + retried_count + failed_count,
|
||||||
|
earliest_task=min(earliest_events) if earliest_events else None,
|
||||||
|
hours=hours,
|
||||||
|
)
|
||||||
130
allianceauth/authentication/task_statistics/event_series.py
Normal file
130
allianceauth/authentication/task_statistics/event_series.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import datetime as dt
|
||||||
|
import logging
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from pytz import utc
|
||||||
|
from redis import Redis, RedisError
|
||||||
|
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class _RedisStub:
|
||||||
|
"""Stub of a Redis client.
|
||||||
|
|
||||||
|
It's purpose is to prevent EventSeries objects from trying to access Redis
|
||||||
|
when it is not available. e.g. when the Sphinx docs are rendered by readthedocs.org.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def incr(self, *args, **kwargs):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def zadd(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def zcount(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def zrangebyscore(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EventSeries:
|
||||||
|
"""API for recording and analyzing a series of events."""
|
||||||
|
|
||||||
|
_ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES"
|
||||||
|
|
||||||
|
def __init__(self, key_id: str, redis: Redis = None) -> None:
|
||||||
|
self._redis = cache.get_master_client() if not redis else redis
|
||||||
|
try:
|
||||||
|
if not self._redis.ping():
|
||||||
|
raise RuntimeError()
|
||||||
|
except (AttributeError, RedisError, RuntimeError):
|
||||||
|
logger.exception(
|
||||||
|
"Failed to establish a connection with Redis. "
|
||||||
|
"This EventSeries object is disabled.",
|
||||||
|
)
|
||||||
|
self._redis = _RedisStub()
|
||||||
|
self._key_id = str(key_id)
|
||||||
|
self.clear()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_disabled(self):
|
||||||
|
"""True when this object is disabled, e.g. Redis was not available at startup."""
|
||||||
|
return isinstance(self._redis, _RedisStub)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _key_counter(self):
|
||||||
|
return f"{self._ROOT_KEY}_{self._key_id}_COUNTER"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _key_sorted_set(self):
|
||||||
|
return f"{self._ROOT_KEY}_{self._key_id}_SORTED_SET"
|
||||||
|
|
||||||
|
def add(self, event_time: dt.datetime = None) -> None:
|
||||||
|
"""Add event.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- event_time: timestamp of event. Will use current time if not specified.
|
||||||
|
"""
|
||||||
|
if not event_time:
|
||||||
|
event_time = dt.datetime.utcnow()
|
||||||
|
id = self._redis.incr(self._key_counter)
|
||||||
|
self._redis.zadd(self._key_sorted_set, {id: event_time.timestamp()})
|
||||||
|
|
||||||
|
def all(self) -> List[dt.datetime]:
|
||||||
|
"""List of all known events."""
|
||||||
|
return [
|
||||||
|
event[1]
|
||||||
|
for event in self._redis.zrangebyscore(
|
||||||
|
self._key_sorted_set,
|
||||||
|
"-inf",
|
||||||
|
"+inf",
|
||||||
|
withscores=True,
|
||||||
|
score_cast_func=self._cast_scores_to_dt,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
"""Clear all events."""
|
||||||
|
self._redis.delete(self._key_sorted_set)
|
||||||
|
self._redis.delete(self._key_counter)
|
||||||
|
|
||||||
|
def count(self, earliest: dt.datetime = None, latest: dt.datetime = None) -> int:
|
||||||
|
"""Count of events, can be restricted to given timeframe.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- earliest: Date of first events to count(inclusive), or -infinite if not specified
|
||||||
|
- latest: Date of last events to count(inclusive), or +infinite if not specified
|
||||||
|
"""
|
||||||
|
min = "-inf" if not earliest else earliest.timestamp()
|
||||||
|
max = "+inf" if not latest else latest.timestamp()
|
||||||
|
return self._redis.zcount(self._key_sorted_set, min=min, max=max)
|
||||||
|
|
||||||
|
def first_event(self, earliest: dt.datetime = None) -> Optional[dt.datetime]:
|
||||||
|
"""Date/Time of first event. Returns `None` if series has no events.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- earliest: Date of first events to count(inclusive), or any if not specified
|
||||||
|
"""
|
||||||
|
min = "-inf" if not earliest else earliest.timestamp()
|
||||||
|
event = self._redis.zrangebyscore(
|
||||||
|
self._key_sorted_set,
|
||||||
|
min,
|
||||||
|
"+inf",
|
||||||
|
withscores=True,
|
||||||
|
start=0,
|
||||||
|
num=1,
|
||||||
|
score_cast_func=self._cast_scores_to_dt,
|
||||||
|
)
|
||||||
|
if not event:
|
||||||
|
return None
|
||||||
|
return event[0][1]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _cast_scores_to_dt(score) -> dt.datetime:
|
||||||
|
return dt.datetime.fromtimestamp(float(score), tz=utc)
|
||||||
54
allianceauth/authentication/task_statistics/signals.py
Normal file
54
allianceauth/authentication/task_statistics/signals.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from celery.signals import (
|
||||||
|
task_failure,
|
||||||
|
task_internal_error,
|
||||||
|
task_retry,
|
||||||
|
task_success,
|
||||||
|
worker_ready
|
||||||
|
)
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from .counters import failed_tasks, retried_tasks, succeeded_tasks
|
||||||
|
|
||||||
|
|
||||||
|
def reset_counters():
|
||||||
|
"""Reset all counters for the celery status."""
|
||||||
|
succeeded_tasks.clear()
|
||||||
|
failed_tasks.clear()
|
||||||
|
retried_tasks.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def is_enabled() -> bool:
|
||||||
|
return not bool(
|
||||||
|
getattr(settings, "ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED", False)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@worker_ready.connect
|
||||||
|
def reset_counters_when_celery_restarted(*args, **kwargs):
|
||||||
|
if is_enabled():
|
||||||
|
reset_counters()
|
||||||
|
|
||||||
|
|
||||||
|
@task_success.connect
|
||||||
|
def record_task_succeeded(*args, **kwargs):
|
||||||
|
if is_enabled():
|
||||||
|
succeeded_tasks.add()
|
||||||
|
|
||||||
|
|
||||||
|
@task_retry.connect
|
||||||
|
def record_task_retried(*args, **kwargs):
|
||||||
|
if is_enabled():
|
||||||
|
retried_tasks.add()
|
||||||
|
|
||||||
|
|
||||||
|
@task_failure.connect
|
||||||
|
def record_task_failed(*args, **kwargs):
|
||||||
|
if is_enabled():
|
||||||
|
failed_tasks.add()
|
||||||
|
|
||||||
|
|
||||||
|
@task_internal_error.connect
|
||||||
|
def record_task_internal_error(*args, **kwargs):
|
||||||
|
if is_enabled():
|
||||||
|
failed_tasks.add()
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
from allianceauth.authentication.task_statistics.counters import (
|
||||||
|
dashboard_results,
|
||||||
|
succeeded_tasks,
|
||||||
|
retried_tasks,
|
||||||
|
failed_tasks,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDashboardResults(TestCase):
|
||||||
|
def test_should_return_counts_for_given_timeframe_only(self):
|
||||||
|
# given
|
||||||
|
earliest_task = now() - dt.timedelta(minutes=15)
|
||||||
|
succeeded_tasks.clear()
|
||||||
|
succeeded_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
|
||||||
|
succeeded_tasks.add(earliest_task)
|
||||||
|
succeeded_tasks.add()
|
||||||
|
succeeded_tasks.add()
|
||||||
|
retried_tasks.clear()
|
||||||
|
retried_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
|
||||||
|
retried_tasks.add(now() - dt.timedelta(seconds=30))
|
||||||
|
retried_tasks.add()
|
||||||
|
failed_tasks.clear()
|
||||||
|
failed_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
|
||||||
|
failed_tasks.add()
|
||||||
|
# when
|
||||||
|
results = dashboard_results(hours=1)
|
||||||
|
# then
|
||||||
|
self.assertEqual(results.succeeded, 3)
|
||||||
|
self.assertEqual(results.retried, 2)
|
||||||
|
self.assertEqual(results.failed, 1)
|
||||||
|
self.assertEqual(results.total, 6)
|
||||||
|
self.assertEqual(results.earliest_task, earliest_task)
|
||||||
|
|
||||||
|
def test_should_work_with_no_data(self):
|
||||||
|
# given
|
||||||
|
succeeded_tasks.clear()
|
||||||
|
retried_tasks.clear()
|
||||||
|
failed_tasks.clear()
|
||||||
|
# when
|
||||||
|
results = dashboard_results(hours=1)
|
||||||
|
# then
|
||||||
|
self.assertEqual(results.succeeded, 0)
|
||||||
|
self.assertEqual(results.retried, 0)
|
||||||
|
self.assertEqual(results.failed, 0)
|
||||||
|
self.assertEqual(results.total, 0)
|
||||||
|
self.assertIsNone(results.earliest_task)
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import datetime as dt
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from pytz import utc
|
||||||
|
from redis import RedisError
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
from allianceauth.authentication.task_statistics.event_series import (
|
||||||
|
EventSeries,
|
||||||
|
_RedisStub,
|
||||||
|
)
|
||||||
|
|
||||||
|
MODULE_PATH = "allianceauth.authentication.task_statistics.event_series"
|
||||||
|
|
||||||
|
|
||||||
|
class TestEventSeries(TestCase):
|
||||||
|
def test_should_abort_without_redis_client(self):
|
||||||
|
# when
|
||||||
|
with patch(MODULE_PATH + ".cache.get_master_client") as mock:
|
||||||
|
mock.return_value = None
|
||||||
|
events = EventSeries("dummy")
|
||||||
|
# then
|
||||||
|
self.assertTrue(events._redis, _RedisStub)
|
||||||
|
self.assertTrue(events.is_disabled)
|
||||||
|
|
||||||
|
def test_should_disable_itself_if_redis_not_available_1(self):
|
||||||
|
# when
|
||||||
|
with patch(MODULE_PATH + ".cache.get_master_client") as mock_get_master_client:
|
||||||
|
mock_get_master_client.return_value.ping.side_effect = RedisError
|
||||||
|
events = EventSeries("dummy")
|
||||||
|
# then
|
||||||
|
self.assertIsInstance(events._redis, _RedisStub)
|
||||||
|
self.assertTrue(events.is_disabled)
|
||||||
|
|
||||||
|
def test_should_disable_itself_if_redis_not_available_2(self):
|
||||||
|
# when
|
||||||
|
with patch(MODULE_PATH + ".cache.get_master_client") as mock_get_master_client:
|
||||||
|
mock_get_master_client.return_value.ping.return_value = False
|
||||||
|
events = EventSeries("dummy")
|
||||||
|
# then
|
||||||
|
self.assertIsInstance(events._redis, _RedisStub)
|
||||||
|
self.assertTrue(events.is_disabled)
|
||||||
|
|
||||||
|
def test_should_add_event(self):
|
||||||
|
# given
|
||||||
|
events = EventSeries("dummy")
|
||||||
|
# when
|
||||||
|
events.add()
|
||||||
|
# then
|
||||||
|
result = events.all()
|
||||||
|
self.assertEqual(len(result), 1)
|
||||||
|
self.assertAlmostEqual(result[0], now(), delta=dt.timedelta(seconds=30))
|
||||||
|
|
||||||
|
def test_should_add_event_with_specified_time(self):
|
||||||
|
# given
|
||||||
|
events = EventSeries("dummy")
|
||||||
|
my_time = dt.datetime(2021, 11, 1, 12, 15, tzinfo=utc)
|
||||||
|
# when
|
||||||
|
events.add(my_time)
|
||||||
|
# then
|
||||||
|
result = events.all()
|
||||||
|
self.assertEqual(len(result), 1)
|
||||||
|
self.assertAlmostEqual(result[0], my_time, delta=dt.timedelta(seconds=30))
|
||||||
|
|
||||||
|
def test_should_count_events(self):
|
||||||
|
# given
|
||||||
|
events = EventSeries("dummy")
|
||||||
|
events.add()
|
||||||
|
events.add()
|
||||||
|
# when
|
||||||
|
result = events.count()
|
||||||
|
# then
|
||||||
|
self.assertEqual(result, 2)
|
||||||
|
|
||||||
|
def test_should_count_zero(self):
|
||||||
|
# given
|
||||||
|
events = EventSeries("dummy")
|
||||||
|
# when
|
||||||
|
result = events.count()
|
||||||
|
# then
|
||||||
|
self.assertEqual(result, 0)
|
||||||
|
|
||||||
|
def test_should_count_events_within_timeframe_1(self):
|
||||||
|
# given
|
||||||
|
events = EventSeries("dummy")
|
||||||
|
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
|
||||||
|
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
|
||||||
|
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
|
||||||
|
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
|
||||||
|
# when
|
||||||
|
result = events.count(
|
||||||
|
earliest=dt.datetime(2021, 12, 1, 12, 8, tzinfo=utc),
|
||||||
|
latest=dt.datetime(2021, 12, 1, 12, 17, tzinfo=utc),
|
||||||
|
)
|
||||||
|
# then
|
||||||
|
self.assertEqual(result, 2)
|
||||||
|
|
||||||
|
def test_should_count_events_within_timeframe_2(self):
|
||||||
|
# given
|
||||||
|
events = EventSeries("dummy")
|
||||||
|
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
|
||||||
|
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
|
||||||
|
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
|
||||||
|
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
|
||||||
|
# when
|
||||||
|
result = events.count(earliest=dt.datetime(2021, 12, 1, 12, 8))
|
||||||
|
# then
|
||||||
|
self.assertEqual(result, 3)
|
||||||
|
|
||||||
|
def test_should_count_events_within_timeframe_3(self):
|
||||||
|
# given
|
||||||
|
events = EventSeries("dummy")
|
||||||
|
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
|
||||||
|
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
|
||||||
|
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
|
||||||
|
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
|
||||||
|
# when
|
||||||
|
result = events.count(latest=dt.datetime(2021, 12, 1, 12, 12))
|
||||||
|
# then
|
||||||
|
self.assertEqual(result, 2)
|
||||||
|
|
||||||
|
def test_should_clear_events(self):
|
||||||
|
# given
|
||||||
|
events = EventSeries("dummy")
|
||||||
|
events.add()
|
||||||
|
events.add()
|
||||||
|
# when
|
||||||
|
events.clear()
|
||||||
|
# then
|
||||||
|
self.assertEqual(events.count(), 0)
|
||||||
|
|
||||||
|
def test_should_return_date_of_first_event(self):
|
||||||
|
# given
|
||||||
|
events = EventSeries("dummy")
|
||||||
|
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
|
||||||
|
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
|
||||||
|
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
|
||||||
|
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
|
||||||
|
# when
|
||||||
|
result = events.first_event()
|
||||||
|
# then
|
||||||
|
self.assertEqual(result, dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
|
||||||
|
|
||||||
|
def test_should_return_date_of_first_event_with_range(self):
|
||||||
|
# given
|
||||||
|
events = EventSeries("dummy")
|
||||||
|
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
|
||||||
|
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
|
||||||
|
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
|
||||||
|
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
|
||||||
|
# when
|
||||||
|
result = events.first_event(
|
||||||
|
earliest=dt.datetime(2021, 12, 1, 12, 8, tzinfo=utc)
|
||||||
|
)
|
||||||
|
# then
|
||||||
|
self.assertEqual(result, dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
|
||||||
|
|
||||||
|
def test_should_return_all_events(self):
|
||||||
|
# given
|
||||||
|
events = EventSeries("dummy")
|
||||||
|
events.add()
|
||||||
|
events.add()
|
||||||
|
# when
|
||||||
|
results = events.all()
|
||||||
|
# then
|
||||||
|
self.assertEqual(len(results), 2)
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from celery.exceptions import Retry
|
||||||
|
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
|
from allianceauth.authentication.task_statistics.counters import (
|
||||||
|
failed_tasks,
|
||||||
|
retried_tasks,
|
||||||
|
succeeded_tasks,
|
||||||
|
)
|
||||||
|
from allianceauth.authentication.task_statistics.signals import (
|
||||||
|
reset_counters,
|
||||||
|
is_enabled,
|
||||||
|
)
|
||||||
|
from allianceauth.eveonline.tasks import update_character
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
CELERY_ALWAYS_EAGER=True,ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=False
|
||||||
|
)
|
||||||
|
class TestTaskSignals(TestCase):
|
||||||
|
fixtures = ["disable_analytics"]
|
||||||
|
|
||||||
|
def test_should_record_successful_task(self):
|
||||||
|
# given
|
||||||
|
succeeded_tasks.clear()
|
||||||
|
retried_tasks.clear()
|
||||||
|
failed_tasks.clear()
|
||||||
|
# when
|
||||||
|
with patch(
|
||||||
|
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
||||||
|
) as mock_update:
|
||||||
|
mock_update.return_value = None
|
||||||
|
update_character.delay(1)
|
||||||
|
# then
|
||||||
|
self.assertEqual(succeeded_tasks.count(), 1)
|
||||||
|
self.assertEqual(retried_tasks.count(), 0)
|
||||||
|
self.assertEqual(failed_tasks.count(), 0)
|
||||||
|
|
||||||
|
def test_should_record_retried_task(self):
|
||||||
|
# given
|
||||||
|
succeeded_tasks.clear()
|
||||||
|
retried_tasks.clear()
|
||||||
|
failed_tasks.clear()
|
||||||
|
# when
|
||||||
|
with patch(
|
||||||
|
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
||||||
|
) as mock_update:
|
||||||
|
mock_update.side_effect = Retry
|
||||||
|
update_character.delay(1)
|
||||||
|
# then
|
||||||
|
self.assertEqual(succeeded_tasks.count(), 0)
|
||||||
|
self.assertEqual(failed_tasks.count(), 0)
|
||||||
|
self.assertEqual(retried_tasks.count(), 1)
|
||||||
|
|
||||||
|
def test_should_record_failed_task(self):
|
||||||
|
# given
|
||||||
|
succeeded_tasks.clear()
|
||||||
|
retried_tasks.clear()
|
||||||
|
failed_tasks.clear()
|
||||||
|
# when
|
||||||
|
with patch(
|
||||||
|
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
||||||
|
) as mock_update:
|
||||||
|
mock_update.side_effect = RuntimeError
|
||||||
|
update_character.delay(1)
|
||||||
|
# then
|
||||||
|
self.assertEqual(succeeded_tasks.count(), 0)
|
||||||
|
self.assertEqual(retried_tasks.count(), 0)
|
||||||
|
self.assertEqual(failed_tasks.count(), 1)
|
||||||
|
|
||||||
|
def test_should_reset_counters(self):
|
||||||
|
# given
|
||||||
|
succeeded_tasks.add()
|
||||||
|
retried_tasks.add()
|
||||||
|
failed_tasks.add()
|
||||||
|
# when
|
||||||
|
reset_counters()
|
||||||
|
# then
|
||||||
|
self.assertEqual(succeeded_tasks.count(), 0)
|
||||||
|
self.assertEqual(retried_tasks.count(), 0)
|
||||||
|
self.assertEqual(failed_tasks.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestIsEnabled(TestCase):
|
||||||
|
@override_settings(ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=False)
|
||||||
|
def test_enabled(self):
|
||||||
|
self.assertTrue(is_enabled())
|
||||||
|
|
||||||
|
@override_settings(ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=True)
|
||||||
|
def test_disabled(self):
|
||||||
|
self.assertFalse(is_enabled())
|
||||||
@@ -22,13 +22,13 @@ def check_character_ownership(owner_hash):
|
|||||||
continue
|
continue
|
||||||
except (KeyError, IncompleteResponseError):
|
except (KeyError, IncompleteResponseError):
|
||||||
# We can't validate the hash hasn't changed but also can't assume it has. Abort for now.
|
# We can't validate the hash hasn't changed but also can't assume it has. Abort for now.
|
||||||
logger.warning("Failed to validate owner hash of {0} due to problems contacting SSO servers.".format(
|
logger.warning("Failed to validate owner hash of {} due to problems contacting SSO servers.".format(
|
||||||
tokens[0].character_name))
|
tokens[0].character_name))
|
||||||
break
|
break
|
||||||
|
|
||||||
if not t.character_owner_hash == old_hash:
|
if not t.character_owner_hash == old_hash:
|
||||||
logger.info(
|
logger.info(
|
||||||
'Character %s has changed ownership. Revoking %s tokens.' % (t.character_name, tokens.count()))
|
f'Character {t.character_name} has changed ownership. Revoking {tokens.count()} tokens.')
|
||||||
tokens.delete()
|
tokens.delete()
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@@ -1,86 +1,133 @@
|
|||||||
{% extends "allianceauth/base.html" %}
|
{% extends "allianceauth/base.html" %}
|
||||||
{% load staticfiles %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block page_title %}{% trans "Dashboard" %}{% endblock %}
|
{% block page_title %}{% translate "Dashboard" %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="page-header text-center">{% trans "Dashboard" %}</h1>
|
<h1 class="page-header text-center">{% translate "Dashboard" %}</h1>
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff %}
|
||||||
{% include 'allianceauth/admin-status/include.html' %}
|
{% include 'allianceauth/admin-status/include.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<div class="row vertical-flexbox-row">
|
<div class="row vertical-flexbox-row2">
|
||||||
<div class="col-sm-6 text-center">
|
<div class="col-sm-6 text-center">
|
||||||
<div class="panel panel-primary" style="height:100%">
|
<div class="panel panel-primary" style="height:100%">
|
||||||
<div class="panel-heading"><h3 class="panel-title">{% trans "Main Character" %}</h3></div>
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">
|
||||||
|
{% blocktrans with state=request.user.profile.state %}
|
||||||
|
Main Character (State: {{ state }})
|
||||||
|
{% endblocktrans %}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% if request.user.profile.main_character %}
|
{% if request.user.profile.main_character %}
|
||||||
{% with request.user.profile.main_character as main %}
|
{% with request.user.profile.main_character as main %}
|
||||||
<div class="col-lg-4 col-sm-2">
|
<div class="hidden-xs">
|
||||||
<table class="table">
|
<div class="col-lg-4 col-sm-2">
|
||||||
<tr>
|
<table class="table">
|
||||||
<td class="text-center">
|
<tr>
|
||||||
<img class="ra-avatar"src="{{ main.portrait_url_128 }}">
|
<td class="text-center">
|
||||||
</td>
|
<img class="ra-avatar"src="{{ main.portrait_url_128 }}">
|
||||||
</tr>
|
</td>
|
||||||
<tr>
|
</tr>
|
||||||
<td class="text-center">{{ main.character_name }}</td>
|
<tr>
|
||||||
</tr>
|
<td class="text-center">{{ main.character_name }}</td>
|
||||||
</table>
|
</tr>
|
||||||
</div>
|
</table>
|
||||||
<div class="col-lg-4 col-sm-2">
|
</div>
|
||||||
<table class="table">
|
<div class="col-lg-4 col-sm-2">
|
||||||
<tr>
|
<table class="table">
|
||||||
<td class="text-center">
|
<tr>
|
||||||
<img class="ra-avatar"src="{{ main.corporation_logo_url_128 }}">
|
<td class="text-center">
|
||||||
</td>
|
<img class="ra-avatar"src="{{ main.corporation_logo_url_128 }}">
|
||||||
</tr>
|
</td>
|
||||||
<tr>
|
</tr>
|
||||||
<td class="text-center">{{ main.corporation_name }}</td>
|
<tr>
|
||||||
</tr>
|
<td class="text-center">{{ main.corporation_name }}</td>
|
||||||
</table>
|
</tr>
|
||||||
</div>
|
</table>
|
||||||
<div class="col-lg-4 col-sm-2">
|
</div>
|
||||||
{% if main.alliance_id %}
|
<div class="col-lg-4 col-sm-2">
|
||||||
<table class="table">
|
{% if main.alliance_id %}
|
||||||
<tr>
|
<table class="table">
|
||||||
<td class="text-center">
|
<tr>
|
||||||
<img class="ra-avatar"src="{{ main.alliance_logo_url_128 }}">
|
<td class="text-center">
|
||||||
</td>
|
<img class="ra-avatar"src="{{ main.alliance_logo_url_128 }}">
|
||||||
</tr>
|
</td>
|
||||||
<tr>
|
</tr>
|
||||||
<td class="text-center">{{ main.alliance_name }}</td>
|
<tr>
|
||||||
<tr>
|
<td class="text-center">{{ main.alliance_name }}</td>
|
||||||
</table>
|
<tr>
|
||||||
{% endif %}
|
</table>
|
||||||
</div>
|
{% elif main.faction_id %}
|
||||||
{% endwith %}
|
<table class="table">
|
||||||
|
<tr>
|
||||||
|
<td class="text-center">
|
||||||
|
<img class="ra-avatar"src="{{ main.faction_logo_url_128 }}">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center">{{ main.faction_name }}</td>
|
||||||
|
<tr>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table visible-xs-block">
|
||||||
|
<p>
|
||||||
|
<img class="ra-avatar" src="{{ main.portrait_url_64 }}">
|
||||||
|
<img class="ra-avatar" src="{{ main.corporation_logo_url_64 }}">
|
||||||
|
{% if main.alliance_id %}
|
||||||
|
<img class="ra-avatar" src="{{ main.alliance_logo_url_64 }}">
|
||||||
|
{% endif %}
|
||||||
|
{% if main.faction_id %}
|
||||||
|
<img class="ra-avatar" src="{{ main.faction_logo_url_64 }}">
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>{{ main.character_name }}</strong><br>
|
||||||
|
{{ main.corporation_name }}<br>
|
||||||
|
{% if main.alliance_id %}
|
||||||
|
{{ main.alliance_name }}<br>
|
||||||
|
{% endif %}
|
||||||
|
{% if main.faction_id %}
|
||||||
|
{{ main.faction_name }}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="alert alert-danger" role="alert">{% trans "No main character set." %}</div>
|
<div class="alert alert-danger" role="alert">
|
||||||
|
{% translate "No main character set." %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
<div class="col-xs-6">
|
<div class="row">
|
||||||
<a href="{% url 'authentication:add_character' %}" class="btn btn-block btn-info"
|
<div class="col-sm-6 button-wrapper">
|
||||||
title="Add Character">{% trans 'Add Character' %}</a>
|
<a href="{% url 'authentication:add_character' %}" class="btn btn-block btn-info"
|
||||||
</div>
|
title="Add Character">{% translate 'Add Character' %}</a>
|
||||||
<div class="col-xs-6">
|
</div>
|
||||||
<a href="{% url 'authentication:change_main_character' %}" class="btn btn-block btn-info"
|
<div class="col-sm-6 button-wrapper">
|
||||||
title="Change Main Character">{% trans "Change Main" %}</a>
|
<a href="{% url 'authentication:change_main_character' %}" class="btn btn-block btn-info"
|
||||||
|
title="Change Main Character">{% translate "Change Main" %}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6 text-center">
|
<div class="col-sm-6 text-center">
|
||||||
<div class="panel panel-success" style="height:100%">
|
<div class="panel panel-success" style="height:100%">
|
||||||
<div class="panel-heading"><h3 class="panel-title">{% trans "Groups" %}</h3></div>
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">{% translate "Group Memberships" %}</h3>
|
||||||
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div style="height: 240px;overflow:-moz-scrollbars-vertical;overflow-y:auto;">
|
<div style="height: 240px;overflow:-moz-scrollbars-vertical;overflow-y:auto;">
|
||||||
<table class="table table-striped">
|
<table class="table table-aa">
|
||||||
{% for group in user.groups.all %}
|
{% for group in groups %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ group.name }}</td>
|
<td>{{ group.name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,26 +137,48 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading" style="display:flex;"><h3 class="panel-title">{% trans 'Characters' %}</h3></div>
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title text-center" style="text-align: center">
|
||||||
|
{% translate 'Characters' %}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<table class="table table-hover">
|
<table class="table table-aa hidden-xs">
|
||||||
<tr>
|
<thead>
|
||||||
<th class="text-center"></th>
|
<tr>
|
||||||
<th class="text-center">{% trans 'Name' %}</th>
|
<th class="text-center"></th>
|
||||||
<th class="text-center">{% trans 'Corp' %}</th>
|
<th class="text-center">{% translate 'Name' %}</th>
|
||||||
<th class="text-center">{% trans 'Alliance' %}</th>
|
<th class="text-center">{% translate 'Corp' %}</th>
|
||||||
</tr>
|
<th class="text-center">{% translate 'Alliance' %}</th>
|
||||||
{% for ownership in request.user.character_ownerships.all %}
|
</tr>
|
||||||
{% with ownership.character as char %}
|
</thead>
|
||||||
<tr>
|
<tbody>
|
||||||
<td class="text-center"><img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}">
|
{% for char in characters %}
|
||||||
</td>
|
<tr>
|
||||||
<td class="text-center">{{ char.character_name }}</td>
|
<td class="text-center"><img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}">
|
||||||
<td class="text-center">{{ char.corporation_name }}</td>
|
</td>
|
||||||
<td class="text-center">{{ char.alliance_name }}</td>
|
<td class="text-center">{{ char.character_name }}</td>
|
||||||
</tr>
|
<td class="text-center">{{ char.corporation_name }}</td>
|
||||||
{% endwith %}
|
<td class="text-center">{{ char.alliance_name }}</td>
|
||||||
{% endfor %}
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<table class="table table-aa visible-xs-block" style="width: 100%">
|
||||||
|
<tbody>
|
||||||
|
{% for char in characters %}
|
||||||
|
<tr>
|
||||||
|
<td class="text-center" style="vertical-align: middle">
|
||||||
|
<img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}">
|
||||||
|
</td>
|
||||||
|
<td class="text-center" style="vertical-align: middle; width: 100%">
|
||||||
|
<strong>{{ char.character_name }}</strong><br>
|
||||||
|
{{ char.corporation_name }}<br>
|
||||||
|
{{ char.alliance_name|default:"" }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,9 +6,13 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<meta name="author" content="">
|
<meta name="author" content="">
|
||||||
|
<meta property="og:title" content="{{ SITE_NAME }}">
|
||||||
|
<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'icons/apple-touch-icon.png' %}">
|
||||||
|
<meta property="og:description" content="Alliance Auth - An auth system for EVE Online to help in-game organizations manage online service access.">
|
||||||
|
|
||||||
{% include 'allianceauth/icons.html' %}
|
{% include 'allianceauth/icons.html' %}
|
||||||
|
|
||||||
<title>{% block title %}{{ SITE_NAME }}{% endblock %}</title>
|
<title>{% block title %}{% block page_title %}{% endblock page_title %} - {{ SITE_NAME }}{% endblock title %}</title>
|
||||||
|
|
||||||
{% include 'bundles/bootstrap-css.html' %}
|
{% include 'bundles/bootstrap-css.html' %}
|
||||||
{% include 'bundles/fontawesome.html' %}
|
{% include 'bundles/fontawesome.html' %}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
{% extends 'public/middle_box.html' %}
|
{% extends 'public/middle_box.html' %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% block page_title %}Login{% endblock %}
|
|
||||||
|
{% block page_title %}{% translate "Login" %}{% endblock %}
|
||||||
|
|
||||||
{% block middle_box_content %}
|
{% block middle_box_content %}
|
||||||
<p style="text-align:center">
|
<a href="{% url 'auth_sso_login' %}{% if request.GET.next %}?next={{request.GET.next}}{%endif%}">
|
||||||
<a href="{% url 'auth_sso_login' %}">
|
<img class="img-responsive center-block" src="{% static 'img/sso/EVE_SSO_Login_Buttons_Large_Black.png' %}" border=0>
|
||||||
<img src="{% static 'img/sso/EVE_SSO_Login_Buttons_Large_Black.png' %}" border=0>
|
</a>
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
{% extends 'public/base.html' %}
|
{% extends 'public/base.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% block title %}Login{% endblock %}
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="col-md-4 col-md-offset-4">
|
<div class="col-md-4 col-md-offset-4">
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
{% load staticfiles %}
|
{% extends 'public/base.html' %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
{% load bootstrap %}
|
{% load bootstrap %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% extends 'public/base.html' %}
|
|
||||||
{% block page_title %}Registration{% endblock %}
|
{% block page_title %}{% translate "Registration" %}{% endblock %}
|
||||||
|
|
||||||
{% block extra_include %}
|
{% block extra_include %}
|
||||||
{% include 'bundles/bootstrap-css.html' %}
|
{% include 'bundles/bootstrap-css.html' %}
|
||||||
{% include 'bundles/fontawesome.html' %}
|
{% include 'bundles/fontawesome.html' %}
|
||||||
{% include 'bundles/bootstrap-js.html' %}
|
{% include 'bundles/bootstrap-js.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="col-md-4 col-md-offset-4">
|
<div class="col-md-4 col-md-offset-4">
|
||||||
<div class="panel panel-default panel-transparent">
|
<div class="panel panel-default panel-transparent">
|
||||||
@@ -15,7 +19,7 @@
|
|||||||
<form method="POST">
|
<form method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form|bootstrap }}
|
{{ form|bootstrap }}
|
||||||
<button class="btn btn-lg btn-primary btn-block" type="submit">{% trans "Register" %}</button>
|
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Register" %}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends 'public/middle_box.html' %}
|
{% extends 'public/middle_box.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block middle_box_content %}
|
{% block middle_box_content %}
|
||||||
<div class="alert alert-danger">{% trans 'Invalid or expired activation link.' %}</div>
|
<div class="alert alert-danger">{% translate 'Invalid or expired activation link.' %}</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
You're receiving this email because someone has entered this email address while registering for an account on {{ site.domain }}
|
You're receiving this email because someone has entered this email address while registering for an account on {{ site.domain }}
|
||||||
|
|
||||||
If this was you, please go to the following URL to confirm your email address:
|
If this was you, please click on the link below to confirm your email address:
|
||||||
|
|
||||||
{{ scheme }}://{{ url }}
|
{{ scheme }}://{{ url }}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<p>
|
||||||
|
You're receiving this email because someone has entered this email address while registering for an account on {{ site.domain }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If this was you, please click on the link below to confirm your email address:
|
||||||
|
<p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="{{ scheme }}://{{ url }}">Confirm email address</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This link will expire in {{ expiration_days }} day(s).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If this was not you, it is safe to ignore this email.
|
||||||
|
</p>
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
{% load i18n %}{% autoescape off %}
|
{% load i18n %}{% autoescape off %}
|
||||||
{% blocktrans %}You're receiving this email because you requested a password reset for your
|
{% blocktrans trimmed %}You're receiving this email because you requested a password reset for your
|
||||||
user account.{% endblocktrans %}
|
user account.{% endblocktrans %}
|
||||||
|
|
||||||
{% trans "Please go to the following page and choose a new password:" %}
|
{% translate "Please go to the following page and choose a new password:" %}
|
||||||
{% block reset_link %}
|
{% block reset_link %}
|
||||||
{{domain}}{% url 'password_reset_confirm' uidb64=uid token=token %}
|
{{domain}}{% url 'password_reset_confirm' uidb64=uid token=token %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}
|
{% translate "Your username, in case you've forgotten:" %} {{ user.get_username }}
|
||||||
|
|
||||||
{% trans "Thanks for using our site!" %}
|
{% translate "Thanks for using our site!" %}
|
||||||
|
|
||||||
{% blocktrans %}Your IT Team{% endblocktrans %}
|
{% blocktrans %}Your IT Team{% endblocktrans %}
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
{% load bootstrap %}
|
{% load bootstrap %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% block page_title %}Register{% endblock %}
|
{% block page_title %}{% translate "Register" %}{% endblock %}
|
||||||
{% block middle_box_content %}
|
{% block middle_box_content %}
|
||||||
<form class="form-signin" role="form" action="" method="POST">
|
<form class="form-signin" role="form" action="" method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form|bootstrap }}
|
{{ form|bootstrap }}
|
||||||
<br/>
|
<br/>
|
||||||
<button class="btn btn-lg btn-primary btn-block" type="submit">{% trans "Submit" %}</button>
|
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Submit" %}</button>
|
||||||
<br/>
|
<br/>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
19
allianceauth/authentication/tests/__init__.py
Normal file
19
allianceauth/authentication/tests/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
|
def get_admin_change_view_url(obj: object) -> str:
|
||||||
|
"""returns URL to admin change view for given object"""
|
||||||
|
return reverse(
|
||||||
|
'admin:{}_{}_change'.format(
|
||||||
|
obj._meta.app_label, type(obj).__name__.lower()
|
||||||
|
),
|
||||||
|
args=(obj.pk,)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_admin_search_url(ModelClass: type) -> str:
|
||||||
|
"""returns URL to search URL for model of given object"""
|
||||||
|
return '{}{}/'.format(
|
||||||
|
reverse('admin:app_list', args=(ModelClass._meta.app_label,)),
|
||||||
|
ModelClass.__name__.lower()
|
||||||
|
)
|
||||||
831
allianceauth/authentication/tests/test_admin.py
Normal file
831
allianceauth/authentication/tests/test_admin.py
Normal file
@@ -0,0 +1,831 @@
|
|||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from urllib.parse import quote
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
from django_webtest import WebTest
|
||||||
|
|
||||||
|
from django.contrib.admin.sites import AdminSite
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
from django.test import TestCase, RequestFactory, Client
|
||||||
|
|
||||||
|
from allianceauth.authentication.models import (
|
||||||
|
CharacterOwnership, State, OwnershipRecord
|
||||||
|
)
|
||||||
|
from allianceauth.eveonline.models import (
|
||||||
|
EveCharacter, EveCorporationInfo, EveAllianceInfo, EveFactionInfo
|
||||||
|
)
|
||||||
|
from allianceauth.services.hooks import ServicesHook
|
||||||
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
|
|
||||||
|
from ..admin import (
|
||||||
|
BaseUserAdmin,
|
||||||
|
CharacterOwnershipAdmin,
|
||||||
|
StateAdmin,
|
||||||
|
MainCorporationsFilter,
|
||||||
|
MainAllianceFilter,
|
||||||
|
MainFactionFilter,
|
||||||
|
OwnershipRecordAdmin,
|
||||||
|
User,
|
||||||
|
UserAdmin,
|
||||||
|
user_main_organization,
|
||||||
|
user_profile_pic,
|
||||||
|
user_username,
|
||||||
|
update_main_character_model,
|
||||||
|
make_service_hooks_update_groups_action,
|
||||||
|
make_service_hooks_sync_nickname_action
|
||||||
|
)
|
||||||
|
from . import get_admin_change_view_url, get_admin_search_url
|
||||||
|
|
||||||
|
|
||||||
|
MODULE_PATH = 'allianceauth.authentication.admin'
|
||||||
|
|
||||||
|
|
||||||
|
class MockRequest:
|
||||||
|
def __init__(self, user=None):
|
||||||
|
self.user = user
|
||||||
|
|
||||||
|
|
||||||
|
class TestCaseWithTestData(TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
|
||||||
|
for MyModel in [
|
||||||
|
EveAllianceInfo, EveCorporationInfo, EveCharacter, Group, User
|
||||||
|
]:
|
||||||
|
MyModel.objects.all().delete()
|
||||||
|
|
||||||
|
# groups
|
||||||
|
cls.group_1 = Group.objects.create(
|
||||||
|
name='Group 1'
|
||||||
|
)
|
||||||
|
cls.group_2 = Group.objects.create(
|
||||||
|
name='Group 2'
|
||||||
|
)
|
||||||
|
|
||||||
|
# user 1 - corp and alliance, normal user
|
||||||
|
character_1 = EveCharacter.objects.create(
|
||||||
|
character_id=1001,
|
||||||
|
character_name='Bruce Wayne',
|
||||||
|
corporation_id=2001,
|
||||||
|
corporation_name='Wayne Technologies',
|
||||||
|
corporation_ticker='WT',
|
||||||
|
alliance_id=3001,
|
||||||
|
alliance_name='Wayne Enterprises',
|
||||||
|
alliance_ticker='WE',
|
||||||
|
)
|
||||||
|
character_1a = EveCharacter.objects.create(
|
||||||
|
character_id=1002,
|
||||||
|
character_name='Batman',
|
||||||
|
corporation_id=2001,
|
||||||
|
corporation_name='Wayne Technologies',
|
||||||
|
corporation_ticker='WT',
|
||||||
|
alliance_id=3001,
|
||||||
|
alliance_name='Wayne Enterprises',
|
||||||
|
alliance_ticker='WE',
|
||||||
|
)
|
||||||
|
alliance = EveAllianceInfo.objects.create(
|
||||||
|
alliance_id=3001,
|
||||||
|
alliance_name='Wayne Enterprises',
|
||||||
|
alliance_ticker='WE',
|
||||||
|
executor_corp_id=2001
|
||||||
|
)
|
||||||
|
EveCorporationInfo.objects.create(
|
||||||
|
corporation_id=2001,
|
||||||
|
corporation_name='Wayne Technologies',
|
||||||
|
corporation_ticker='WT',
|
||||||
|
member_count=42,
|
||||||
|
alliance=alliance
|
||||||
|
)
|
||||||
|
cls.user_1 = User.objects.create_user(
|
||||||
|
character_1.character_name.replace(' ', '_'),
|
||||||
|
'abc@example.com',
|
||||||
|
'password'
|
||||||
|
)
|
||||||
|
CharacterOwnership.objects.create(
|
||||||
|
character=character_1,
|
||||||
|
owner_hash='x1' + character_1.character_name,
|
||||||
|
user=cls.user_1
|
||||||
|
)
|
||||||
|
CharacterOwnership.objects.create(
|
||||||
|
character=character_1a,
|
||||||
|
owner_hash='x1' + character_1a.character_name,
|
||||||
|
user=cls.user_1
|
||||||
|
)
|
||||||
|
cls.user_1.profile.main_character = character_1
|
||||||
|
cls.user_1.profile.save()
|
||||||
|
cls.user_1.groups.add(cls.group_1)
|
||||||
|
|
||||||
|
# user 2 - corp only, staff
|
||||||
|
character_2 = EveCharacter.objects.create(
|
||||||
|
character_id=1003,
|
||||||
|
character_name='Clark Kent',
|
||||||
|
corporation_id=2002,
|
||||||
|
corporation_name='Daily Planet',
|
||||||
|
corporation_ticker='DP',
|
||||||
|
alliance_id=None
|
||||||
|
)
|
||||||
|
EveCorporationInfo.objects.create(
|
||||||
|
corporation_id=2002,
|
||||||
|
corporation_name='Daily Plane',
|
||||||
|
corporation_ticker='DP',
|
||||||
|
member_count=99,
|
||||||
|
alliance=None
|
||||||
|
)
|
||||||
|
cls.user_2 = User.objects.create_user(
|
||||||
|
character_2.character_name.replace(' ', '_'),
|
||||||
|
'abc@example.com',
|
||||||
|
'password'
|
||||||
|
)
|
||||||
|
CharacterOwnership.objects.create(
|
||||||
|
character=character_2,
|
||||||
|
owner_hash='x1' + character_2.character_name,
|
||||||
|
user=cls.user_2
|
||||||
|
)
|
||||||
|
cls.user_2.profile.main_character = character_2
|
||||||
|
cls.user_2.profile.save()
|
||||||
|
cls.user_2.groups.add(cls.group_2)
|
||||||
|
cls.user_2.is_staff = True
|
||||||
|
cls.user_2.save()
|
||||||
|
|
||||||
|
# user 3 - no main, no group, superuser
|
||||||
|
character_3 = EveCharacter.objects.create(
|
||||||
|
character_id=1101,
|
||||||
|
character_name='Lex Luthor',
|
||||||
|
corporation_id=2101,
|
||||||
|
corporation_name='Lex Corp',
|
||||||
|
corporation_ticker='LC',
|
||||||
|
alliance_id=None
|
||||||
|
)
|
||||||
|
EveCorporationInfo.objects.create(
|
||||||
|
corporation_id=2101,
|
||||||
|
corporation_name='Lex Corp',
|
||||||
|
corporation_ticker='LC',
|
||||||
|
member_count=666,
|
||||||
|
alliance=None
|
||||||
|
)
|
||||||
|
EveAllianceInfo.objects.create(
|
||||||
|
alliance_id=3101,
|
||||||
|
alliance_name='Lex World Domination',
|
||||||
|
alliance_ticker='LWD',
|
||||||
|
executor_corp_id=2101
|
||||||
|
)
|
||||||
|
cls.user_3 = User.objects.create_user(
|
||||||
|
character_3.character_name.replace(' ', '_'),
|
||||||
|
'abc@example.com',
|
||||||
|
'password'
|
||||||
|
)
|
||||||
|
CharacterOwnership.objects.create(
|
||||||
|
character=character_3,
|
||||||
|
owner_hash='x1' + character_3.character_name,
|
||||||
|
user=cls.user_3
|
||||||
|
)
|
||||||
|
cls.user_3.is_superuser = True
|
||||||
|
cls.user_3.save()
|
||||||
|
|
||||||
|
# user 4 - corp and faction, no alliance
|
||||||
|
cls.character_4 = EveCharacter.objects.create(
|
||||||
|
character_id=4321,
|
||||||
|
character_name='Professor X',
|
||||||
|
corporation_id=5432,
|
||||||
|
corporation_name="Xavier's School for Gifted Youngsters",
|
||||||
|
corporation_ticker='MUTNT',
|
||||||
|
alliance_id=None,
|
||||||
|
faction_id=999,
|
||||||
|
faction_name='The X-Men',
|
||||||
|
)
|
||||||
|
cls.user_4 = User.objects.create_user(
|
||||||
|
cls.character_4.character_name.replace(' ', '_'),
|
||||||
|
'abc@example.com',
|
||||||
|
'password'
|
||||||
|
)
|
||||||
|
CharacterOwnership.objects.create(
|
||||||
|
character=cls.character_4,
|
||||||
|
owner_hash='x1' + cls.character_4.character_name,
|
||||||
|
user=cls.user_4
|
||||||
|
)
|
||||||
|
cls.user_4.profile.main_character = cls.character_4
|
||||||
|
cls.user_4.profile.save()
|
||||||
|
EveFactionInfo.objects.create(faction_id=999, faction_name='The X-Men')
|
||||||
|
|
||||||
|
|
||||||
|
def make_generic_search_request(ModelClass: type, search_term: str):
|
||||||
|
User.objects.create_superuser(
|
||||||
|
username='superuser', password='secret', email='admin@example.com'
|
||||||
|
)
|
||||||
|
c = Client()
|
||||||
|
c.login(username='superuser', password='secret')
|
||||||
|
return c.get(
|
||||||
|
f'{get_admin_search_url(ModelClass)}?q={quote(search_term)}'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCharacterOwnershipAdmin(TestCaseWithTestData):
|
||||||
|
fixtures = ["disable_analytics"]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.modeladmin = CharacterOwnershipAdmin(
|
||||||
|
model=User, admin_site=AdminSite()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_change_view_loads_normally(self):
|
||||||
|
User.objects.create_superuser(
|
||||||
|
username='superuser', password='secret', email='admin@example.com'
|
||||||
|
)
|
||||||
|
c = Client()
|
||||||
|
c.login(username='superuser', password='secret')
|
||||||
|
ownership = self.user_1.character_ownerships.first()
|
||||||
|
response = c.get(get_admin_change_view_url(ownership))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_search_works(self):
|
||||||
|
obj = CharacterOwnership.objects\
|
||||||
|
.filter(user=self.user_1)\
|
||||||
|
.first()
|
||||||
|
response = make_generic_search_request(type(obj), obj.user.username)
|
||||||
|
expected = 200
|
||||||
|
self.assertEqual(response.status_code, expected)
|
||||||
|
|
||||||
|
|
||||||
|
class TestOwnershipRecordAdmin(TestCaseWithTestData):
|
||||||
|
fixtures = ["disable_analytics"]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.modeladmin = OwnershipRecordAdmin(
|
||||||
|
model=User, admin_site=AdminSite()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_change_view_loads_normally(self):
|
||||||
|
User.objects.create_superuser(
|
||||||
|
username='superuser', password='secret', email='admin@example.com'
|
||||||
|
)
|
||||||
|
c = Client()
|
||||||
|
c.login(username='superuser', password='secret')
|
||||||
|
ownership_record = OwnershipRecord.objects\
|
||||||
|
.filter(user=self.user_1)\
|
||||||
|
.first()
|
||||||
|
response = c.get(get_admin_change_view_url(ownership_record))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_search_works(self):
|
||||||
|
obj = OwnershipRecord.objects.first()
|
||||||
|
response = make_generic_search_request(type(obj), obj.user.username)
|
||||||
|
expected = 200
|
||||||
|
self.assertEqual(response.status_code, expected)
|
||||||
|
|
||||||
|
|
||||||
|
class TestStateAdmin(TestCaseWithTestData):
|
||||||
|
fixtures = ["disable_analytics"]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls) -> None:
|
||||||
|
super().setUpClass()
|
||||||
|
cls.modeladmin = StateAdmin(model=User, admin_site=AdminSite())
|
||||||
|
|
||||||
|
def test_change_view_loads_normally(self):
|
||||||
|
User.objects.create_superuser(
|
||||||
|
username='superuser', password='secret', email='admin@example.com'
|
||||||
|
)
|
||||||
|
c = Client()
|
||||||
|
c.login(username='superuser', password='secret')
|
||||||
|
|
||||||
|
guest_state = AuthUtils.get_guest_state()
|
||||||
|
response = c.get(get_admin_change_view_url(guest_state))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
member_state = AuthUtils.get_member_state()
|
||||||
|
response = c.get(get_admin_change_view_url(member_state))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_search_works(self):
|
||||||
|
obj = State.objects.first()
|
||||||
|
response = make_generic_search_request(type(obj), obj.name)
|
||||||
|
expected = 200
|
||||||
|
self.assertEqual(response.status_code, expected)
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserAdmin(TestCaseWithTestData):
|
||||||
|
fixtures = ["disable_analytics"]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.modeladmin = UserAdmin(
|
||||||
|
model=User, admin_site=AdminSite()
|
||||||
|
)
|
||||||
|
self.character_1 = self.user_1.character_ownerships.first().character
|
||||||
|
|
||||||
|
def test_user_profile_pic_u1(self):
|
||||||
|
expected = (
|
||||||
|
'<img src="https://images.evetech.net/characters/1001/'
|
||||||
|
'portrait?size=32" class="img-circle">'
|
||||||
|
)
|
||||||
|
self.assertEqual(user_profile_pic(self.user_1), expected)
|
||||||
|
|
||||||
|
def test_user_profile_pic_u3(self):
|
||||||
|
self.assertIsNone(user_profile_pic(self.user_3))
|
||||||
|
|
||||||
|
def test_user_username_u1(self):
|
||||||
|
expected = (
|
||||||
|
'<strong><a href="/admin/authentication/user/{}/change/">'
|
||||||
|
'Bruce_Wayne</a></strong><br>Bruce Wayne'.format(self.user_1.pk)
|
||||||
|
)
|
||||||
|
self.assertEqual(user_username(self.user_1), expected)
|
||||||
|
|
||||||
|
def test_user_username_u3(self):
|
||||||
|
expected = (
|
||||||
|
'<strong><a href="/admin/authentication/user/{}/change/">'
|
||||||
|
'Lex_Luthor</a></strong>'.format(self.user_3.pk)
|
||||||
|
)
|
||||||
|
self.assertEqual(user_username(self.user_3), expected)
|
||||||
|
|
||||||
|
def test_user_main_organization_u1(self):
|
||||||
|
expected = 'Wayne Technologies<br>Wayne Enterprises'
|
||||||
|
self.assertEqual(user_main_organization(self.user_1), expected)
|
||||||
|
|
||||||
|
def test_user_main_organization_u2(self):
|
||||||
|
expected = 'Daily Planet'
|
||||||
|
self.assertEqual(user_main_organization(self.user_2), expected)
|
||||||
|
|
||||||
|
def test_user_main_organization_u3(self):
|
||||||
|
expected = ''
|
||||||
|
self.assertEqual(user_main_organization(self.user_3), expected)
|
||||||
|
|
||||||
|
def test_user_main_organization_u4(self):
|
||||||
|
expected = "Xavier's School for Gifted Youngsters<br>The X-Men"
|
||||||
|
self.assertEqual(user_main_organization(self.user_4), expected)
|
||||||
|
|
||||||
|
def test_characters_u1(self):
|
||||||
|
expected = 'Batman, Bruce Wayne'
|
||||||
|
result = self.modeladmin._characters(self.user_1)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_characters_u2(self):
|
||||||
|
expected = 'Clark Kent'
|
||||||
|
result = self.modeladmin._characters(self.user_2)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_characters_u3(self):
|
||||||
|
expected = 'Lex Luthor'
|
||||||
|
result = self.modeladmin._characters(self.user_3)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_groups_u1(self):
|
||||||
|
expected = 'Group 1'
|
||||||
|
result = self.modeladmin._groups(self.user_1)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_groups_u2(self):
|
||||||
|
expected = 'Group 2'
|
||||||
|
result = self.modeladmin._groups(self.user_2)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_groups_u3(self):
|
||||||
|
result = self.modeladmin._groups(self.user_3)
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
def test_state(self):
|
||||||
|
expected = 'Guest'
|
||||||
|
result = self.modeladmin._state(self.user_1)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_role_u1(self):
|
||||||
|
expected = 'User'
|
||||||
|
result = self.modeladmin._role(self.user_1)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_role_u2(self):
|
||||||
|
expected = 'Staff'
|
||||||
|
result = self.modeladmin._role(self.user_2)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_role_u3(self):
|
||||||
|
expected = 'Superuser'
|
||||||
|
result = self.modeladmin._role(self.user_3)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_list_2_html_w_tooltips_no_cutoff(self):
|
||||||
|
items = ['one', 'two', 'three']
|
||||||
|
expected = 'one, two, three'
|
||||||
|
result = self.modeladmin._list_2_html_w_tooltips(items, 5)
|
||||||
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
|
def test_list_2_html_w_tooltips_w_cutoff(self):
|
||||||
|
items = ['one', 'two', 'three']
|
||||||
|
expected = (
|
||||||
|
'<span data-tooltip="one, two, three" '
|
||||||
|
'class="tooltip">one, two, (...)</span>'
|
||||||
|
)
|
||||||
|
result = self.modeladmin._list_2_html_w_tooltips(items, 2)
|
||||||
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
|
def test_list_2_html_w_tooltips_empty_list(self):
|
||||||
|
items = []
|
||||||
|
expected = None
|
||||||
|
result = self.modeladmin._list_2_html_w_tooltips(items, 5)
|
||||||
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
|
# actions
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '.UserAdmin.message_user', auto_spec=True, unsafe=True)
|
||||||
|
@patch(MODULE_PATH + '.update_character')
|
||||||
|
def test_action_update_main_character_model(
|
||||||
|
self, mock_task, mock_message_user
|
||||||
|
):
|
||||||
|
users_qs = User.objects.filter(pk__in=[self.user_1.pk, self.user_2.pk])
|
||||||
|
update_main_character_model(
|
||||||
|
self.modeladmin, MockRequest(self.user_1), users_qs
|
||||||
|
)
|
||||||
|
self.assertEqual(mock_task.delay.call_count, 2)
|
||||||
|
self.assertTrue(mock_message_user.called)
|
||||||
|
|
||||||
|
# filters
|
||||||
|
|
||||||
|
def test_filter_main_corporations(self):
|
||||||
|
|
||||||
|
class UserAdminTest(BaseUserAdmin):
|
||||||
|
list_filter = (MainCorporationsFilter,)
|
||||||
|
|
||||||
|
my_modeladmin = UserAdminTest(User, AdminSite())
|
||||||
|
|
||||||
|
# Make sure the lookups are correct
|
||||||
|
request = self.factory.get('/')
|
||||||
|
request.user = self.user_1
|
||||||
|
changelist = my_modeladmin.get_changelist_instance(request)
|
||||||
|
filters = changelist.get_filters(request)
|
||||||
|
filterspec = filters[0][0]
|
||||||
|
expected = [
|
||||||
|
(2002, 'Daily Planet'),
|
||||||
|
(2001, 'Wayne Technologies'),
|
||||||
|
(5432, "Xavier's School for Gifted Youngsters"),
|
||||||
|
]
|
||||||
|
self.assertEqual(filterspec.lookup_choices, expected)
|
||||||
|
|
||||||
|
# Make sure the correct queryset is returned
|
||||||
|
request = self.factory.get(
|
||||||
|
'/',
|
||||||
|
{'main_corporation_id__exact': self.character_1.corporation_id}
|
||||||
|
)
|
||||||
|
request.user = self.user_1
|
||||||
|
changelist = my_modeladmin.get_changelist_instance(request)
|
||||||
|
queryset = changelist.get_queryset(request)
|
||||||
|
expected = [self.user_1]
|
||||||
|
self.assertSetEqual(set(queryset), set(expected))
|
||||||
|
|
||||||
|
def test_filter_main_alliances(self):
|
||||||
|
|
||||||
|
class UserAdminTest(BaseUserAdmin):
|
||||||
|
list_filter = (MainAllianceFilter,)
|
||||||
|
|
||||||
|
my_modeladmin = UserAdminTest(User, AdminSite())
|
||||||
|
|
||||||
|
# Make sure the lookups are correct
|
||||||
|
request = self.factory.get('/')
|
||||||
|
request.user = self.user_1
|
||||||
|
changelist = my_modeladmin.get_changelist_instance(request)
|
||||||
|
filters = changelist.get_filters(request)
|
||||||
|
filterspec = filters[0][0]
|
||||||
|
expected = [
|
||||||
|
(3001, 'Wayne Enterprises'),
|
||||||
|
]
|
||||||
|
self.assertEqual(filterspec.lookup_choices, expected)
|
||||||
|
|
||||||
|
# Make sure the correct queryset is returned
|
||||||
|
request = self.factory.get(
|
||||||
|
'/',
|
||||||
|
{'main_alliance_id__exact': self.character_1.alliance_id}
|
||||||
|
)
|
||||||
|
request.user = self.user_1
|
||||||
|
changelist = my_modeladmin.get_changelist_instance(request)
|
||||||
|
queryset = changelist.get_queryset(request)
|
||||||
|
expected = [self.user_1]
|
||||||
|
self.assertSetEqual(set(queryset), set(expected))
|
||||||
|
|
||||||
|
def test_filter_main_factions(self):
|
||||||
|
class UserAdminTest(BaseUserAdmin):
|
||||||
|
list_filter = (MainFactionFilter,)
|
||||||
|
|
||||||
|
my_modeladmin = UserAdminTest(User, AdminSite())
|
||||||
|
|
||||||
|
# Make sure the lookups are correct
|
||||||
|
request = self.factory.get('/')
|
||||||
|
request.user = self.user_4
|
||||||
|
changelist = my_modeladmin.get_changelist_instance(request)
|
||||||
|
filters = changelist.get_filters(request)
|
||||||
|
filterspec = filters[0][0]
|
||||||
|
expected = [
|
||||||
|
(999, 'The X-Men'),
|
||||||
|
]
|
||||||
|
self.assertEqual(filterspec.lookup_choices, expected)
|
||||||
|
|
||||||
|
# Make sure the correct queryset is returned
|
||||||
|
request = self.factory.get(
|
||||||
|
'/',
|
||||||
|
{'main_faction_id__exact': self.character_4.faction_id}
|
||||||
|
)
|
||||||
|
request.user = self.user_4
|
||||||
|
changelist = my_modeladmin.get_changelist_instance(request)
|
||||||
|
queryset = changelist.get_queryset(request)
|
||||||
|
expected = [self.user_4]
|
||||||
|
self.assertSetEqual(set(queryset), set(expected))
|
||||||
|
|
||||||
|
def test_change_view_loads_normally(self):
|
||||||
|
User.objects.create_superuser(
|
||||||
|
username='superuser', password='secret', email='admin@example.com'
|
||||||
|
)
|
||||||
|
c = Client()
|
||||||
|
c.login(username='superuser', password='secret')
|
||||||
|
response = c.get(get_admin_change_view_url(self.user_1))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_search_works(self):
|
||||||
|
obj = User.objects.first()
|
||||||
|
response = make_generic_search_request(type(obj), obj.username)
|
||||||
|
expected = 200
|
||||||
|
self.assertEqual(response.status_code, expected)
|
||||||
|
|
||||||
|
|
||||||
|
class TestStateAdminChangeFormSuperuserExclusiveEdits(WebTest):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls) -> None:
|
||||||
|
super().setUpClass()
|
||||||
|
cls.super_admin = User.objects.create_superuser("super_admin")
|
||||||
|
cls.staff_admin = User.objects.create_user("staff_admin")
|
||||||
|
cls.staff_admin.is_staff = True
|
||||||
|
cls.staff_admin.save()
|
||||||
|
cls.staff_admin = AuthUtils.add_permissions_to_user_by_name(
|
||||||
|
[
|
||||||
|
"authentication.add_state",
|
||||||
|
"authentication.change_state",
|
||||||
|
"authentication.view_state",
|
||||||
|
],
|
||||||
|
cls.staff_admin
|
||||||
|
)
|
||||||
|
cls.superuser_exclusive_fields = ["permissions",]
|
||||||
|
|
||||||
|
def test_should_show_all_fields_to_superuser_for_add(self):
|
||||||
|
# given
|
||||||
|
self.app.set_user(self.super_admin)
|
||||||
|
page = self.app.get("/admin/authentication/state/add/")
|
||||||
|
# when
|
||||||
|
form = page.forms["state_form"]
|
||||||
|
# then
|
||||||
|
for field in self.superuser_exclusive_fields:
|
||||||
|
with self.subTest(field=field):
|
||||||
|
self.assertIn(field, form.fields)
|
||||||
|
|
||||||
|
def test_should_not_show_all_fields_to_staff_admins_for_add(self):
|
||||||
|
# given
|
||||||
|
self.app.set_user(self.staff_admin)
|
||||||
|
page = self.app.get("/admin/authentication/state/add/")
|
||||||
|
# when
|
||||||
|
form = page.forms["state_form"]
|
||||||
|
# then
|
||||||
|
for field in self.superuser_exclusive_fields:
|
||||||
|
with self.subTest(field=field):
|
||||||
|
self.assertNotIn(field, form.fields)
|
||||||
|
|
||||||
|
def test_should_show_all_fields_to_superuser_for_change(self):
|
||||||
|
# given
|
||||||
|
self.app.set_user(self.super_admin)
|
||||||
|
state = AuthUtils.get_member_state()
|
||||||
|
page = self.app.get(f"/admin/authentication/state/{state.pk}/change/")
|
||||||
|
# when
|
||||||
|
form = page.forms["state_form"]
|
||||||
|
# then
|
||||||
|
for field in self.superuser_exclusive_fields:
|
||||||
|
with self.subTest(field=field):
|
||||||
|
self.assertIn(field, form.fields)
|
||||||
|
|
||||||
|
def test_should_not_show_all_fields_to_staff_admin_for_change(self):
|
||||||
|
# given
|
||||||
|
self.app.set_user(self.staff_admin)
|
||||||
|
state = AuthUtils.get_member_state()
|
||||||
|
page = self.app.get(f"/admin/authentication/state/{state.pk}/change/")
|
||||||
|
# when
|
||||||
|
form = page.forms["state_form"]
|
||||||
|
# then
|
||||||
|
for field in self.superuser_exclusive_fields:
|
||||||
|
with self.subTest(field=field):
|
||||||
|
self.assertNotIn(field, form.fields)
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserAdminChangeForm(TestCase):
|
||||||
|
fixtures = ["disable_analytics"]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls) -> None:
|
||||||
|
super().setUpClass()
|
||||||
|
cls.modeladmin = UserAdmin(model=User, admin_site=AdminSite())
|
||||||
|
|
||||||
|
def test_should_show_groups_available_to_user_with_blue_state_only(self):
|
||||||
|
# given
|
||||||
|
superuser = User.objects.create_superuser("Super")
|
||||||
|
user = AuthUtils.create_user("bruce_wayne")
|
||||||
|
character = AuthUtils.add_main_character_2(
|
||||||
|
user,
|
||||||
|
name="Bruce Wayne",
|
||||||
|
character_id=1001,
|
||||||
|
corp_id=2001,
|
||||||
|
corp_name="Wayne Technologies"
|
||||||
|
)
|
||||||
|
blue_state = State.objects.get(name="Blue")
|
||||||
|
blue_state.member_characters.add(character)
|
||||||
|
member_state = AuthUtils.get_member_state()
|
||||||
|
group_1 = Group.objects.create(name="Group 1")
|
||||||
|
group_2 = Group.objects.create(name="Group 2")
|
||||||
|
group_2.authgroup.states.add(blue_state)
|
||||||
|
group_3 = Group.objects.create(name="Group 3")
|
||||||
|
group_3.authgroup.states.add(member_state)
|
||||||
|
self.client.force_login(superuser)
|
||||||
|
# when
|
||||||
|
response = self.client.get(f"/admin/authentication/user/{user.pk}/change/")
|
||||||
|
# then
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
soup = BeautifulSoup(response.rendered_content, features="html.parser")
|
||||||
|
groups_select = soup.find("select", {"id": "id_groups"}).find_all('option')
|
||||||
|
group_ids = {int(option["value"]) for option in groups_select}
|
||||||
|
self.assertSetEqual(group_ids, {group_1.pk, group_2.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserAdminChangeFormSuperuserExclusiveEdits(WebTest):
|
||||||
|
fixtures = ["disable_analytics"]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls) -> None:
|
||||||
|
super().setUpClass()
|
||||||
|
cls.super_admin = User.objects.create_superuser("super_admin")
|
||||||
|
cls.staff_admin = User.objects.create_user("staff_admin")
|
||||||
|
cls.staff_admin.is_staff = True
|
||||||
|
cls.staff_admin.save()
|
||||||
|
cls.staff_admin = AuthUtils.add_permissions_to_user_by_name(
|
||||||
|
[
|
||||||
|
"auth.change_user",
|
||||||
|
"auth.view_user",
|
||||||
|
"authentication.change_user",
|
||||||
|
"authentication.change_userprofile",
|
||||||
|
"authentication.view_user"
|
||||||
|
],
|
||||||
|
cls.staff_admin
|
||||||
|
)
|
||||||
|
cls.superuser_exclusive_fields = [
|
||||||
|
"is_staff", "is_superuser", "user_permissions"
|
||||||
|
]
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.user = AuthUtils.create_user("bruce_wayne")
|
||||||
|
|
||||||
|
def test_should_show_all_fields_to_superuser_for_change(self):
|
||||||
|
# given
|
||||||
|
self.app.set_user(self.super_admin)
|
||||||
|
|
||||||
|
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
|
||||||
|
# when
|
||||||
|
form = page.forms["user_form"]
|
||||||
|
# then
|
||||||
|
for field in self.superuser_exclusive_fields:
|
||||||
|
with self.subTest(field=field):
|
||||||
|
self.assertIn(field, form.fields)
|
||||||
|
|
||||||
|
def test_should_not_show_all_fields_to_staff_admin_for_change(self):
|
||||||
|
# given
|
||||||
|
self.app.set_user(self.staff_admin)
|
||||||
|
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
|
||||||
|
# when
|
||||||
|
form = page.forms["user_form"]
|
||||||
|
# then
|
||||||
|
for field in self.superuser_exclusive_fields:
|
||||||
|
with self.subTest(field=field):
|
||||||
|
self.assertNotIn(field, form.fields)
|
||||||
|
|
||||||
|
def test_should_allow_super_admin_to_add_restricted_group_to_user(self):
|
||||||
|
# given
|
||||||
|
self.app.set_user(self.super_admin)
|
||||||
|
group_restricted = Group.objects.create(name="restricted group")
|
||||||
|
group_restricted.authgroup.restricted = True
|
||||||
|
group_restricted.authgroup.save()
|
||||||
|
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
|
||||||
|
form = page.forms["user_form"]
|
||||||
|
# when
|
||||||
|
form["groups"].select_multiple(texts=["restricted group"])
|
||||||
|
response = form.submit("save")
|
||||||
|
# then
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.user.refresh_from_db()
|
||||||
|
self.assertIn(
|
||||||
|
"restricted group", self.user.groups.values_list("name", flat=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_should_not_allow_staff_admin_to_add_restricted_group_to_user(self):
|
||||||
|
# given
|
||||||
|
self.app.set_user(self.staff_admin)
|
||||||
|
group_restricted = Group.objects.create(name="restricted group")
|
||||||
|
group_restricted.authgroup.restricted = True
|
||||||
|
group_restricted.authgroup.save()
|
||||||
|
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
|
||||||
|
form = page.forms["user_form"]
|
||||||
|
# when
|
||||||
|
form["groups"].select_multiple(texts=["restricted group"])
|
||||||
|
response = form.submit("save")
|
||||||
|
# then
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn(
|
||||||
|
"You are not allowed to add or remove these restricted groups",
|
||||||
|
response.text
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_should_not_allow_staff_admin_to_remove_restricted_group_from_user(self):
|
||||||
|
# given
|
||||||
|
self.app.set_user(self.staff_admin)
|
||||||
|
group_restricted = Group.objects.create(name="restricted group")
|
||||||
|
group_restricted.authgroup.restricted = True
|
||||||
|
group_restricted.authgroup.save()
|
||||||
|
self.user.groups.add(group_restricted)
|
||||||
|
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
|
||||||
|
form = page.forms["user_form"]
|
||||||
|
# when
|
||||||
|
form["groups"].select_multiple(texts=[])
|
||||||
|
response = form.submit("save")
|
||||||
|
# then
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn(
|
||||||
|
"You are not allowed to add or remove these restricted groups",
|
||||||
|
response.text
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_should_allow_staff_admin_to_add_normal_group_to_user(self):
|
||||||
|
# given
|
||||||
|
self.app.set_user(self.super_admin)
|
||||||
|
Group.objects.create(name="normal group")
|
||||||
|
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
|
||||||
|
form = page.forms["user_form"]
|
||||||
|
# when
|
||||||
|
form["groups"].select_multiple(texts=["normal group"])
|
||||||
|
response = form.submit("save")
|
||||||
|
# then
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.user.refresh_from_db()
|
||||||
|
self.assertIn("normal group", self.user.groups.values_list("name", flat=True))
|
||||||
|
|
||||||
|
|
||||||
|
class TestMakeServicesHooksActions(TestCaseWithTestData):
|
||||||
|
|
||||||
|
class MyServicesHookTypeA(ServicesHook):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.name = 'My Service A'
|
||||||
|
|
||||||
|
def update_groups(self, user):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def sync_nicknames(self, user):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class MyServicesHookTypeB(ServicesHook):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.name = 'My Service B'
|
||||||
|
|
||||||
|
def update_groups(self, user):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update_groups_bulk(self, user):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def sync_nicknames(self, user):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def sync_nicknames_bulk(self, user):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_service_has_update_groups_only(self):
|
||||||
|
service = self.MyServicesHookTypeA()
|
||||||
|
mock_service = MagicMock(spec=service)
|
||||||
|
action = make_service_hooks_update_groups_action(mock_service)
|
||||||
|
action(MagicMock(), MagicMock(), [self.user_1])
|
||||||
|
self.assertTrue(mock_service.update_groups.called)
|
||||||
|
|
||||||
|
def test_service_has_update_groups_bulk(self):
|
||||||
|
service = self.MyServicesHookTypeB()
|
||||||
|
mock_service = MagicMock(spec=service)
|
||||||
|
action = make_service_hooks_update_groups_action(mock_service)
|
||||||
|
action(MagicMock(), MagicMock(), [self.user_1])
|
||||||
|
self.assertFalse(mock_service.update_groups.called)
|
||||||
|
self.assertTrue(mock_service.update_groups_bulk.called)
|
||||||
|
|
||||||
|
def test_service_has_sync_nickname_only(self):
|
||||||
|
service = self.MyServicesHookTypeA()
|
||||||
|
mock_service = MagicMock(spec=service)
|
||||||
|
action = make_service_hooks_sync_nickname_action(mock_service)
|
||||||
|
action(MagicMock(), MagicMock(), [self.user_1])
|
||||||
|
self.assertTrue(mock_service.sync_nickname.called)
|
||||||
|
|
||||||
|
def test_service_has_sync_nicknames_bulk(self):
|
||||||
|
service = self.MyServicesHookTypeB()
|
||||||
|
mock_service = MagicMock(spec=service)
|
||||||
|
action = make_service_hooks_sync_nickname_action(mock_service)
|
||||||
|
action(MagicMock(), MagicMock(), [self.user_1])
|
||||||
|
self.assertFalse(mock_service.sync_nickname.called)
|
||||||
|
self.assertTrue(mock_service.sync_nicknames_bulk.called)
|
||||||
102
allianceauth/authentication/tests/test_app_settings.py
Normal file
102
allianceauth/authentication/tests/test_app_settings.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
from unittest.mock import Mock, patch
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from .. import app_settings
|
||||||
|
|
||||||
|
MODULE_PATH = 'allianceauth.authentication'
|
||||||
|
|
||||||
|
|
||||||
|
class TestSetAppSetting(TestCase):
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '.app_settings.settings')
|
||||||
|
def test_default_if_not_set(self, mock_settings):
|
||||||
|
mock_settings.TEST_SETTING_DUMMY = Mock(spec=None)
|
||||||
|
result = app_settings._clean_setting(
|
||||||
|
'TEST_SETTING_DUMMY',
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
self.assertEqual(result, False)
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '.app_settings.settings')
|
||||||
|
def test_default_if_not_set_for_none(self, mock_settings):
|
||||||
|
mock_settings.TEST_SETTING_DUMMY = Mock(spec=None)
|
||||||
|
result = app_settings._clean_setting(
|
||||||
|
'TEST_SETTING_DUMMY',
|
||||||
|
None,
|
||||||
|
required_type=int
|
||||||
|
)
|
||||||
|
self.assertEqual(result, None)
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '.app_settings.settings')
|
||||||
|
def test_true_stays_true(self, mock_settings):
|
||||||
|
mock_settings.TEST_SETTING_DUMMY = True
|
||||||
|
result = app_settings._clean_setting(
|
||||||
|
'TEST_SETTING_DUMMY',
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
self.assertEqual(result, True)
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '.app_settings.settings')
|
||||||
|
def test_false_stays_false(self, mock_settings):
|
||||||
|
mock_settings.TEST_SETTING_DUMMY = False
|
||||||
|
result = app_settings._clean_setting(
|
||||||
|
'TEST_SETTING_DUMMY',
|
||||||
|
False
|
||||||
|
)
|
||||||
|
self.assertEqual(result, False)
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '.app_settings.settings')
|
||||||
|
def test_default_for_invalid_type_bool(self, mock_settings):
|
||||||
|
mock_settings.TEST_SETTING_DUMMY = 'invalid type'
|
||||||
|
result = app_settings._clean_setting(
|
||||||
|
'TEST_SETTING_DUMMY',
|
||||||
|
False
|
||||||
|
)
|
||||||
|
self.assertEqual(result, False)
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '.app_settings.settings')
|
||||||
|
def test_default_for_invalid_type_int(self, mock_settings):
|
||||||
|
mock_settings.TEST_SETTING_DUMMY = 'invalid type'
|
||||||
|
result = app_settings._clean_setting(
|
||||||
|
'TEST_SETTING_DUMMY',
|
||||||
|
50
|
||||||
|
)
|
||||||
|
self.assertEqual(result, 50)
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '.app_settings.settings')
|
||||||
|
def test_default_if_below_minimum_1(self, mock_settings):
|
||||||
|
mock_settings.TEST_SETTING_DUMMY = -5
|
||||||
|
result = app_settings._clean_setting(
|
||||||
|
'TEST_SETTING_DUMMY',
|
||||||
|
default_value=50
|
||||||
|
)
|
||||||
|
self.assertEqual(result, 50)
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '.app_settings.settings')
|
||||||
|
def test_default_if_below_minimum_2(self, mock_settings):
|
||||||
|
mock_settings.TEST_SETTING_DUMMY = -50
|
||||||
|
result = app_settings._clean_setting(
|
||||||
|
'TEST_SETTING_DUMMY',
|
||||||
|
default_value=50,
|
||||||
|
min_value=-10
|
||||||
|
)
|
||||||
|
self.assertEqual(result, 50)
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '.app_settings.settings')
|
||||||
|
def test_default_for_invalid_type_int(self, mock_settings):
|
||||||
|
mock_settings.TEST_SETTING_DUMMY = 1000
|
||||||
|
result = app_settings._clean_setting(
|
||||||
|
'TEST_SETTING_DUMMY',
|
||||||
|
default_value=50,
|
||||||
|
max_value=100
|
||||||
|
)
|
||||||
|
self.assertEqual(result, 50)
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '.app_settings.settings')
|
||||||
|
def test_default_is_none_needs_required_type(self, mock_settings):
|
||||||
|
mock_settings.TEST_SETTING_DUMMY = 'invalid type'
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
result = app_settings._clean_setting(
|
||||||
|
'TEST_SETTING_DUMMY',
|
||||||
|
default_value=None
|
||||||
|
)
|
||||||
149
allianceauth/authentication/tests/test_backend.py
Normal file
149
allianceauth/authentication/tests/test_backend.py
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
from django.contrib.auth.models import User, Group
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from allianceauth.eveonline.models import EveCharacter
|
||||||
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
|
|
||||||
|
from esi.models import Token
|
||||||
|
|
||||||
|
from ..backends import StateBackend
|
||||||
|
from ..models import CharacterOwnership, UserProfile, OwnershipRecord
|
||||||
|
|
||||||
|
MODULE_PATH = 'allianceauth.authentication'
|
||||||
|
|
||||||
|
PERMISSION_1 = "authentication.add_user"
|
||||||
|
PERMISSION_2 = "authentication.change_user"
|
||||||
|
|
||||||
|
|
||||||
|
class TestStatePermissions(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# permissions
|
||||||
|
self.permission_1 = AuthUtils.get_permission_by_name(PERMISSION_1)
|
||||||
|
self.permission_2 = AuthUtils.get_permission_by_name(PERMISSION_2)
|
||||||
|
|
||||||
|
# group
|
||||||
|
self.group_1 = Group.objects.create(name="Group 1")
|
||||||
|
self.group_2 = Group.objects.create(name="Group 2")
|
||||||
|
|
||||||
|
# state
|
||||||
|
self.state_1 = AuthUtils.get_member_state()
|
||||||
|
self.state_2 = AuthUtils.create_state("Other State", 75)
|
||||||
|
|
||||||
|
# user
|
||||||
|
self.user = AuthUtils.create_user("Bruce Wayne")
|
||||||
|
self.main = AuthUtils.add_main_character_2(self.user, self.user.username, 123)
|
||||||
|
|
||||||
|
def test_user_has_user_permissions(self):
|
||||||
|
self.user.user_permissions.add(self.permission_1)
|
||||||
|
|
||||||
|
user = User.objects.get(pk=self.user.pk)
|
||||||
|
self.assertTrue(user.has_perm(PERMISSION_1))
|
||||||
|
|
||||||
|
def test_user_has_group_permissions(self):
|
||||||
|
self.group_1.permissions.add(self.permission_1)
|
||||||
|
self.user.groups.add(self.group_1)
|
||||||
|
|
||||||
|
user = User.objects.get(pk=self.user.pk)
|
||||||
|
self.assertTrue(user.has_perm(PERMISSION_1))
|
||||||
|
|
||||||
|
def test_user_has_state_permissions(self):
|
||||||
|
self.state_1.permissions.add(self.permission_1)
|
||||||
|
self.state_1.member_characters.add(self.main)
|
||||||
|
user = User.objects.get(pk=self.user.pk)
|
||||||
|
|
||||||
|
self.assertTrue(user.has_perm(PERMISSION_1))
|
||||||
|
|
||||||
|
def test_when_user_changes_state_perms_change_accordingly(self):
|
||||||
|
self.state_1.permissions.add(self.permission_1)
|
||||||
|
self.state_1.member_characters.add(self.main)
|
||||||
|
user = User.objects.get(pk=self.user.pk)
|
||||||
|
self.assertTrue(user.has_perm(PERMISSION_1))
|
||||||
|
|
||||||
|
self.state_2.permissions.add(self.permission_2)
|
||||||
|
self.state_2.member_characters.add(self.main)
|
||||||
|
self.state_1.member_characters.remove(self.main)
|
||||||
|
user = User.objects.get(pk=self.user.pk)
|
||||||
|
self.assertFalse(user.has_perm(PERMISSION_1))
|
||||||
|
self.assertTrue(user.has_perm(PERMISSION_2))
|
||||||
|
|
||||||
|
def test_state_permissions_are_returned_for_current_user_object(self):
|
||||||
|
# verify state permissions are returns for the current user object
|
||||||
|
# and not for it's instance in the database, which might be outdated
|
||||||
|
self.state_1.permissions.add(self.permission_1)
|
||||||
|
self.state_2.permissions.add(self.permission_2)
|
||||||
|
self.state_1.member_characters.add(self.main)
|
||||||
|
user = User.objects.get(pk=self.user.pk)
|
||||||
|
user.profile.state = self.state_2
|
||||||
|
self.assertFalse(user.has_perm(PERMISSION_1))
|
||||||
|
self.assertTrue(user.has_perm(PERMISSION_2))
|
||||||
|
|
||||||
|
|
||||||
|
class TestAuthenticate(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.main_character = EveCharacter.objects.create(
|
||||||
|
character_id=1,
|
||||||
|
character_name='Main Character',
|
||||||
|
corporation_id=1,
|
||||||
|
corporation_name='Corp',
|
||||||
|
corporation_ticker='CORP',
|
||||||
|
)
|
||||||
|
cls.alt_character = EveCharacter.objects.create(
|
||||||
|
character_id=2,
|
||||||
|
character_name='Alt Character',
|
||||||
|
corporation_id=1,
|
||||||
|
corporation_name='Corp',
|
||||||
|
corporation_ticker='CORP',
|
||||||
|
)
|
||||||
|
cls.unclaimed_character = EveCharacter.objects.create(
|
||||||
|
character_id=3,
|
||||||
|
character_name='Unclaimed Character',
|
||||||
|
corporation_id=1,
|
||||||
|
corporation_name='Corp',
|
||||||
|
corporation_ticker='CORP',
|
||||||
|
)
|
||||||
|
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
|
||||||
|
cls.old_user = AuthUtils.create_user('old_user', disconnect_signals=True)
|
||||||
|
AuthUtils.disconnect_signals()
|
||||||
|
CharacterOwnership.objects.create(user=cls.user, character=cls.main_character, owner_hash='1')
|
||||||
|
CharacterOwnership.objects.create(user=cls.user, character=cls.alt_character, owner_hash='2')
|
||||||
|
UserProfile.objects.update_or_create(user=cls.user, defaults={'main_character': cls.main_character})
|
||||||
|
AuthUtils.connect_signals()
|
||||||
|
|
||||||
|
def test_authenticate_main_character(self):
|
||||||
|
t = Token(character_id=self.main_character.character_id, character_owner_hash='1')
|
||||||
|
user = StateBackend().authenticate(token=t)
|
||||||
|
self.assertEqual(user, self.user)
|
||||||
|
|
||||||
|
def test_authenticate_alt_character(self):
|
||||||
|
t = Token(character_id=self.alt_character.character_id, character_owner_hash='2')
|
||||||
|
user = StateBackend().authenticate(token=t)
|
||||||
|
self.assertEqual(user, self.user)
|
||||||
|
|
||||||
|
def test_authenticate_unclaimed_character(self):
|
||||||
|
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='3')
|
||||||
|
user = StateBackend().authenticate(token=t)
|
||||||
|
self.assertNotEqual(user, self.user)
|
||||||
|
self.assertEqual(user.username, 'Unclaimed_Character')
|
||||||
|
self.assertEqual(user.profile.main_character, self.unclaimed_character)
|
||||||
|
|
||||||
|
def test_authenticate_character_record(self):
|
||||||
|
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='4')
|
||||||
|
OwnershipRecord.objects.create(user=self.old_user, character=self.unclaimed_character, owner_hash='4')
|
||||||
|
user = StateBackend().authenticate(token=t)
|
||||||
|
self.assertEqual(user, self.old_user)
|
||||||
|
self.assertTrue(CharacterOwnership.objects.filter(owner_hash='4', user=self.old_user).exists())
|
||||||
|
self.assertTrue(user.profile.main_character)
|
||||||
|
|
||||||
|
def test_iterate_username(self):
|
||||||
|
t = Token(character_id=self.unclaimed_character.character_id,
|
||||||
|
character_name=self.unclaimed_character.character_name, character_owner_hash='3')
|
||||||
|
username = StateBackend().authenticate(token=t).username
|
||||||
|
t.character_owner_hash = '4'
|
||||||
|
username_1 = StateBackend().authenticate(token=t).username
|
||||||
|
t.character_owner_hash = '5'
|
||||||
|
username_2 = StateBackend().authenticate(token=t).username
|
||||||
|
self.assertNotEqual(username, username_1, username_2)
|
||||||
|
self.assertTrue(username_1.endswith('_1'))
|
||||||
|
self.assertTrue(username_2.endswith('_2'))
|
||||||
35
allianceauth/authentication/tests/test_commands.py
Normal file
35
allianceauth/authentication/tests/test_commands.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
from django.core.management import call_command
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
|
|
||||||
|
from ..models import CharacterOwnership, UserProfile
|
||||||
|
|
||||||
|
|
||||||
|
class ManagementCommandTestCase(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.user = AuthUtils.create_user('test user', disconnect_signals=True)
|
||||||
|
AuthUtils.add_main_character(cls.user, 'test character', '1', '2', 'test corp', 'test')
|
||||||
|
character = UserProfile.objects.get(user=cls.user).main_character
|
||||||
|
CharacterOwnership.objects.create(user=cls.user, character=character, owner_hash='test')
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.stdout = StringIO()
|
||||||
|
|
||||||
|
def test_ownership(self):
|
||||||
|
call_command('checkmains', stdout=self.stdout)
|
||||||
|
self.assertFalse(UserProfile.objects.filter(main_character__isnull=True).count())
|
||||||
|
self.assertNotIn(self.user.username, self.stdout.getvalue())
|
||||||
|
self.assertIn('All main characters', self.stdout.getvalue())
|
||||||
|
|
||||||
|
def test_no_ownership(self):
|
||||||
|
user = AuthUtils.create_user('v1 user', disconnect_signals=True)
|
||||||
|
AuthUtils.add_main_character(user, 'v1 character', '10', '20', 'test corp', 'test')
|
||||||
|
self.assertFalse(UserProfile.objects.filter(main_character__isnull=True).count())
|
||||||
|
|
||||||
|
call_command('checkmains', stdout=self.stdout)
|
||||||
|
self.assertEqual(UserProfile.objects.filter(main_character__isnull=True).count(), 1)
|
||||||
|
self.assertIn(user.username, self.stdout.getvalue())
|
||||||
68
allianceauth/authentication/tests/test_decorators.py
Normal file
68
allianceauth/authentication/tests/test_decorators.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
from unittest import mock
|
||||||
|
from urllib import parse
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.http.response import HttpResponse
|
||||||
|
from django.shortcuts import reverse
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
from allianceauth.eveonline.models import EveCharacter
|
||||||
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
|
|
||||||
|
from ..decorators import main_character_required
|
||||||
|
from ..models import CharacterOwnership
|
||||||
|
|
||||||
|
|
||||||
|
MODULE_PATH = 'allianceauth.authentication'
|
||||||
|
|
||||||
|
|
||||||
|
class DecoratorTestCase(TestCase):
|
||||||
|
@staticmethod
|
||||||
|
@main_character_required
|
||||||
|
def dummy_view(*args, **kwargs):
|
||||||
|
return HttpResponse(status=200)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.main_user = AuthUtils.create_user('main_user', disconnect_signals=True)
|
||||||
|
cls.no_main_user = AuthUtils.create_user(
|
||||||
|
'no_main_user', disconnect_signals=True
|
||||||
|
)
|
||||||
|
main_character = EveCharacter.objects.create(
|
||||||
|
character_id=1,
|
||||||
|
character_name='Main Character',
|
||||||
|
corporation_id=1,
|
||||||
|
corporation_name='Corp',
|
||||||
|
corporation_ticker='CORP',
|
||||||
|
)
|
||||||
|
CharacterOwnership.objects.create(
|
||||||
|
user=cls.main_user, character=main_character, owner_hash='1'
|
||||||
|
)
|
||||||
|
cls.main_user.profile.main_character = main_character
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.request = RequestFactory().get('/test/')
|
||||||
|
|
||||||
|
@mock.patch(MODULE_PATH + '.decorators.messages')
|
||||||
|
def test_login_redirect(self, m):
|
||||||
|
setattr(self.request, 'user', AnonymousUser())
|
||||||
|
response = self.dummy_view(self.request)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
url = getattr(response, 'url', None)
|
||||||
|
self.assertEqual(parse.urlparse(url).path, reverse(settings.LOGIN_URL))
|
||||||
|
|
||||||
|
@mock.patch(MODULE_PATH + '.decorators.messages')
|
||||||
|
def test_main_character_redirect(self, m):
|
||||||
|
setattr(self.request, 'user', self.no_main_user)
|
||||||
|
response = self.dummy_view(self.request)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
url = getattr(response, 'url', None)
|
||||||
|
self.assertEqual(url, reverse('authentication:dashboard'))
|
||||||
|
|
||||||
|
@mock.patch(MODULE_PATH + '.decorators.messages')
|
||||||
|
def test_successful_request(self, m):
|
||||||
|
setattr(self.request, 'user', self.main_user)
|
||||||
|
response = self.dummy_view(self.request)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
@@ -1,142 +1,20 @@
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
from io import StringIO
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo,\
|
||||||
|
EveAllianceInfo, EveFactionInfo
|
||||||
from allianceauth.tests.auth_utils import AuthUtils
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
from .models import CharacterOwnership, UserProfile, State, get_guest_state, OwnershipRecord
|
|
||||||
from .backends import StateBackend
|
|
||||||
from .tasks import check_character_ownership
|
|
||||||
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo
|
|
||||||
from esi.models import Token
|
|
||||||
from esi.errors import IncompleteResponseError
|
from esi.errors import IncompleteResponseError
|
||||||
from allianceauth.authentication.decorators import main_character_required
|
from esi.models import Token
|
||||||
from django.test.client import RequestFactory
|
|
||||||
from django.http.response import HttpResponse
|
from ..models import CharacterOwnership, State, get_guest_state
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from ..tasks import check_character_ownership
|
||||||
from django.conf import settings
|
|
||||||
from django.shortcuts import reverse
|
|
||||||
from django.core.management import call_command
|
|
||||||
from urllib import parse
|
|
||||||
|
|
||||||
MODULE_PATH = 'allianceauth.authentication'
|
MODULE_PATH = 'allianceauth.authentication'
|
||||||
|
|
||||||
|
|
||||||
class DecoratorTestCase(TestCase):
|
|
||||||
@staticmethod
|
|
||||||
@main_character_required
|
|
||||||
def dummy_view(*args, **kwargs):
|
|
||||||
return HttpResponse(status=200)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpTestData(cls):
|
|
||||||
cls.main_user = AuthUtils.create_user('main_user', disconnect_signals=True)
|
|
||||||
cls.no_main_user = AuthUtils.create_user('no_main_user', disconnect_signals=True)
|
|
||||||
main_character = EveCharacter.objects.create(
|
|
||||||
character_id=1,
|
|
||||||
character_name='Main Character',
|
|
||||||
corporation_id=1,
|
|
||||||
corporation_name='Corp',
|
|
||||||
corporation_ticker='CORP',
|
|
||||||
)
|
|
||||||
CharacterOwnership.objects.create(user=cls.main_user, character=main_character, owner_hash='1')
|
|
||||||
cls.main_user.profile.main_character = main_character
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.request = RequestFactory().get('/test/')
|
|
||||||
|
|
||||||
@mock.patch(MODULE_PATH + '.decorators.messages')
|
|
||||||
def test_login_redirect(self, m):
|
|
||||||
setattr(self.request, 'user', AnonymousUser())
|
|
||||||
response = self.dummy_view(self.request)
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
url = getattr(response, 'url', None)
|
|
||||||
self.assertEqual(parse.urlparse(url).path, reverse(settings.LOGIN_URL))
|
|
||||||
|
|
||||||
@mock.patch(MODULE_PATH + '.decorators.messages')
|
|
||||||
def test_main_character_redirect(self, m):
|
|
||||||
setattr(self.request, 'user', self.no_main_user)
|
|
||||||
response = self.dummy_view(self.request)
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
url = getattr(response, 'url', None)
|
|
||||||
self.assertEqual(url, reverse('authentication:dashboard'))
|
|
||||||
|
|
||||||
@mock.patch(MODULE_PATH + '.decorators.messages')
|
|
||||||
def test_successful_request(self, m):
|
|
||||||
setattr(self.request, 'user', self.main_user)
|
|
||||||
response = self.dummy_view(self.request)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
class BackendTestCase(TestCase):
|
|
||||||
@classmethod
|
|
||||||
def setUpTestData(cls):
|
|
||||||
cls.main_character = EveCharacter.objects.create(
|
|
||||||
character_id=1,
|
|
||||||
character_name='Main Character',
|
|
||||||
corporation_id=1,
|
|
||||||
corporation_name='Corp',
|
|
||||||
corporation_ticker='CORP',
|
|
||||||
)
|
|
||||||
cls.alt_character = EveCharacter.objects.create(
|
|
||||||
character_id=2,
|
|
||||||
character_name='Alt Character',
|
|
||||||
corporation_id=1,
|
|
||||||
corporation_name='Corp',
|
|
||||||
corporation_ticker='CORP',
|
|
||||||
)
|
|
||||||
cls.unclaimed_character = EveCharacter.objects.create(
|
|
||||||
character_id=3,
|
|
||||||
character_name='Unclaimed Character',
|
|
||||||
corporation_id=1,
|
|
||||||
corporation_name='Corp',
|
|
||||||
corporation_ticker='CORP',
|
|
||||||
)
|
|
||||||
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
|
|
||||||
cls.old_user = AuthUtils.create_user('old_user', disconnect_signals=True)
|
|
||||||
AuthUtils.disconnect_signals()
|
|
||||||
CharacterOwnership.objects.create(user=cls.user, character=cls.main_character, owner_hash='1')
|
|
||||||
CharacterOwnership.objects.create(user=cls.user, character=cls.alt_character, owner_hash='2')
|
|
||||||
UserProfile.objects.update_or_create(user=cls.user, defaults={'main_character': cls.main_character})
|
|
||||||
AuthUtils.connect_signals()
|
|
||||||
|
|
||||||
def test_authenticate_main_character(self):
|
|
||||||
t = Token(character_id=self.main_character.character_id, character_owner_hash='1')
|
|
||||||
user = StateBackend().authenticate(token=t)
|
|
||||||
self.assertEquals(user, self.user)
|
|
||||||
|
|
||||||
def test_authenticate_alt_character(self):
|
|
||||||
t = Token(character_id=self.alt_character.character_id, character_owner_hash='2')
|
|
||||||
user = StateBackend().authenticate(token=t)
|
|
||||||
self.assertEquals(user, self.user)
|
|
||||||
|
|
||||||
def test_authenticate_unclaimed_character(self):
|
|
||||||
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='3')
|
|
||||||
user = StateBackend().authenticate(token=t)
|
|
||||||
self.assertNotEqual(user, self.user)
|
|
||||||
self.assertEqual(user.username, 'Unclaimed_Character')
|
|
||||||
self.assertEqual(user.profile.main_character, self.unclaimed_character)
|
|
||||||
|
|
||||||
def test_authenticate_character_record(self):
|
|
||||||
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='4')
|
|
||||||
record = OwnershipRecord.objects.create(user=self.old_user, character=self.unclaimed_character, owner_hash='4')
|
|
||||||
user = StateBackend().authenticate(token=t)
|
|
||||||
self.assertEqual(user, self.old_user)
|
|
||||||
self.assertTrue(CharacterOwnership.objects.filter(owner_hash='4', user=self.old_user).exists())
|
|
||||||
self.assertTrue(user.profile.main_character)
|
|
||||||
|
|
||||||
def test_iterate_username(self):
|
|
||||||
t = Token(character_id=self.unclaimed_character.character_id,
|
|
||||||
character_name=self.unclaimed_character.character_name, character_owner_hash='3')
|
|
||||||
username = StateBackend().authenticate(token=t).username
|
|
||||||
t.character_owner_hash = '4'
|
|
||||||
username_1 = StateBackend().authenticate(token=t).username
|
|
||||||
t.character_owner_hash = '5'
|
|
||||||
username_2 = StateBackend().authenticate(token=t).username
|
|
||||||
self.assertNotEqual(username, username_1, username_2)
|
|
||||||
self.assertTrue(username_1.endswith('_1'))
|
|
||||||
self.assertTrue(username_2.endswith('_2'))
|
|
||||||
|
|
||||||
|
|
||||||
class CharacterOwnershipTestCase(TestCase):
|
class CharacterOwnershipTestCase(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
@@ -158,8 +36,8 @@ class CharacterOwnershipTestCase(TestCase):
|
|||||||
character_owner_hash='1',
|
character_owner_hash='1',
|
||||||
)
|
)
|
||||||
co = CharacterOwnership.objects.get(character=self.character)
|
co = CharacterOwnership.objects.get(character=self.character)
|
||||||
self.assertEquals(co.user, self.user)
|
self.assertEqual(co.user, self.user)
|
||||||
self.assertEquals(co.owner_hash, '1')
|
self.assertEqual(co.owner_hash, '1')
|
||||||
|
|
||||||
def test_transfer_ownership(self):
|
def test_transfer_ownership(self):
|
||||||
Token.objects.create(
|
Token.objects.create(
|
||||||
@@ -176,7 +54,7 @@ class CharacterOwnershipTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
co = CharacterOwnership.objects.get(character=self.character)
|
co = CharacterOwnership.objects.get(character=self.character)
|
||||||
self.assertNotEqual(self.user, co.user)
|
self.assertNotEqual(self.user, co.user)
|
||||||
self.assertEquals(self.alt_user, co.user)
|
self.assertEqual(self.alt_user, co.user)
|
||||||
|
|
||||||
def test_clear_main_character(self):
|
def test_clear_main_character(self):
|
||||||
Token.objects.create(
|
Token.objects.create(
|
||||||
@@ -202,13 +80,15 @@ class StateTestCase(TestCase):
|
|||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
|
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
|
||||||
AuthUtils.add_main_character(cls.user, 'Test Character', '1', corp_id='1', alliance_id='1',
|
AuthUtils.add_main_character(cls.user, 'Test Character', '1', corp_id='1', alliance_id='1',
|
||||||
corp_name='Test Corp', alliance_name='Test Alliance')
|
corp_name='Test Corp', alliance_name='Test Alliance', faction_id=1337,
|
||||||
|
faction_name='Permabanned')
|
||||||
cls.guest_state = get_guest_state()
|
cls.guest_state = get_guest_state()
|
||||||
cls.test_character = EveCharacter.objects.get(character_id='1')
|
cls.test_character = EveCharacter.objects.get(character_id='1')
|
||||||
cls.test_corporation = EveCorporationInfo.objects.create(corporation_id='1', corporation_name='Test Corp',
|
cls.test_corporation = EveCorporationInfo.objects.create(corporation_id='1', corporation_name='Test Corp',
|
||||||
corporation_ticker='TEST', member_count=1)
|
corporation_ticker='TEST', member_count=1)
|
||||||
cls.test_alliance = EveAllianceInfo.objects.create(alliance_id='1', alliance_name='Test Alliance',
|
cls.test_alliance = EveAllianceInfo.objects.create(alliance_id='1', alliance_name='Test Alliance',
|
||||||
alliance_ticker='TEST', executor_corp_id='1')
|
alliance_ticker='TEST', executor_corp_id='1')
|
||||||
|
cls.test_faction = EveFactionInfo.objects.create(faction_id=1337, faction_name='Permabanned')
|
||||||
cls.member_state = State.objects.create(
|
cls.member_state = State.objects.create(
|
||||||
name='Test Member',
|
name='Test Member',
|
||||||
priority=150,
|
priority=150,
|
||||||
@@ -220,29 +100,38 @@ class StateTestCase(TestCase):
|
|||||||
def test_state_assignment_on_character_change(self):
|
def test_state_assignment_on_character_change(self):
|
||||||
self.member_state.member_characters.add(self.test_character)
|
self.member_state.member_characters.add(self.test_character)
|
||||||
self._refresh_user()
|
self._refresh_user()
|
||||||
self.assertEquals(self.user.profile.state, self.member_state)
|
self.assertEqual(self.user.profile.state, self.member_state)
|
||||||
|
|
||||||
self.member_state.member_characters.remove(self.test_character)
|
self.member_state.member_characters.remove(self.test_character)
|
||||||
self._refresh_user()
|
self._refresh_user()
|
||||||
self.assertEquals(self.user.profile.state, self.guest_state)
|
self.assertEqual(self.user.profile.state, self.guest_state)
|
||||||
|
|
||||||
def test_state_assignment_on_corporation_change(self):
|
def test_state_assignment_on_corporation_change(self):
|
||||||
self.member_state.member_corporations.add(self.test_corporation)
|
self.member_state.member_corporations.add(self.test_corporation)
|
||||||
self._refresh_user()
|
self._refresh_user()
|
||||||
self.assertEquals(self.user.profile.state, self.member_state)
|
self.assertEqual(self.user.profile.state, self.member_state)
|
||||||
|
|
||||||
self.member_state.member_corporations.remove(self.test_corporation)
|
self.member_state.member_corporations.remove(self.test_corporation)
|
||||||
self._refresh_user()
|
self._refresh_user()
|
||||||
self.assertEquals(self.user.profile.state, self.guest_state)
|
self.assertEqual(self.user.profile.state, self.guest_state)
|
||||||
|
|
||||||
def test_state_assignment_on_alliance_addition(self):
|
def test_state_assignment_on_alliance_addition(self):
|
||||||
self.member_state.member_alliances.add(self.test_alliance)
|
self.member_state.member_alliances.add(self.test_alliance)
|
||||||
self._refresh_user()
|
self._refresh_user()
|
||||||
self.assertEquals(self.user.profile.state, self.member_state)
|
self.assertEqual(self.user.profile.state, self.member_state)
|
||||||
|
|
||||||
self.member_state.member_alliances.remove(self.test_alliance)
|
self.member_state.member_alliances.remove(self.test_alliance)
|
||||||
self._refresh_user()
|
self._refresh_user()
|
||||||
self.assertEquals(self.user.profile.state, self.guest_state)
|
self.assertEqual(self.user.profile.state, self.guest_state)
|
||||||
|
|
||||||
|
def test_state_assignment_on_faction_change(self):
|
||||||
|
self.member_state.member_factions.add(self.test_faction)
|
||||||
|
self._refresh_user()
|
||||||
|
self.assertEqual(self.user.profile.state, self.member_state)
|
||||||
|
|
||||||
|
self.member_state.member_factions.remove(self.test_faction)
|
||||||
|
self._refresh_user()
|
||||||
|
self.assertEqual(self.user.profile.state, self.guest_state)
|
||||||
|
|
||||||
def test_state_assignment_on_higher_priority_state_creation(self):
|
def test_state_assignment_on_higher_priority_state_creation(self):
|
||||||
self.member_state.member_characters.add(self.test_character)
|
self.member_state.member_characters.add(self.test_character)
|
||||||
@@ -252,10 +141,10 @@ class StateTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
higher_state.member_characters.add(self.test_character)
|
higher_state.member_characters.add(self.test_character)
|
||||||
self._refresh_user()
|
self._refresh_user()
|
||||||
self.assertEquals(higher_state, self.user.profile.state)
|
self.assertEqual(higher_state, self.user.profile.state)
|
||||||
higher_state.member_characters.clear()
|
higher_state.member_characters.clear()
|
||||||
self._refresh_user()
|
self._refresh_user()
|
||||||
self.assertEquals(self.member_state, self.user.profile.state)
|
self.assertEqual(self.member_state, self.user.profile.state)
|
||||||
self.member_state.member_characters.clear()
|
self.member_state.member_characters.clear()
|
||||||
|
|
||||||
def test_state_assignment_on_lower_priority_state_creation(self):
|
def test_state_assignment_on_lower_priority_state_creation(self):
|
||||||
@@ -266,10 +155,10 @@ class StateTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
lower_state.member_characters.add(self.test_character)
|
lower_state.member_characters.add(self.test_character)
|
||||||
self._refresh_user()
|
self._refresh_user()
|
||||||
self.assertEquals(self.member_state, self.user.profile.state)
|
self.assertEqual(self.member_state, self.user.profile.state)
|
||||||
lower_state.member_characters.clear()
|
lower_state.member_characters.clear()
|
||||||
self._refresh_user()
|
self._refresh_user()
|
||||||
self.assertEquals(self.member_state, self.user.profile.state)
|
self.assertEqual(self.member_state, self.user.profile.state)
|
||||||
self.member_state.member_characters.clear()
|
self.member_state.member_characters.clear()
|
||||||
|
|
||||||
def test_state_assignment_on_priority_change(self):
|
def test_state_assignment_on_priority_change(self):
|
||||||
@@ -283,11 +172,11 @@ class StateTestCase(TestCase):
|
|||||||
lower_state.priority = 500
|
lower_state.priority = 500
|
||||||
lower_state.save()
|
lower_state.save()
|
||||||
self._refresh_user()
|
self._refresh_user()
|
||||||
self.assertEquals(lower_state, self.user.profile.state)
|
self.assertEqual(lower_state, self.user.profile.state)
|
||||||
lower_state.priority = 125
|
lower_state.priority = 125
|
||||||
lower_state.save()
|
lower_state.save()
|
||||||
self._refresh_user()
|
self._refresh_user()
|
||||||
self.assertEquals(self.member_state, self.user.profile.state)
|
self.assertEqual(self.member_state, self.user.profile.state)
|
||||||
|
|
||||||
def test_state_assignment_on_state_deletion(self):
|
def test_state_assignment_on_state_deletion(self):
|
||||||
self.member_state.member_characters.add(self.test_character)
|
self.member_state.member_characters.add(self.test_character)
|
||||||
@@ -297,11 +186,11 @@ class StateTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
higher_state.member_characters.add(self.test_character)
|
higher_state.member_characters.add(self.test_character)
|
||||||
self._refresh_user()
|
self._refresh_user()
|
||||||
self.assertEquals(higher_state, self.user.profile.state)
|
self.assertEqual(higher_state, self.user.profile.state)
|
||||||
higher_state.delete()
|
higher_state.delete()
|
||||||
self.assertFalse(State.objects.filter(name='Higher State').count())
|
self.assertFalse(State.objects.filter(name='Higher State').count())
|
||||||
self._refresh_user()
|
self._refresh_user()
|
||||||
self.assertEquals(self.member_state, self.user.profile.state)
|
self.assertEqual(self.member_state, self.user.profile.state)
|
||||||
|
|
||||||
def test_state_assignment_on_public_toggle(self):
|
def test_state_assignment_on_public_toggle(self):
|
||||||
self.member_state.member_characters.add(self.test_character)
|
self.member_state.member_characters.add(self.test_character)
|
||||||
@@ -310,26 +199,26 @@ class StateTestCase(TestCase):
|
|||||||
priority=200,
|
priority=200,
|
||||||
)
|
)
|
||||||
self._refresh_user()
|
self._refresh_user()
|
||||||
self.assertEquals(self.member_state, self.user.profile.state)
|
self.assertEqual(self.member_state, self.user.profile.state)
|
||||||
higher_state.public = True
|
higher_state.public = True
|
||||||
higher_state.save()
|
higher_state.save()
|
||||||
self._refresh_user()
|
self._refresh_user()
|
||||||
self.assertEquals(higher_state, self.user.profile.state)
|
self.assertEqual(higher_state, self.user.profile.state)
|
||||||
higher_state.public = False
|
higher_state.public = False
|
||||||
higher_state.save()
|
higher_state.save()
|
||||||
self._refresh_user()
|
self._refresh_user()
|
||||||
self.assertEquals(self.member_state, self.user.profile.state)
|
self.assertEqual(self.member_state, self.user.profile.state)
|
||||||
|
|
||||||
def test_state_assignment_on_active_changed(self):
|
def test_state_assignment_on_active_changed(self):
|
||||||
self.member_state.member_characters.add(self.test_character)
|
self.member_state.member_characters.add(self.test_character)
|
||||||
self.user.is_active = False
|
self.user.is_active = False
|
||||||
self.user.save()
|
self.user.save()
|
||||||
self._refresh_user()
|
self._refresh_user()
|
||||||
self.assertEquals(self.user.profile.state, self.guest_state)
|
self.assertEqual(self.user.profile.state, self.guest_state)
|
||||||
self.user.is_active = True
|
self.user.is_active = True
|
||||||
self.user.save()
|
self.user.save()
|
||||||
self._refresh_user()
|
self._refresh_user()
|
||||||
self.assertEquals(self.user.profile.state, self.member_state)
|
self.assertEqual(self.user.profile.state, self.member_state)
|
||||||
|
|
||||||
|
|
||||||
class CharacterOwnershipCheckTestCase(TestCase):
|
class CharacterOwnershipCheckTestCase(TestCase):
|
||||||
@@ -337,11 +226,11 @@ class CharacterOwnershipCheckTestCase(TestCase):
|
|||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
|
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
|
||||||
AuthUtils.add_main_character(cls.user, 'Test Character', '1', corp_id='1', alliance_id='1',
|
AuthUtils.add_main_character(cls.user, 'Test Character', '1', corp_id='1', alliance_id='1',
|
||||||
corp_name='Test Corp', alliance_name='Test Alliance')
|
corp_name='Test Corp', alliance_name='Test Alliance')
|
||||||
cls.character = EveCharacter.objects.get(character_id='1')
|
cls.character = EveCharacter.objects.get(character_id=1)
|
||||||
cls.token = Token.objects.create(
|
cls.token = Token.objects.create(
|
||||||
user=cls.user,
|
user=cls.user,
|
||||||
character_id='1',
|
character_id=1,
|
||||||
character_name='Test',
|
character_name='Test',
|
||||||
character_owner_hash='1',
|
character_owner_hash='1',
|
||||||
)
|
)
|
||||||
@@ -373,30 +262,3 @@ class CharacterOwnershipCheckTestCase(TestCase):
|
|||||||
filter.return_value.exists.return_value = False
|
filter.return_value.exists.return_value = False
|
||||||
check_character_ownership(self.ownership)
|
check_character_ownership(self.ownership)
|
||||||
self.assertTrue(filter.return_value.delete.called)
|
self.assertTrue(filter.return_value.delete.called)
|
||||||
|
|
||||||
|
|
||||||
class ManagementCommandTestCase(TestCase):
|
|
||||||
@classmethod
|
|
||||||
def setUpTestData(cls):
|
|
||||||
cls.user = AuthUtils.create_user('test user', disconnect_signals=True)
|
|
||||||
AuthUtils.add_main_character(cls.user, 'test character', '1', '2', 'test corp', 'test')
|
|
||||||
character = UserProfile.objects.get(user=cls.user).main_character
|
|
||||||
CharacterOwnership.objects.create(user=cls.user, character=character, owner_hash='test')
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.stdout = StringIO()
|
|
||||||
|
|
||||||
def test_ownership(self):
|
|
||||||
call_command('checkmains', stdout=self.stdout)
|
|
||||||
self.assertFalse(UserProfile.objects.filter(main_character__isnull=True).count())
|
|
||||||
self.assertNotIn(self.user.username, self.stdout.getvalue())
|
|
||||||
self.assertIn('All main characters', self.stdout.getvalue())
|
|
||||||
|
|
||||||
def test_no_ownership(self):
|
|
||||||
user = AuthUtils.create_user('v1 user', disconnect_signals=True)
|
|
||||||
AuthUtils.add_main_character(user, 'v1 character', '10', '20', 'test corp', 'test')
|
|
||||||
self.assertFalse(UserProfile.objects.filter(main_character__isnull=True).count())
|
|
||||||
|
|
||||||
call_command('checkmains', stdout=self.stdout)
|
|
||||||
self.assertEqual(UserProfile.objects.filter(main_character__isnull=True).count(), 1)
|
|
||||||
self.assertIn(user.username, self.stdout.getvalue())
|
|
||||||
330
allianceauth/authentication/tests/test_templatetags.py
Normal file
330
allianceauth/authentication/tests/test_templatetags.py
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
from math import ceil
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import requests_mock
|
||||||
|
from packaging.version import Version as Pep440Version
|
||||||
|
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from allianceauth.templatetags.admin_status import (
|
||||||
|
status_overview,
|
||||||
|
_fetch_list_from_gitlab,
|
||||||
|
_current_notifications,
|
||||||
|
_current_version_summary,
|
||||||
|
_fetch_notification_issues_from_gitlab,
|
||||||
|
_latests_versions
|
||||||
|
)
|
||||||
|
|
||||||
|
MODULE_PATH = 'allianceauth.templatetags'
|
||||||
|
|
||||||
|
|
||||||
|
def create_tags_list(tag_names: list):
|
||||||
|
return [{'name': str(tag_name)} for tag_name in tag_names]
|
||||||
|
|
||||||
|
|
||||||
|
GITHUB_TAGS = create_tags_list(['v2.4.6a1', 'v2.4.5', 'v2.4.0', 'v2.0.0', 'v1.1.1'])
|
||||||
|
GITHUB_NOTIFICATION_ISSUES = [
|
||||||
|
{
|
||||||
|
'id': 1,
|
||||||
|
'title': 'first issue'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 2,
|
||||||
|
'title': 'second issue'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 3,
|
||||||
|
'title': 'third issue'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 4,
|
||||||
|
'title': 'forth issue'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 5,
|
||||||
|
'title': 'fifth issue'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 6,
|
||||||
|
'title': 'sixth issue'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
TEST_VERSION = '2.6.5'
|
||||||
|
|
||||||
|
|
||||||
|
class TestStatusOverviewTag(TestCase):
|
||||||
|
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
|
||||||
|
@patch(MODULE_PATH + '.admin_status._fetch_celery_queue_length')
|
||||||
|
@patch(MODULE_PATH + '.admin_status._current_version_summary')
|
||||||
|
@patch(MODULE_PATH + '.admin_status._current_notifications')
|
||||||
|
def test_status_overview(
|
||||||
|
self,
|
||||||
|
mock_current_notifications,
|
||||||
|
mock_current_version_info,
|
||||||
|
mock_fetch_celery_queue_length
|
||||||
|
):
|
||||||
|
# given
|
||||||
|
notifications = {
|
||||||
|
'notifications': GITHUB_NOTIFICATION_ISSUES[:5]
|
||||||
|
}
|
||||||
|
mock_current_notifications.return_value = notifications
|
||||||
|
version_info = {
|
||||||
|
'latest_major': True,
|
||||||
|
'latest_minor': True,
|
||||||
|
'latest_patch': True,
|
||||||
|
'latest_beta': False,
|
||||||
|
'current_version': TEST_VERSION,
|
||||||
|
'latest_major_version': '2.4.5',
|
||||||
|
'latest_minor_version': '2.4.0',
|
||||||
|
'latest_patch_version': '2.4.5',
|
||||||
|
'latest_beta_version': '2.4.4a1',
|
||||||
|
}
|
||||||
|
mock_current_version_info.return_value = version_info
|
||||||
|
mock_fetch_celery_queue_length.return_value = 3
|
||||||
|
# when
|
||||||
|
result = status_overview()
|
||||||
|
# then
|
||||||
|
self.assertEqual(result["notifications"], GITHUB_NOTIFICATION_ISSUES[:5])
|
||||||
|
self.assertTrue(result["latest_major"])
|
||||||
|
self.assertTrue(result["latest_minor"])
|
||||||
|
self.assertTrue(result["latest_patch"])
|
||||||
|
self.assertFalse(result["latest_beta"])
|
||||||
|
self.assertEqual(result["current_version"], TEST_VERSION)
|
||||||
|
self.assertEqual(result["latest_major_version"], '2.4.5')
|
||||||
|
self.assertEqual(result["latest_minor_version"], '2.4.0')
|
||||||
|
self.assertEqual(result["latest_patch_version"], '2.4.5')
|
||||||
|
self.assertEqual(result["latest_beta_version"], '2.4.4a1')
|
||||||
|
self.assertEqual(result["task_queue_length"], 3)
|
||||||
|
|
||||||
|
|
||||||
|
class TestNotifications(TestCase):
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
cache.clear()
|
||||||
|
|
||||||
|
@requests_mock.mock()
|
||||||
|
def test_fetch_notification_issues_from_gitlab(self, requests_mocker):
|
||||||
|
# given
|
||||||
|
url = (
|
||||||
|
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues'
|
||||||
|
'?labels=announcement'
|
||||||
|
)
|
||||||
|
requests_mocker.get(url, json=GITHUB_NOTIFICATION_ISSUES)
|
||||||
|
# when
|
||||||
|
result = _fetch_notification_issues_from_gitlab()
|
||||||
|
# then
|
||||||
|
self.assertEqual(result, GITHUB_NOTIFICATION_ISSUES)
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '.admin_status.cache')
|
||||||
|
def test_current_notifications_normal(self, mock_cache):
|
||||||
|
# given
|
||||||
|
mock_cache.get_or_set.return_value = GITHUB_NOTIFICATION_ISSUES
|
||||||
|
# when
|
||||||
|
result = _current_notifications()
|
||||||
|
# then
|
||||||
|
self.assertEqual(result['notifications'], GITHUB_NOTIFICATION_ISSUES[:5])
|
||||||
|
|
||||||
|
@requests_mock.mock()
|
||||||
|
def test_current_notifications_failed(self, requests_mocker):
|
||||||
|
# given
|
||||||
|
url = (
|
||||||
|
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues'
|
||||||
|
'?labels=announcement'
|
||||||
|
)
|
||||||
|
requests_mocker.get(url, status_code=404)
|
||||||
|
# when
|
||||||
|
result = _current_notifications()
|
||||||
|
# then
|
||||||
|
self.assertEqual(result['notifications'], list())
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '.admin_status.cache')
|
||||||
|
def test_current_notifications_is_none(self, mock_cache):
|
||||||
|
# given
|
||||||
|
mock_cache.get_or_set.return_value = None
|
||||||
|
# when
|
||||||
|
result = _current_notifications()
|
||||||
|
# then
|
||||||
|
self.assertEqual(result['notifications'], list())
|
||||||
|
|
||||||
|
|
||||||
|
class TestCeleryQueueLength(TestCase):
|
||||||
|
|
||||||
|
def test_get_celery_queue_length(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestVersionTags(TestCase):
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
cache.clear()
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
|
||||||
|
@patch(MODULE_PATH + '.admin_status.cache')
|
||||||
|
def test_current_version_info_normal(self, mock_cache):
|
||||||
|
# given
|
||||||
|
mock_cache.get_or_set.return_value = GITHUB_TAGS
|
||||||
|
# when
|
||||||
|
result = _current_version_summary()
|
||||||
|
# then
|
||||||
|
self.assertTrue(result['latest_patch'])
|
||||||
|
self.assertEqual(result['latest_patch_version'], '2.4.5')
|
||||||
|
self.assertEqual(result['latest_beta_version'], '2.4.6a1')
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
|
||||||
|
@requests_mock.mock()
|
||||||
|
def test_current_version_info_failed(self, requests_mocker):
|
||||||
|
# given
|
||||||
|
url = (
|
||||||
|
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth'
|
||||||
|
'/repository/tags'
|
||||||
|
)
|
||||||
|
requests_mocker.get(url, status_code=500)
|
||||||
|
# when
|
||||||
|
result = _current_version_summary()
|
||||||
|
# then
|
||||||
|
self.assertEqual(result, {})
|
||||||
|
|
||||||
|
@requests_mock.mock()
|
||||||
|
def test_fetch_tags_from_gitlab(self, requests_mocker):
|
||||||
|
# given
|
||||||
|
url = (
|
||||||
|
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth'
|
||||||
|
'/repository/tags'
|
||||||
|
)
|
||||||
|
requests_mocker.get(url, json=GITHUB_TAGS)
|
||||||
|
# when
|
||||||
|
result = _current_version_summary()
|
||||||
|
# then
|
||||||
|
self.assertTrue(result)
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
|
||||||
|
@patch(MODULE_PATH + '.admin_status.cache')
|
||||||
|
def test_current_version_info_return_no_data(self, mock_cache):
|
||||||
|
# given
|
||||||
|
mock_cache.get_or_set.return_value = None
|
||||||
|
# when
|
||||||
|
result = _current_version_summary()
|
||||||
|
# then
|
||||||
|
self.assertEqual(result, {})
|
||||||
|
|
||||||
|
|
||||||
|
class TestLatestsVersion(TestCase):
|
||||||
|
|
||||||
|
def test_all_version_types_defined(self):
|
||||||
|
|
||||||
|
tags = create_tags_list(
|
||||||
|
['2.1.1', '2.1.0', '2.0.0', '2.1.1a1', '1.1.1', '1.1.0', '1.0.0']
|
||||||
|
)
|
||||||
|
patch, beta = _latests_versions(tags)
|
||||||
|
self.assertEqual(patch, Pep440Version('2.1.1'))
|
||||||
|
self.assertEqual(beta, Pep440Version('2.1.1a1'))
|
||||||
|
|
||||||
|
def test_major_and_minor_not_defined_with_zero(self):
|
||||||
|
|
||||||
|
tags = create_tags_list(
|
||||||
|
['2.1.2', '2.1.1', '2.0.1', '2.1.1a1', '1.1.1', '1.1.0', '1.0.0']
|
||||||
|
)
|
||||||
|
patch, beta = _latests_versions(tags)
|
||||||
|
self.assertEqual(patch, Pep440Version('2.1.2'))
|
||||||
|
self.assertEqual(beta, Pep440Version('2.1.1a1'))
|
||||||
|
|
||||||
|
def test_can_ignore_invalid_versions(self):
|
||||||
|
|
||||||
|
tags = create_tags_list(
|
||||||
|
['2.1.1', '2.1.0', '2.0.0', '2.1.1a1', 'invalid']
|
||||||
|
)
|
||||||
|
patch, beta = _latests_versions(tags)
|
||||||
|
self.assertEqual(patch, Pep440Version('2.1.1'))
|
||||||
|
self.assertEqual(beta, Pep440Version('2.1.1a1'))
|
||||||
|
|
||||||
|
|
||||||
|
class TestFetchListFromGitlab(TestCase):
|
||||||
|
|
||||||
|
page_size = 2
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.url = (
|
||||||
|
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth'
|
||||||
|
'/repository/tags'
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def my_callback(cls, request, context):
|
||||||
|
page = int(request.qs['page'][0])
|
||||||
|
start = (page - 1) * cls.page_size
|
||||||
|
end = start + cls.page_size
|
||||||
|
return GITHUB_TAGS[start:end]
|
||||||
|
|
||||||
|
@requests_mock.mock()
|
||||||
|
def test_can_fetch_one_page_with_header(self, requests_mocker):
|
||||||
|
headers = {
|
||||||
|
'x-total-pages': '1'
|
||||||
|
}
|
||||||
|
requests_mocker.get(self.url, json=GITHUB_TAGS, headers=headers)
|
||||||
|
result = _fetch_list_from_gitlab(self.url)
|
||||||
|
self.assertEqual(result, GITHUB_TAGS)
|
||||||
|
self.assertEqual(requests_mocker.call_count, 1)
|
||||||
|
|
||||||
|
@requests_mock.mock()
|
||||||
|
def test_can_fetch_one_page_wo_header(self, requests_mocker):
|
||||||
|
requests_mocker.get(self.url, json=GITHUB_TAGS)
|
||||||
|
result = _fetch_list_from_gitlab(self.url)
|
||||||
|
self.assertEqual(result, GITHUB_TAGS)
|
||||||
|
self.assertEqual(requests_mocker.call_count, 1)
|
||||||
|
|
||||||
|
@requests_mock.mock()
|
||||||
|
def test_can_fetch_one_page_and_ignore_invalid_header(self, requests_mocker):
|
||||||
|
headers = {
|
||||||
|
'x-total-pages': 'invalid'
|
||||||
|
}
|
||||||
|
requests_mocker.get(self.url, json=GITHUB_TAGS, headers=headers)
|
||||||
|
result = _fetch_list_from_gitlab(self.url)
|
||||||
|
self.assertEqual(result, GITHUB_TAGS)
|
||||||
|
self.assertEqual(requests_mocker.call_count, 1)
|
||||||
|
|
||||||
|
@requests_mock.mock()
|
||||||
|
def test_can_fetch_multiple_pages(self, requests_mocker):
|
||||||
|
total_pages = ceil(len(GITHUB_TAGS) / self.page_size)
|
||||||
|
headers = {
|
||||||
|
'x-total-pages': str(total_pages)
|
||||||
|
}
|
||||||
|
requests_mocker.get(self.url, json=self.my_callback, headers=headers)
|
||||||
|
result = _fetch_list_from_gitlab(self.url)
|
||||||
|
self.assertEqual(result, GITHUB_TAGS)
|
||||||
|
self.assertEqual(requests_mocker.call_count, total_pages)
|
||||||
|
|
||||||
|
@requests_mock.mock()
|
||||||
|
def test_can_fetch_given_number_of_pages_only(self, requests_mocker):
|
||||||
|
total_pages = ceil(len(GITHUB_TAGS) / self.page_size)
|
||||||
|
headers = {
|
||||||
|
'x-total-pages': str(total_pages)
|
||||||
|
}
|
||||||
|
requests_mocker.get(self.url, json=self.my_callback, headers=headers)
|
||||||
|
max_pages = 2
|
||||||
|
result = _fetch_list_from_gitlab(self.url, max_pages=max_pages)
|
||||||
|
self.assertEqual(result, GITHUB_TAGS[:4])
|
||||||
|
self.assertEqual(requests_mocker.call_count, max_pages)
|
||||||
|
|
||||||
|
@requests_mock.mock()
|
||||||
|
@patch(MODULE_PATH + '.admin_status.logger')
|
||||||
|
def test_should_not_raise_any_exception_from_github_request_but_log_as_warning(
|
||||||
|
self, requests_mocker, mock_logger
|
||||||
|
):
|
||||||
|
for my_exception in [
|
||||||
|
requests.exceptions.ConnectionError,
|
||||||
|
requests.exceptions.HTTPError,
|
||||||
|
requests.exceptions.URLRequired,
|
||||||
|
requests.exceptions.TooManyRedirects,
|
||||||
|
requests.exceptions.ConnectTimeout,
|
||||||
|
requests.exceptions.Timeout,
|
||||||
|
|
||||||
|
]:
|
||||||
|
requests_mocker.get(self.url, exc=my_exception)
|
||||||
|
try:
|
||||||
|
result = _fetch_list_from_gitlab(self.url)
|
||||||
|
except Exception as ex:
|
||||||
|
self.fail(f"Unexpected exception raised: {ex}")
|
||||||
|
self.assertTrue(mock_logger.warning.called)
|
||||||
|
self.assertListEqual(result, [])
|
||||||
@@ -7,11 +7,21 @@ from . import views
|
|||||||
app_name = 'authentication'
|
app_name = 'authentication'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^$', login_required(TemplateView.as_view(template_name='authentication/dashboard.html')),),
|
url(r'^$', views.index, name='index'),
|
||||||
url(r'^account/login/$', TemplateView.as_view(template_name='public/login.html'), name='login'),
|
url(
|
||||||
url(r'^account/characters/main/$', views.main_character_change, name='change_main_character'),
|
r'^account/login/$',
|
||||||
url(r'^account/characters/add/$', views.add_character, name='add_character'),
|
TemplateView.as_view(template_name='public/login.html'),
|
||||||
url(r'^help/$', login_required(TemplateView.as_view(template_name='allianceauth/help.html')), name='help'),
|
name='login'
|
||||||
url(r'^dashboard/$',
|
),
|
||||||
login_required(TemplateView.as_view(template_name='authentication/dashboard.html')), name='dashboard'),
|
url(
|
||||||
|
r'^account/characters/main/$',
|
||||||
|
views.main_character_change,
|
||||||
|
name='change_main_character'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^account/characters/add/$',
|
||||||
|
views.add_character,
|
||||||
|
name='add_character'
|
||||||
|
),
|
||||||
|
url(r'^dashboard/$', views.dashboard, name='dashboard'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,32 +6,76 @@ from django.contrib.auth import login, authenticate
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core import signing
|
from django.core import signing
|
||||||
from django.urls import reverse
|
from django.core.mail import EmailMultiAlternatives
|
||||||
from django.shortcuts import redirect
|
from django.http import JsonResponse
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.shortcuts import redirect, render
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
from django.urls import reverse, reverse_lazy
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from allianceauth.eveonline.models import EveCharacter
|
||||||
from esi.decorators import token_required
|
from esi.decorators import token_required
|
||||||
from esi.models import Token
|
from esi.models import Token
|
||||||
from registration.backends.hmac.views import RegistrationView as BaseRegistrationView, \
|
|
||||||
ActivationView as BaseActivationView, REGISTRATION_SALT
|
from django_registration.backends.activation.views import (
|
||||||
from registration.signals import user_registered
|
RegistrationView as BaseRegistrationView,
|
||||||
|
ActivationView as BaseActivationView,
|
||||||
|
REGISTRATION_SALT
|
||||||
|
)
|
||||||
|
from django_registration.signals import user_registered
|
||||||
|
|
||||||
from .models import CharacterOwnership
|
from .models import CharacterOwnership
|
||||||
from .forms import RegistrationForm
|
from .forms import RegistrationForm
|
||||||
|
|
||||||
|
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
|
||||||
|
_has_auto_groups = True
|
||||||
|
from allianceauth.eveonline.autogroups.models import *
|
||||||
|
else:
|
||||||
|
_has_auto_groups = False
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def index(request):
|
||||||
|
return redirect('authentication:dashboard')
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def dashboard(request):
|
||||||
|
groups = request.user.groups.all()
|
||||||
|
if _has_auto_groups:
|
||||||
|
groups = groups\
|
||||||
|
.filter(managedalliancegroup__isnull=True)\
|
||||||
|
.filter(managedcorpgroup__isnull=True)
|
||||||
|
groups = groups.order_by('name')
|
||||||
|
characters = EveCharacter.objects\
|
||||||
|
.filter(character_ownership__user=request.user)\
|
||||||
|
.select_related()\
|
||||||
|
.order_by('character_name')
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'groups': groups,
|
||||||
|
'characters': characters
|
||||||
|
}
|
||||||
|
return render(request, 'authentication/dashboard.html', context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@token_required(scopes=settings.LOGIN_TOKEN_SCOPES)
|
@token_required(scopes=settings.LOGIN_TOKEN_SCOPES)
|
||||||
def main_character_change(request, token):
|
def main_character_change(request, token):
|
||||||
logger.debug("main_character_change called by user %s for character %s" % (request.user, token.character_name))
|
logger.debug(f"main_character_change called by user {request.user} for character {token.character_name}")
|
||||||
try:
|
try:
|
||||||
co = CharacterOwnership.objects.get(character__character_id=token.character_id, user=request.user)
|
co = CharacterOwnership.objects.get(character__character_id=token.character_id, user=request.user)
|
||||||
except CharacterOwnership.DoesNotExist:
|
except CharacterOwnership.DoesNotExist:
|
||||||
if not CharacterOwnership.objects.filter(character__character_id=token.character_id).exists():
|
if not CharacterOwnership.objects.filter(character__character_id=token.character_id).exists():
|
||||||
co = CharacterOwnership.objects.create_by_token(token)
|
co = CharacterOwnership.objects.create_by_token(token)
|
||||||
else:
|
else:
|
||||||
messages.error(request, 'Cannot change main character to %(char)s: character owned by a different account.' % ({'char': token.character_name}))
|
messages.error(
|
||||||
|
request,
|
||||||
|
_('Cannot change main character to %(char)s: character owned by a different account.') % ({'char': token.character_name})
|
||||||
|
)
|
||||||
co = None
|
co = None
|
||||||
if co:
|
if co:
|
||||||
request.user.profile.main_character = co.character
|
request.user.profile.main_character = co.character
|
||||||
@@ -93,11 +137,57 @@ def sso_login(request, token):
|
|||||||
# Step 2
|
# Step 2
|
||||||
class RegistrationView(BaseRegistrationView):
|
class RegistrationView(BaseRegistrationView):
|
||||||
form_class = RegistrationForm
|
form_class = RegistrationForm
|
||||||
success_url = 'authentication:dashboard'
|
template_name = "public/register.html"
|
||||||
|
email_body_template = "registration/activation_email.txt"
|
||||||
|
email_body_template_html = "registration/activation_email_html.txt"
|
||||||
|
email_subject_template = "registration/activation_email_subject.txt"
|
||||||
|
success_url = reverse_lazy('registration_complete')
|
||||||
|
|
||||||
|
def send_activation_email(self, user):
|
||||||
|
"""
|
||||||
|
Implement our own way to send a mail to make sure we
|
||||||
|
send a RFC conform multipart email
|
||||||
|
:param user:
|
||||||
|
:type user:
|
||||||
|
"""
|
||||||
|
|
||||||
|
activation_key = self.get_activation_key(user)
|
||||||
|
context = self.get_email_context(activation_key)
|
||||||
|
context["user"] = user
|
||||||
|
|
||||||
|
# email subject
|
||||||
|
subject = render_to_string(
|
||||||
|
template_name=self.email_subject_template,
|
||||||
|
context=context,
|
||||||
|
request=self.request,
|
||||||
|
)
|
||||||
|
subject = "".join(subject.splitlines())
|
||||||
|
|
||||||
|
# plaintext email body part
|
||||||
|
message = render_to_string(
|
||||||
|
template_name=self.email_body_template,
|
||||||
|
context=context,
|
||||||
|
request=self.request,
|
||||||
|
)
|
||||||
|
|
||||||
|
# html email body part
|
||||||
|
message_html = render_to_string(
|
||||||
|
template_name=self.email_body_template_html,
|
||||||
|
context=context,
|
||||||
|
request=self.request,
|
||||||
|
)
|
||||||
|
|
||||||
|
# send it
|
||||||
|
user.email_user(
|
||||||
|
subject,
|
||||||
|
message,
|
||||||
|
settings.DEFAULT_FROM_EMAIL,
|
||||||
|
**{'html_message': message_html},
|
||||||
|
)
|
||||||
|
|
||||||
def get_success_url(self, user):
|
def get_success_url(self, user):
|
||||||
if not getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
|
if not getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
|
||||||
return 'authentication:dashboard', (), {}
|
return reverse_lazy('authentication:dashboard')
|
||||||
return super().get_success_url(user)
|
return super().get_success_url(user)
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
@@ -109,7 +199,7 @@ class RegistrationView(BaseRegistrationView):
|
|||||||
if not getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
|
if not getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
|
||||||
# Keep the request so the user can be automagically logged in.
|
# Keep the request so the user can be automagically logged in.
|
||||||
setattr(self, 'request', request)
|
setattr(self, 'request', request)
|
||||||
return super(RegistrationView, self).dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def register(self, form):
|
def register(self, form):
|
||||||
user = User.objects.get(pk=self.request.session.get('registration_uid'))
|
user = User.objects.get(pk=self.request.session.get('registration_uid'))
|
||||||
@@ -128,17 +218,20 @@ class RegistrationView(BaseRegistrationView):
|
|||||||
return signing.dumps(obj=[getattr(user, User.USERNAME_FIELD), user.email], salt=REGISTRATION_SALT)
|
return signing.dumps(obj=[getattr(user, User.USERNAME_FIELD), user.email], salt=REGISTRATION_SALT)
|
||||||
|
|
||||||
def get_email_context(self, activation_key):
|
def get_email_context(self, activation_key):
|
||||||
context = super(RegistrationView, self).get_email_context(activation_key)
|
context = super().get_email_context(activation_key)
|
||||||
context['url'] = context['site'].domain + reverse('registration_activate', args=[activation_key])
|
context['url'] = context['site'].domain + reverse('registration_activate', args=[activation_key])
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
# Step 3
|
# Step 3
|
||||||
class ActivationView(BaseActivationView):
|
class ActivationView(BaseActivationView):
|
||||||
|
template_name = "registration/activate.html"
|
||||||
|
success_url = reverse_lazy('registration_activation_complete')
|
||||||
|
|
||||||
def validate_key(self, activation_key):
|
def validate_key(self, activation_key):
|
||||||
try:
|
try:
|
||||||
dump = signing.loads(activation_key, salt=REGISTRATION_SALT,
|
dump = signing.loads(activation_key, salt=REGISTRATION_SALT,
|
||||||
max_age=settings.ACCOUNT_ACTIVATION_DAYS * 86400)
|
max_age=settings.ACCOUNT_ACTIVATION_DAYS * 86400)
|
||||||
return dump
|
return dump
|
||||||
except signing.BadSignature:
|
except signing.BadSignature:
|
||||||
return None
|
return None
|
||||||
@@ -166,5 +259,5 @@ def activation_complete(request):
|
|||||||
|
|
||||||
|
|
||||||
def registration_closed(request):
|
def registration_closed(request):
|
||||||
messages.error(request, _('Registraion of new accounts it not allowed at this time.'))
|
messages.error(request, _('Registration of new accounts is not allowed at this time.'))
|
||||||
return redirect('authentication:login')
|
return redirect('authentication:login')
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ def create_project(parser, options, args):
|
|||||||
# Call the command with extra context
|
# Call the command with extra context
|
||||||
call_command(StartProject(), *args, **command_options)
|
call_command(StartProject(), *args, **command_options)
|
||||||
|
|
||||||
print("Success! %(project_name)s has been created." % {'project_name': args[0]}) # noqa
|
print(f"Success! {args[0]} has been created.") # noqa
|
||||||
|
|
||||||
|
|
||||||
def update_settings(parser, options, args):
|
def update_settings(parser, options, args):
|
||||||
@@ -69,10 +69,10 @@ def update_settings(parser, options, args):
|
|||||||
template_settings_path = os.path.join(template_path, 'project_name/settings/base.py')
|
template_settings_path = os.path.join(template_path, 'project_name/settings/base.py')
|
||||||
|
|
||||||
# overwrite the local project's base settings
|
# overwrite the local project's base settings
|
||||||
with open(template_settings_path, 'r') as template, open(settings_path, 'w') as target:
|
with open(template_settings_path) as template, open(settings_path, 'w') as target:
|
||||||
target.write(template.read())
|
target.write(template.read())
|
||||||
|
|
||||||
print("Successfully updated %(project_name)s settings." % {'project_name': project_name})
|
print(f"Successfully updated {project_name} settings.")
|
||||||
|
|
||||||
|
|
||||||
COMMANDS = {
|
COMMANDS = {
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ from allianceauth.corputils import urls
|
|||||||
|
|
||||||
class CorpStats(MenuItemHook):
|
class CorpStats(MenuItemHook):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
MenuItemHook.__init__(self,
|
MenuItemHook.__init__(
|
||||||
_('Corporation Stats'),
|
self,
|
||||||
'fa fa-share-alt fa-fw',
|
_('Corporation Stats'),
|
||||||
'corputils:view',
|
'fas fa-share-alt fa-fw',
|
||||||
navactive=['corputils:'])
|
'corputils:view',
|
||||||
|
navactive=['corputils:']
|
||||||
|
)
|
||||||
|
|
||||||
def render(self, request):
|
def render(self, request):
|
||||||
if request.user.has_perm('corputils.view_corp_corpstats') or request.user.has_perm(
|
if request.user.has_perm('corputils.view_corp_corpstats') or request.user.has_perm(
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class CorpStatsQuerySet(models.QuerySet):
|
|||||||
if user.has_perm('corputils.view_state_corpstats'):
|
if user.has_perm('corputils.view_state_corpstats'):
|
||||||
queries.append(models.Q(corp__in=user.profile.state.member_corporations.all()))
|
queries.append(models.Q(corp__in=user.profile.state.member_corporations.all()))
|
||||||
queries.append(models.Q(corp__alliance__in=user.profile.state.member_alliances.all()))
|
queries.append(models.Q(corp__alliance__in=user.profile.state.member_alliances.all()))
|
||||||
logger.debug('%s queries for user %s visible corpstats.' % (len(queries), user))
|
logger.debug(f'{len(queries)} queries for user {user} visible corpstats.')
|
||||||
# filter based on queries
|
# filter based on queries
|
||||||
query = queries.pop()
|
query = queries.pop()
|
||||||
for q in queries:
|
for q in queries:
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.10.1 on 2016-12-14 21:36
|
# Generated by Django 1.10.1 on 2016-12-14 21:36
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.10.1 on 2016-12-14 21:48
|
# Generated by Django 1.10.1 on 2016-12-14 21:48
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.10.5 on 2017-03-22 23:35
|
# Generated by Django 1.10.5 on 2017-03-22 23:35
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.10.5 on 2017-03-26 20:13
|
# Generated by Django 1.10.5 on 2017-03-26 20:13
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@@ -13,8 +11,7 @@ def convert_json_to_members(apps, schema_editor):
|
|||||||
for cs in CorpStats.objects.all():
|
for cs in CorpStats.objects.all():
|
||||||
members = json.loads(cs._members)
|
members = json.loads(cs._members)
|
||||||
CorpMember.objects.bulk_create(
|
CorpMember.objects.bulk_create(
|
||||||
[CorpMember(corpstats=cs, character_id=member_id, character_name=member_name) for member_id, member_name in
|
[CorpMember(corpstats=cs, character_id=member_id, character_name=member_name) for member_id, member_name in members.items()]
|
||||||
members.items()]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -50,7 +47,7 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='corpmember',
|
name='corpmember',
|
||||||
unique_together=set([('corpstats', 'character_id')]),
|
unique_together={('corpstats', 'character_id')},
|
||||||
),
|
),
|
||||||
migrations.RunPython(convert_json_to_members, convert_members_to_json),
|
migrations.RunPython(convert_json_to_members, convert_members_to_json),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Generated by Django 1.11.2 on 2017-06-10 15:34
|
# Generated by Django 1.11.2 on 2017-06-10 15:34
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ from bravado.exception import HTTPForbidden
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from esi.errors import TokenError
|
from esi.errors import TokenError
|
||||||
from esi.models import Token
|
from esi.models import Token
|
||||||
from allianceauth.eveonline.models import EveCorporationInfo, EveCharacter,\
|
from allianceauth.eveonline.models import EveCorporationInfo, EveCharacter, EveAllianceInfo
|
||||||
EveAllianceInfo
|
|
||||||
from allianceauth.notifications import notify
|
from allianceauth.notifications import notify
|
||||||
|
|
||||||
from allianceauth.corputils.managers import CorpStatsManager
|
from allianceauth.corputils.managers import CorpStatsManager
|
||||||
@@ -44,13 +43,12 @@ class CorpStats(models.Model):
|
|||||||
objects = CorpStatsManager()
|
objects = CorpStatsManager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s for %s" % (self.__class__.__name__, self.corp)
|
return f"{self.__class__.__name__} for {self.corp}"
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
try:
|
try:
|
||||||
c = self.token.get_esi_client(spec_file=SWAGGER_SPEC_PATH)
|
c = self.token.get_esi_client(spec_file=SWAGGER_SPEC_PATH)
|
||||||
assert c.Character.get_characters_character_id(character_id=self.token.character_id).result()[
|
assert c.Character.get_characters_character_id(character_id=self.token.character_id).result()['corporation_id'] == int(self.corp.corporation_id)
|
||||||
'corporation_id'] == int(self.corp.corporation_id)
|
|
||||||
member_ids = c.Corporation.get_corporations_corporation_id_members(
|
member_ids = c.Corporation.get_corporations_corporation_id_members(
|
||||||
corporation_id=self.corp.corporation_id).result()
|
corporation_id=self.corp.corporation_id).result()
|
||||||
|
|
||||||
@@ -58,18 +56,15 @@ class CorpStats(models.Model):
|
|||||||
# the swagger spec doesn't have a maxItems count
|
# the swagger spec doesn't have a maxItems count
|
||||||
# manual testing says we can do over 350, but let's not risk it
|
# manual testing says we can do over 350, but let's not risk it
|
||||||
member_id_chunks = [member_ids[i:i + 255] for i in range(0, len(member_ids), 255)]
|
member_id_chunks = [member_ids[i:i + 255] for i in range(0, len(member_ids), 255)]
|
||||||
member_name_chunks = [c.Universe.post_universe_names(ids=id_chunk).result() for id_chunk in
|
member_name_chunks = [c.Universe.post_universe_names(ids=id_chunk).result() for id_chunk in member_id_chunks]
|
||||||
member_id_chunks]
|
|
||||||
member_list = {}
|
member_list = {}
|
||||||
for name_chunk in member_name_chunks:
|
for name_chunk in member_name_chunks:
|
||||||
member_list.update({m['id']: m['name'] for m in name_chunk})
|
member_list.update({m['id']: m['name'] for m in name_chunk})
|
||||||
|
|
||||||
# bulk create new member models
|
# bulk create new member models
|
||||||
missing_members = [m_id for m_id in member_ids if
|
missing_members = [m_id for m_id in member_ids if not CorpMember.objects.filter(corpstats=self, character_id=m_id).exists()]
|
||||||
not CorpMember.objects.filter(corpstats=self, character_id=m_id).exists()]
|
|
||||||
CorpMember.objects.bulk_create(
|
CorpMember.objects.bulk_create(
|
||||||
[CorpMember(character_id=m_id, character_name=member_list[m_id], corpstats=self) for m_id in
|
[CorpMember(character_id=m_id, character_name=member_list[m_id], corpstats=self) for m_id in missing_members])
|
||||||
missing_members])
|
|
||||||
|
|
||||||
# purge old members
|
# purge old members
|
||||||
self.members.exclude(character_id__in=member_ids).delete()
|
self.members.exclude(character_id__in=member_ids).delete()
|
||||||
@@ -78,23 +73,24 @@ class CorpStats(models.Model):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
except TokenError as e:
|
except TokenError as e:
|
||||||
logger.warning("%s failed to update: %s" % (self, e))
|
logger.warning(f"{self} failed to update: {e}")
|
||||||
if self.token.user:
|
if self.token.user:
|
||||||
notify(self.token.user, "%s failed to update with your ESI token." % self,
|
notify(
|
||||||
message="Your token has expired or is no longer valid. Please add a new one to create a new CorpStats.",
|
self.token.user, "%s failed to update with your ESI token." % self,
|
||||||
level="error")
|
message="Your token has expired or is no longer valid. Please add a new one to create a new CorpStats.",
|
||||||
|
level="error")
|
||||||
self.delete()
|
self.delete()
|
||||||
except HTTPForbidden as e:
|
except HTTPForbidden as e:
|
||||||
logger.warning("%s failed to update: %s" % (self, e))
|
logger.warning(f"{self} failed to update: {e}")
|
||||||
if self.token.user:
|
if self.token.user:
|
||||||
notify(self.token.user, "%s failed to update with your ESI token." % self,
|
notify(self.token.user, "%s failed to update with your ESI token." % self, message=f"{e.status_code}: {e.message}", level="error")
|
||||||
message="%s: %s" % (e.status_code, e.message), level="error")
|
|
||||||
self.delete()
|
self.delete()
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
logger.warning("%s token character no longer in corp." % self)
|
logger.warning("%s token character no longer in corp." % self)
|
||||||
if self.token.user:
|
if self.token.user:
|
||||||
notify(self.token.user, "%s cannot update with your ESI token." % self,
|
notify(
|
||||||
message="%s cannot update with your ESI token as you have left corp." % self, level="error")
|
self.token.user, "%s cannot update with your ESI token." % self,
|
||||||
|
message="%s cannot update with your ESI token as you have left corp." % self, level="error")
|
||||||
self.delete()
|
self.delete()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -103,7 +99,7 @@ class CorpStats(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def user_count(self):
|
def user_count(self):
|
||||||
return len(set([m.main_character for m in self.members.all() if m.main_character]))
|
return len({m.main_character for m in self.members.all() if m.main_character})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def registered_member_count(self):
|
def registered_member_count(self):
|
||||||
@@ -127,9 +123,7 @@ class CorpStats(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def mains(self):
|
def mains(self):
|
||||||
return self.members.filter(pk__in=[m.pk for m in self.members.all() if
|
return self.members.filter(pk__in=[m.pk for m in self.members.all() if m.main_character and int(m.main_character.character_id) == int(m.character_id)])
|
||||||
m.main_character and int(m.main_character.character_id) == int(
|
|
||||||
m.character_id)])
|
|
||||||
|
|
||||||
def visible_to(self, user):
|
def visible_to(self, user):
|
||||||
return CorpStats.objects.filter(pk=self.pk).visible_to(user).exists()
|
return CorpStats.objects.filter(pk=self.pk).visible_to(user).exists()
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user