Compare commits
1022 commits
Author | SHA1 | Date | |
---|---|---|---|
|
c5666e8ff1 | ||
|
6d85b5e00d | ||
|
dbe3caadb3 | ||
|
f771aac922 | ||
|
18d23c771a | ||
|
3d67fb274e | ||
|
72eada0a4e | ||
|
9c01ec8a22 | ||
|
1dd4ccd324 | ||
|
c54bcd4a16 | ||
|
05ab90e1d4 | ||
|
88845aebd8 | ||
|
a230441862 | ||
|
c833dc06d9 | ||
|
67888bcd27 | ||
|
7250c71e93 | ||
|
a42bae51f7 | ||
|
f98549e92d | ||
|
ff2c4e91d2 | ||
|
091c662b3b | ||
|
37cdd2dd04 | ||
|
1f91299d41 | ||
|
34e8ac6b8e | ||
|
8cd7327d20 | ||
|
7406d081aa | ||
|
cdbed121cd | ||
|
48d0a58dae | ||
|
7aecf6b046 | ||
|
44554d7de6 | ||
|
66f6212947 | ||
|
6421e4f5be | ||
|
6224649dd0 | ||
|
9dd779f4fa | ||
|
c00db58cb1 | ||
|
08ede3f797 | ||
|
309f0dd993 | ||
|
981e0f1b4e | ||
|
d08010a2ab | ||
|
5537ef5522 | ||
|
cc666c8361 | ||
|
73541ef8dd | ||
|
0470bfec68 | ||
|
caf599b5b3 | ||
|
2c3e982b7b | ||
|
3c99a2c477 | ||
|
a711fb9768 | ||
|
326b3d5dd3 | ||
|
02ba30a29e | ||
|
67839fbbd0 | ||
|
45c1f400a1 | ||
|
d7cc620a0a | ||
|
a75a080b9c | ||
|
44682900e1 | ||
|
46c950dc37 | ||
|
570c3071ee | ||
|
bb891e5b49 | ||
|
0406dea2a2 | ||
|
308006f7bd | ||
|
dc86e3a095 | ||
|
f3a393b767 | ||
|
938af7c720 | ||
|
10e50795d9 | ||
|
796a670cd7 | ||
|
c6acfa5e05 | ||
|
829e6a802d | ||
|
9f4ae9f78f | ||
|
6c8926ec56 | ||
|
5335bc248f | ||
|
ee17bc0778 | ||
|
5d79374b92 | ||
|
adb7c182d8 | ||
|
0ea822f7a0 | ||
|
f1ba0e79c1 | ||
|
896eec6c22 | ||
|
eb1f6942f0 | ||
|
4549d57ee1 | ||
|
fae8422508 | ||
|
913ec45acf | ||
|
e5de51f8fa | ||
|
4d56d18a24 | ||
|
04f9fe8252 | ||
|
4ee2e1b940 | ||
|
9270a017d3 | ||
|
8ff0f369a6 | ||
|
cad679ad90 | ||
|
a63afd6f3c | ||
|
66f2184b39 | ||
|
c8f21aa9a5 | ||
|
8d0045ec82 | ||
|
55a85689e9 | ||
|
1439dd12c8 | ||
|
bf5514032b | ||
|
571596dcbf | ||
|
86d1ff4eaa | ||
|
345684300f | ||
|
e24c208e1d | ||
|
ba96d83db8 | ||
|
d6832a0550 | ||
|
86815fb460 | ||
|
4a2bc46206 | ||
|
c6c5ea0c7a | ||
|
e6a1c04509 | ||
|
8f1c8d6c96 | ||
|
67b06d7856 | ||
|
7d83efed7f | ||
|
08eef6408f | ||
|
208a4e4ef7 | ||
|
5147980d43 | ||
|
2f64c87d56 | ||
|
361d3fc33a | ||
|
04877b47c1 | ||
|
7223dccf75 | ||
|
28156a9952 | ||
|
45469ff103 | ||
|
a4c2fad04f | ||
|
64a3d1bbb2 | ||
|
fd77975390 | ||
|
664dc3ca71 | ||
|
44c4795282 | ||
|
680de724ba | ||
|
d0d6b1e4f9 | ||
|
d9d821c6db | ||
|
0c4a49d446 | ||
|
d93767abdd | ||
|
6dcd3ed0c9 | ||
|
6bc0eb4b1c | ||
|
d835ee741a | ||
|
e923d1b2fe | ||
|
5e72b472e6 | ||
|
acdf94a8c9 | ||
|
6dbe3932de | ||
|
85bee3e77c | ||
|
2189de9433 | ||
|
1e7957a2cd | ||
|
3f53c62d4d | ||
|
7bba448ef5 | ||
|
2831a24b90 | ||
|
df34e547ad | ||
|
c2f6dba462 | ||
|
6d763f2db5 | ||
|
fcaf76e89f | ||
|
a0feaa8fc9 | ||
|
83579891b9 | ||
|
d409f0e9b4 | ||
|
c5aff93fb8 | ||
|
4259b909a0 | ||
|
e996e9e47f | ||
|
d9b477c441 | ||
|
1073ad45ff | ||
|
031e4f5f6b | ||
|
c87566673a | ||
|
08d2437d49 | ||
|
b2c9be7bda | ||
|
2a4882f598 | ||
|
ea2c7d2f36 | ||
|
ce04b268c3 | ||
|
76afe927b6 | ||
|
695fa85b19 | ||
|
847e8b6660 | ||
|
9376750a1b | ||
|
c8b7848f5a | ||
|
2a4d4e3bc1 | ||
|
24a9969738 | ||
|
9b7cdf811a | ||
|
faedcc5c70 | ||
|
96ad972600 | ||
|
0de11d26a6 | ||
|
2ff1ed0f54 | ||
|
9fd93a3b53 | ||
|
4b6ba43c42 | ||
|
e22061fc84 | ||
|
a079827aa6 | ||
|
f4eb9c54a4 | ||
|
458598b1cb | ||
|
f7fa1c01cb | ||
|
350492c6e4 | ||
|
545c655e73 | ||
|
07c3a8859b | ||
|
aef795b40c | ||
|
d75a498d18 | ||
|
995ff88c87 | ||
|
80b91435cf | ||
|
e09008bcb7 | ||
|
ee4a947385 | ||
|
3457eff339 | ||
|
ad73e4cdf3 | ||
|
2cf386e8b5 | ||
|
104cee9e81 | ||
|
999e0dcb15 | ||
|
3126b6ecad | ||
|
2b84d62260 | ||
|
d7080a4d1b | ||
|
81a31eafa2 | ||
|
04c6af1dff | ||
|
14ecef649e | ||
|
65e7d4961d | ||
|
88b07644ee | ||
|
eb07dd8fa7 | ||
|
bfe939564b | ||
|
c9f1d69b47 | ||
|
015cbcc758 | ||
|
dd32982687 | ||
|
f970d11d67 | ||
|
5d54b6b3c4 | ||
|
c188051aee | ||
|
f75c2a391f | ||
|
ff6988f29c | ||
|
b02a30819f | ||
|
1689f7ec7b | ||
|
e1054077fa | ||
|
038aa8674a | ||
|
de8adf983e | ||
|
39b42a29a4 | ||
|
26f26a1722 | ||
|
09e01d620e | ||
|
126ed83ee5 | ||
|
c36884846d | ||
|
b2a91af011 | ||
|
159b2b4cc0 | ||
|
77b441eb5e | ||
|
de4ee1a655 | ||
|
9bc4ed00e9 | ||
|
d21e24dd6c | ||
|
729a28bce5 | ||
|
32a1ea8919 | ||
|
2ac300ff02 | ||
|
fdb5293edb | ||
|
d143218206 | ||
|
5cbf3489b3 | ||
|
20f55047b8 | ||
|
34756c399d | ||
|
0cb981db9f | ||
|
0b4eeb8751 | ||
|
b6c9ce4763 | ||
|
45587d0884 | ||
|
87d7484a89 | ||
|
e5b2481345 | ||
|
58f3807b8d | ||
|
df49a0e353 | ||
|
fcbf74ca97 | ||
|
7a05ad1c3b | ||
|
b053c43b19 | ||
|
27586b25b8 | ||
|
3a7d4d8f0a | ||
|
93d032fc72 | ||
|
97d2029b5a | ||
|
35756d2f15 | ||
|
1291d750de | ||
|
6312f143f5 | ||
|
ae3f1090bf | ||
|
af17ca9665 | ||
|
eb6fd48e21 | ||
|
edb0bf30d8 | ||
|
36e505952c | ||
|
aa1b627e6f | ||
|
11bc57f2b1 | ||
|
e1a38d42c3 | ||
|
c3cecb3f58 | ||
|
130c96a2fe | ||
|
d4e0c5ca86 | ||
|
67a3c7274e | ||
|
8da03aebb0 | ||
|
a377f45e0b | ||
|
94395ae71a | ||
|
5741866c07 | ||
|
801738fea9 | ||
|
9940d9cc4c | ||
|
56c2b6d9b9 | ||
|
ed287b6ff3 | ||
|
7f854c40dd | ||
|
5f97491f0d | ||
|
b4c1620497 | ||
|
9decd11218 | ||
|
44aca3c2e5 | ||
|
49d1bc2e1b | ||
|
458b9e425e | ||
|
c64baad181 | ||
|
067214b587 | ||
|
21905fd1db | ||
|
0e1a3992eb | ||
|
cbcbbc5277 | ||
|
c535ae4415 | ||
|
7d517e1a7d | ||
|
66bf42cb61 | ||
|
555191009c | ||
|
1cbac6fb1c | ||
|
b417c08bfe | ||
|
db37cb4c40 | ||
|
caf9fc15fd | ||
|
8a73a8ff78 | ||
|
a2243ca668 | ||
|
ad278f58a9 | ||
|
3f72466e3d | ||
|
d6410d5acf | ||
|
34da746408 | ||
|
8bb3812f97 | ||
|
f3bcc25eb0 | ||
|
9a1381c2dc | ||
|
7e4784ea0e | ||
|
32fbdb4530 | ||
|
a74056f101 | ||
|
3542d6ba64 | ||
|
6c64e42322 | ||
|
0b5afc96e1 | ||
|
c6a268114a | ||
|
a5bad300b8 | ||
|
5f80859022 | ||
|
036509c47f | ||
|
ef47461ee9 | ||
|
4401fa7376 | ||
|
41ba0894ac | ||
|
9a89c5a15a | ||
|
345e9985e3 | ||
|
fa21cde2c9 | ||
|
b7c6d45b80 | ||
|
120cc3c06d | ||
|
1f03106766 | ||
|
b03836715f | ||
|
abbe5f15e1 | ||
|
1473118c5a | ||
|
1749e64802 | ||
|
3cd95e9349 | ||
|
944d3a733e | ||
|
accf586413 | ||
|
20a3a61206 | ||
|
00113848c8 | ||
|
f22412b827 | ||
|
9f0213865d | ||
|
2854ac97e9 | ||
|
f11d9b4b53 | ||
|
3ff608e4ff | ||
|
9a825225dd | ||
|
8f046c59c1 | ||
|
97d0239963 | ||
|
0da2098bbe | ||
|
d34fe19754 | ||
|
20a8f29cd6 | ||
|
0d3ebaba47 | ||
|
b8bee7e63d | ||
|
af5bee5147 | ||
|
164a712902 | ||
|
37e5e6a3b3 | ||
|
47d3811aa5 | ||
|
c083ba5a3c | ||
|
532137e54c | ||
|
fdbfa01f6b | ||
|
57136d9652 | ||
|
a22afe0c57 | ||
|
105b7e3c75 | ||
|
c459226604 | ||
|
752d21b500 | ||
|
5de5e278ee | ||
|
807ba554ca | ||
|
68e705404e | ||
|
8f42885482 | ||
|
f9bdc75176 | ||
|
7ac43188a4 | ||
|
dfae9607fe | ||
|
9a889d158c | ||
|
a75cd01fe2 | ||
|
d1e66228fc | ||
|
5bd904bac1 | ||
|
d45bdbc408 | ||
|
d5c492d96b | ||
|
abe5b5eb92 | ||
|
4ceae6d066 | ||
|
36e27321f7 | ||
|
64495be799 | ||
|
220a7e7237 | ||
|
1e88e73082 | ||
|
daee631ef6 | ||
|
9019bcb990 | ||
|
8c7bbd4b39 | ||
|
ec7f5a2047 | ||
|
2116dda86b | ||
|
0b27c9da23 | ||
|
1ca0d51f67 | ||
|
1595063463 | ||
|
a02814621e | ||
|
8cfc58ad91 | ||
|
9070036978 | ||
|
2291d6bbb8 | ||
|
fbd1d865fa | ||
|
f65238ba42 | ||
|
1cbd4dee49 | ||
|
57ded6ff5e | ||
|
b5816bbcd6 | ||
|
1056a470b0 | ||
|
d80ec962a5 | ||
|
966e8c9db5 | ||
|
ad974530d5 | ||
|
167fa1ad66 | ||
|
30fb38f52e | ||
|
7565d6ff01 | ||
|
d4db6bc0be | ||
|
998182f65e | ||
|
bfedef666e | ||
|
a7c555c853 | ||
|
00f2ea2648 | ||
|
da07a9a9a4 | ||
|
9cdcfa54d6 | ||
|
7ef0c47427 | ||
|
f17519fbc4 | ||
|
9f225157d2 | ||
|
2bd0e41653 | ||
|
c454a3ce83 | ||
|
d0511f46c0 | ||
|
57a6233779 | ||
|
9d645841ae | ||
|
ff91a1e5fa | ||
|
9ce925fd6a | ||
|
eb36ed4f79 | ||
|
d4cb680199 | ||
|
d36a15d02d | ||
|
90ea6a7844 | ||
|
ef813f5dbe | ||
|
8952683505 | ||
|
a07b147ea6 | ||
|
c8d4a57270 | ||
|
2b0058ca4c | ||
|
4792134990 | ||
|
56c5b3dd3c | ||
|
1af44ac9fa | ||
|
5200a7c2ad | ||
|
d58ef657e0 | ||
|
79d6397fd7 | ||
|
954e7a15ea | ||
|
b44b6e58bd | ||
|
99c9475ff7 | ||
|
a62df01ecf | ||
|
03cd608b9d | ||
|
533b5aa7c8 | ||
|
580a4019c7 | ||
|
a4b06a354c | ||
|
195450ebc3 | ||
|
bdae79eeda | ||
|
28923f755b | ||
|
f373ecbc84 | ||
|
8b814640ea | ||
|
9b5f0c0969 | ||
|
d2f261acfe | ||
|
ec2c2ccf13 | ||
|
2822f7acaf | ||
|
92e2b45ada | ||
|
d5b21935ae | ||
|
8a6288f8e6 | ||
|
0475ce21aa | ||
|
5d15cb9bad | ||
|
0708eb7bb0 | ||
|
e2d3d11591 | ||
|
cf31735e80 | ||
|
d01f536935 | ||
|
f911345984 | ||
|
82a6f910e3 | ||
|
6b2bd7c550 | ||
|
de25c2045c | ||
|
603b251511 | ||
|
9d42119a09 | ||
|
227946db8c | ||
|
4c4cee1a05 | ||
|
f9207b811b | ||
|
799ff5f8f8 | ||
|
4b558bd656 | ||
|
3f6841932b | ||
|
a9dc97a46d | ||
|
212029e755 | ||
|
56651e0e0c | ||
|
a5df36c476 | ||
|
0f3e5edc8c | ||
|
9569b096cc | ||
|
f656ca69d9 | ||
|
426145db5e | ||
|
3de0d7f745 | ||
|
0a5251f533 | ||
|
7e8906ae7e | ||
|
7b2c4126f9 | ||
|
5e4b289833 | ||
|
b95acb6202 | ||
|
fc852d1bc7 | ||
|
3eda48f3ea | ||
|
2f7274b3f0 | ||
|
0ac620b9bd | ||
|
6bc8b54d94 | ||
|
e2067d4d18 | ||
|
5a4bac2911 | ||
|
f489287707 | ||
|
b629459b46 | ||
|
8e6bcd90af | ||
|
2ee279098f | ||
|
993309d56b | ||
|
5f12afdda9 | ||
|
ec893149d1 | ||
|
e0116f9c54 | ||
|
1e79a53575 | ||
|
4e9cb329d6 | ||
|
262514acee | ||
|
4380117e62 | ||
|
4e7b8daada | ||
|
88943f40fb | ||
|
024e89f2aa | ||
|
66a1f62986 | ||
|
b04370f7ce | ||
|
ef1096c9f3 | ||
|
d736381beb | ||
|
38d5c2cab1 | ||
|
d2405de7dd | ||
|
131adf3cbb | ||
|
542595a1cd | ||
|
d519c41802 | ||
|
8c135a5396 | ||
|
d59828df1c | ||
|
a40e577dd1 | ||
|
c269e34873 | ||
|
a03624b73e | ||
|
92d64c3a87 | ||
|
037feef8f8 | ||
|
51006bc773 | ||
|
b049f6eace | ||
|
41837db827 | ||
|
69d23fded4 | ||
|
e46775301c | ||
|
c3f2930d4a | ||
|
e0ce058989 | ||
|
ca13ee4e8d | ||
|
03455fca2c | ||
|
bd05ba6b78 | ||
|
64a5c73c21 | ||
|
f33dd38269 | ||
|
dfb0921ff7 | ||
|
d3c6c1b4f6 | ||
|
c5c97f0e58 | ||
|
52f1e77fe1 | ||
|
9a208ca108 | ||
|
65eb49a840 | ||
|
840209b2cb | ||
|
08fae0d0dc | ||
|
3791d2e90f | ||
|
15913e51f3 | ||
|
2270730407 | ||
|
dc243dc136 | ||
|
1628e63084 | ||
|
dea24f7f71 | ||
|
2259596ef7 | ||
|
b9d0c9b0f9 | ||
|
d40b599a9c | ||
|
7228146c95 | ||
|
2803ed5907 | ||
|
b9ae053927 | ||
|
2444ca8ecb | ||
|
5fa8249030 | ||
|
563b42031f | ||
|
ed774098db | ||
|
24dbcd8824 | ||
|
7733ce04e5 | ||
|
d085b54f82 | ||
|
cbbe0c4a0a | ||
|
91e3080240 | ||
|
f8d08c477c | ||
|
8bf07d7722 | ||
|
ea9758772d | ||
|
d344282425 | ||
|
bcddc2fb13 | ||
|
e09c28031b | ||
|
5f5e5b654c | ||
|
ceb48b5bbe | ||
|
619388a36a | ||
|
bb78b743dd | ||
|
48adf7e6b8 | ||
|
d42cf64abd | ||
|
a512694976 | ||
|
69692504aa | ||
|
13f1f03462 | ||
|
6c1d0c7672 | ||
|
07d5ad2216 | ||
|
e10a8aca5c | ||
|
8f15fc8f65 | ||
|
cdb7834de5 | ||
|
45ada7995a | ||
|
5f7199669c | ||
|
02e2539606 | ||
|
39586c5d64 | ||
|
46260b6592 | ||
|
7ef27a1412 | ||
|
0650223d10 | ||
|
309b6298e2 | ||
|
6635c3ee99 | ||
|
52cff64981 | ||
|
a9cf695786 | ||
|
fac462d99d | ||
|
6746d5769a | ||
|
62cb2e96d2 | ||
|
6faa39affd | ||
|
e8d5f59267 | ||
|
17637e7543 | ||
|
7d5efa60c7 | ||
|
304fa0e0f3 | ||
|
58e4b16a90 | ||
|
b9fc7bcb06 | ||
|
4bf1c79515 | ||
|
ca8c095e96 | ||
|
ad85942c33 | ||
|
faaf4dac88 | ||
|
5665bb1f34 | ||
|
62a6f9f64c | ||
|
2295ff6290 | ||
|
581c760243 | ||
|
23ee47bb26 | ||
|
51ecaba048 | ||
|
9f384387b9 | ||
|
1d45f31690 | ||
|
5b78ea65e4 | ||
|
a5f6d32977 | ||
|
51707df439 | ||
|
bb58054b77 | ||
|
2520b3ba54 | ||
|
0c8319c138 | ||
|
78ab7b752a | ||
|
52dea1a3bd | ||
|
56a9b5d6a0 | ||
|
9522e17acd | ||
|
465dd08043 | ||
|
36007e86ed | ||
|
793ccb52fd | ||
|
6bd5bb290f | ||
|
bdd30f94b7 | ||
|
e4f46146cc | ||
|
551c9c3503 | ||
|
cf431b8fab | ||
|
fe9b899232 | ||
|
97a56145b2 | ||
|
de875bd384 | ||
|
be83844d28 | ||
|
87ff346d7f | ||
|
813e7d92ce | ||
|
613c002d5a | ||
|
de15d61ebb | ||
|
6bd8b9c3f1 | ||
|
cff62ae98f | ||
|
c3e0136a64 | ||
|
073138986c | ||
|
c368756032 | ||
|
38ada59938 | ||
|
30af55054f | ||
|
4c95aba764 | ||
|
6b476ce2f7 | ||
|
9c6cd01ec2 | ||
|
756023a097 | ||
|
6c9551e089 | ||
|
602c245645 | ||
|
3df9b6ab79 | ||
|
51a8400fa5 | ||
|
756e4749c9 | ||
|
7a74f940fa | ||
|
e35f91ca2d | ||
|
392a39c43e | ||
|
a79f18a748 | ||
|
c2716348f3 | ||
|
f21de0f1e1 | ||
|
ccb78128b0 | ||
|
dbb189444d | ||
|
af8ab310ce | ||
|
181127d632 | ||
|
152913f916 | ||
|
26baaa113b | ||
|
8b0d5e8f47 | ||
|
01c0365208 | ||
|
65dea68be3 | ||
|
76c22c074b | ||
|
5b3238ca55 | ||
|
b6a09c9ebb | ||
|
a5c045986b | ||
|
e3040d99fa | ||
|
e229b53226 | ||
|
a35ea0f6d5 | ||
|
2c39774ff7 | ||
|
bba7a761ac | ||
|
a447cdfcf1 | ||
|
6e72c08ef4 | ||
|
8108967959 | ||
|
d0e4d9512c | ||
|
56982806e4 | ||
|
36511d1a13 | ||
|
03db2db899 | ||
|
7174fc1066 | ||
|
b75a52fe74 | ||
|
f9e9df6056 | ||
|
d86f3d8f3e | ||
|
3ee2f2afb0 | ||
|
710fae5fe3 | ||
|
6bd15ded9c | ||
|
c3d86ae28a | ||
|
187206ba2b | ||
|
fb3ceb51ac | ||
|
b91fdb5f4c | ||
|
affcca39f1 | ||
|
0998f8c782 | ||
|
ff45665bbc | ||
|
d02d195c94 | ||
|
ed5893a097 | ||
|
98f8f52bde | ||
|
7150718a51 | ||
|
f6b16a30ac | ||
|
30417e1cde | ||
|
924f209311 | ||
|
139cb93811 | ||
|
fddde73583 | ||
|
b2950cfcdc | ||
|
149e1b5507 | ||
|
4b0de87f1e | ||
|
ac2fd3dfe0 | ||
|
c86e34fba1 | ||
|
78de6e2b34 | ||
|
e2ef2bdb21 | ||
|
c0ffbb3ada | ||
|
796f135530 | ||
|
58486846c1 | ||
|
16f23b6e01 | ||
|
6c170cccdd | ||
|
409d2e3115 | ||
|
c1f710c9ac | ||
|
2a7a33fe1a | ||
|
7d009c36de | ||
|
091826afdb | ||
|
9ac06bbd51 | ||
|
0146d9a33e | ||
|
b43772c3e5 | ||
|
42eb5115d1 | ||
|
bfadc499b3 | ||
|
d0547ec106 | ||
|
45b844d6c9 | ||
|
cffb76e220 | ||
|
5fc18d31d6 | ||
|
ec3821ec2e | ||
|
edfee1f643 | ||
|
6b83a1ff0d | ||
|
f3bed03d8f | ||
|
0eaffafc73 | ||
|
f768a20b97 | ||
|
75b04140f3 | ||
|
1ec1127f21 | ||
|
2d5c04fde8 | ||
|
8f95b070cd | ||
|
2a0276d0fd | ||
|
122c387420 | ||
|
38368e9641 | ||
|
9af5c40075 | ||
|
152b1bb187 | ||
|
77aaae6735 | ||
|
412945626b | ||
|
1af5d38a0c | ||
|
b82bb88771 | ||
|
b929964ae6 | ||
|
572026aecd | ||
|
b4e922226f | ||
|
f203c25201 | ||
|
898daf8d4a | ||
|
8f15e327b8 | ||
|
30f1c105cb | ||
|
21ca51650c | ||
|
103a4bb69d | ||
|
f34529c3d8 | ||
|
87eb082a18 | ||
|
512bc92116 | ||
|
50bb2c4361 | ||
|
70e74fd619 | ||
|
f4163fd017 | ||
|
4345e289e8 | ||
|
7a9dad71b4 | ||
|
56a9811170 | ||
|
36e0cd797c | ||
|
cf58f8dacb | ||
|
d49a41d56b | ||
|
af74edecbb | ||
|
81a490ca93 | ||
|
7fafe394c5 | ||
|
0ee7de1b46 | ||
|
f001ae4e0a | ||
|
cd9647463b | ||
|
ad8c1c5e43 | ||
|
339a1b8817 | ||
|
8ebcd68865 | ||
|
f3fef31e8d | ||
|
d060259396 | ||
|
855d2b2738 | ||
|
b87cfbb774 | ||
|
a3b5ebbc95 | ||
|
c9d495d53c | ||
|
fbc57f8b33 | ||
|
e584c75991 | ||
|
d03150a396 | ||
|
9d42082b52 | ||
|
43ffca160b | ||
|
046c82e77d | ||
|
995f5bc06b | ||
|
5f40da64c9 | ||
|
7cac4157c6 | ||
|
36abf602e6 | ||
|
dfc12f81f2 | ||
|
2646d12fe2 | ||
|
11e4bc5a10 | ||
|
c44fb6c47a | ||
|
87e8f3d966 | ||
|
9438fa3e14 | ||
|
d0a0f86d46 | ||
|
539a2639ee | ||
|
f36a5497e0 | ||
|
3da6a25867 | ||
|
1258d6dc25 | ||
|
fe5e7198ee | ||
|
8459d3c6e6 | ||
|
fa8c683439 | ||
|
9e336a7bc9 | ||
|
aeab9e8d0e | ||
|
2633db27d8 | ||
|
13b1d54ac3 | ||
|
1c0686e30a | ||
|
979b9862fb | ||
|
071da74b3d | ||
|
67ef6b9111 | ||
|
196e2a9bb0 | ||
|
88c1f6324d | ||
|
e6a4519291 | ||
|
9d9d171209 | ||
|
ba75b5c086 | ||
|
737cd8a068 | ||
|
b6e450260b | ||
|
1f86b3fc07 | ||
|
e832391621 | ||
|
cc6c692cc5 | ||
|
96ef0bfd2b | ||
|
019976d90a | ||
|
fe405dbca4 | ||
|
bddb00050a | ||
|
a7f7aab444 | ||
|
1c47d87b61 | ||
|
66b9240fd4 | ||
|
7f3e22e3bf | ||
|
dddaf69fb7 | ||
|
69f08e84b5 | ||
|
98b8cbe518 | ||
|
2f94879278 | ||
|
6ee4eaf0f0 | ||
|
f208930a3d | ||
|
cef059ea9d | ||
|
57268d65b8 | ||
|
793bcee491 | ||
|
7311a050b0 | ||
|
e3881c7409 | ||
|
69e3303f61 | ||
|
83192b540e | ||
|
f25c389b7f | ||
|
88c9d687d7 | ||
|
f5234dde56 | ||
|
af670f7437 | ||
|
b43850d776 | ||
|
f5c124a2e9 | ||
|
04b9849b40 | ||
|
644d640682 | ||
|
db096c83f8 | ||
|
7f2e3f2594 | ||
|
427e391416 | ||
|
627cf37996 | ||
|
3c6108298c | ||
|
c9f8fbe098 | ||
|
89b74e8ef8 | ||
|
f969dbb290 | ||
|
724aadbe20 | ||
|
3ed4fc7a0e | ||
|
dc8cbc6976 | ||
|
bd703af53e | ||
|
c0dc6f4474 | ||
|
aa8abdec20 | ||
|
d1e5aaa3e1 | ||
|
1ef68e0fdb | ||
|
ab64ab6134 | ||
|
8a4ec11d82 | ||
|
12dc691f20 | ||
|
91ab000d80 | ||
|
ee6aaaf1af | ||
|
4cf77f95d8 | ||
|
a19a795316 | ||
|
0e324a3cce | ||
|
50afd6cd59 | ||
|
ed90cdab57 | ||
|
7c171d59c1 | ||
|
2eaf71c535 | ||
|
b0cbd4aecc | ||
|
0bc355132e | ||
|
d5fdd110b8 | ||
|
c2cc59c452 | ||
|
6270becd4a | ||
|
c69127748a | ||
|
bce34a0c40 | ||
|
289e9ab7db | ||
|
cd5672abf0 | ||
|
df1b721968 | ||
|
2baa17f9dd | ||
|
eb44b6510a | ||
|
fdb71ba3c4 | ||
|
6beba95332 | ||
|
a1fd7caee4 | ||
|
e07fdc318f | ||
|
fd4b78178f | ||
|
3dfd3a5dab | ||
|
0b6031eef7 | ||
|
56a147191d | ||
|
da551060da | ||
|
b523dcd2f6 | ||
|
ad2f4f5952 | ||
|
1efbf4cc91 | ||
|
247e0e4740 | ||
|
d0ac92914d | ||
|
a0c6454933 | ||
|
7128845dcf | ||
|
f7d1db4dd5 | ||
|
819e436af1 | ||
|
0f0dd502c2 | ||
|
81d6a57b26 | ||
|
6e6b247bf9 | ||
|
1802458408 | ||
|
2ee1885880 | ||
|
253693d1ea | ||
|
d26597c9b6 | ||
|
3d2b7b44d5 | ||
|
ba69ec2ffc | ||
|
5e963c6494 | ||
|
b89c127707 | ||
|
8ba39a1f97 | ||
|
a1bcc2cb46 | ||
|
c9091f2e75 | ||
|
76b631366b | ||
|
78b818901e | ||
|
1e35ba864f | ||
|
957f04dcf1 | ||
|
752ba6c605 | ||
|
3fb78e714d | ||
|
9b1724cdad | ||
|
d38342ad43 | ||
|
eaa7be6ec6 | ||
|
9f43e712d0 | ||
|
4ef9e09c6e | ||
|
d1d0c6e1f4 | ||
|
e269a4d63f | ||
|
5c9acd0bf1 | ||
|
22470c2f58 | ||
|
8c846bf414 | ||
|
a05b0f7a73 | ||
|
a2fa61bfd8 | ||
|
5fff6b1510 | ||
|
99fc3d1015 | ||
|
a78f1db5e6 | ||
|
1b3b27f1d9 | ||
|
982c975a20 | ||
|
b8876ba023 | ||
|
b1a52cc872 | ||
|
cae65de1d5 | ||
|
82ec9e098d | ||
|
05ff359f89 | ||
|
cd36b54f56 | ||
|
f79f5a8455 | ||
|
0b2c410fe5 | ||
|
300572d6da | ||
|
ac0b7afc1a | ||
|
088d78d762 | ||
|
cce42995b9 | ||
|
c0e27205ac | ||
|
6047d292dc | ||
|
1533607551 | ||
|
a87fb49918 | ||
|
efee4ad081 | ||
|
b1f91d432c | ||
|
a19bfefa8c | ||
|
e5560cd6e9 | ||
|
e70618ed19 | ||
|
e51129d36f | ||
|
1dfc6d312d | ||
|
f095729cb6 | ||
|
01c19b172a | ||
|
308234166b | ||
|
27d9ea8326 | ||
|
81644a7415 | ||
|
ebe09cf0ad | ||
|
15370d8862 | ||
|
1526b8a0ff | ||
|
8028b9ab55 | ||
|
0b32af57d6 | ||
|
4d9d1f206a | ||
|
6797a8523b | ||
|
7ce3bdda49 | ||
|
ff8f12c134 | ||
|
122cd0fa73 | ||
|
2831b73b83 | ||
|
b46d046b95 | ||
|
432b1ce946 | ||
|
f539a90635 | ||
|
5c6391fb76 | ||
|
e9725d27aa | ||
|
2fe571379c | ||
|
34523ab53f | ||
|
891e2b0d92 | ||
|
30a856dcb9 | ||
|
ce77242008 | ||
|
329a5293dd | ||
|
ca2c74af7c | ||
|
68a53b8749 | ||
|
847fc11bc5 | ||
|
0dfad5b0cd | ||
|
d0e2c7daa9 | ||
|
3bf31ff073 | ||
|
3d3ab23d27 | ||
|
d10173df5d | ||
|
e8ee2ac4aa | ||
|
f50b41188d | ||
|
cb7e4559e4 | ||
|
77d7244ad2 | ||
|
ca3133c17d | ||
|
6db0d0dcfb | ||
|
3bf3754baf | ||
|
bc40d293d6 | ||
|
573fb7a954 | ||
|
2e3a45168e | ||
|
68c6f13926 |
2872 changed files with 118059 additions and 22751 deletions
17
.github/workflows/build.yml
vendored
17
.github/workflows/build.yml
vendored
|
@ -1,17 +0,0 @@
|
||||||
name: build
|
|
||||||
on: [push, pull_request]
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v1
|
|
||||||
- name: Set up Python 3.7
|
|
||||||
uses: actions/setup-python@v1
|
|
||||||
with:
|
|
||||||
python-version: 3.7
|
|
||||||
- name: Install flake8
|
|
||||||
run: pip install flake8 flake8-import-order flake8-future-import flake8-commas flake8-logging-format
|
|
||||||
- name: Lint with flake8
|
|
||||||
run: |
|
|
||||||
flake8 --version
|
|
||||||
flake8
|
|
40
.github/workflows/compilemessages.yml
vendored
40
.github/workflows/compilemessages.yml
vendored
|
@ -1,40 +0,0 @@
|
||||||
name: compilemessages
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- 'locale/**'
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- 'locale/**'
|
|
||||||
jobs:
|
|
||||||
compilemessages:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v1
|
|
||||||
- name: Set up Python 3.7
|
|
||||||
uses: actions/setup-python@v1
|
|
||||||
with:
|
|
||||||
python-version: 3.7
|
|
||||||
- name: Checkout submodules
|
|
||||||
run: |
|
|
||||||
git submodule init
|
|
||||||
git submodule update
|
|
||||||
- name: Install requirements
|
|
||||||
run: |
|
|
||||||
sudo apt-get install gettext
|
|
||||||
pip install -r requirements.txt
|
|
||||||
pip install pymysql
|
|
||||||
- name: Check .po file validity
|
|
||||||
run: |
|
|
||||||
fail=0
|
|
||||||
while read -r file; do
|
|
||||||
if ! msgfmt --check-format "$file"; then
|
|
||||||
fail=$((fail + 1))
|
|
||||||
fi
|
|
||||||
done < <(find locale -name '*.po')
|
|
||||||
exit "$fail"
|
|
||||||
shell: bash
|
|
||||||
- name: Compile messages
|
|
||||||
run: |
|
|
||||||
echo "STATIC_ROOT = '/tmp'" > dmoj/local_settings.py
|
|
||||||
python manage.py compilemessages
|
|
33
.github/workflows/init.yml
vendored
Normal file
33
.github/workflows/init.yml
vendored
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
name: Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Trigger the workflow on push or pull request,
|
||||||
|
# but only for the main branch
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-linters:
|
||||||
|
name: Run linters
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out Git repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: 3.8
|
||||||
|
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: pip install black
|
||||||
|
|
||||||
|
- name: Run linters
|
||||||
|
uses: wearerequired/lint-action@v2
|
||||||
|
with:
|
||||||
|
black: true
|
53
.github/workflows/makemessages.yml
vendored
53
.github/workflows/makemessages.yml
vendored
|
@ -1,53 +0,0 @@
|
||||||
name: makemessages
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
jobs:
|
|
||||||
makemessages:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v1
|
|
||||||
- name: Set up Python 3.7
|
|
||||||
uses: actions/setup-python@v1
|
|
||||||
with:
|
|
||||||
python-version: 3.7
|
|
||||||
- name: Checkout submodules
|
|
||||||
run: |
|
|
||||||
git submodule init
|
|
||||||
git submodule update
|
|
||||||
- name: Install requirements
|
|
||||||
run: |
|
|
||||||
sudo apt-get install gettext
|
|
||||||
curl -O https://artifacts.crowdin.com/repo/deb/crowdin.deb
|
|
||||||
sudo dpkg -i crowdin.deb
|
|
||||||
pip install -r requirements.txt
|
|
||||||
pip install pymysql
|
|
||||||
- name: Collect localizable strings
|
|
||||||
run: |
|
|
||||||
echo "STATIC_ROOT = '/tmp'" > dmoj/local_settings.py
|
|
||||||
python manage.py makemessages -l en -e py,html,txt
|
|
||||||
python manage.py makemessages -l en -d djangojs
|
|
||||||
- name: Upload strings to Crowdin
|
|
||||||
env:
|
|
||||||
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
|
|
||||||
run: |
|
|
||||||
cat > crowdin.yaml <<EOF
|
|
||||||
project_identifier: dmoj
|
|
||||||
|
|
||||||
files:
|
|
||||||
- source: /locale/en/LC_MESSAGES/django.po
|
|
||||||
translation: /locale/%two_letters_code%/LC_MESSAGES/django.po
|
|
||||||
languages_mapping:
|
|
||||||
two_letters_code:
|
|
||||||
zh-CN: zh_Hans
|
|
||||||
sr-CS: sr_Latn
|
|
||||||
- source: /locale/en/LC_MESSAGES/djangojs.po
|
|
||||||
translation: /locale/%two_letters_code%/LC_MESSAGES/djangojs.po
|
|
||||||
languages_mapping:
|
|
||||||
two_letters_code:
|
|
||||||
zh-CN: zh_Hans
|
|
||||||
sr-CS: sr_Latn
|
|
||||||
EOF
|
|
||||||
echo "api_key: ${CROWDIN_API_TOKEN}" >> crowdin.yaml
|
|
||||||
crowdin upload sources
|
|
67
.github/workflows/updatemessages.yml
vendored
67
.github/workflows/updatemessages.yml
vendored
|
@ -1,67 +0,0 @@
|
||||||
name: updatemessages
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 * * * *'
|
|
||||||
jobs:
|
|
||||||
updatemessages:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v1
|
|
||||||
- name: Set up Python 3.7
|
|
||||||
uses: actions/setup-python@v1
|
|
||||||
with:
|
|
||||||
python-version: 3.7
|
|
||||||
- name: Checkout submodules
|
|
||||||
run: |
|
|
||||||
git submodule init
|
|
||||||
git submodule update
|
|
||||||
- name: Install requirements
|
|
||||||
run: |
|
|
||||||
sudo apt-get install gettext
|
|
||||||
curl -O https://artifacts.crowdin.com/repo/deb/crowdin.deb
|
|
||||||
sudo dpkg -i crowdin.deb
|
|
||||||
pip install -r requirements.txt
|
|
||||||
pip install pymysql
|
|
||||||
- name: Download strings from Crowdin
|
|
||||||
env:
|
|
||||||
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
|
|
||||||
run: |
|
|
||||||
cat > crowdin.yaml <<EOF
|
|
||||||
project_identifier: dmoj
|
|
||||||
|
|
||||||
files:
|
|
||||||
- source: /locale/en/LC_MESSAGES/django.po
|
|
||||||
translation: /locale/%two_letters_code%/LC_MESSAGES/django.po
|
|
||||||
languages_mapping:
|
|
||||||
two_letters_code:
|
|
||||||
zh-CN: zh_Hans
|
|
||||||
zh-TW: zh_Hant
|
|
||||||
sr-CS: sr_Latn
|
|
||||||
- source: /locale/en/LC_MESSAGES/djangojs.po
|
|
||||||
translation: /locale/%two_letters_code%/LC_MESSAGES/djangojs.po
|
|
||||||
languages_mapping:
|
|
||||||
two_letters_code:
|
|
||||||
zh-CN: zh_Hans
|
|
||||||
zh-TW: zh_Hant
|
|
||||||
sr-CS: sr_Latn
|
|
||||||
EOF
|
|
||||||
echo "api_key: ${CROWDIN_API_TOKEN}" >> crowdin.yaml
|
|
||||||
crowdin download
|
|
||||||
rm crowdin.yaml
|
|
||||||
- name: Cleanup
|
|
||||||
run: |
|
|
||||||
rm -rf src/
|
|
||||||
git add locale
|
|
||||||
git checkout .
|
|
||||||
git clean -fd
|
|
||||||
- name: Create pull request
|
|
||||||
uses: peter-evans/create-pull-request@v1.4.1-multi
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.REPO_SCOPED_TOKEN }}
|
|
||||||
COMMIT_MESSAGE: 'i18n: update translations from Crowdin'
|
|
||||||
PULL_REQUEST_TITLE: 'Update translations from Crowdin'
|
|
||||||
PULL_REQUEST_BODY: This PR has been auto-generated to pull in latest translations from [Crowdin](https://translate.dmoj.ca).
|
|
||||||
PULL_REQUEST_LABELS: i18n, enhancement
|
|
||||||
PULL_REQUEST_REVIEWERS: Xyene, quantum5
|
|
||||||
PULL_REQUEST_BRANCH: update-i18n
|
|
||||||
BRANCH_SUFFIX: none
|
|
8
.gitmodules
vendored
8
.gitmodules
vendored
|
@ -1,8 +0,0 @@
|
||||||
[submodule "resources/pagedown"]
|
|
||||||
path = resources/pagedown
|
|
||||||
url = https://github.com/DMOJ/dmoj-pagedown.git
|
|
||||||
branch = master
|
|
||||||
[submodule "resources/libs"]
|
|
||||||
path = resources/libs
|
|
||||||
url = https://github.com/DMOJ/site-assets.git
|
|
||||||
branch = master
|
|
17
.pre-commit-config.yaml
Normal file
17
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/rtts/djhtml
|
||||||
|
rev: 'v1.5.2' # replace with the latest tag on GitHub
|
||||||
|
hooks:
|
||||||
|
- id: djhtml
|
||||||
|
entry: djhtml -i -t 2
|
||||||
|
files: templates/.
|
||||||
|
- id: djcss
|
||||||
|
types: [scss]
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 22.12.0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
- repo: https://github.com/hadialqattan/pycln
|
||||||
|
rev: 'v2.3.0'
|
||||||
|
hooks:
|
||||||
|
- id: pycln
|
2
502.html
2
502.html
|
@ -49,7 +49,7 @@
|
||||||
<br>
|
<br>
|
||||||
<div class="popup">
|
<div class="popup">
|
||||||
<div>
|
<div>
|
||||||
<img class="logo" src="logo.png" alt="LQDOJ">
|
<img class="logo" src="logo.svg" alt="LQDOJ">
|
||||||
</div>
|
</div>
|
||||||
<h1 style="width: 100%;">Oops, LQDOJ is down now.</h1>
|
<h1 style="width: 100%;">Oops, LQDOJ is down now.</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
232
README.md
232
README.md
|
@ -17,11 +17,12 @@ Supported languages:
|
||||||
- Assembly (x64)
|
- Assembly (x64)
|
||||||
- AWK
|
- AWK
|
||||||
- C
|
- C
|
||||||
- C++03 / C++11 / C++14 / C++17
|
- C++03 / C++11 / C++14 / C++17 / C++20
|
||||||
- Java 11
|
- Java 11
|
||||||
- Pascal
|
- Pascal
|
||||||
- Perl
|
- Perl
|
||||||
- Python 2 / Python 3
|
- Python 2 / Python 3
|
||||||
|
- PyPy 2 / PyPy 3
|
||||||
|
|
||||||
Support plagiarism detection via [Stanford MOSS](https://theory.stanford.edu/~aiken/moss/).
|
Support plagiarism detection via [Stanford MOSS](https://theory.stanford.edu/~aiken/moss/).
|
||||||
|
|
||||||
|
@ -30,10 +31,196 @@ Support plagiarism detection via [Stanford MOSS](https://theory.stanford.edu/~ai
|
||||||
Most of the setup are the same as DMOJ installations. You can view the installation guide of DMOJ here: https://docs.dmoj.ca/#/site/installation.
|
Most of the setup are the same as DMOJ installations. You can view the installation guide of DMOJ here: https://docs.dmoj.ca/#/site/installation.
|
||||||
There is one minor change: Instead of `git clone https://github.com/DMOJ/site.git`, you clone this repo `git clone https://github.com/LQDJudge/online-judge.git`.
|
There is one minor change: Instead of `git clone https://github.com/DMOJ/site.git`, you clone this repo `git clone https://github.com/LQDJudge/online-judge.git`.
|
||||||
|
|
||||||
### Additional Steps in Production:
|
|
||||||
|
|
||||||
1. To use newsletter (email sending), go to admin and create a newsletter.
|
- Bước 1: cài các thư viện cần thiết
|
||||||
2. Change the domain name and website name in Admin page: Navigation Bars/Sites
|
- $ ở đây nghĩa là sudo. Ví dụ dòng đầu nghĩa là chạy lệnh `sudo apt update`
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
$ apt update
|
||||||
|
$ apt install git gcc g++ make python3-dev python3-pip libxml2-dev libxslt1-dev zlib1g-dev gettext curl redis-server
|
||||||
|
$ curl -sL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
||||||
|
$ apt install nodejs
|
||||||
|
$ npm install -g sass postcss-cli postcss autoprefixer
|
||||||
|
```
|
||||||
|
|
||||||
|
- Bước 2: tạo DB
|
||||||
|
- Server đang dùng MariaDB ≥ 10.5, các bạn cũng có thể dùng Mysql nếu bị conflict
|
||||||
|
- Nếu các bạn chạy lệnh dưới này xong mà version mariadb bị cũ (< 10.5) thì có thể tra google cách cài MariaDB mới nhất (10.5 hoặc 10.6).
|
||||||
|
- Các bạn có thể thấy version MariaDB bằng cách gõ lệnh `sudo mysql` (Ctrl + C để quit)
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
$ apt update
|
||||||
|
$ apt install mariadb-server libmysqlclient-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- Bước 3: tạo table trong DB
|
||||||
|
- Các bạn có thể thay tên table và password
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
$ sudo mysql
|
||||||
|
mariadb> CREATE DATABASE dmoj DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;
|
||||||
|
mariadb> GRANT ALL PRIVILEGES ON dmoj.* TO 'dmoj'@'localhost' IDENTIFIED BY '<password>';
|
||||||
|
mariadb> exit
|
||||||
|
```
|
||||||
|
|
||||||
|
- Bước 4: Cài đặt môi trường ảo (virtual env) và pull code
|
||||||
|
- Nếu `pip3 install mysqlclient` bị lỗi thì thử chạy `pip3 install mysqlclient==2.1.1`
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
$ python3 -m venv dmojsite
|
||||||
|
$ . dmojsite/bin/activate
|
||||||
|
|
||||||
|
(dmojsite) $ git clone https://github.com/LQDJudge/online-judge.git
|
||||||
|
(dmojsite) $ cd online-judge
|
||||||
|
(dmojsite) $ git submodule init
|
||||||
|
(dmojsite) $ git submodule update
|
||||||
|
(dmojsite) $ pip3 install -r requirements.txt
|
||||||
|
(dmojsite) $ pip3 install mysqlclient
|
||||||
|
(dmojsite) $ pre-commit install
|
||||||
|
```
|
||||||
|
|
||||||
|
- Bước 5: tạo local_settings.py. Đây là file để custom setting cho Django. Các bạn tạo file vào `online-judge/dmoj/local_settings.py`
|
||||||
|
- File mẫu: https://github.com/DMOJ/docs/blob/master/sample_files/local_settings.py
|
||||||
|
- Nếu bạn đổi tên hoặc mật khẩu table databases thì thay đổi thông tin tương ứng trong `Databases`
|
||||||
|
- Sau khi xong, chạy lệnh `(dmojsite) $ python3 manage.py check` để kiểm tra
|
||||||
|
- Bước 6: Compile CSS và translation
|
||||||
|
- Giải thích:
|
||||||
|
- Lệnh 1 và 2 gọi sau mỗi lần thay đổi 1 file css hoặc file js (file html thì không cần)
|
||||||
|
- Lệnh 3 và 4 gọi sau mỗi lần thay đổi file dịch
|
||||||
|
- Note: Sau khi chạy lệnh này, folder tương ứng với STATIC_ROOT trong local_settings phải được tạo. Nếu chưa được tạo thì mình cần tạo folder đó trước khi chạy 2 lệnh đầu.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
(dmojsite) $ ./make_style.sh
|
||||||
|
(dmojsite) $ python3 manage.py collectstatic
|
||||||
|
(dmojsite) $ python3 manage.py compilemessages
|
||||||
|
(dmojsite) $ python3 manage.py compilejsi18n
|
||||||
|
```
|
||||||
|
|
||||||
|
- Bước 7: Thêm dữ liệu vào DB
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
(dmojsite) $ python3 manage.py migrate
|
||||||
|
(dmojsite) $ python3 manage.py loaddata navbar
|
||||||
|
(dmojsite) $ python3 manage.py loaddata language_small
|
||||||
|
(dmojsite) $ python3 manage.py loaddata demo
|
||||||
|
```
|
||||||
|
|
||||||
|
- Bước 8: Chạy site. Đến đây thì cơ bản đã hoàn thành (chưa có judge, websocket, celery). Các bạn có thể truy cập tại `localhost:8000`
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
python3 manage.py runserver 0.0.0.0:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Một số lưu ý:**
|
||||||
|
|
||||||
|
1. (WSL) có thể tải ứng dụng Terminal trong Windows Store
|
||||||
|
2. (WSL) mỗi lần mở ubuntu, các bạn cần chạy lệnh sau để mariadb khởi động: `sudo service mysql restart` (tương tự cho một số service khác như memcached, celery)
|
||||||
|
3. Sau khi cài đặt, các bạn chỉ cần activate virtual env và chạy lệnh runserver là ok
|
||||||
|
```jsx
|
||||||
|
. dmojsite/bin/activate
|
||||||
|
python3 manage.py runserver
|
||||||
|
```
|
||||||
|
5. Đối với nginx, sau khi config xong theo guide của DMOJ, bạn cần thêm location như sau để sử dụng được tính năng profile image, thay thế `path/to/oj` thành đường dẫn nơi bạn đã clone source code.
|
||||||
|
|
||||||
|
```
|
||||||
|
location /profile_images/ {
|
||||||
|
root /path/to/oj;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Quy trình dev:
|
||||||
|
1. Sau khi thay đổi code thì django tự build lại, các bạn chỉ cần F5
|
||||||
|
2. Một số style nằm trong các file .scss. Các bạn cần recompile css thì mới thấy được thay đổi.
|
||||||
|
|
||||||
|
**Optional:**
|
||||||
|
|
||||||
|
************Alias:************ Các bạn có thể lưu các alias này để sau này dùng cho nhanh
|
||||||
|
|
||||||
|
- mtrans: để generate translation khi các bạn add một string trong code
|
||||||
|
- trans: compile translation (sau khi bạn đã dịch tiếng Việt)
|
||||||
|
- cr: chuyển tới folder OJ
|
||||||
|
- pr: chạy server
|
||||||
|
- sm: restart service (chủ yếu dùng cho WSL)
|
||||||
|
- sd: activate virtual env
|
||||||
|
- css: compile các file css
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
alias mtrans='python3 manage.py makemessages -l vi && python3 manage.py makedmojmessages -l vi'
|
||||||
|
alias pr='python3 manage.py runserver'
|
||||||
|
alias sd='source ~/LQDOJ/dmojsite/bin/activate'
|
||||||
|
alias sm='sudo service mysql restart && sudo service redis-server start && sudo service memcached start'
|
||||||
|
alias trans='python3 manage.py compilemessages -l vi && python3 manage.py compilejsi18n -l vi'
|
||||||
|
alias cr='cd ~/LQDOJ/online-judge'
|
||||||
|
alias css='./make_style.sh && python3 manage.py collectstatic --noinput'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Memcached:** dùng cho in-memory cache
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
$ sudo apt install memcached
|
||||||
|
```
|
||||||
|
|
||||||
|
**Websocket:** dùng để live update (như chat)
|
||||||
|
|
||||||
|
- Tạo file online-judge/websocket/config.js
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
module.exports = {
|
||||||
|
get_host: '127.0.0.1',
|
||||||
|
get_port: 15100,
|
||||||
|
post_host: '127.0.0.1',
|
||||||
|
post_port: 15101,
|
||||||
|
http_host: '127.0.0.1',
|
||||||
|
http_port: 15102,
|
||||||
|
long_poll_timeout: 29000,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- Cài các thư viện
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
(dmojsite) $ npm install qu ws simplesets
|
||||||
|
(dmojsite) $ pip3 install websocket-client
|
||||||
|
```
|
||||||
|
|
||||||
|
- Khởi động (trong 1 tab riêng)
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
(dmojsite) $ node websocket/daemon.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**************Celery:************** (dùng cho một số task như batch rejudge_
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
celery -A dmoj_celery worker
|
||||||
|
```
|
||||||
|
|
||||||
|
**************Judge:**************
|
||||||
|
|
||||||
|
- Cài đặt ở 1 folder riêng bên ngoài site:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
$ apt install python3-dev python3-pip build-essential libseccomp-dev
|
||||||
|
$ git clone https://github.com/LQDJudge/judge-server.git
|
||||||
|
$ cd judge-server
|
||||||
|
$ sudo pip3 install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
- Tạo một file judge.yml ở bên ngoài folder judge-server (file mẫu https://github.com/DMOJ/docs/blob/master/sample_files/judge_conf.yml)
|
||||||
|
- Thêm judge vào site bằng UI: Admin → Judge → Thêm Judge → nhập id và key (chỉ cần thêm 1 lần) hoặc dùng lệnh `(dmojsite) $ python3 managed.py addjudge <id> <key>`.
|
||||||
|
- Chạy Bridge (cầu nối giữa judge và site) trong 1 tab riêng trong folder online-judge:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
(dmojsite) $ python3 managed.py runbridged
|
||||||
|
```
|
||||||
|
|
||||||
|
- Khởi động Judge (trong 1 tab riêng):
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
$ dmoj -c judge.yml localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
- Lưu ý: mỗi lần sau này muốn chạy judge thì mở 1 tab cho bridge và n tab cho judge. Mỗi judge cần 1 file yml khác nhau (chứa authentication khác nhau)
|
||||||
|
|
||||||
### Some frequent difficulties when installation:
|
### Some frequent difficulties when installation:
|
||||||
|
|
||||||
|
@ -41,6 +228,21 @@ There is one minor change: Instead of `git clone https://github.com/DMOJ/site.gi
|
||||||
2. Missing the problem folder in `local_settings.py`. You need to create a folder to contain all problem packages and configure in `local_settings.py`.
|
2. Missing the problem folder in `local_settings.py`. You need to create a folder to contain all problem packages and configure in `local_settings.py`.
|
||||||
3. Missing static folder in `local_settings.py`. Similar to problem folder, make sure to configure `STATIC_FILES` inside `local_settings.py`.
|
3. Missing static folder in `local_settings.py`. Similar to problem folder, make sure to configure `STATIC_FILES` inside `local_settings.py`.
|
||||||
4. Missing configure file for judges. Each judge must have a seperate configure file. To create this file, you can run `python dmojauto-conf`. Checkout all sample files here https://github.com/DMOJ/docs/blob/master/sample_files.
|
4. Missing configure file for judges. Each judge must have a seperate configure file. To create this file, you can run `python dmojauto-conf`. Checkout all sample files here https://github.com/DMOJ/docs/blob/master/sample_files.
|
||||||
|
5. Missing timezone data for SQL. If you're using Ubuntu and you're following DMOJ's installation guide for the server, and you are getting the error mentioned in https://github.com/LQDJudge/online-judge/issues/45, then you can follow this method to fix:
|
||||||
|
```
|
||||||
|
mysql
|
||||||
|
-- You may have to do this if you haven't set root password for MySQL, replace mypass with your password
|
||||||
|
-- SET PASSWORD FOR 'root'@'localhost' = PASSWORD('mypass');
|
||||||
|
-- FLUSH PRIVILEGES;
|
||||||
|
exit
|
||||||
|
mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -D mysql -u root -p
|
||||||
|
mysql -u root -p -e "flush tables;" mysql
|
||||||
|
```
|
||||||
|
6. Missing the chat secret key, you must generate a Fernet key, and assign a variable in `local_settings.py` like this
|
||||||
|
```python
|
||||||
|
CHAT_SECRET_KEY = "81HqDtbqAywKSOumSxxxxxxxxxxxxxxxxx="
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
@ -53,12 +255,12 @@ source dmojsite/bin/activate
|
||||||
|
|
||||||
2. Run server:
|
2. Run server:
|
||||||
```bash
|
```bash
|
||||||
python manage.py runserver 0.0.0.0:8000
|
python3 manage.py runserver 0.0.0.0:8000
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Create a bridge (this is opened in a different terminal with the second step if you are using the same machine)
|
3. Create a bridge (this is opened in a different terminal with the second step if you are using the same machine)
|
||||||
```bash
|
```bash
|
||||||
python manage.py runbridged
|
python3 manage.py runbridged
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Create a judge (another terminal)
|
4. Create a judge (another terminal)
|
||||||
|
@ -79,20 +281,22 @@ celery -A dmoj_celery worker
|
||||||
node websocket/daemon.js
|
node websocket/daemon.js
|
||||||
```
|
```
|
||||||
|
|
||||||
|
7. To use subdomain for each organization, go to admin page -> navigation bar -> sites, add domain name (e.g, "localhost:8000"). Then go to add `USE_SUBDOMAIN = True` to local_settings.py.
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
Most of the steps are similar to Django tutorials. Here are two usual steps:
|
Most of the steps are similar to Django tutorials. Here are two usual steps:
|
||||||
|
|
||||||
1. Update vietnamese translation:
|
1. Update vietnamese translation:
|
||||||
- If you add any new phrases in the code, ```python manage.py makemessages```
|
- If you add any new phrases in the code, ```python3 manage.py makemessages```
|
||||||
- go to `locale/vi`
|
- go to `locale/vi`
|
||||||
- modify `.po` file
|
- modify `.po` file
|
||||||
- ```python manage.py compilemessages```
|
- ```python3 manage.py compilemessages```
|
||||||
- ```python manage.py compilejsi18n```
|
- ```python3 manage.py compilejsi18n```
|
||||||
|
|
||||||
2. Update styles (using SASS)
|
2. Update styles (using SASS)
|
||||||
- Change .css/.scss files in `resources` folder
|
- Change .css/.scss files in `resources` folder
|
||||||
- ```./make_style && python manage.py collectstatic```
|
- ```./make_style.sh && python3 manage.py collectstatic```
|
||||||
- Sometimes you need to `Ctrl + F5` to see the new user interface in browser.
|
- Sometimes you need to press `Ctrl + F5` to see the new user interface in browser.
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
@ -100,7 +304,8 @@ Most of the steps are similar to Django tutorials. Here are two usual steps:
|
||||||
|
|
||||||
Leaderboard with information about contest rating, performance points and real name of all users.
|
Leaderboard with information about contest rating, performance points and real name of all users.
|
||||||
|
|
||||||
![](https://i.imgur.com/ampxHXM.png)
|
![](https://raw.githubusercontent.com/emladevops/LQDOJ-image/main/brave_SK67WA26FA.png#gh-light-mode-only)
|
||||||
|
![](https://raw.githubusercontent.com/emladevops/LQDOJ-image/main/brave_cmqqCnwaFc.png#gh-dark-mode-only)
|
||||||
|
|
||||||
### Admin dashboard
|
### Admin dashboard
|
||||||
|
|
||||||
|
@ -118,4 +323,5 @@ You can write the problems' statement in Markdown with LaTeX figures and formula
|
||||||
|
|
||||||
Users can communicate with each other and can see who's online.
|
Users can communicate with each other and can see who's online.
|
||||||
|
|
||||||
![](https://i.imgur.com/y9SGCgl.png)
|
![](https://raw.githubusercontent.com/emladevops/LQDOJ-image/main/brave_kPsC5bJluc.png#gh-light-mode-only)
|
||||||
|
![](https://raw.githubusercontent.com/emladevops/LQDOJ-image/main/brave_AtrEzXzEAx.png#gh-dark-mode-only)
|
||||||
|
|
|
@ -2,4 +2,7 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class ChatBoxConfig(AppConfig):
|
class ChatBoxConfig(AppConfig):
|
||||||
name = 'chat_box'
|
name = "chat_box"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from . import models
|
||||||
|
|
|
@ -9,22 +9,43 @@ class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('judge', '0100_auto_20200127_0059'),
|
("judge", "0100_auto_20200127_0059"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Message',
|
name="Message",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('time', models.DateTimeField(auto_now_add=True, verbose_name='posted time')),
|
"id",
|
||||||
('body', models.TextField(max_length=8192, verbose_name='body of comment')),
|
models.AutoField(
|
||||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Profile', verbose_name='user')),
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"time",
|
||||||
|
models.DateTimeField(auto_now_add=True, verbose_name="posted time"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"body",
|
||||||
|
models.TextField(max_length=8192, verbose_name="body of comment"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"author",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="judge.Profile",
|
||||||
|
verbose_name="user",
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'message',
|
"verbose_name": "message",
|
||||||
'verbose_name_plural': 'messages',
|
"verbose_name_plural": "messages",
|
||||||
'ordering': ('-time',),
|
"ordering": ("-time",),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('chat_box', '0001_initial'),
|
("chat_box", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='message',
|
model_name="message",
|
||||||
name='hidden',
|
name="hidden",
|
||||||
field=models.BooleanField(default=False, verbose_name='is hidden'),
|
field=models.BooleanField(default=False, verbose_name="is hidden"),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('chat_box', '0002_message_hidden'),
|
("chat_box", "0002_message_hidden"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='message',
|
model_name="message",
|
||||||
name='hidden',
|
name="hidden",
|
||||||
field=models.BooleanField(default=True, verbose_name='is hidden'),
|
field=models.BooleanField(default=True, verbose_name="is hidden"),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('chat_box', '0003_auto_20200505_2306'),
|
("chat_box", "0003_auto_20200505_2306"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='message',
|
model_name="message",
|
||||||
name='hidden',
|
name="hidden",
|
||||||
field=models.BooleanField(default=False, verbose_name='is hidden'),
|
field=models.BooleanField(default=False, verbose_name="is hidden"),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,22 +7,52 @@ import django.db.models.deletion
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('judge', '0116_auto_20211011_0645'),
|
("judge", "0116_auto_20211011_0645"),
|
||||||
('chat_box', '0004_auto_20200505_2336'),
|
("chat_box", "0004_auto_20200505_2336"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Room',
|
name="Room",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('user_one', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_one', to='judge.Profile', verbose_name='user 1')),
|
"id",
|
||||||
('user_two', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_two', to='judge.Profile', verbose_name='user 2')),
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user_one",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="user_one",
|
||||||
|
to="judge.Profile",
|
||||||
|
verbose_name="user 1",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user_two",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="user_two",
|
||||||
|
to="judge.Profile",
|
||||||
|
verbose_name="user 2",
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='message',
|
model_name="message",
|
||||||
name='room',
|
name="room",
|
||||||
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='chat_box.Room', verbose_name='room id'),
|
field=models.ForeignKey(
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="chat_box.Room",
|
||||||
|
verbose_name="room id",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,18 +7,42 @@ import django.db.models.deletion
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('judge', '0116_auto_20211011_0645'),
|
("judge", "0116_auto_20211011_0645"),
|
||||||
('chat_box', '0005_auto_20211011_0714'),
|
("chat_box", "0005_auto_20211011_0714"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='UserRoom',
|
name="UserRoom",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('last_seen', models.DateTimeField(verbose_name='last seen')),
|
"id",
|
||||||
('room', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='chat_box.Room', verbose_name='room id')),
|
models.AutoField(
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Profile', verbose_name='user')),
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("last_seen", models.DateTimeField(verbose_name="last seen")),
|
||||||
|
(
|
||||||
|
"room",
|
||||||
|
models.ForeignKey(
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="chat_box.Room",
|
||||||
|
verbose_name="room id",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="judge.Profile",
|
||||||
|
verbose_name="user",
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('chat_box', '0006_userroom'),
|
("chat_box", "0006_userroom"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='userroom',
|
model_name="userroom",
|
||||||
name='last_seen',
|
name="last_seen",
|
||||||
field=models.DateTimeField(auto_now_add=True, verbose_name='last seen'),
|
field=models.DateTimeField(auto_now_add=True, verbose_name="last seen"),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,17 +7,33 @@ import django.db.models.deletion
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('judge', '0116_auto_20211011_0645'),
|
("judge", "0116_auto_20211011_0645"),
|
||||||
('chat_box', '0007_auto_20211112_1255'),
|
("chat_box", "0007_auto_20211112_1255"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Ignore',
|
name="Ignore",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('ignored_users', models.ManyToManyField(to='judge.Profile')),
|
"id",
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ignored_chat_users', to='judge.Profile', verbose_name='user')),
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("ignored_users", models.ManyToManyField(to="judge.Profile")),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="ignored_chat_users",
|
||||||
|
to="judge.Profile",
|
||||||
|
verbose_name="user",
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
24
chat_box/migrations/0009_auto_20220618_1452.py
Normal file
24
chat_box/migrations/0009_auto_20220618_1452.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 2.2.25 on 2022-06-18 07:52
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("chat_box", "0008_ignore"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="ignore",
|
||||||
|
name="user",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="ignored_chat_users",
|
||||||
|
to="judge.Profile",
|
||||||
|
verbose_name="user",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
18
chat_box/migrations/0010_auto_20221028_0300.py
Normal file
18
chat_box/migrations/0010_auto_20221028_0300.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.25 on 2022-10-27 20:00
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0135_auto_20221028_0300"),
|
||||||
|
("chat_box", "0009_auto_20220618_1452"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="userroom",
|
||||||
|
unique_together={("user", "room")},
|
||||||
|
),
|
||||||
|
]
|
20
chat_box/migrations/0011_alter_message_hidden.py
Normal file
20
chat_box/migrations/0011_alter_message_hidden.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-02-18 21:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("chat_box", "0010_auto_20221028_0300"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="message",
|
||||||
|
name="hidden",
|
||||||
|
field=models.BooleanField(
|
||||||
|
db_index=True, default=False, verbose_name="is hidden"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
34
chat_box/migrations/0012_auto_20230308_1417.py
Normal file
34
chat_box/migrations/0012_auto_20230308_1417.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-03-08 07:17
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0154_add_submission_indexes"),
|
||||||
|
("chat_box", "0011_alter_message_hidden"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="message",
|
||||||
|
options={
|
||||||
|
"ordering": ("-id",),
|
||||||
|
"verbose_name": "message",
|
||||||
|
"verbose_name_plural": "messages",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="message",
|
||||||
|
name="hidden",
|
||||||
|
field=models.BooleanField(default=False, verbose_name="is hidden"),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="message",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["hidden", "room", "-id"], name="chat_box_me_hidden_b2307a_idx"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
20
chat_box/migrations/0013_alter_message_time.py
Normal file
20
chat_box/migrations/0013_alter_message_time.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-08-28 01:24
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("chat_box", "0012_auto_20230308_1417"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="message",
|
||||||
|
name="time",
|
||||||
|
field=models.DateTimeField(
|
||||||
|
auto_now_add=True, db_index=True, verbose_name="posted time"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
38
chat_box/migrations/0014_userroom_unread_count.py
Normal file
38
chat_box/migrations/0014_userroom_unread_count.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-08-28 06:02
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(apps, schema_editor):
|
||||||
|
UserRoom = apps.get_model("chat_box", "UserRoom")
|
||||||
|
Message = apps.get_model("chat_box", "Message")
|
||||||
|
|
||||||
|
for ur in UserRoom.objects.all():
|
||||||
|
if not ur.room:
|
||||||
|
continue
|
||||||
|
messages = ur.room.message_set
|
||||||
|
last_msg = messages.first()
|
||||||
|
try:
|
||||||
|
if last_msg and last_msg.author != ur.user:
|
||||||
|
ur.unread_count = messages.filter(time__gte=ur.last_seen).count()
|
||||||
|
else:
|
||||||
|
ur.unread_count = 0
|
||||||
|
ur.save()
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("chat_box", "0013_alter_message_time"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="userroom",
|
||||||
|
name="unread_count",
|
||||||
|
field=models.IntegerField(db_index=True, default=0),
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate, migrations.RunPython.noop, atomic=True),
|
||||||
|
]
|
33
chat_box/migrations/0015_room_last_msg_time.py
Normal file
33
chat_box/migrations/0015_room_last_msg_time.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-11-02 01:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(apps, schema_editor):
|
||||||
|
Room = apps.get_model("chat_box", "Room")
|
||||||
|
Message = apps.get_model("chat_box", "Message")
|
||||||
|
|
||||||
|
for room in Room.objects.all():
|
||||||
|
messages = room.message_set
|
||||||
|
last_msg = messages.first()
|
||||||
|
if last_msg:
|
||||||
|
room.last_msg_time = last_msg.time
|
||||||
|
room.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("chat_box", "0014_userroom_unread_count"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="room",
|
||||||
|
name="last_msg_time",
|
||||||
|
field=models.DateTimeField(
|
||||||
|
db_index=True, null=True, verbose_name="last seen"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate, migrations.RunPython.noop, atomic=True),
|
||||||
|
]
|
32
chat_box/migrations/0016_alter_room_unique_together.py
Normal file
32
chat_box/migrations/0016_alter_room_unique_together.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# Generated by Django 3.2.18 on 2024-08-22 03:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def remove_duplicates(apps, schema_editor):
|
||||||
|
Room = apps.get_model("chat_box", "Room")
|
||||||
|
seen = set()
|
||||||
|
|
||||||
|
for room in Room.objects.all():
|
||||||
|
pair = (room.user_one_id, room.user_two_id)
|
||||||
|
reverse_pair = (room.user_two_id, room.user_one_id)
|
||||||
|
|
||||||
|
if pair in seen or reverse_pair in seen:
|
||||||
|
room.delete()
|
||||||
|
else:
|
||||||
|
seen.add(pair)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("chat_box", "0015_room_last_msg_time"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(remove_duplicates),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="room",
|
||||||
|
unique_together={("user_one", "user_two")},
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,57 +1,113 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import CASCADE
|
from django.db.models import CASCADE, Q
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
|
||||||
from judge.models.profile import Profile
|
from judge.models.profile import Profile
|
||||||
|
from judge.caching import cache_wrapper
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Message']
|
__all__ = ["Message", "Room", "UserRoom", "Ignore"]
|
||||||
|
|
||||||
|
|
||||||
class Room(models.Model):
|
class Room(models.Model):
|
||||||
user_one = models.ForeignKey(Profile, related_name="user_one", verbose_name='user 1', on_delete=CASCADE)
|
user_one = models.ForeignKey(
|
||||||
user_two = models.ForeignKey(Profile, related_name="user_two", verbose_name='user 2', on_delete=CASCADE)
|
Profile, related_name="user_one", verbose_name="user 1", on_delete=CASCADE
|
||||||
|
)
|
||||||
|
user_two = models.ForeignKey(
|
||||||
|
Profile, related_name="user_two", verbose_name="user 2", on_delete=CASCADE
|
||||||
|
)
|
||||||
|
last_msg_time = models.DateTimeField(
|
||||||
|
verbose_name=_("last seen"), null=True, db_index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "chat_box"
|
||||||
|
unique_together = ("user_one", "user_two")
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def _cached_info(self):
|
||||||
|
return get_room_info(self.id)
|
||||||
|
|
||||||
def contain(self, profile):
|
def contain(self, profile):
|
||||||
return self.user_one == profile or self.user_two == profile
|
return profile.id in [self.user_one_id, self.user_two_id]
|
||||||
|
|
||||||
def other_user(self, profile):
|
def other_user(self, profile):
|
||||||
return self.user_one if profile == self.user_two else self.user_two
|
return self.user_one if profile == self.user_two else self.user_two
|
||||||
|
|
||||||
|
def other_user_id(self, profile):
|
||||||
|
user_ids = [self.user_one_id, self.user_two_id]
|
||||||
|
return sum(user_ids) - profile.id
|
||||||
|
|
||||||
def users(self):
|
def users(self):
|
||||||
return [self.user_one, self.user_two]
|
return [self.user_one, self.user_two]
|
||||||
|
|
||||||
|
def last_message_body(self):
|
||||||
|
return self._cached_info["last_message"]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def prefetch_room_cache(self, room_ids):
|
||||||
|
get_room_info.prefetch_multi([(i,) for i in room_ids])
|
||||||
|
|
||||||
|
|
||||||
class Message(models.Model):
|
class Message(models.Model):
|
||||||
author = models.ForeignKey(Profile, verbose_name=_('user'), on_delete=CASCADE)
|
author = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE)
|
||||||
time = models.DateTimeField(verbose_name=_('posted time'), auto_now_add=True)
|
time = models.DateTimeField(
|
||||||
body = models.TextField(verbose_name=_('body of comment'), max_length=8192)
|
verbose_name=_("posted time"), auto_now_add=True, db_index=True
|
||||||
hidden = models.BooleanField(verbose_name='is hidden', default=False)
|
)
|
||||||
room = models.ForeignKey(Room, verbose_name='room id', on_delete=CASCADE, default=None, null=True)
|
body = models.TextField(verbose_name=_("body of comment"), max_length=8192)
|
||||||
|
hidden = models.BooleanField(verbose_name="is hidden", default=False)
|
||||||
|
room = models.ForeignKey(
|
||||||
|
Room, verbose_name="room id", on_delete=CASCADE, default=None, null=True
|
||||||
|
)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
new_message = self.id
|
|
||||||
self.body = self.body.strip()
|
self.body = self.body.strip()
|
||||||
super(Message, self).save(*args, **kwargs)
|
super(Message, self).save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
app_label = 'chat_box'
|
verbose_name = "message"
|
||||||
verbose_name = 'message'
|
verbose_name_plural = "messages"
|
||||||
verbose_name_plural = 'messages'
|
ordering = ("-id",)
|
||||||
ordering = ('-time',)
|
indexes = [
|
||||||
|
models.Index(fields=["hidden", "room", "-id"]),
|
||||||
|
]
|
||||||
|
app_label = "chat_box"
|
||||||
|
|
||||||
|
|
||||||
class UserRoom(models.Model):
|
class UserRoom(models.Model):
|
||||||
user = models.ForeignKey(Profile, verbose_name=_('user'), on_delete=CASCADE)
|
user = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE)
|
||||||
room = models.ForeignKey(Room, verbose_name='room id', on_delete=CASCADE, default=None, null=True)
|
room = models.ForeignKey(
|
||||||
last_seen = models.DateTimeField(verbose_name=_('last seen'), auto_now_add=True)
|
Room, verbose_name="room id", on_delete=CASCADE, default=None, null=True
|
||||||
|
)
|
||||||
|
last_seen = models.DateTimeField(verbose_name=_("last seen"), auto_now_add=True)
|
||||||
|
unread_count = models.IntegerField(default=0, db_index=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ("user", "room")
|
||||||
|
app_label = "chat_box"
|
||||||
|
|
||||||
|
|
||||||
class Ignore(models.Model):
|
class Ignore(models.Model):
|
||||||
user = models.ForeignKey(Profile, related_name="ignored_chat_users", verbose_name=_('user'), on_delete=CASCADE)
|
user = models.OneToOneField(
|
||||||
|
Profile,
|
||||||
|
related_name="ignored_chat_users",
|
||||||
|
verbose_name=_("user"),
|
||||||
|
on_delete=CASCADE,
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
ignored_users = models.ManyToManyField(Profile)
|
ignored_users = models.ManyToManyField(Profile)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "chat_box"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_ignored(self, current_user, new_friend):
|
def is_ignored(self, current_user, new_friend):
|
||||||
try:
|
try:
|
||||||
return current_user.ignored_chat_users.get().ignored_users \
|
return current_user.ignored_chat_users.ignored_users.filter(
|
||||||
.filter(id=new_friend.id).exists()
|
id=new_friend.id
|
||||||
|
).exists()
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -59,26 +115,40 @@ class Ignore(models.Model):
|
||||||
def get_ignored_users(self, user):
|
def get_ignored_users(self, user):
|
||||||
try:
|
try:
|
||||||
return self.objects.get(user=user).ignored_users.all()
|
return self.objects.get(user=user).ignored_users.all()
|
||||||
except Ignore.DoesNotExist:
|
except:
|
||||||
return Profile.objects.none()
|
return Profile.objects.none()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_ignore(self, current_user, friend):
|
def get_ignored_rooms(self, user):
|
||||||
ignore, created = self.objects.get_or_create(
|
try:
|
||||||
user = current_user
|
ignored_users = self.objects.get(user=user).ignored_users.all()
|
||||||
|
return Room.objects.filter(Q(user_one=user) | Q(user_two=user)).filter(
|
||||||
|
Q(user_one__in=ignored_users) | Q(user_two__in=ignored_users)
|
||||||
)
|
)
|
||||||
|
except:
|
||||||
|
return Room.objects.none()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_ignore(self, current_user, friend):
|
||||||
|
ignore, created = self.objects.get_or_create(user=current_user)
|
||||||
ignore.ignored_users.add(friend)
|
ignore.ignored_users.add(friend)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def remove_ignore(self, current_user, friend):
|
def remove_ignore(self, current_user, friend):
|
||||||
ignore, created = self.objects.get_or_create(
|
ignore, created = self.objects.get_or_create(user=current_user)
|
||||||
user = current_user
|
|
||||||
)
|
|
||||||
ignore.ignored_users.remove(friend)
|
ignore.ignored_users.remove(friend)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def toggle_ignore(self, current_user, friend):
|
def toggle_ignore(self, current_user, friend):
|
||||||
if (self.is_ignored(current_user, friend)):
|
if self.is_ignored(current_user, friend):
|
||||||
self.remove_ignore(current_user, friend)
|
self.remove_ignore(current_user, friend)
|
||||||
else:
|
else:
|
||||||
self.add_ignore(current_user, friend)
|
self.add_ignore(current_user, friend)
|
||||||
|
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="Rinfo")
|
||||||
|
def get_room_info(room_id):
|
||||||
|
last_msg = Message.objects.filter(room_id=room_id).first()
|
||||||
|
return {
|
||||||
|
"last_message": last_msg.body if last_msg else None,
|
||||||
|
}
|
||||||
|
|
|
@ -1,18 +1,51 @@
|
||||||
from cryptography.fernet import Fernet
|
from cryptography.fernet import Fernet
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db.models import OuterRef, Count, Subquery, IntegerField, Q
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
|
||||||
|
from chat_box.models import Ignore, Message, UserRoom, Room
|
||||||
|
|
||||||
|
from judge.caching import cache_wrapper
|
||||||
|
|
||||||
secret_key = settings.CHAT_SECRET_KEY
|
secret_key = settings.CHAT_SECRET_KEY
|
||||||
fernet = Fernet(secret_key)
|
fernet = Fernet(secret_key)
|
||||||
|
|
||||||
|
|
||||||
def encrypt_url(creator_id, other_id):
|
def encrypt_url(creator_id, other_id):
|
||||||
message = str(creator_id) + '_' + str(other_id)
|
message = str(creator_id) + "_" + str(other_id)
|
||||||
return fernet.encrypt(message.encode()).decode()
|
return fernet.encrypt(message.encode()).decode()
|
||||||
|
|
||||||
|
|
||||||
def decrypt_url(message_encrypted):
|
def decrypt_url(message_encrypted):
|
||||||
try:
|
try:
|
||||||
dec_message = fernet.decrypt(message_encrypted.encode()).decode()
|
dec_message = fernet.decrypt(message_encrypted.encode()).decode()
|
||||||
creator_id, other_id = dec_message.split('_')
|
creator_id, other_id = dec_message.split("_")
|
||||||
return int(creator_id), int(other_id)
|
return int(creator_id), int(other_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_channel(channel):
|
||||||
|
return (
|
||||||
|
hmac.new(
|
||||||
|
settings.CHAT_SECRET_KEY.encode(),
|
||||||
|
channel.encode(),
|
||||||
|
hashlib.sha512,
|
||||||
|
).hexdigest()[:16]
|
||||||
|
+ "%s" % channel
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="gub")
|
||||||
|
def get_unread_boxes(profile):
|
||||||
|
ignored_rooms = Ignore.get_ignored_rooms(profile)
|
||||||
|
unread_boxes = (
|
||||||
|
UserRoom.objects.filter(user=profile, unread_count__gt=0)
|
||||||
|
.exclude(room__in=ignored_rooms)
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
|
||||||
|
return unread_boxes
|
||||||
|
|
|
@ -1,47 +1,77 @@
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest, HttpResponsePermanentRedirect, HttpResponseRedirect
|
from django.http import (
|
||||||
|
HttpResponse,
|
||||||
|
JsonResponse,
|
||||||
|
HttpResponseBadRequest,
|
||||||
|
HttpResponsePermanentRedirect,
|
||||||
|
HttpResponseRedirect,
|
||||||
|
)
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.forms.models import model_to_dict
|
from django.forms.models import model_to_dict
|
||||||
from django.db.models import Case, BooleanField, When, Q, Subquery, OuterRef, Exists, Count, IntegerField
|
from django.db.models import (
|
||||||
|
Case,
|
||||||
|
BooleanField,
|
||||||
|
When,
|
||||||
|
Q,
|
||||||
|
Subquery,
|
||||||
|
OuterRef,
|
||||||
|
Exists,
|
||||||
|
Count,
|
||||||
|
IntegerField,
|
||||||
|
F,
|
||||||
|
Max,
|
||||||
|
)
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from judge import event_poster as event
|
from judge import event_poster as event
|
||||||
from judge.jinja2.gravatar import gravatar
|
from judge.jinja2.gravatar import gravatar
|
||||||
from judge.models import Friend
|
from judge.models import Friend
|
||||||
|
|
||||||
from chat_box.models import Message, Profile, Room, UserRoom, Ignore
|
from chat_box.models import Message, Profile, Room, UserRoom, Ignore, get_room_info
|
||||||
from chat_box.utils import encrypt_url, decrypt_url
|
from chat_box.utils import encrypt_url, decrypt_url, encrypt_channel, get_unread_boxes
|
||||||
|
|
||||||
import json
|
from reversion import revisions
|
||||||
|
|
||||||
|
|
||||||
class ChatView(ListView):
|
class ChatView(ListView):
|
||||||
context_object_name = 'message'
|
context_object_name = "message"
|
||||||
template_name = 'chat/chat.html'
|
template_name = "chat/chat.html"
|
||||||
title = _('Chat Box')
|
title = _("LQDOJ Chat")
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.room_id = None
|
self.room_id = None
|
||||||
self.room = None
|
self.room = None
|
||||||
self.paginate_by = 50
|
|
||||||
self.messages = None
|
self.messages = None
|
||||||
self.paginator = None
|
self.first_page_size = 20 # only for first request
|
||||||
|
self.follow_up_page_size = 50
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.messages
|
return self.messages
|
||||||
|
|
||||||
|
def has_next(self):
|
||||||
|
try:
|
||||||
|
msg = Message.objects.filter(room=self.room_id).earliest("id")
|
||||||
|
except Exception as e:
|
||||||
|
return False
|
||||||
|
return msg not in self.messages
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
request_room = kwargs['room_id']
|
request_room = kwargs["room_id"]
|
||||||
page = request.GET.get('page')
|
page_size = self.follow_up_page_size
|
||||||
|
try:
|
||||||
|
last_id = int(request.GET.get("last_id"))
|
||||||
|
except Exception:
|
||||||
|
last_id = 1e15
|
||||||
|
page_size = self.first_page_size
|
||||||
|
only_messages = request.GET.get("only_messages")
|
||||||
|
|
||||||
if request_room:
|
if request_room:
|
||||||
try:
|
try:
|
||||||
|
@ -53,180 +83,261 @@ class ChatView(ListView):
|
||||||
else:
|
else:
|
||||||
request_room = None
|
request_room = None
|
||||||
|
|
||||||
if request_room != self.room_id or not self.messages:
|
|
||||||
self.room_id = request_room
|
self.room_id = request_room
|
||||||
self.messages = Message.objects.filter(hidden=False, room=self.room_id)
|
self.messages = (
|
||||||
self.paginator = Paginator(self.messages, self.paginate_by)
|
Message.objects.filter(hidden=False, room=self.room_id, id__lt=last_id)
|
||||||
|
.select_related("author")
|
||||||
if page == None:
|
.only("body", "time", "author__rating", "author__display_rank")[:page_size]
|
||||||
update_last_seen(request, **kwargs)
|
)
|
||||||
|
if not only_messages:
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
cur_page = self.paginator.get_page(page)
|
return render(
|
||||||
|
request,
|
||||||
return render(request, 'chat/message_list.html', {
|
"chat/message_list.html",
|
||||||
'object_list': cur_page.object_list,
|
{
|
||||||
'num_pages': self.paginator.num_pages
|
"object_list": self.messages,
|
||||||
})
|
"has_next": self.has_next(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
context['title'] = self.title
|
context["title"] = self.title
|
||||||
context['last_msg'] = event.last()
|
context["last_msg"] = event.last()
|
||||||
context['status_sections'] = get_status_context(self.request)
|
context["status_sections"] = get_status_context(self.request.profile)
|
||||||
context['room'] = self.room_id
|
context["room"] = self.room_id
|
||||||
context['unread_count_lobby'] = get_unread_count(None, self.request.profile)
|
context["has_next"] = self.has_next()
|
||||||
|
context["unread_count_lobby"] = get_unread_count(None, self.request.profile)
|
||||||
|
context["chat_channel"] = encrypt_channel(
|
||||||
|
"chat_" + str(self.request.profile.id)
|
||||||
|
)
|
||||||
|
context["chat_lobby_channel"] = encrypt_channel("chat_lobby")
|
||||||
if self.room:
|
if self.room:
|
||||||
users_room = [self.room.user_one, self.room.user_two]
|
users_room = [self.room.user_one, self.room.user_two]
|
||||||
users_room.remove(self.request.profile)
|
users_room.remove(self.request.profile)
|
||||||
context['other_user'] = users_room[0]
|
context["other_user"] = users_room[0]
|
||||||
context['other_online'] = get_user_online_status(context['other_user'])
|
context["other_online"] = get_user_online_status(context["other_user"])
|
||||||
context['is_ignored'] = Ignore.is_ignored(self.request.profile, context['other_user'])
|
context["is_ignored"] = Ignore.is_ignored(
|
||||||
|
self.request.profile, context["other_user"]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
context['online_count'] = get_online_count()
|
context["online_count"] = get_online_count()
|
||||||
context['message_template'] = {
|
context["message_template"] = {
|
||||||
'author': self.request.profile,
|
"author": self.request.profile,
|
||||||
'id': '$id',
|
"id": "$id",
|
||||||
'time': timezone.now(),
|
"time": timezone.now(),
|
||||||
'body': '$body'
|
"body": "$body",
|
||||||
}
|
}
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
def delete_message(request):
|
def delete_message(request):
|
||||||
ret = {'delete': 'done'}
|
ret = {"delete": "done"}
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == "GET":
|
||||||
return JsonResponse(ret)
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
if request.user.is_staff:
|
|
||||||
try:
|
try:
|
||||||
messid = int(request.POST.get('message'))
|
messid = int(request.POST.get("message"))
|
||||||
mess = Message.objects.get(id=messid)
|
mess = Message.objects.get(id=messid)
|
||||||
except:
|
except:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
if not request.user.is_staff and request.profile != mess.author:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
mess.hidden = True
|
mess.hidden = True
|
||||||
mess.save()
|
mess.save()
|
||||||
|
|
||||||
return JsonResponse(ret)
|
return JsonResponse(ret)
|
||||||
|
|
||||||
|
|
||||||
|
def mute_message(request):
|
||||||
|
ret = {"mute": "done"}
|
||||||
|
|
||||||
|
if request.method == "GET":
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
if not request.user.is_staff:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
try:
|
||||||
|
messid = int(request.POST.get("message"))
|
||||||
|
mess = Message.objects.get(id=messid)
|
||||||
|
except:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
with revisions.create_revision():
|
||||||
|
revisions.set_comment(_("Mute chat") + ": " + mess.body)
|
||||||
|
revisions.set_user(request.user)
|
||||||
|
mess.author.mute = True
|
||||||
|
mess.author.save()
|
||||||
|
|
||||||
|
Message.objects.filter(room=None, author=mess.author).update(hidden=True)
|
||||||
|
|
||||||
return JsonResponse(ret)
|
return JsonResponse(ret)
|
||||||
|
|
||||||
|
|
||||||
|
def check_valid_message(request, room):
|
||||||
|
if not room and len(request.POST["body"]) > 200:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not can_access_room(request, room) or request.profile.mute:
|
||||||
|
return False
|
||||||
|
|
||||||
|
last_msg = Message.objects.filter(room=room).first()
|
||||||
|
if (
|
||||||
|
last_msg
|
||||||
|
and last_msg.author == request.profile
|
||||||
|
and last_msg.body == request.POST["body"].strip()
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not room:
|
||||||
|
four_last_msg = Message.objects.filter(room=room).order_by("-id")[:4]
|
||||||
|
if len(four_last_msg) >= 4:
|
||||||
|
same_author = all(msg.author == request.profile for msg in four_last_msg)
|
||||||
|
time_diff = timezone.now() - four_last_msg[3].time
|
||||||
|
if same_author and time_diff.total_seconds() < 300:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def post_message(request):
|
def post_message(request):
|
||||||
ret = {'msg': 'posted'}
|
ret = {"msg": "posted"}
|
||||||
if request.method != 'POST':
|
|
||||||
|
if request.method != "POST":
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
if len(request.POST["body"]) > 5000 or len(request.POST["body"].strip()) == 0:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
room = None
|
room = None
|
||||||
if request.POST['room']:
|
if request.POST["room"]:
|
||||||
room = Room.objects.get(id=request.POST['room'])
|
room = Room.objects.get(id=request.POST["room"])
|
||||||
|
|
||||||
if not can_access_room(request, room) or request.profile.mute:
|
if not check_valid_message(request, room):
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
new_message = Message(author=request.profile,
|
new_message = Message(author=request.profile, body=request.POST["body"], room=room)
|
||||||
body=request.POST['body'],
|
|
||||||
room=room)
|
|
||||||
new_message.save()
|
new_message.save()
|
||||||
|
|
||||||
if not room:
|
if not room:
|
||||||
event.post('chat_lobby', {
|
event.post(
|
||||||
'type': 'lobby',
|
encrypt_channel("chat_lobby"),
|
||||||
'author_id': request.profile.id,
|
{
|
||||||
'message': new_message.id,
|
"type": "lobby",
|
||||||
'room': 'None',
|
"author_id": request.profile.id,
|
||||||
'tmp_id': request.POST.get('tmp_id')
|
"message": new_message.id,
|
||||||
})
|
"room": "None",
|
||||||
|
"tmp_id": request.POST.get("tmp_id"),
|
||||||
|
},
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
|
get_room_info.dirty(room.id)
|
||||||
|
room.last_msg_time = new_message.time
|
||||||
|
room.save()
|
||||||
|
|
||||||
for user in room.users():
|
for user in room.users():
|
||||||
event.post('chat_' + str(user.id), {
|
event.post(
|
||||||
'type': 'private',
|
encrypt_channel("chat_" + str(user.id)),
|
||||||
'author_id': request.profile.id,
|
{
|
||||||
'message': new_message.id,
|
"type": "private",
|
||||||
'room': room.id,
|
"author_id": request.profile.id,
|
||||||
'tmp_id': request.POST.get('tmp_id')
|
"message": new_message.id,
|
||||||
})
|
"room": room.id,
|
||||||
|
"tmp_id": request.POST.get("tmp_id"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if user != request.profile:
|
||||||
|
UserRoom.objects.filter(user=user, room=room).update(
|
||||||
|
unread_count=F("unread_count") + 1
|
||||||
|
)
|
||||||
|
get_unread_boxes.dirty(user)
|
||||||
|
|
||||||
return JsonResponse(ret)
|
return JsonResponse(ret)
|
||||||
|
|
||||||
|
|
||||||
def can_access_room(request, room):
|
def can_access_room(request, room):
|
||||||
return not room or room.user_one == request.profile or room.user_two == request.profile
|
return not room or room.contain(request.profile)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def chat_message_ajax(request):
|
def chat_message_ajax(request):
|
||||||
if request.method != 'GET':
|
if request.method != "GET":
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message_id = request.GET['message']
|
message_id = request.GET["message"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message = Message.objects.filter(hidden=False).get(id=message_id)
|
message = Message.objects.filter(hidden=False).get(id=message_id)
|
||||||
room = message.room
|
room = message.room
|
||||||
if room and not room.contain(request.profile):
|
if not can_access_room(request, room):
|
||||||
return HttpResponse('Unauthorized', status=401)
|
return HttpResponse("Unauthorized", status=401)
|
||||||
except Message.DoesNotExist:
|
except Message.DoesNotExist:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
return render(request, 'chat/message.html', {
|
return render(
|
||||||
'message': message,
|
request,
|
||||||
})
|
"chat/message.html",
|
||||||
|
{
|
||||||
|
"message": message,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def update_last_seen(request, **kwargs):
|
def update_last_seen(request, **kwargs):
|
||||||
if 'room_id' in kwargs:
|
if "room_id" in kwargs:
|
||||||
room_id = kwargs['room_id']
|
room_id = kwargs["room_id"]
|
||||||
elif request.method == 'GET':
|
elif request.method == "GET":
|
||||||
room_id = request.GET.get('room')
|
room_id = request.GET.get("room")
|
||||||
elif request.method == 'POST':
|
elif request.method == "POST":
|
||||||
room_id = request.POST.get('room')
|
room_id = request.POST.get("room")
|
||||||
else:
|
else:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
profile = request.profile
|
profile = request.profile
|
||||||
room = None
|
room = None
|
||||||
if room_id:
|
if room_id:
|
||||||
room = Room.objects.get(id=int(room_id))
|
room = Room.objects.filter(id=int(room_id)).first()
|
||||||
except Room.DoesNotExist:
|
except Room.DoesNotExist:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
except Exception as e:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
if room and not room.contain(profile):
|
if not can_access_room(request, room):
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
user_room, _ = UserRoom.objects.get_or_create(user=profile, room=room)
|
user_room, _ = UserRoom.objects.get_or_create(user=profile, room=room)
|
||||||
user_room.last_seen = timezone.now()
|
user_room.last_seen = timezone.now()
|
||||||
|
user_room.unread_count = 0
|
||||||
user_room.save()
|
user_room.save()
|
||||||
|
|
||||||
return JsonResponse({'msg': 'updated'})
|
get_unread_boxes.dirty(profile)
|
||||||
|
|
||||||
|
return JsonResponse({"msg": "updated"})
|
||||||
|
|
||||||
|
|
||||||
def get_online_count():
|
def get_online_count():
|
||||||
last_two_minutes = timezone.now()-timezone.timedelta(minutes=2)
|
last_5_minutes = timezone.now() - timezone.timedelta(minutes=5)
|
||||||
return Profile.objects.filter(last_access__gte=last_two_minutes).count()
|
return Profile.objects.filter(last_access__gte=last_5_minutes).count()
|
||||||
|
|
||||||
|
|
||||||
def get_user_online_status(user):
|
def get_user_online_status(user):
|
||||||
time_diff = timezone.now() - user.last_access
|
time_diff = timezone.now() - user.last_access
|
||||||
is_online = time_diff <= timezone.timedelta(minutes=2)
|
is_online = time_diff <= timezone.timedelta(minutes=5)
|
||||||
return is_online
|
return is_online
|
||||||
|
|
||||||
|
|
||||||
def user_online_status_ajax(request):
|
def user_online_status_ajax(request):
|
||||||
if request.method != 'GET':
|
if request.method != "GET":
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
user_id = request.GET.get('user')
|
user_id = request.GET.get("user")
|
||||||
|
|
||||||
if user_id:
|
if user_id:
|
||||||
try:
|
try:
|
||||||
|
@ -236,109 +347,126 @@ def user_online_status_ajax(request):
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
is_online = get_user_online_status(user)
|
is_online = get_user_online_status(user)
|
||||||
return render(request, 'chat/user_online_status.html', {
|
return render(
|
||||||
'other_user': user,
|
request,
|
||||||
'other_online': is_online,
|
"chat/user_online_status.html",
|
||||||
'is_ignored': Ignore.is_ignored(request.profile, user)
|
{
|
||||||
})
|
"other_user": user,
|
||||||
|
"other_online": is_online,
|
||||||
|
"is_ignored": Ignore.is_ignored(request.profile, user),
|
||||||
|
},
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return render(request, 'chat/user_online_status.html', {
|
return render(
|
||||||
'online_count': get_online_count(),
|
request,
|
||||||
})
|
"chat/user_online_status.html",
|
||||||
|
{
|
||||||
|
"online_count": get_online_count(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_online_status(request_user, queryset, rooms=None):
|
def get_online_status(profile, other_profile_ids, rooms=None):
|
||||||
if not queryset:
|
if not other_profile_ids:
|
||||||
return None
|
return None
|
||||||
last_two_minutes = timezone.now()-timezone.timedelta(minutes=2)
|
Profile.prefetch_profile_cache(other_profile_ids)
|
||||||
|
|
||||||
|
joined_ids = ",".join([str(id) for id in other_profile_ids])
|
||||||
|
other_profiles = Profile.objects.raw(
|
||||||
|
f"SELECT * from judge_profile where id in ({joined_ids}) order by field(id,{joined_ids})"
|
||||||
|
)
|
||||||
|
last_5_minutes = timezone.now() - timezone.timedelta(minutes=5)
|
||||||
ret = []
|
ret = []
|
||||||
|
|
||||||
if rooms:
|
if rooms:
|
||||||
unread_count = get_unread_count(rooms, request_user)
|
unread_count = get_unread_count(rooms, profile)
|
||||||
count = {}
|
count = {}
|
||||||
|
last_msg = {}
|
||||||
|
room_of_user = {}
|
||||||
for i in unread_count:
|
for i in unread_count:
|
||||||
count[i['other_user']] = i['unread_count']
|
room = Room.objects.get(id=i["room"])
|
||||||
|
other_profile = room.other_user(profile)
|
||||||
|
count[other_profile.id] = i["unread_count"]
|
||||||
|
rooms = Room.objects.filter(id__in=rooms)
|
||||||
|
for room in rooms:
|
||||||
|
other_profile_id = room.other_user_id(profile)
|
||||||
|
last_msg[other_profile_id] = room.last_message_body()
|
||||||
|
room_of_user[other_profile_id] = room.id
|
||||||
|
|
||||||
for user in queryset:
|
for other_profile in other_profiles:
|
||||||
is_online = False
|
is_online = False
|
||||||
if (user.last_access >= last_two_minutes):
|
if other_profile.last_access >= last_5_minutes:
|
||||||
is_online = True
|
is_online = True
|
||||||
user_dict = {'user': user, 'is_online': is_online}
|
user_dict = {"user": other_profile, "is_online": is_online}
|
||||||
if rooms and user.id in count:
|
if rooms:
|
||||||
user_dict['unread_count'] = count[user.id]
|
user_dict.update(
|
||||||
user_dict['url'] = encrypt_url(request_user.id, user.id)
|
{
|
||||||
|
"unread_count": count.get(other_profile.id),
|
||||||
|
"last_msg": last_msg.get(other_profile.id),
|
||||||
|
"room": room_of_user.get(other_profile.id),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
user_dict["url"] = encrypt_url(profile.id, other_profile.id)
|
||||||
ret.append(user_dict)
|
ret.append(user_dict)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def get_status_context(request, include_ignored=False):
|
def get_status_context(profile, include_ignored=False):
|
||||||
if include_ignored:
|
if include_ignored:
|
||||||
ignored_users = Profile.objects.none()
|
ignored_users = []
|
||||||
queryset = Profile.objects
|
queryset = Profile.objects
|
||||||
else:
|
else:
|
||||||
ignored_users = Ignore.get_ignored_users(request.profile)
|
ignored_users = list(
|
||||||
|
Ignore.get_ignored_users(profile).values_list("id", flat=True)
|
||||||
|
)
|
||||||
queryset = Profile.objects.exclude(id__in=ignored_users)
|
queryset = Profile.objects.exclude(id__in=ignored_users)
|
||||||
|
|
||||||
last_two_minutes = timezone.now()-timezone.timedelta(minutes=2)
|
last_5_minutes = timezone.now() - timezone.timedelta(minutes=5)
|
||||||
recent_profile = Room.objects.filter(
|
recent_profile = (
|
||||||
Q(user_one=request.profile) | Q(user_two=request.profile)
|
Room.objects.filter(Q(user_one=profile) | Q(user_two=profile))
|
||||||
).annotate(
|
.annotate(
|
||||||
last_msg_time=Subquery(
|
|
||||||
Message.objects.filter(room=OuterRef('pk')).values('time')[:1]
|
|
||||||
),
|
|
||||||
other_user=Case(
|
other_user=Case(
|
||||||
When(user_one=request.profile, then='user_two'),
|
When(user_one=profile, then="user_two"),
|
||||||
default='user_one',
|
default="user_one",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.filter(last_msg_time__isnull=False)
|
||||||
|
.exclude(other_user__in=ignored_users)
|
||||||
|
.order_by("-last_msg_time")
|
||||||
|
.values("other_user", "id")[:20]
|
||||||
)
|
)
|
||||||
).filter(last_msg_time__isnull=False)\
|
|
||||||
.exclude(other_user__in=ignored_users)\
|
|
||||||
.order_by('-last_msg_time').values('other_user', 'id')[:20]
|
|
||||||
|
|
||||||
recent_profile_id = [str(i['other_user']) for i in recent_profile]
|
recent_profile_ids = [str(i["other_user"]) for i in recent_profile]
|
||||||
joined_id = ','.join(recent_profile_id)
|
recent_rooms = [int(i["id"]) for i in recent_profile]
|
||||||
recent_rooms = [int(i['id']) for i in recent_profile]
|
Room.prefetch_room_cache(recent_rooms)
|
||||||
recent_list = None
|
|
||||||
if joined_id:
|
admin_list = (
|
||||||
recent_list = Profile.objects.raw(
|
queryset.filter(display_rank="admin")
|
||||||
f'SELECT * from judge_profile where id in ({joined_id}) order by field(id,{joined_id})')
|
.exclude(id__in=recent_profile_ids)
|
||||||
friend_list = Friend.get_friend_profiles(request.profile).exclude(id__in=recent_profile_id)\
|
.values_list("id", flat=True)
|
||||||
.exclude(id__in=ignored_users)\
|
)
|
||||||
.order_by('-last_access')
|
|
||||||
admin_list = queryset.filter(display_rank='admin')\
|
|
||||||
.exclude(id__in=friend_list).exclude(id__in=recent_profile_id)
|
|
||||||
all_user_status = queryset\
|
|
||||||
.filter(display_rank='user',
|
|
||||||
last_access__gte = last_two_minutes)\
|
|
||||||
.annotate(is_online=Case(default=True,output_field=BooleanField()))\
|
|
||||||
.order_by('-rating').exclude(id__in=friend_list).exclude(id__in=admin_list)\
|
|
||||||
.exclude(id__in=recent_profile_id)[:30]
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'title': 'Recent',
|
"title": _("Recent"),
|
||||||
'user_list': get_online_status(request.profile, recent_list, recent_rooms),
|
"user_list": get_online_status(profile, recent_profile_ids, recent_rooms),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'Following',
|
"title": _("Admin"),
|
||||||
'user_list': get_online_status(request.profile, friend_list),
|
"user_list": get_online_status(profile, admin_list),
|
||||||
},
|
|
||||||
{
|
|
||||||
'title': 'Admin',
|
|
||||||
'user_list': get_online_status(request.profile, admin_list),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'title': 'Other',
|
|
||||||
'user_list': get_online_status(request.profile, all_user_status),
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def online_status_ajax(request):
|
def online_status_ajax(request):
|
||||||
return render(request, 'chat/online_status.html', {
|
return render(
|
||||||
'status_sections': get_status_context(request),
|
request,
|
||||||
'unread_count_lobby': get_unread_count(None, request.profile),
|
"chat/online_status.html",
|
||||||
})
|
{
|
||||||
|
"status_sections": get_status_context(request.profile),
|
||||||
|
"unread_count_lobby": get_unread_count(None, request.profile),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -351,15 +479,14 @@ def get_room(user_one, user_two):
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def get_or_create_room(request):
|
def get_or_create_room(request):
|
||||||
if request.method == 'GET':
|
if request.method == "GET":
|
||||||
decrypted_other_id = request.GET.get('other')
|
decrypted_other_id = request.GET.get("other")
|
||||||
elif request.method == 'POST':
|
elif request.method == "POST":
|
||||||
decrypted_other_id = request.POST.get('other')
|
decrypted_other_id = request.POST.get("other")
|
||||||
else:
|
else:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
request_id, other_id = decrypt_url(decrypted_other_id)
|
request_id, other_id = decrypt_url(decrypted_other_id)
|
||||||
|
|
||||||
if not other_id or not request_id or request_id != request.profile.id:
|
if not other_id or not request_id or request_id != request.profile.id:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
@ -380,47 +507,41 @@ def get_or_create_room(request):
|
||||||
user_room.last_seen = timezone.now()
|
user_room.last_seen = timezone.now()
|
||||||
user_room.save()
|
user_room.save()
|
||||||
|
|
||||||
if request.method == 'GET':
|
room_url = reverse("chat", kwargs={"room_id": room.id})
|
||||||
return JsonResponse({'room': room.id, 'other_user_id': other_user.id})
|
if request.method == "GET":
|
||||||
return HttpResponseRedirect(reverse('chat', kwargs={'room_id': room.id}))
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"room": room.id,
|
||||||
|
"other_user_id": other_user.id,
|
||||||
|
"url": room_url,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(room_url)
|
||||||
|
|
||||||
|
|
||||||
def get_unread_count(rooms, user):
|
def get_unread_count(rooms, user):
|
||||||
if rooms:
|
if rooms:
|
||||||
mess = Message.objects.filter(room=OuterRef('room'),
|
return UserRoom.objects.filter(
|
||||||
time__gte=OuterRef('last_seen'))\
|
user=user, room__in=rooms, unread_count__gt=0
|
||||||
.exclude(author=user)\
|
).values("unread_count", "room")
|
||||||
.order_by().values('room')\
|
|
||||||
.annotate(unread_count=Count('pk')).values('unread_count')
|
|
||||||
|
|
||||||
return UserRoom.objects\
|
|
||||||
.filter(user=user, room__in=rooms)\
|
|
||||||
.annotate(
|
|
||||||
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
|
|
||||||
other_user=Case(
|
|
||||||
When(room__user_one=user, then='room__user_two'),
|
|
||||||
default='room__user_one',
|
|
||||||
)
|
|
||||||
).filter(unread_count__gte=1).values('other_user', 'unread_count')
|
|
||||||
else: # lobby
|
else: # lobby
|
||||||
mess = Message.objects.filter(room__isnull=True,
|
user_room = UserRoom.objects.filter(user=user, room__isnull=True).first()
|
||||||
time__gte=OuterRef('last_seen'))\
|
if not user_room:
|
||||||
.exclude(author=user)\
|
return 0
|
||||||
.order_by().values('room')\
|
last_seen = user_room.last_seen
|
||||||
.annotate(unread_count=Count('pk')).values('unread_count')
|
res = (
|
||||||
|
Message.objects.filter(room__isnull=True, time__gte=last_seen)
|
||||||
|
.exclude(author=user)
|
||||||
|
.exclude(hidden=True)
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
|
||||||
res = UserRoom.objects\
|
return res
|
||||||
.filter(user=user, room__isnull=True)\
|
|
||||||
.annotate(
|
|
||||||
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
|
|
||||||
).values_list('unread_count', flat=True)
|
|
||||||
|
|
||||||
return res[0] if len(res) else 0
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def toggle_ignore(request, **kwargs):
|
def toggle_ignore(request, **kwargs):
|
||||||
user_id = kwargs['user_id']
|
user_id = kwargs["user_id"]
|
||||||
if not user_id:
|
if not user_id:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
try:
|
try:
|
||||||
|
@ -429,25 +550,5 @@ def toggle_ignore(request, **kwargs):
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
Ignore.toggle_ignore(request.profile, other_user)
|
Ignore.toggle_ignore(request.profile, other_user)
|
||||||
next_url = request.GET.get('next', '/')
|
next_url = request.GET.get("next", "/")
|
||||||
return HttpResponseRedirect(next_url)
|
return HttpResponseRedirect(next_url)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def get_unread_boxes(request):
|
|
||||||
if (request.method != 'GET'):
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
mess = Message.objects.filter(room=OuterRef('room'),
|
|
||||||
time__gte=OuterRef('last_seen'))\
|
|
||||||
.exclude(author=request.profile)\
|
|
||||||
.order_by().values('room')\
|
|
||||||
.annotate(unread_count=Count('pk')).values('unread_count')
|
|
||||||
|
|
||||||
unread_boxes = UserRoom.objects\
|
|
||||||
.filter(user=request.profile, room__isnull=False)\
|
|
||||||
.annotate(
|
|
||||||
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
|
|
||||||
).filter(unread_count__gte=1).count()
|
|
||||||
|
|
||||||
return JsonResponse({'unread_boxes': unread_boxes})
|
|
|
@ -12,6 +12,6 @@ if (2, 2) <= django.VERSION < (3,):
|
||||||
# attribute where the exact query sent to the database is saved.
|
# attribute where the exact query sent to the database is saved.
|
||||||
# See MySQLdb/cursors.py in the source distribution.
|
# See MySQLdb/cursors.py in the source distribution.
|
||||||
# MySQLdb returns string, PyMySQL bytes.
|
# MySQLdb returns string, PyMySQL bytes.
|
||||||
return force_text(getattr(cursor, '_executed', None), errors='replace')
|
return force_text(getattr(cursor, "_executed", None), errors="replace")
|
||||||
|
|
||||||
DatabaseOperations.last_executed_query = last_executed_query
|
DatabaseOperations.last_executed_query = last_executed_query
|
||||||
|
|
|
@ -11,6 +11,12 @@
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ace_editor {
|
||||||
|
overflow: hidden;
|
||||||
|
font: 12px/normal 'Fira Code', 'Monaco', 'Menlo', monospace;
|
||||||
|
direction: 1tr;
|
||||||
|
}
|
||||||
|
|
||||||
.django-ace-widget.loading {
|
.django-ace-widget.loading {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,15 +77,17 @@
|
||||||
mode = widget.getAttribute('data-mode'),
|
mode = widget.getAttribute('data-mode'),
|
||||||
theme = widget.getAttribute('data-theme'),
|
theme = widget.getAttribute('data-theme'),
|
||||||
wordwrap = widget.getAttribute('data-wordwrap'),
|
wordwrap = widget.getAttribute('data-wordwrap'),
|
||||||
toolbar = prev(widget),
|
toolbar = prev(widget);
|
||||||
main_block = toolbar.parentNode;
|
var main_block = div.parentNode.parentNode;
|
||||||
|
|
||||||
|
if (toolbar != null) {
|
||||||
// Toolbar maximize/minimize button
|
// Toolbar maximize/minimize button
|
||||||
var min_max = toolbar.getElementsByClassName('django-ace-max_min');
|
var min_max = toolbar.getElementsByClassName('django-ace-max_min');
|
||||||
min_max[0].onclick = function () {
|
min_max[0].onclick = function () {
|
||||||
minimizeMaximize(widget, main_block, editor);
|
minimizeMaximize(widget, main_block, editor);
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
editor.getSession().setValue(textarea.value);
|
editor.getSession().setValue(textarea.value);
|
||||||
|
|
||||||
|
@ -160,7 +162,7 @@
|
||||||
]);
|
]);
|
||||||
|
|
||||||
window[widget.id] = editor;
|
window[widget.id] = editor;
|
||||||
$(widget).trigger('ace_load', [editor]);
|
setTimeout(() => $(widget).trigger('ace_load', [editor]), 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
|
|
@ -11,22 +11,33 @@ from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
|
||||||
class AceWidget(forms.Textarea):
|
class AceWidget(forms.Textarea):
|
||||||
def __init__(self, mode=None, theme=None, wordwrap=False, width='100%', height='300px',
|
def __init__(
|
||||||
no_ace_media=False, *args, **kwargs):
|
self,
|
||||||
|
mode=None,
|
||||||
|
theme=None,
|
||||||
|
wordwrap=False,
|
||||||
|
width="100%",
|
||||||
|
height="300px",
|
||||||
|
no_ace_media=False,
|
||||||
|
toolbar=True,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.wordwrap = wordwrap
|
self.wordwrap = wordwrap
|
||||||
self.width = width
|
self.width = width
|
||||||
self.height = height
|
self.height = height
|
||||||
self.ace_media = not no_ace_media
|
self.ace_media = not no_ace_media
|
||||||
|
self.toolbar = toolbar
|
||||||
super(AceWidget, self).__init__(*args, **kwargs)
|
super(AceWidget, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media(self):
|
def media(self):
|
||||||
js = [urljoin(settings.ACE_URL, 'ace.js')] if self.ace_media else []
|
js = [urljoin(settings.ACE_URL, "ace.js")] if self.ace_media else []
|
||||||
js.append('django_ace/widget.js')
|
js.append("django_ace/widget.js")
|
||||||
css = {
|
css = {
|
||||||
'screen': ['django_ace/widget.css'],
|
"screen": ["django_ace/widget.css"],
|
||||||
}
|
}
|
||||||
return forms.Media(js=js, css=css)
|
return forms.Media(js=js, css=css)
|
||||||
|
|
||||||
|
@ -34,24 +45,32 @@ class AceWidget(forms.Textarea):
|
||||||
attrs = attrs or {}
|
attrs = attrs or {}
|
||||||
|
|
||||||
ace_attrs = {
|
ace_attrs = {
|
||||||
'class': 'django-ace-widget loading',
|
"class": "django-ace-widget loading",
|
||||||
'style': 'width:%s; height:%s' % (self.width, self.height),
|
"style": "width:%s; height:%s" % (self.width, self.height),
|
||||||
'id': 'ace_%s' % name,
|
"id": "ace_%s" % name,
|
||||||
}
|
}
|
||||||
if self.mode:
|
if self.mode:
|
||||||
ace_attrs['data-mode'] = self.mode
|
ace_attrs["data-mode"] = self.mode
|
||||||
if self.theme:
|
if self.theme:
|
||||||
ace_attrs['data-theme'] = self.theme
|
ace_attrs["data-theme"] = self.theme
|
||||||
if self.wordwrap:
|
if self.wordwrap:
|
||||||
ace_attrs['data-wordwrap'] = 'true'
|
ace_attrs["data-wordwrap"] = "true"
|
||||||
|
|
||||||
attrs.update(style='width: 100%; min-width: 100%; max-width: 100%; resize: none')
|
attrs.update(
|
||||||
|
style="width: 100%; min-width: 100%; max-width: 100%; resize: none"
|
||||||
|
)
|
||||||
textarea = super(AceWidget, self).render(name, value, attrs)
|
textarea = super(AceWidget, self).render(name, value, attrs)
|
||||||
|
|
||||||
html = '<div%s><div></div></div>%s' % (flatatt(ace_attrs), textarea)
|
html = "<div%s><div></div></div>%s" % (flatatt(ace_attrs), textarea)
|
||||||
|
|
||||||
# add toolbar
|
if self.toolbar:
|
||||||
html = ('<div class="django-ace-editor"><div style="width: 100%%" class="django-ace-toolbar">'
|
toolbar = (
|
||||||
'<a href="./" class="django-ace-max_min"></a></div>%s</div>') % html
|
'<div style="width: {}" class="django-ace-toolbar">'
|
||||||
|
'<a href="#" class="django-ace-max_min"></a>'
|
||||||
|
"</div>"
|
||||||
|
).format(self.width)
|
||||||
|
html = toolbar + html
|
||||||
|
|
||||||
|
html = '<div class="django-ace-editor">{}</div>'.format(html)
|
||||||
|
|
||||||
return mark_safe(html)
|
return mark_safe(html)
|
||||||
|
|
|
@ -4,24 +4,30 @@ import socket
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
from celery.signals import task_failure
|
from celery.signals import task_failure
|
||||||
|
|
||||||
app = Celery('dmoj')
|
app = Celery("dmoj")
|
||||||
|
|
||||||
from django.conf import settings # noqa: E402, I202, django must be imported here
|
from django.conf import settings # noqa: E402, I202, django must be imported here
|
||||||
app.config_from_object(settings, namespace='CELERY')
|
|
||||||
|
|
||||||
if hasattr(settings, 'CELERY_BROKER_URL_SECRET'):
|
app.config_from_object(settings, namespace="CELERY")
|
||||||
|
|
||||||
|
if hasattr(settings, "CELERY_BROKER_URL_SECRET"):
|
||||||
app.conf.broker_url = settings.CELERY_BROKER_URL_SECRET
|
app.conf.broker_url = settings.CELERY_BROKER_URL_SECRET
|
||||||
if hasattr(settings, 'CELERY_RESULT_BACKEND_SECRET'):
|
if hasattr(settings, "CELERY_RESULT_BACKEND_SECRET"):
|
||||||
app.conf.result_backend = settings.CELERY_RESULT_BACKEND_SECRET
|
app.conf.result_backend = settings.CELERY_RESULT_BACKEND_SECRET
|
||||||
|
|
||||||
# Load task modules from all registered Django app configs.
|
# Load task modules from all registered Django app configs.
|
||||||
app.autodiscover_tasks()
|
app.autodiscover_tasks()
|
||||||
|
|
||||||
# Logger to enable errors be reported.
|
# Logger to enable errors be reported.
|
||||||
logger = logging.getLogger('judge.celery')
|
logger = logging.getLogger("judge.celery")
|
||||||
|
|
||||||
|
|
||||||
@task_failure.connect()
|
@task_failure.connect()
|
||||||
def celery_failure_log(sender, task_id, exception, traceback, *args, **kwargs):
|
def celery_failure_log(sender, task_id, exception, traceback, *args, **kwargs):
|
||||||
logger.error('Celery Task %s: %s on %s', sender.name, task_id, socket.gethostname(), # noqa: G201
|
logger.error(
|
||||||
exc_info=(type(exception), exception, traceback))
|
"Celery Task %s: %s on %s",
|
||||||
|
sender.name,
|
||||||
|
task_id,
|
||||||
|
socket.gethostname(), # noqa: G201
|
||||||
|
exc_info=(type(exception), exception, traceback),
|
||||||
|
)
|
||||||
|
|
526
dmoj/settings.py
526
dmoj/settings.py
|
@ -22,7 +22,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||||
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = '5*9f5q57mqmlz2#f$x1h76&jxy#yortjl1v+l*6hd18$d*yx#0'
|
SECRET_KEY = "5*9f5q57mqmlz2#f$x1h76&jxy#yortjl1v+l*6hd18$d*yx#0"
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
@ -30,9 +30,10 @@ DEBUG = True
|
||||||
ALLOWED_HOSTS = []
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
SITE_ID = 1
|
SITE_ID = 1
|
||||||
SITE_NAME = 'LQDOJ'
|
SITE_NAME = "LQDOJ"
|
||||||
SITE_LONG_NAME = 'LQDOJ: Le Quy Don Online Judge'
|
SITE_LONG_NAME = "LQDOJ: Le Quy Don Online Judge"
|
||||||
SITE_ADMIN_EMAIL = False
|
SITE_ADMIN_EMAIL = False
|
||||||
|
SITE_DOMAIN = "lqdoj.edu.vn"
|
||||||
|
|
||||||
DMOJ_REQUIRE_STAFF_2FA = True
|
DMOJ_REQUIRE_STAFF_2FA = True
|
||||||
|
|
||||||
|
@ -46,11 +47,9 @@ DMOJ_PP_STEP = 0.95
|
||||||
DMOJ_PP_ENTRIES = 100
|
DMOJ_PP_ENTRIES = 100
|
||||||
DMOJ_PP_BONUS_FUNCTION = lambda n: 300 * (1 - 0.997**n) # noqa: E731
|
DMOJ_PP_BONUS_FUNCTION = lambda n: 300 * (1 - 0.997**n) # noqa: E731
|
||||||
|
|
||||||
NODEJS = '/usr/bin/node'
|
NODEJS = "/usr/bin/node"
|
||||||
EXIFTOOL = '/usr/bin/exiftool'
|
EXIFTOOL = "/usr/bin/exiftool"
|
||||||
ACE_URL = '//cdnjs.cloudflare.com/ajax/libs/ace/1.1.3'
|
ACE_URL = "//cdnjs.cloudflare.com/ajax/libs/ace/1.1.3"
|
||||||
SELECT2_JS_URL = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js'
|
|
||||||
DEFAULT_SELECT2_CSS = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css'
|
|
||||||
|
|
||||||
DMOJ_CAMO_URL = None
|
DMOJ_CAMO_URL = None
|
||||||
DMOJ_CAMO_KEY = None
|
DMOJ_CAMO_KEY = None
|
||||||
|
@ -62,6 +61,7 @@ DMOJ_PROBLEM_MAX_TIME_LIMIT = 60 # seconds
|
||||||
DMOJ_PROBLEM_MIN_MEMORY_LIMIT = 0 # kilobytes
|
DMOJ_PROBLEM_MIN_MEMORY_LIMIT = 0 # kilobytes
|
||||||
DMOJ_PROBLEM_MAX_MEMORY_LIMIT = 1048576 # kilobytes
|
DMOJ_PROBLEM_MAX_MEMORY_LIMIT = 1048576 # kilobytes
|
||||||
DMOJ_PROBLEM_MIN_PROBLEM_POINTS = 0
|
DMOJ_PROBLEM_MIN_PROBLEM_POINTS = 0
|
||||||
|
DMOJ_SUBMISSION_ROOT = "/tmp"
|
||||||
DMOJ_RATING_COLORS = True
|
DMOJ_RATING_COLORS = True
|
||||||
DMOJ_EMAIL_THROTTLING = (10, 60)
|
DMOJ_EMAIL_THROTTLING = (10, 60)
|
||||||
DMOJ_STATS_LANGUAGE_THRESHOLD = 10
|
DMOJ_STATS_LANGUAGE_THRESHOLD = 10
|
||||||
|
@ -73,16 +73,20 @@ DMOJ_BLOG_NEW_CONTEST_COUNT = 7
|
||||||
DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT = 7
|
DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT = 7
|
||||||
DMOJ_TOTP_TOLERANCE_HALF_MINUTES = 1
|
DMOJ_TOTP_TOLERANCE_HALF_MINUTES = 1
|
||||||
DMOJ_USER_MAX_ORGANIZATION_COUNT = 10
|
DMOJ_USER_MAX_ORGANIZATION_COUNT = 10
|
||||||
|
DMOJ_USER_MAX_ORGANIZATION_ADD = 5
|
||||||
DMOJ_COMMENT_VOTE_HIDE_THRESHOLD = -5
|
DMOJ_COMMENT_VOTE_HIDE_THRESHOLD = -5
|
||||||
DMOJ_PDF_PROBLEM_CACHE = ''
|
DMOJ_PDF_PROBLEM_CACHE = ""
|
||||||
DMOJ_PDF_PROBLEM_TEMP_DIR = tempfile.gettempdir()
|
DMOJ_PDF_PROBLEM_TEMP_DIR = tempfile.gettempdir()
|
||||||
DMOJ_STATS_SUBMISSION_RESULT_COLORS = {
|
DMOJ_STATS_SUBMISSION_RESULT_COLORS = {
|
||||||
'TLE': '#a3bcbd',
|
"TLE": "#a3bcbd",
|
||||||
'AC': '#00a92a',
|
"AC": "#00a92a",
|
||||||
'WA': '#ed4420',
|
"WA": "#ed4420",
|
||||||
'CE': '#42586d',
|
"CE": "#42586d",
|
||||||
'ERR': '#ffa71c',
|
"ERR": "#ffa71c",
|
||||||
}
|
}
|
||||||
|
DMOJ_PROFILE_IMAGE_ROOT = "profile_images"
|
||||||
|
DMOJ_ORGANIZATION_IMAGE_ROOT = "organization_images"
|
||||||
|
DMOJ_TEST_FORMATTER_ROOT = "test_formatter"
|
||||||
|
|
||||||
MARKDOWN_STYLES = {}
|
MARKDOWN_STYLES = {}
|
||||||
MARKDOWN_DEFAULT_STYLE = {}
|
MARKDOWN_DEFAULT_STYLE = {}
|
||||||
|
@ -90,16 +94,15 @@ MARKDOWN_DEFAULT_STYLE = {}
|
||||||
MATHOID_URL = False
|
MATHOID_URL = False
|
||||||
MATHOID_GZIP = False
|
MATHOID_GZIP = False
|
||||||
MATHOID_MML_CACHE = None
|
MATHOID_MML_CACHE = None
|
||||||
MATHOID_CSS_CACHE = 'default'
|
MATHOID_CSS_CACHE = "default"
|
||||||
MATHOID_DEFAULT_TYPE = 'auto'
|
MATHOID_DEFAULT_TYPE = "auto"
|
||||||
MATHOID_MML_CACHE_TTL = 86400
|
MATHOID_MML_CACHE_TTL = 86400
|
||||||
MATHOID_CACHE_ROOT = tempfile.gettempdir() + '/mathoidCache'
|
MATHOID_CACHE_ROOT = tempfile.gettempdir() + "/mathoidCache"
|
||||||
MATHOID_CACHE_URL = False
|
MATHOID_CACHE_URL = False
|
||||||
|
|
||||||
TEXOID_GZIP = False
|
TEXOID_GZIP = False
|
||||||
TEXOID_META_CACHE = 'default'
|
TEXOID_META_CACHE = "default"
|
||||||
TEXOID_META_CACHE_TTL = 86400
|
TEXOID_META_CACHE_TTL = 86400
|
||||||
DMOJ_NEWSLETTER_ID_ON_REGISTER = 1
|
|
||||||
|
|
||||||
BAD_MAIL_PROVIDERS = ()
|
BAD_MAIL_PROVIDERS = ()
|
||||||
BAD_MAIL_PROVIDER_REGEX = ()
|
BAD_MAIL_PROVIDER_REGEX = ()
|
||||||
|
@ -110,31 +113,30 @@ TIMEZONE_MAP = None
|
||||||
TIMEZONE_DETECT_BACKEND = None
|
TIMEZONE_DETECT_BACKEND = None
|
||||||
|
|
||||||
TERMS_OF_SERVICE_URL = None
|
TERMS_OF_SERVICE_URL = None
|
||||||
DEFAULT_USER_LANGUAGE = 'PY3'
|
DEFAULT_USER_LANGUAGE = "PY3"
|
||||||
|
|
||||||
PHANTOMJS = ''
|
PHANTOMJS = ""
|
||||||
PHANTOMJS_PDF_ZOOM = 0.75
|
PHANTOMJS_PDF_ZOOM = 0.75
|
||||||
PHANTOMJS_PDF_TIMEOUT = 5.0
|
PHANTOMJS_PDF_TIMEOUT = 5.0
|
||||||
PHANTOMJS_PAPER_SIZE = 'Letter'
|
PHANTOMJS_PAPER_SIZE = "Letter"
|
||||||
|
|
||||||
SLIMERJS = ''
|
SLIMERJS = ""
|
||||||
SLIMERJS_PDF_ZOOM = 0.75
|
SLIMERJS_PDF_ZOOM = 0.75
|
||||||
SLIMERJS_FIREFOX_PATH = ''
|
SLIMERJS_FIREFOX_PATH = ""
|
||||||
SLIMERJS_PAPER_SIZE = 'Letter'
|
SLIMERJS_PAPER_SIZE = "Letter"
|
||||||
|
|
||||||
PUPPETEER_MODULE = '/usr/lib/node_modules/puppeteer'
|
PUPPETEER_MODULE = "/usr/lib/node_modules/puppeteer"
|
||||||
PUPPETEER_PAPER_SIZE = 'Letter'
|
PUPPETEER_PAPER_SIZE = "Letter"
|
||||||
|
|
||||||
USE_SELENIUM = False
|
USE_SELENIUM = False
|
||||||
SELENIUM_CUSTOM_CHROME_PATH = None
|
SELENIUM_CUSTOM_CHROME_PATH = None
|
||||||
SELENIUM_CHROMEDRIVER_PATH = 'chromedriver'
|
SELENIUM_CHROMEDRIVER_PATH = "chromedriver"
|
||||||
|
|
||||||
PYGMENT_THEME = 'pygment-github.css'
|
|
||||||
INLINE_JQUERY = True
|
INLINE_JQUERY = True
|
||||||
INLINE_FONTAWESOME = True
|
INLINE_FONTAWESOME = True
|
||||||
JQUERY_JS = '//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js'
|
JQUERY_JS = "//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"
|
||||||
FONTAWESOME_CSS = '//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css'
|
FONTAWESOME_CSS = "//cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
|
||||||
DMOJ_CANONICAL = ''
|
DMOJ_CANONICAL = ""
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
|
@ -146,356 +148,315 @@ except ImportError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
del wpadmin
|
del wpadmin
|
||||||
INSTALLED_APPS += ('wpadmin',)
|
INSTALLED_APPS += ("wpadmin",)
|
||||||
|
|
||||||
WPADMIN = {
|
WPADMIN = {
|
||||||
'admin': {
|
"admin": {
|
||||||
'title': 'LQDOJ Admin',
|
"title": "LQDOJ Admin",
|
||||||
'menu': {
|
"menu": {
|
||||||
'top': 'wpadmin.menu.menus.BasicTopMenu',
|
"top": "wpadmin.menu.menus.BasicTopMenu",
|
||||||
'left': 'wpadmin.menu.custom.CustomModelLeftMenuWithDashboard',
|
"left": "wpadmin.menu.custom.CustomModelLeftMenuWithDashboard",
|
||||||
},
|
},
|
||||||
'custom_menu': [
|
"custom_menu": [
|
||||||
{
|
{
|
||||||
'model': 'judge.Problem',
|
"model": "judge.Problem",
|
||||||
'icon': 'fa-question-circle',
|
"icon": "fa-question-circle",
|
||||||
'children': [
|
"children": [
|
||||||
'judge.ProblemGroup',
|
"judge.ProblemGroup",
|
||||||
'judge.ProblemType',
|
"judge.ProblemType",
|
||||||
|
"judge.ProblemPointsVote",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'model': 'judge.Submission',
|
"model": "judge.Submission",
|
||||||
'icon': 'fa-check-square-o',
|
"icon": "fa-check-square",
|
||||||
'children': [
|
"children": [
|
||||||
'judge.Language',
|
"judge.Language",
|
||||||
'judge.Judge',
|
"judge.Judge",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'model': 'judge.Contest',
|
"model": "judge.Contest",
|
||||||
'icon': 'fa-bar-chart',
|
"icon": "fa-bar-chart",
|
||||||
'children': [
|
"children": [
|
||||||
'judge.ContestParticipation',
|
"judge.ContestParticipation",
|
||||||
'judge.ContestTag',
|
"judge.ContestTag",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'model': 'auth.User',
|
"model": "auth.User",
|
||||||
'icon': 'fa-user',
|
"icon": "fa-user",
|
||||||
'children': [
|
"children": [
|
||||||
'auth.Group',
|
"auth.Group",
|
||||||
'registration.RegistrationProfile',
|
"registration.RegistrationProfile",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'model': 'judge.Profile',
|
"model": "judge.Profile",
|
||||||
'icon': 'fa-user-plus',
|
"icon": "fa-user-plus",
|
||||||
'children': [
|
"children": [
|
||||||
'judge.Organization',
|
"judge.Organization",
|
||||||
'judge.OrganizationRequest',
|
"judge.OrganizationRequest",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'model': 'judge.NavigationBar',
|
"model": "judge.NavigationBar",
|
||||||
'icon': 'fa-bars',
|
"icon": "fa-bars",
|
||||||
'children': [
|
"children": [
|
||||||
'judge.MiscConfig',
|
"judge.MiscConfig",
|
||||||
'judge.License',
|
"judge.License",
|
||||||
'sites.Site',
|
"sites.Site",
|
||||||
'redirects.Redirect',
|
"redirects.Redirect",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
('judge.BlogPost', 'fa-rss-square'),
|
("judge.BlogPost", "fa-rss-square"),
|
||||||
('judge.Comment', 'fa-comment-o'),
|
("judge.Ticket", "fa-exclamation-circle"),
|
||||||
('judge.Ticket', 'fa-exclamation-circle'),
|
("admin.LogEntry", "fa-empire"),
|
||||||
('flatpages.FlatPage', 'fa-file-text-o'),
|
|
||||||
('judge.Solution', 'fa-pencil'),
|
|
||||||
],
|
],
|
||||||
'dashboard': {
|
"dashboard": {
|
||||||
'breadcrumbs': True,
|
"breadcrumbs": True,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
INSTALLED_APPS += (
|
INSTALLED_APPS += (
|
||||||
'django.contrib.admin',
|
"django.contrib.admin",
|
||||||
'judge',
|
"judge",
|
||||||
'django.contrib.auth',
|
"django.contrib.auth",
|
||||||
'django.contrib.contenttypes',
|
"django.contrib.contenttypes",
|
||||||
'django.contrib.flatpages',
|
"django.contrib.flatpages",
|
||||||
'django.contrib.sessions',
|
"django.contrib.sessions",
|
||||||
'django.contrib.messages',
|
"django.contrib.messages",
|
||||||
'django.contrib.redirects',
|
"django.contrib.redirects",
|
||||||
'django.contrib.staticfiles',
|
"django.contrib.staticfiles",
|
||||||
'django.contrib.sites',
|
"django.contrib.sites",
|
||||||
'django.contrib.sitemaps',
|
"django.contrib.sitemaps",
|
||||||
'registration',
|
"registration",
|
||||||
'mptt',
|
"mptt",
|
||||||
'reversion',
|
"reversion",
|
||||||
'django_social_share',
|
"reversion_compare",
|
||||||
'social_django',
|
"django_social_share",
|
||||||
'compressor',
|
"social_django",
|
||||||
'django_ace',
|
"compressor",
|
||||||
'pagedown',
|
"django_ace",
|
||||||
'sortedm2m',
|
"pagedown",
|
||||||
'statici18n',
|
"sortedm2m",
|
||||||
'impersonate',
|
"statici18n",
|
||||||
'django_jinja',
|
"impersonate",
|
||||||
'chat_box',
|
"django_jinja",
|
||||||
'newsletter',
|
"chat_box",
|
||||||
'django.forms',
|
"django.forms",
|
||||||
)
|
)
|
||||||
|
|
||||||
MIDDLEWARE = (
|
MIDDLEWARE = (
|
||||||
'judge.middleware.ShortCircuitMiddleware',
|
"judge.middleware.SlowRequestMiddleware",
|
||||||
'django.middleware.common.CommonMiddleware',
|
"judge.middleware.ShortCircuitMiddleware",
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
'django.middleware.locale.LocaleMiddleware',
|
"django.middleware.locale.LocaleMiddleware",
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
"django.middleware.common.CommonMiddleware",
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
'judge.middleware.DMOJLoginMiddleware',
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
"judge.middleware.DMOJLoginMiddleware",
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
'judge.user_log.LogUserAccessMiddleware',
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
'judge.timezone.TimezoneMiddleware',
|
"judge.user_log.LogUserAccessMiddleware",
|
||||||
'impersonate.middleware.ImpersonateMiddleware',
|
"judge.timezone.TimezoneMiddleware",
|
||||||
'judge.middleware.DMOJImpersonationMiddleware',
|
"impersonate.middleware.ImpersonateMiddleware",
|
||||||
'judge.middleware.ContestMiddleware',
|
"judge.middleware.DMOJImpersonationMiddleware",
|
||||||
'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
|
"judge.middleware.ContestMiddleware",
|
||||||
'judge.social_auth.SocialAuthExceptionMiddleware',
|
"judge.middleware.DarkModeMiddleware",
|
||||||
'django.contrib.redirects.middleware.RedirectFallbackMiddleware',
|
"judge.middleware.SubdomainMiddleware",
|
||||||
|
"django.contrib.flatpages.middleware.FlatpageFallbackMiddleware",
|
||||||
|
"judge.social_auth.SocialAuthExceptionMiddleware",
|
||||||
|
"django.contrib.redirects.middleware.RedirectFallbackMiddleware",
|
||||||
)
|
)
|
||||||
|
|
||||||
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
|
X_FRAME_OPTIONS = "SAMEORIGIN"
|
||||||
|
|
||||||
IMPERSONATE_REQUIRE_SUPERUSER = True
|
LANGUAGE_COOKIE_AGE = 8640000
|
||||||
IMPERSONATE_DISABLE_LOGGING = True
|
|
||||||
|
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
|
||||||
|
|
||||||
|
IMPERSONATE = {
|
||||||
|
"REQUIRE_SUPERUSER": True,
|
||||||
|
"DISABLE_LOGGING": True,
|
||||||
|
"ADMIN_DELETE_PERMISSION": True,
|
||||||
|
}
|
||||||
|
|
||||||
ACCOUNT_ACTIVATION_DAYS = 7
|
ACCOUNT_ACTIVATION_DAYS = 7
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
"NAME": "judge.utils.pwned.PwnedPasswordsValidator",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'judge.utils.pwned.PwnedPasswordsValidator',
|
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
SILENCED_SYSTEM_CHECKS = ['urls.W002', 'fields.W342']
|
SILENCED_SYSTEM_CHECKS = ["urls.W002", "fields.W342"]
|
||||||
|
|
||||||
ROOT_URLCONF = 'dmoj.urls'
|
ROOT_URLCONF = "dmoj.urls"
|
||||||
LOGIN_REDIRECT_URL = '/user'
|
LOGIN_REDIRECT_URL = "/user"
|
||||||
WSGI_APPLICATION = 'dmoj.wsgi.application'
|
WSGI_APPLICATION = "dmoj.wsgi.application"
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django_jinja.backend.Jinja2',
|
"BACKEND": "django_jinja.backend.Jinja2",
|
||||||
'DIRS': [
|
"DIRS": [
|
||||||
os.path.join(BASE_DIR, 'templates'),
|
os.path.join(BASE_DIR, "templates"),
|
||||||
],
|
],
|
||||||
'APP_DIRS': False,
|
"APP_DIRS": False,
|
||||||
'OPTIONS': {
|
"OPTIONS": {
|
||||||
'match_extension': ('.html', '.txt'),
|
"match_extension": (".html", ".txt"),
|
||||||
'match_regex': '^(?!admin/)',
|
"match_regex": "^(?!admin/)",
|
||||||
'context_processors': [
|
"context_processors": [
|
||||||
'django.template.context_processors.media',
|
"django.template.context_processors.media",
|
||||||
'django.template.context_processors.tz',
|
"django.template.context_processors.tz",
|
||||||
'django.template.context_processors.i18n',
|
"django.template.context_processors.i18n",
|
||||||
'django.template.context_processors.request',
|
"django.template.context_processors.request",
|
||||||
'django.contrib.messages.context_processors.messages',
|
"django.contrib.messages.context_processors.messages",
|
||||||
'judge.template_context.comet_location',
|
"judge.template_context.comet_location",
|
||||||
'judge.template_context.get_resource',
|
"judge.template_context.get_resource",
|
||||||
'judge.template_context.general_info',
|
"judge.template_context.general_info",
|
||||||
'judge.template_context.site',
|
"judge.template_context.site",
|
||||||
'judge.template_context.site_name',
|
"judge.template_context.site_name",
|
||||||
'judge.template_context.misc_config',
|
"judge.template_context.misc_config",
|
||||||
'judge.template_context.math_setting',
|
"social_django.context_processors.backends",
|
||||||
'social_django.context_processors.backends',
|
"social_django.context_processors.login_redirect",
|
||||||
'social_django.context_processors.login_redirect',
|
|
||||||
],
|
],
|
||||||
'autoescape': select_autoescape(['html', 'xml']),
|
"autoescape": select_autoescape(["html", "xml"]),
|
||||||
'trim_blocks': True,
|
"trim_blocks": True,
|
||||||
'lstrip_blocks': True,
|
"lstrip_blocks": True,
|
||||||
'extensions': DEFAULT_EXTENSIONS + [
|
"extensions": DEFAULT_EXTENSIONS
|
||||||
'compressor.contrib.jinja2ext.CompressorExtension',
|
+ [
|
||||||
'judge.jinja2.DMOJExtension',
|
"compressor.contrib.jinja2ext.CompressorExtension",
|
||||||
'judge.jinja2.spaceless.SpacelessExtension',
|
"judge.jinja2.DMOJExtension",
|
||||||
|
"judge.jinja2.spaceless.SpacelessExtension",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
'APP_DIRS': True,
|
"APP_DIRS": True,
|
||||||
'DIRS': [
|
"DIRS": [
|
||||||
os.path.join(BASE_DIR, 'templates'),
|
os.path.join(BASE_DIR, "templates"),
|
||||||
],
|
],
|
||||||
'OPTIONS': {
|
"OPTIONS": {
|
||||||
'context_processors': [
|
"context_processors": [
|
||||||
'django.contrib.auth.context_processors.auth',
|
"django.contrib.auth.context_processors.auth",
|
||||||
'django.template.context_processors.media',
|
"django.template.context_processors.media",
|
||||||
'django.template.context_processors.tz',
|
"django.template.context_processors.tz",
|
||||||
'django.template.context_processors.i18n',
|
"django.template.context_processors.i18n",
|
||||||
'django.template.context_processors.request',
|
"django.template.context_processors.request",
|
||||||
'django.contrib.messages.context_processors.messages',
|
"django.contrib.messages.context_processors.messages",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
LOCALE_PATHS = [
|
LOCALE_PATHS = [
|
||||||
os.path.join(BASE_DIR, 'locale'),
|
os.path.join(BASE_DIR, "locale"),
|
||||||
]
|
]
|
||||||
|
|
||||||
LANGUAGES = [
|
LANGUAGES = [
|
||||||
('de', _('German')),
|
("vi", _("Vietnamese")),
|
||||||
('en', _('English')),
|
("en", _("English")),
|
||||||
('es', _('Spanish')),
|
|
||||||
('fr', _('French')),
|
|
||||||
('hr', _('Croatian')),
|
|
||||||
('hu', _('Hungarian')),
|
|
||||||
('ja', _('Japanese')),
|
|
||||||
('ko', _('Korean')),
|
|
||||||
('pt', _('Brazilian Portuguese')),
|
|
||||||
('ro', _('Romanian')),
|
|
||||||
('ru', _('Russian')),
|
|
||||||
('sr-latn', _('Serbian (Latin)')),
|
|
||||||
('tr', _('Turkish')),
|
|
||||||
('vi', _('Vietnamese')),
|
|
||||||
('zh-hans', _('Simplified Chinese')),
|
|
||||||
('zh-hant', _('Traditional Chinese')),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
MARKDOWN_ADMIN_EDITABLE_STYLE = {
|
|
||||||
'safe_mode': False,
|
|
||||||
'use_camo': True,
|
|
||||||
'texoid': True,
|
|
||||||
'math': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
MARKDOWN_DEFAULT_STYLE = {
|
|
||||||
'safe_mode': True,
|
|
||||||
'nofollow': True,
|
|
||||||
'use_camo': True,
|
|
||||||
'math': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
MARKDOWN_USER_LARGE_STYLE = {
|
|
||||||
'safe_mode': True,
|
|
||||||
'nofollow': True,
|
|
||||||
'use_camo': True,
|
|
||||||
'math': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
MARKDOWN_STYLES = {
|
|
||||||
'comment': MARKDOWN_DEFAULT_STYLE,
|
|
||||||
'self-description': MARKDOWN_USER_LARGE_STYLE,
|
|
||||||
'problem': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
|
||||||
'contest': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
|
||||||
'language': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
|
||||||
'license': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
|
||||||
'judge': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
|
||||||
'blog': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
|
||||||
'solution': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
|
||||||
'contest_tag': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
|
||||||
'organization-about': MARKDOWN_USER_LARGE_STYLE,
|
|
||||||
'ticket': MARKDOWN_USER_LARGE_STYLE,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
|
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
"default": {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ENABLE_FTS = False
|
ENABLE_FTS = False
|
||||||
|
|
||||||
# Bridged configuration
|
# Bridged configuration
|
||||||
BRIDGED_JUDGE_ADDRESS = [('localhost', 9999)]
|
BRIDGED_JUDGE_ADDRESS = [("localhost", 9999)]
|
||||||
BRIDGED_JUDGE_PROXIES = None
|
BRIDGED_JUDGE_PROXIES = None
|
||||||
BRIDGED_DJANGO_ADDRESS = [('localhost', 9998)]
|
BRIDGED_DJANGO_ADDRESS = [("localhost", 9998)]
|
||||||
BRIDGED_DJANGO_CONNECT = None
|
BRIDGED_DJANGO_CONNECT = None
|
||||||
|
BRIDGED_AUTO_CREATE_JUDGE = False
|
||||||
|
|
||||||
# Event Server configuration
|
# Event Server configuration
|
||||||
EVENT_DAEMON_USE = False
|
EVENT_DAEMON_USE = False
|
||||||
EVENT_DAEMON_POST = 'ws://localhost:9997/'
|
EVENT_DAEMON_POST = "ws://localhost:9997/"
|
||||||
EVENT_DAEMON_GET = 'ws://localhost:9996/'
|
EVENT_DAEMON_GET = "ws://localhost:9996/"
|
||||||
EVENT_DAEMON_POLL = '/channels/'
|
EVENT_DAEMON_POLL = "/channels/"
|
||||||
EVENT_DAEMON_KEY = None
|
EVENT_DAEMON_KEY = None
|
||||||
EVENT_DAEMON_AMQP_EXCHANGE = 'dmoj-events'
|
EVENT_DAEMON_AMQP_EXCHANGE = "dmoj-events"
|
||||||
EVENT_DAEMON_SUBMISSION_KEY = '6Sdmkx^%pk@GsifDfXcwX*Y7LRF%RGT8vmFpSxFBT$fwS7trc8raWfN#CSfQuKApx&$B#Gh2L7p%W!Ww'
|
EVENT_DAEMON_SUBMISSION_KEY = (
|
||||||
|
"6Sdmkx^%pk@GsifDfXcwX*Y7LRF%RGT8vmFpSxFBT$fwS7trc8raWfN#CSfQuKApx&$B#Gh2L7p%W!Ww"
|
||||||
|
)
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
||||||
|
|
||||||
# Whatever you do, this better be one of the entries in `LANGUAGES`.
|
# Whatever you do, this better be one of the entries in `LANGUAGES`.
|
||||||
LANGUAGE_CODE = 'vi'
|
LANGUAGE_CODE = "vi"
|
||||||
TIME_ZONE = 'Asia/Ho_Chi_Minh'
|
TIME_ZONE = "Asia/Ho_Chi_Minh"
|
||||||
DEFAULT_USER_TIME_ZONE = 'Asia/Ho_Chi_Minh'
|
DEFAULT_USER_TIME_ZONE = "Asia/Ho_Chi_Minh"
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
USE_L10N = True
|
USE_L10N = True
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
# Cookies
|
# Cookies
|
||||||
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
|
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
||||||
|
|
||||||
DMOJ_RESOURCES = os.path.join(BASE_DIR, 'resources')
|
DMOJ_RESOURCES = os.path.join(BASE_DIR, "resources")
|
||||||
STATICFILES_FINDERS = (
|
STATICFILES_FINDERS = (
|
||||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||||
)
|
)
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
os.path.join(BASE_DIR, 'resources'),
|
os.path.join(BASE_DIR, "resources"),
|
||||||
]
|
]
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = "/static/"
|
||||||
|
|
||||||
# Define a cache
|
# Define a cache
|
||||||
CACHES = {}
|
CACHES = {}
|
||||||
|
|
||||||
# Authentication
|
# Authentication
|
||||||
AUTHENTICATION_BACKENDS = (
|
AUTHENTICATION_BACKENDS = (
|
||||||
'social_core.backends.google.GoogleOAuth2',
|
"social_core.backends.google.GoogleOAuth2",
|
||||||
'social_core.backends.facebook.FacebookOAuth2',
|
"social_core.backends.facebook.FacebookOAuth2",
|
||||||
'judge.social_auth.GitHubSecureEmailOAuth2',
|
"judge.social_auth.GitHubSecureEmailOAuth2",
|
||||||
'django.contrib.auth.backends.ModelBackend',
|
"judge.authentication.CustomModelBackend",
|
||||||
)
|
)
|
||||||
|
|
||||||
SOCIAL_AUTH_PIPELINE = (
|
SOCIAL_AUTH_PIPELINE = (
|
||||||
'social_core.pipeline.social_auth.social_details',
|
"social_core.pipeline.social_auth.social_details",
|
||||||
'social_core.pipeline.social_auth.social_uid',
|
"social_core.pipeline.social_auth.social_uid",
|
||||||
'social_core.pipeline.social_auth.auth_allowed',
|
"social_core.pipeline.social_auth.auth_allowed",
|
||||||
'judge.social_auth.verify_email',
|
"judge.social_auth.verify_email",
|
||||||
'social_core.pipeline.social_auth.social_user',
|
"social_core.pipeline.social_auth.social_user",
|
||||||
'social_core.pipeline.user.get_username',
|
"social_core.pipeline.user.get_username",
|
||||||
'social_core.pipeline.social_auth.associate_by_email',
|
"social_core.pipeline.social_auth.associate_by_email",
|
||||||
'judge.social_auth.choose_username',
|
"judge.social_auth.choose_username",
|
||||||
'social_core.pipeline.user.create_user',
|
"social_core.pipeline.user.create_user",
|
||||||
'judge.social_auth.make_profile',
|
"judge.social_auth.make_profile",
|
||||||
'social_core.pipeline.social_auth.associate_user',
|
"social_core.pipeline.social_auth.associate_user",
|
||||||
'social_core.pipeline.social_auth.load_extra_data',
|
"social_core.pipeline.social_auth.load_extra_data",
|
||||||
'social_core.pipeline.user.user_details',
|
"social_core.pipeline.user.user_details",
|
||||||
)
|
)
|
||||||
|
|
||||||
SOCIAL_AUTH_PROTECTED_USER_FIELDS = ['first_name', 'last_name']
|
SOCIAL_AUTH_PROTECTED_USER_FIELDS = ["first_name", "last_name"]
|
||||||
SOCIAL_AUTH_GOOGLE_OAUTH2_USER_FIELDS = ['email', 'username']
|
SOCIAL_AUTH_GOOGLE_OAUTH2_USER_FIELDS = ["email", "username"]
|
||||||
SOCIAL_AUTH_GITHUB_SECURE_SCOPE = ['user:email']
|
SOCIAL_AUTH_GITHUB_SECURE_SCOPE = ["user:email"]
|
||||||
SOCIAL_AUTH_FACEBOOK_SCOPE = ['email']
|
SOCIAL_AUTH_FACEBOOK_SCOPE = ["email"]
|
||||||
SOCIAL_AUTH_SLUGIFY_USERNAMES = True
|
SOCIAL_AUTH_SLUGIFY_USERNAMES = True
|
||||||
SOCIAL_AUTH_SLUGIFY_FUNCTION = 'judge.social_auth.slugify_username'
|
SOCIAL_AUTH_SLUGIFY_FUNCTION = "judge.social_auth.slugify_username"
|
||||||
|
|
||||||
JUDGE_AMQP_PATH = None
|
JUDGE_AMQP_PATH = None
|
||||||
|
|
||||||
|
@ -512,22 +473,29 @@ FILE_UPLOAD_PERMISSIONS = 0o644
|
||||||
|
|
||||||
MESSAGES_TO_LOAD = 15
|
MESSAGES_TO_LOAD = 15
|
||||||
|
|
||||||
NEWSLETTER_CONFIRM_EMAIL = False
|
ML_OUTPUT_PATH = None
|
||||||
|
|
||||||
# Amount of seconds to wait between each email. Here 100ms is used.
|
# Use subdomain for organizations
|
||||||
NEWSLETTER_EMAIL_DELAY = 0.1
|
USE_SUBDOMAIN = False
|
||||||
|
|
||||||
# Amount of seconds to wait between each batch. Here one minute is used.
|
# Chat
|
||||||
NEWSLETTER_BATCH_DELAY = 60
|
CHAT_SECRET_KEY = "QUdVFsxk6f5-Hd8g9BXv81xMqvIZFRqMl-KbRzztW-U="
|
||||||
|
|
||||||
# Number of emails in one batch
|
# Nginx
|
||||||
NEWSLETTER_BATCH_SIZE = 100
|
META_REMOTE_ADDRESS_KEY = "REMOTE_ADDR"
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||||
|
|
||||||
|
# Chunk upload
|
||||||
|
CHUNK_UPLOAD_DIR = "/tmp/chunk_upload_tmp"
|
||||||
|
|
||||||
|
# Rate limit
|
||||||
|
RL_VOTE = "200/h"
|
||||||
|
RL_COMMENT = "30/h"
|
||||||
|
|
||||||
# Google form to request name
|
|
||||||
REGISTER_NAME_URL = None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(os.path.join(os.path.dirname(__file__), 'local_settings.py')) as f:
|
with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f:
|
||||||
exec(f.read(), globals())
|
exec(f.read(), globals())
|
||||||
except IOError:
|
except IOError:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -8,8 +8,8 @@ DEFAULT_THROTTLE = (10, 60)
|
||||||
|
|
||||||
|
|
||||||
def new_email():
|
def new_email():
|
||||||
cache.add('error_email_throttle', 0, settings.DMOJ_EMAIL_THROTTLING[1])
|
cache.add("error_email_throttle", 0, settings.DMOJ_EMAIL_THROTTLING[1])
|
||||||
return cache.incr('error_email_throttle')
|
return cache.incr("error_email_throttle")
|
||||||
|
|
||||||
|
|
||||||
class ThrottledEmailHandler(AdminEmailHandler):
|
class ThrottledEmailHandler(AdminEmailHandler):
|
||||||
|
|
1599
dmoj/urls.py
1599
dmoj/urls.py
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,6 @@
|
||||||
import os
|
import os
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings')
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import MySQLdb # noqa: F401, imported for side effect
|
import MySQLdb # noqa: F401, imported for side effect
|
||||||
|
@ -8,5 +9,8 @@ except ImportError:
|
||||||
|
|
||||||
pymysql.install_as_MySQLdb()
|
pymysql.install_as_MySQLdb()
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application # noqa: E402, django must be imported here
|
from django.core.wsgi import (
|
||||||
|
get_wsgi_application,
|
||||||
|
) # noqa: E402, django must be imported here
|
||||||
|
|
||||||
application = get_wsgi_application()
|
application = get_wsgi_application()
|
||||||
|
|
|
@ -2,13 +2,17 @@ import os
|
||||||
|
|
||||||
import gevent.monkey # noqa: I100, gevent must be imported here
|
import gevent.monkey # noqa: I100, gevent must be imported here
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings')
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings")
|
||||||
gevent.monkey.patch_all()
|
gevent.monkey.patch_all()
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
import dmoj_install_pymysql # noqa: F401, I100, I202, imported for side effect
|
import dmoj_install_pymysql # noqa: F401, I100, I202, imported for side effect
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application # noqa: E402, I100, I202, django must be imported here
|
from django.core.wsgi import (
|
||||||
|
get_wsgi_application,
|
||||||
|
) # noqa: E402, I100, I202, django must be imported here
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
import django_2_2_pymysql_patch # noqa: I100, F401, I202, imported for side effect
|
import django_2_2_pymysql_patch # noqa: I100, F401, I202, imported for side effect
|
||||||
|
|
||||||
application = get_wsgi_application()
|
application = get_wsgi_application()
|
||||||
|
|
|
@ -2,19 +2,22 @@ import os
|
||||||
|
|
||||||
import gevent.monkey # noqa: I100, gevent must be imported here
|
import gevent.monkey # noqa: I100, gevent must be imported here
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings')
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings")
|
||||||
gevent.monkey.patch_all()
|
gevent.monkey.patch_all()
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
import dmoj_install_pymysql # noqa: E402, F401, I100, I202, imported for side effect
|
import dmoj_install_pymysql # noqa: E402, F401, I100, I202, imported for side effect
|
||||||
|
|
||||||
import django # noqa: E402, F401, I100, I202, django must be imported here
|
import django # noqa: E402, F401, I100, I202, django must be imported here
|
||||||
|
|
||||||
django.setup()
|
django.setup()
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
import django_2_2_pymysql_patch # noqa: E402, I100, F401, I202, imported for side effect
|
import django_2_2_pymysql_patch # noqa: E402, I100, F401, I202, imported for side effect
|
||||||
|
|
||||||
from judge.bridge.daemon import judge_daemon # noqa: E402, I100, I202, django code must be imported here
|
from judge.bridge.daemon import (
|
||||||
|
judge_daemon,
|
||||||
|
) # noqa: E402, I100, I202, django code must be imported here
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
judge_daemon()
|
judge_daemon()
|
||||||
|
|
|
@ -6,7 +6,7 @@ except ImportError:
|
||||||
import dmoj_install_pymysql # noqa: F401, imported for side effect
|
import dmoj_install_pymysql # noqa: F401, imported for side effect
|
||||||
|
|
||||||
# set the default Django settings module for the 'celery' program.
|
# set the default Django settings module for the 'celery' program.
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings')
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings")
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
import django_2_2_pymysql_patch # noqa: I100, F401, I202, imported for side effect
|
import django_2_2_pymysql_patch # noqa: I100, F401, I202, imported for side effect
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import pymysql
|
import pymysql
|
||||||
|
|
||||||
pymysql.install_as_MySQLdb()
|
pymysql.install_as_MySQLdb()
|
||||||
pymysql.version_info = (1, 3, 13, "final", 0)
|
pymysql.version_info = (1, 4, 0, "final", 0)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
default_app_config = 'judge.apps.JudgeAppConfig'
|
default_app_config = "judge.apps.JudgeAppConfig"
|
||||||
|
|
|
@ -1,19 +1,62 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.admin.models import LogEntry
|
from django.contrib.admin.models import LogEntry
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
from judge.admin.comments import CommentAdmin
|
from judge.admin.comments import CommentAdmin
|
||||||
from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin
|
from judge.admin.contest import (
|
||||||
from judge.admin.interface import BlogPostAdmin, LicenseAdmin, LogEntryAdmin, NavigationBarAdmin
|
ContestAdmin,
|
||||||
|
ContestParticipationAdmin,
|
||||||
|
ContestTagAdmin,
|
||||||
|
ContestsSummaryAdmin,
|
||||||
|
)
|
||||||
|
from judge.admin.interface import (
|
||||||
|
BlogPostAdmin,
|
||||||
|
LicenseAdmin,
|
||||||
|
LogEntryAdmin,
|
||||||
|
NavigationBarAdmin,
|
||||||
|
)
|
||||||
from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin
|
from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin
|
||||||
from judge.admin.problem import ProblemAdmin
|
from judge.admin.problem import ProblemAdmin, ProblemPointsVoteAdmin
|
||||||
from judge.admin.profile import ProfileAdmin
|
from judge.admin.profile import ProfileAdmin, UserAdmin
|
||||||
from judge.admin.runtime import JudgeAdmin, LanguageAdmin
|
from judge.admin.runtime import JudgeAdmin, LanguageAdmin
|
||||||
from judge.admin.submission import SubmissionAdmin
|
from judge.admin.submission import SubmissionAdmin
|
||||||
from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin
|
from judge.admin.taxon import (
|
||||||
|
ProblemGroupAdmin,
|
||||||
|
ProblemTypeAdmin,
|
||||||
|
OfficialContestCategoryAdmin,
|
||||||
|
OfficialContestLocationAdmin,
|
||||||
|
)
|
||||||
from judge.admin.ticket import TicketAdmin
|
from judge.admin.ticket import TicketAdmin
|
||||||
from judge.models import BlogPost, Comment, CommentLock, Contest, ContestParticipation, \
|
from judge.admin.volunteer import VolunteerProblemVoteAdmin
|
||||||
ContestTag, Judge, Language, License, MiscConfig, NavigationBar, Organization, \
|
from judge.admin.course import CourseAdmin
|
||||||
OrganizationRequest, Problem, ProblemGroup, ProblemType, Profile, Submission, Ticket
|
from judge.models import (
|
||||||
|
BlogPost,
|
||||||
|
Comment,
|
||||||
|
CommentLock,
|
||||||
|
Contest,
|
||||||
|
ContestParticipation,
|
||||||
|
ContestTag,
|
||||||
|
Judge,
|
||||||
|
Language,
|
||||||
|
License,
|
||||||
|
MiscConfig,
|
||||||
|
NavigationBar,
|
||||||
|
Organization,
|
||||||
|
OrganizationRequest,
|
||||||
|
Problem,
|
||||||
|
ProblemGroup,
|
||||||
|
ProblemPointsVote,
|
||||||
|
ProblemType,
|
||||||
|
Profile,
|
||||||
|
Submission,
|
||||||
|
Ticket,
|
||||||
|
VolunteerProblemVote,
|
||||||
|
Course,
|
||||||
|
ContestsSummary,
|
||||||
|
OfficialContestCategory,
|
||||||
|
OfficialContestLocation,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(BlogPost, BlogPostAdmin)
|
admin.site.register(BlogPost, BlogPostAdmin)
|
||||||
admin.site.register(Comment, CommentAdmin)
|
admin.site.register(Comment, CommentAdmin)
|
||||||
|
@ -31,7 +74,15 @@ admin.site.register(Organization, OrganizationAdmin)
|
||||||
admin.site.register(OrganizationRequest, OrganizationRequestAdmin)
|
admin.site.register(OrganizationRequest, OrganizationRequestAdmin)
|
||||||
admin.site.register(Problem, ProblemAdmin)
|
admin.site.register(Problem, ProblemAdmin)
|
||||||
admin.site.register(ProblemGroup, ProblemGroupAdmin)
|
admin.site.register(ProblemGroup, ProblemGroupAdmin)
|
||||||
|
admin.site.register(ProblemPointsVote, ProblemPointsVoteAdmin)
|
||||||
admin.site.register(ProblemType, ProblemTypeAdmin)
|
admin.site.register(ProblemType, ProblemTypeAdmin)
|
||||||
admin.site.register(Profile, ProfileAdmin)
|
admin.site.register(Profile, ProfileAdmin)
|
||||||
admin.site.register(Submission, SubmissionAdmin)
|
admin.site.register(Submission, SubmissionAdmin)
|
||||||
admin.site.register(Ticket, TicketAdmin)
|
admin.site.register(Ticket, TicketAdmin)
|
||||||
|
admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin)
|
||||||
|
admin.site.register(Course, CourseAdmin)
|
||||||
|
admin.site.unregister(User)
|
||||||
|
admin.site.register(User, UserAdmin)
|
||||||
|
admin.site.register(ContestsSummary, ContestsSummaryAdmin)
|
||||||
|
admin.site.register(OfficialContestCategory, OfficialContestCategoryAdmin)
|
||||||
|
admin.site.register(OfficialContestLocation, OfficialContestLocationAdmin)
|
||||||
|
|
|
@ -11,52 +11,71 @@ from judge.widgets import AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidg
|
||||||
class CommentForm(ModelForm):
|
class CommentForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
'author': AdminHeavySelect2Widget(data_view='profile_select2'),
|
"author": AdminHeavySelect2Widget(data_view="profile_select2"),
|
||||||
'parent': AdminHeavySelect2Widget(data_view='comment_select2'),
|
|
||||||
}
|
}
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets['body'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('comment_preview'))
|
widgets["body"] = HeavyPreviewAdminPageDownWidget(
|
||||||
|
preview=reverse_lazy("comment_preview")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CommentAdmin(VersionAdmin):
|
class CommentAdmin(VersionAdmin):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {'fields': ('author', 'page', 'parent', 'score', 'hidden')}),
|
(
|
||||||
('Content', {'fields': ('body',)}),
|
None,
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"author",
|
||||||
|
"parent",
|
||||||
|
"score",
|
||||||
|
"hidden",
|
||||||
|
"content_type",
|
||||||
|
"object_id",
|
||||||
)
|
)
|
||||||
list_display = ['author', 'linked_page', 'time']
|
},
|
||||||
search_fields = ['author__user__username', 'page', 'body']
|
),
|
||||||
actions = ['hide_comment', 'unhide_comment']
|
("Content", {"fields": ("body",)}),
|
||||||
list_filter = ['hidden']
|
)
|
||||||
|
list_display = ["author", "linked_object", "time"]
|
||||||
|
search_fields = ["author__user__username", "body"]
|
||||||
|
readonly_fields = ["score", "parent"]
|
||||||
|
actions = ["hide_comment", "unhide_comment"]
|
||||||
|
list_filter = ["hidden"]
|
||||||
actions_on_top = True
|
actions_on_top = True
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
form = CommentForm
|
form = CommentForm
|
||||||
date_hierarchy = 'time'
|
date_hierarchy = "time"
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
return Comment.objects.order_by('-time')
|
return Comment.objects.order_by("-time")
|
||||||
|
|
||||||
def hide_comment(self, request, queryset):
|
def hide_comment(self, request, queryset):
|
||||||
count = queryset.update(hidden=True)
|
count = queryset.update(hidden=True)
|
||||||
self.message_user(request, ungettext('%d comment successfully hidden.',
|
self.message_user(
|
||||||
'%d comments successfully hidden.',
|
request,
|
||||||
count) % count)
|
ungettext(
|
||||||
hide_comment.short_description = _('Hide comments')
|
"%d comment successfully hidden.",
|
||||||
|
"%d comments successfully hidden.",
|
||||||
|
count,
|
||||||
|
)
|
||||||
|
% count,
|
||||||
|
)
|
||||||
|
|
||||||
|
hide_comment.short_description = _("Hide comments")
|
||||||
|
|
||||||
def unhide_comment(self, request, queryset):
|
def unhide_comment(self, request, queryset):
|
||||||
count = queryset.update(hidden=False)
|
count = queryset.update(hidden=False)
|
||||||
self.message_user(request, ungettext('%d comment successfully unhidden.',
|
self.message_user(
|
||||||
'%d comments successfully unhidden.',
|
request,
|
||||||
count) % count)
|
ungettext(
|
||||||
unhide_comment.short_description = _('Unhide comments')
|
"%d comment successfully unhidden.",
|
||||||
|
"%d comments successfully unhidden.",
|
||||||
|
count,
|
||||||
|
)
|
||||||
|
% count,
|
||||||
|
)
|
||||||
|
|
||||||
def linked_page(self, obj):
|
unhide_comment.short_description = _("Unhide comments")
|
||||||
link = obj.link
|
|
||||||
if link is not None:
|
|
||||||
return format_html('<a href="{0}">{1}</a>', link, obj.page)
|
|
||||||
else:
|
|
||||||
return format_html('{0}', obj.page)
|
|
||||||
linked_page.short_description = _('Associated page')
|
|
||||||
linked_page.admin_order_field = 'page'
|
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
super(CommentAdmin, self).save_model(request, obj, form, change)
|
super(CommentAdmin, self).save_model(request, obj, form, change)
|
||||||
|
|
|
@ -3,7 +3,7 @@ from django.contrib import admin
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db import connection, transaction
|
from django.db import connection, transaction
|
||||||
from django.db.models import Q, TextField
|
from django.db.models import Q, TextField
|
||||||
from django.forms import ModelForm, ModelMultipleChoiceField
|
from django.forms import ModelForm, ModelMultipleChoiceField, TextInput
|
||||||
from django.http import Http404, HttpResponseRedirect
|
from django.http import Http404, HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
|
@ -11,12 +11,28 @@ from django.utils import timezone
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.translation import gettext_lazy as _, ungettext
|
from django.utils.translation import gettext_lazy as _, ungettext
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
|
from reversion_compare.admin import CompareVersionAdmin
|
||||||
|
|
||||||
from django_ace import AceWidget
|
from django_ace import AceWidget
|
||||||
from judge.models import Contest, ContestProblem, ContestSubmission, Profile, Rating
|
from judge.models import (
|
||||||
|
Contest,
|
||||||
|
ContestProblem,
|
||||||
|
ContestSubmission,
|
||||||
|
Profile,
|
||||||
|
Rating,
|
||||||
|
OfficialContest,
|
||||||
|
)
|
||||||
from judge.ratings import rate_contest
|
from judge.ratings import rate_contest
|
||||||
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, AdminPagedownWidget, \
|
from judge.widgets import (
|
||||||
AdminSelect2MultipleWidget, AdminSelect2Widget, HeavyPreviewAdminPageDownWidget
|
AdminHeavySelect2MultipleWidget,
|
||||||
|
AdminHeavySelect2Widget,
|
||||||
|
AdminPagedownWidget,
|
||||||
|
AdminSelect2MultipleWidget,
|
||||||
|
AdminSelect2Widget,
|
||||||
|
HeavyPreviewAdminPageDownWidget,
|
||||||
|
)
|
||||||
|
from judge.views.contests import recalculate_contest_summary_result
|
||||||
|
from judge.utils.contest import maybe_trigger_contest_rescore
|
||||||
|
|
||||||
|
|
||||||
class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
|
class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
|
||||||
|
@ -27,247 +43,394 @@ class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
|
||||||
|
|
||||||
class ContestTagForm(ModelForm):
|
class ContestTagForm(ModelForm):
|
||||||
contests = ModelMultipleChoiceField(
|
contests = ModelMultipleChoiceField(
|
||||||
label=_('Included contests'),
|
label=_("Included contests"),
|
||||||
queryset=Contest.objects.all(),
|
queryset=Contest.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=AdminHeavySelect2MultipleWidget(data_view='contest_select2'))
|
widget=AdminHeavySelect2MultipleWidget(data_view="contest_select2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ContestTagAdmin(admin.ModelAdmin):
|
class ContestTagAdmin(admin.ModelAdmin):
|
||||||
fields = ('name', 'color', 'description', 'contests')
|
fields = ("name", "color", "description", "contests")
|
||||||
list_display = ('name', 'color')
|
list_display = ("name", "color")
|
||||||
actions_on_top = True
|
actions_on_top = True
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
form = ContestTagForm
|
form = ContestTagForm
|
||||||
|
|
||||||
if AdminPagedownWidget is not None:
|
if AdminPagedownWidget is not None:
|
||||||
formfield_overrides = {
|
formfield_overrides = {
|
||||||
TextField: {'widget': AdminPagedownWidget},
|
TextField: {"widget": AdminPagedownWidget},
|
||||||
}
|
}
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
super(ContestTagAdmin, self).save_model(request, obj, form, change)
|
super(ContestTagAdmin, self).save_model(request, obj, form, change)
|
||||||
obj.contests.set(form.cleaned_data['contests'])
|
obj.contests.set(form.cleaned_data["contests"])
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
form = super(ContestTagAdmin, self).get_form(request, obj, **kwargs)
|
form = super(ContestTagAdmin, self).get_form(request, obj, **kwargs)
|
||||||
if obj is not None:
|
if obj is not None:
|
||||||
form.base_fields['contests'].initial = obj.contests.all()
|
form.base_fields["contests"].initial = obj.contests.all()
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
|
||||||
class ContestProblemInlineForm(ModelForm):
|
class ContestProblemInlineForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {'problem': AdminHeavySelect2Widget(data_view='problem_select2')}
|
widgets = {
|
||||||
|
"problem": AdminHeavySelect2Widget(data_view="problem_select2"),
|
||||||
|
"hidden_subtasks": TextInput(attrs={"size": "3"}),
|
||||||
|
"points": TextInput(attrs={"size": "1"}),
|
||||||
|
"order": TextInput(attrs={"size": "1"}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ContestProblemInline(admin.TabularInline):
|
class ContestProblemInline(admin.TabularInline):
|
||||||
model = ContestProblem
|
model = ContestProblem
|
||||||
verbose_name = _('Problem')
|
verbose_name = _("Problem")
|
||||||
verbose_name_plural = 'Problems'
|
verbose_name_plural = "Problems"
|
||||||
fields = ('problem', 'points', 'partial', 'is_pretested', 'max_submissions', 'output_prefix_override', 'order',
|
fields = (
|
||||||
'rejudge_column')
|
"problem",
|
||||||
readonly_fields = ('rejudge_column',)
|
"points",
|
||||||
|
"partial",
|
||||||
|
"is_pretested",
|
||||||
|
"max_submissions",
|
||||||
|
"hidden_subtasks",
|
||||||
|
"show_testcases",
|
||||||
|
"order",
|
||||||
|
"rejudge_column",
|
||||||
|
)
|
||||||
|
readonly_fields = ("rejudge_column",)
|
||||||
form = ContestProblemInlineForm
|
form = ContestProblemInlineForm
|
||||||
|
|
||||||
def rejudge_column(self, obj):
|
def rejudge_column(self, obj):
|
||||||
if obj.id is None:
|
if obj.id is None:
|
||||||
return ''
|
return ""
|
||||||
return format_html('<a class="button rejudge-link" href="{}">Rejudge</a>',
|
return format_html(
|
||||||
reverse('admin:judge_contest_rejudge', args=(obj.contest.id, obj.id)))
|
'<a class="button rejudge-link" href="{}">Rejudge</a>',
|
||||||
rejudge_column.short_description = ''
|
reverse("admin:judge_contest_rejudge", args=(obj.contest.id, obj.id)),
|
||||||
|
)
|
||||||
|
|
||||||
|
rejudge_column.short_description = ""
|
||||||
|
|
||||||
|
|
||||||
class ContestForm(ModelForm):
|
class ContestForm(ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(ContestForm, self).__init__(*args, **kwargs)
|
super(ContestForm, self).__init__(*args, **kwargs)
|
||||||
if 'rate_exclude' in self.fields:
|
if "rate_exclude" in self.fields:
|
||||||
if self.instance and self.instance.id:
|
if self.instance and self.instance.id:
|
||||||
self.fields['rate_exclude'].queryset = \
|
self.fields["rate_exclude"].queryset = Profile.objects.filter(
|
||||||
Profile.objects.filter(contest_history__contest=self.instance).distinct()
|
contest_history__contest=self.instance
|
||||||
|
).distinct()
|
||||||
else:
|
else:
|
||||||
self.fields['rate_exclude'].queryset = Profile.objects.none()
|
self.fields["rate_exclude"].queryset = Profile.objects.none()
|
||||||
self.fields['banned_users'].widget.can_add_related = False
|
self.fields["banned_users"].widget.can_add_related = False
|
||||||
self.fields['view_contest_scoreboard'].widget.can_add_related = False
|
self.fields["view_contest_scoreboard"].widget.can_add_related = False
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super(ContestForm, self).clean()
|
cleaned_data = super(ContestForm, self).clean()
|
||||||
cleaned_data['banned_users'].filter(current_contest__contest=self.instance).update(current_contest=None)
|
cleaned_data["banned_users"].filter(
|
||||||
|
current_contest__contest=self.instance
|
||||||
|
).update(current_contest=None)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
|
"authors": AdminHeavySelect2MultipleWidget(data_view="profile_select2"),
|
||||||
'curators': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
|
"curators": AdminHeavySelect2MultipleWidget(data_view="profile_select2"),
|
||||||
'testers': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
|
"testers": AdminHeavySelect2MultipleWidget(data_view="profile_select2"),
|
||||||
'private_contestants': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
|
"private_contestants": AdminHeavySelect2MultipleWidget(
|
||||||
attrs={'style': 'width: 100%'}),
|
data_view="profile_select2", attrs={"style": "width: 100%"}
|
||||||
'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2'),
|
),
|
||||||
'tags': AdminSelect2MultipleWidget,
|
"organizations": AdminHeavySelect2MultipleWidget(
|
||||||
'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
|
data_view="organization_select2"
|
||||||
attrs={'style': 'width: 100%'}),
|
),
|
||||||
'view_contest_scoreboard': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
|
"tags": AdminSelect2MultipleWidget,
|
||||||
attrs={'style': 'width: 100%'}),
|
"banned_users": AdminHeavySelect2MultipleWidget(
|
||||||
|
data_view="profile_select2", attrs={"style": "width: 100%"}
|
||||||
|
),
|
||||||
|
"view_contest_scoreboard": AdminHeavySelect2MultipleWidget(
|
||||||
|
data_view="profile_select2", attrs={"style": "width: 100%"}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets['description'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('contest_preview'))
|
widgets["description"] = HeavyPreviewAdminPageDownWidget(
|
||||||
|
preview=reverse_lazy("contest_preview")
|
||||||
|
|
||||||
class ContestAdmin(VersionAdmin):
|
|
||||||
fieldsets = (
|
|
||||||
(None, {'fields': ('key', 'name', 'authors', 'curators', 'testers')}),
|
|
||||||
(_('Settings'), {'fields': ('is_visible', 'use_clarifications', 'hide_problem_tags', 'scoreboard_visibility',
|
|
||||||
'run_pretests_only', 'points_precision')}),
|
|
||||||
(_('Scheduling'), {'fields': ('start_time', 'end_time', 'time_limit')}),
|
|
||||||
(_('Details'), {'fields': ('description', 'og_image', 'logo_override_image', 'tags', 'summary')}),
|
|
||||||
(_('Format'), {'fields': ('format_name', 'format_config', 'problem_label_script')}),
|
|
||||||
(_('Rating'), {'fields': ('is_rated', 'rate_all', 'rating_floor', 'rating_ceiling', 'rate_exclude')}),
|
|
||||||
(_('Access'), {'fields': ('access_code', 'is_private', 'private_contestants', 'is_organization_private',
|
|
||||||
'organizations', 'view_contest_scoreboard')}),
|
|
||||||
(_('Justice'), {'fields': ('banned_users',)}),
|
|
||||||
)
|
)
|
||||||
list_display = ('key', 'name', 'is_visible', 'is_rated', 'start_time', 'end_time', 'time_limit', 'user_count')
|
|
||||||
search_fields = ('key', 'name')
|
|
||||||
inlines = [ContestProblemInline]
|
class OfficialContestInlineForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
widgets = {
|
||||||
|
"category": AdminSelect2Widget,
|
||||||
|
"location": AdminSelect2Widget,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class OfficialContestInline(admin.StackedInline):
|
||||||
|
fields = (
|
||||||
|
"category",
|
||||||
|
"year",
|
||||||
|
"location",
|
||||||
|
)
|
||||||
|
model = OfficialContest
|
||||||
|
can_delete = True
|
||||||
|
form = OfficialContestInlineForm
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
|
class ContestAdmin(CompareVersionAdmin):
|
||||||
|
fieldsets = (
|
||||||
|
(None, {"fields": ("key", "name", "authors", "curators", "testers")}),
|
||||||
|
(
|
||||||
|
_("Settings"),
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"is_visible",
|
||||||
|
"use_clarifications",
|
||||||
|
"hide_problem_tags",
|
||||||
|
"public_scoreboard",
|
||||||
|
"scoreboard_visibility",
|
||||||
|
"run_pretests_only",
|
||||||
|
"points_precision",
|
||||||
|
"rate_limit",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
_("Scheduling"),
|
||||||
|
{"fields": ("start_time", "end_time", "time_limit", "freeze_after")},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
_("Details"),
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"description",
|
||||||
|
"og_image",
|
||||||
|
"logo_override_image",
|
||||||
|
"tags",
|
||||||
|
"summary",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
_("Format"),
|
||||||
|
{"fields": ("format_name", "format_config", "problem_label_script")},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
_("Rating"),
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"is_rated",
|
||||||
|
"rate_all",
|
||||||
|
"rating_floor",
|
||||||
|
"rating_ceiling",
|
||||||
|
"rate_exclude",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
_("Access"),
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"access_code",
|
||||||
|
"private_contestants",
|
||||||
|
"organizations",
|
||||||
|
"view_contest_scoreboard",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(_("Justice"), {"fields": ("banned_users",)}),
|
||||||
|
)
|
||||||
|
list_display = (
|
||||||
|
"key",
|
||||||
|
"name",
|
||||||
|
"is_visible",
|
||||||
|
"is_rated",
|
||||||
|
"start_time",
|
||||||
|
"end_time",
|
||||||
|
"time_limit",
|
||||||
|
"user_count",
|
||||||
|
)
|
||||||
|
search_fields = ("key", "name")
|
||||||
|
inlines = [ContestProblemInline, OfficialContestInline]
|
||||||
actions_on_top = True
|
actions_on_top = True
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
form = ContestForm
|
form = ContestForm
|
||||||
change_list_template = 'admin/judge/contest/change_list.html'
|
change_list_template = "admin/judge/contest/change_list.html"
|
||||||
filter_horizontal = ['rate_exclude']
|
filter_horizontal = ["rate_exclude"]
|
||||||
date_hierarchy = 'start_time'
|
date_hierarchy = "start_time"
|
||||||
|
|
||||||
def get_actions(self, request):
|
def get_actions(self, request):
|
||||||
actions = super(ContestAdmin, self).get_actions(request)
|
actions = super(ContestAdmin, self).get_actions(request)
|
||||||
|
|
||||||
if request.user.has_perm('judge.change_contest_visibility') or \
|
if request.user.has_perm(
|
||||||
request.user.has_perm('judge.create_private_contest'):
|
"judge.change_contest_visibility"
|
||||||
for action in ('make_visible', 'make_hidden'):
|
) or request.user.has_perm("judge.create_private_contest"):
|
||||||
|
for action in ("make_visible", "make_hidden"):
|
||||||
actions[action] = self.get_action(action)
|
actions[action] = self.get_action(action)
|
||||||
|
|
||||||
return actions
|
return actions
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
queryset = Contest.objects.all()
|
queryset = Contest.objects.all()
|
||||||
if request.user.has_perm('judge.edit_all_contest'):
|
if request.user.has_perm("judge.edit_all_contest"):
|
||||||
return queryset
|
return queryset
|
||||||
else:
|
else:
|
||||||
return queryset.filter(Q(authors=request.profile) | Q(curators=request.profile)).distinct()
|
return queryset.filter(
|
||||||
|
Q(authors=request.profile) | Q(curators=request.profile)
|
||||||
|
).distinct()
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
readonly = []
|
readonly = []
|
||||||
if not request.user.has_perm('judge.contest_rating'):
|
if not request.user.has_perm("judge.contest_rating"):
|
||||||
readonly += ['is_rated', 'rate_all', 'rate_exclude']
|
readonly += ["is_rated", "rate_all", "rate_exclude"]
|
||||||
if not request.user.has_perm('judge.contest_access_code'):
|
if not request.user.has_perm("judge.contest_access_code"):
|
||||||
readonly += ['access_code']
|
readonly += ["access_code"]
|
||||||
if not request.user.has_perm('judge.create_private_contest'):
|
if not request.user.has_perm("judge.create_private_contest"):
|
||||||
readonly += ['is_private', 'private_contestants', 'is_organization_private', 'organizations']
|
readonly += [
|
||||||
if not request.user.has_perm('judge.change_contest_visibility'):
|
"private_contestants",
|
||||||
readonly += ['is_visible']
|
"organizations",
|
||||||
if not request.user.has_perm('judge.contest_problem_label'):
|
]
|
||||||
readonly += ['problem_label_script']
|
if not request.user.has_perm("judge.change_contest_visibility"):
|
||||||
|
readonly += ["is_visible"]
|
||||||
|
if not request.user.has_perm("judge.contest_problem_label"):
|
||||||
|
readonly += ["problem_label_script"]
|
||||||
return readonly
|
return readonly
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
# `is_visible` will not appear in `cleaned_data` if user cannot edit it
|
# `is_visible` will not appear in `cleaned_data` if user cannot edit it
|
||||||
if form.cleaned_data.get('is_visible') and not request.user.has_perm('judge.change_contest_visibility'):
|
if form.cleaned_data.get("is_visible") and not request.user.has_perm(
|
||||||
if not form.cleaned_data['is_private'] and not form.cleaned_data['is_organization_private']:
|
"judge.change_contest_visibility"
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
not len(form.cleaned_data["organizations"]) > 0
|
||||||
|
and not len(form.cleaned_data["private_contestants"]) > 0
|
||||||
|
):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
if not request.user.has_perm('judge.create_private_contest'):
|
if not request.user.has_perm("judge.create_private_contest"):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
|
||||||
super().save_model(request, obj, form, change)
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
# We need this flag because `save_related` deals with the inlines, but does not know if we have already rescored
|
|
||||||
self._rescored = False
|
|
||||||
if form.changed_data and any(f in form.changed_data for f in ('format_config', 'format_name')):
|
|
||||||
self._rescore(obj.key)
|
|
||||||
self._rescored = True
|
|
||||||
|
|
||||||
def save_related(self, request, form, formsets, change):
|
def save_related(self, request, form, formsets, change):
|
||||||
super().save_related(request, form, formsets, change)
|
super().save_related(request, form, formsets, change)
|
||||||
# Only rescored if we did not already do so in `save_model`
|
# Only rescored if we did not already do so in `save_model`
|
||||||
if not self._rescored and any(formset.has_changed() for formset in formsets):
|
formset_changed = False
|
||||||
self._rescore(form.cleaned_data['key'])
|
if any(formset.has_changed() for formset in formsets):
|
||||||
|
formset_changed = True
|
||||||
|
|
||||||
|
maybe_trigger_contest_rescore(form, form.instance, formset_changed)
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
def has_change_permission(self, request, obj=None):
|
||||||
if not request.user.has_perm('judge.edit_own_contest'):
|
if not request.user.has_perm("judge.edit_own_contest"):
|
||||||
return False
|
return False
|
||||||
if obj is None:
|
if obj is None:
|
||||||
return True
|
return True
|
||||||
return obj.is_editable_by(request.user)
|
return obj.is_editable_by(request.user)
|
||||||
|
|
||||||
def _rescore(self, contest_key):
|
|
||||||
from judge.tasks import rescore_contest
|
|
||||||
transaction.on_commit(rescore_contest.s(contest_key).delay)
|
|
||||||
|
|
||||||
def make_visible(self, request, queryset):
|
def make_visible(self, request, queryset):
|
||||||
if not request.user.has_perm('judge.change_contest_visibility'):
|
if not request.user.has_perm("judge.change_contest_visibility"):
|
||||||
queryset = queryset.filter(Q(is_private=True) | Q(is_organization_private=True))
|
queryset = queryset.filter(
|
||||||
|
Q(is_private=True) | Q(is_organization_private=True)
|
||||||
|
)
|
||||||
count = queryset.update(is_visible=True)
|
count = queryset.update(is_visible=True)
|
||||||
self.message_user(request, ungettext('%d contest successfully marked as visible.',
|
self.message_user(
|
||||||
'%d contests successfully marked as visible.',
|
request,
|
||||||
count) % count)
|
ungettext(
|
||||||
make_visible.short_description = _('Mark contests as visible')
|
"%d contest successfully marked as visible.",
|
||||||
|
"%d contests successfully marked as visible.",
|
||||||
|
count,
|
||||||
|
)
|
||||||
|
% count,
|
||||||
|
)
|
||||||
|
|
||||||
|
make_visible.short_description = _("Mark contests as visible")
|
||||||
|
|
||||||
def make_hidden(self, request, queryset):
|
def make_hidden(self, request, queryset):
|
||||||
if not request.user.has_perm('judge.change_contest_visibility'):
|
if not request.user.has_perm("judge.change_contest_visibility"):
|
||||||
queryset = queryset.filter(Q(is_private=True) | Q(is_organization_private=True))
|
queryset = queryset.filter(
|
||||||
|
Q(is_private=True) | Q(is_organization_private=True)
|
||||||
|
)
|
||||||
count = queryset.update(is_visible=True)
|
count = queryset.update(is_visible=True)
|
||||||
self.message_user(request, ungettext('%d contest successfully marked as hidden.',
|
self.message_user(
|
||||||
'%d contests successfully marked as hidden.',
|
request,
|
||||||
count) % count)
|
ungettext(
|
||||||
make_hidden.short_description = _('Mark contests as hidden')
|
"%d contest successfully marked as hidden.",
|
||||||
|
"%d contests successfully marked as hidden.",
|
||||||
|
count,
|
||||||
|
)
|
||||||
|
% count,
|
||||||
|
)
|
||||||
|
|
||||||
|
make_hidden.short_description = _("Mark contests as hidden")
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
return [
|
return [
|
||||||
url(r'^rate/all/$', self.rate_all_view, name='judge_contest_rate_all'),
|
url(r"^rate/all/$", self.rate_all_view, name="judge_contest_rate_all"),
|
||||||
url(r'^(\d+)/rate/$', self.rate_view, name='judge_contest_rate'),
|
url(r"^(\d+)/rate/$", self.rate_view, name="judge_contest_rate"),
|
||||||
url(r'^(\d+)/judge/(\d+)/$', self.rejudge_view, name='judge_contest_rejudge'),
|
url(
|
||||||
|
r"^(\d+)/judge/(\d+)/$", self.rejudge_view, name="judge_contest_rejudge"
|
||||||
|
),
|
||||||
] + super(ContestAdmin, self).get_urls()
|
] + super(ContestAdmin, self).get_urls()
|
||||||
|
|
||||||
def rejudge_view(self, request, contest_id, problem_id):
|
def rejudge_view(self, request, contest_id, problem_id):
|
||||||
queryset = ContestSubmission.objects.filter(problem_id=problem_id).select_related('submission')
|
queryset = ContestSubmission.objects.filter(
|
||||||
|
problem_id=problem_id
|
||||||
|
).select_related("submission")
|
||||||
for model in queryset:
|
for model in queryset:
|
||||||
model.submission.judge(rejudge=True)
|
model.submission.judge(rejudge=True)
|
||||||
|
|
||||||
self.message_user(request, ungettext('%d submission was successfully scheduled for rejudging.',
|
self.message_user(
|
||||||
'%d submissions were successfully scheduled for rejudging.',
|
request,
|
||||||
len(queryset)) % len(queryset))
|
ungettext(
|
||||||
return HttpResponseRedirect(reverse('admin:judge_contest_change', args=(contest_id,)))
|
"%d submission was successfully scheduled for rejudging.",
|
||||||
|
"%d submissions were successfully scheduled for rejudging.",
|
||||||
|
len(queryset),
|
||||||
|
)
|
||||||
|
% len(queryset),
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(
|
||||||
|
reverse("admin:judge_contest_change", args=(contest_id,))
|
||||||
|
)
|
||||||
|
|
||||||
def rate_all_view(self, request):
|
def rate_all_view(self, request):
|
||||||
if not request.user.has_perm('judge.contest_rating'):
|
if not request.user.has_perm("judge.contest_rating"):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute('TRUNCATE TABLE `%s`' % Rating._meta.db_table)
|
cursor.execute("TRUNCATE TABLE `%s`" % Rating._meta.db_table)
|
||||||
Profile.objects.update(rating=None)
|
Profile.objects.update(rating=None)
|
||||||
for contest in Contest.objects.filter(is_rated=True, end_time__lte=timezone.now()).order_by('end_time'):
|
for contest in Contest.objects.filter(
|
||||||
|
is_rated=True, end_time__lte=timezone.now()
|
||||||
|
).order_by("end_time"):
|
||||||
rate_contest(contest)
|
rate_contest(contest)
|
||||||
return HttpResponseRedirect(reverse('admin:judge_contest_changelist'))
|
return HttpResponseRedirect(reverse("admin:judge_contest_changelist"))
|
||||||
|
|
||||||
def rate_view(self, request, id):
|
def rate_view(self, request, id):
|
||||||
if not request.user.has_perm('judge.contest_rating'):
|
if not request.user.has_perm("judge.contest_rating"):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
contest = get_object_or_404(Contest, id=id)
|
contest = get_object_or_404(Contest, id=id)
|
||||||
if not contest.is_rated or not contest.ended:
|
if not contest.is_rated or not contest.ended:
|
||||||
raise Http404()
|
raise Http404()
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
contest.rate()
|
contest.rate()
|
||||||
return HttpResponseRedirect(request.META.get('HTTP_REFERER', reverse('admin:judge_contest_changelist')))
|
return HttpResponseRedirect(
|
||||||
|
request.META.get("HTTP_REFERER", reverse("admin:judge_contest_changelist"))
|
||||||
|
)
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
form = super(ContestAdmin, self).get_form(request, obj, **kwargs)
|
form = super(ContestAdmin, self).get_form(request, obj, **kwargs)
|
||||||
if 'problem_label_script' in form.base_fields:
|
if "problem_label_script" in form.base_fields:
|
||||||
# form.base_fields['problem_label_script'] does not exist when the user has only view permission
|
# form.base_fields['problem_label_script'] does not exist when the user has only view permission
|
||||||
# on the model.
|
# on the model.
|
||||||
form.base_fields['problem_label_script'].widget = AceWidget('lua', request.profile.ace_theme)
|
form.base_fields["problem_label_script"].widget = AceWidget(
|
||||||
|
"lua", request.profile.ace_theme
|
||||||
|
)
|
||||||
|
|
||||||
perms = ('edit_own_contest', 'edit_all_contest')
|
perms = ("edit_own_contest", "edit_all_contest")
|
||||||
form.base_fields['curators'].queryset = Profile.objects.filter(
|
form.base_fields["curators"].queryset = Profile.objects.filter(
|
||||||
Q(user__is_superuser=True) |
|
Q(user__is_superuser=True)
|
||||||
Q(user__groups__permissions__codename__in=perms) |
|
| Q(user__groups__permissions__codename__in=perms)
|
||||||
Q(user__user_permissions__codename__in=perms),
|
| Q(user__user_permissions__codename__in=perms),
|
||||||
).distinct()
|
).distinct()
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
@ -275,29 +438,48 @@ class ContestAdmin(VersionAdmin):
|
||||||
class ContestParticipationForm(ModelForm):
|
class ContestParticipationForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
'contest': AdminSelect2Widget(),
|
"contest": AdminSelect2Widget(),
|
||||||
'user': AdminHeavySelect2Widget(data_view='profile_select2'),
|
"user": AdminHeavySelect2Widget(data_view="profile_select2"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ContestParticipationAdmin(admin.ModelAdmin):
|
class ContestParticipationAdmin(admin.ModelAdmin):
|
||||||
fields = ('contest', 'user', 'real_start', 'virtual', 'is_disqualified')
|
fields = ("contest", "user", "real_start", "virtual", "is_disqualified")
|
||||||
list_display = ('contest', 'username', 'show_virtual', 'real_start', 'score', 'cumtime', 'tiebreaker')
|
list_display = (
|
||||||
actions = ['recalculate_results']
|
"contest",
|
||||||
|
"username",
|
||||||
|
"show_virtual",
|
||||||
|
"real_start",
|
||||||
|
"score",
|
||||||
|
"cumtime",
|
||||||
|
"tiebreaker",
|
||||||
|
)
|
||||||
|
actions = ["recalculate_results"]
|
||||||
actions_on_bottom = actions_on_top = True
|
actions_on_bottom = actions_on_top = True
|
||||||
search_fields = ('contest__key', 'contest__name', 'user__user__username')
|
search_fields = ("contest__key", "contest__name", "user__user__username")
|
||||||
form = ContestParticipationForm
|
form = ContestParticipationForm
|
||||||
date_hierarchy = 'real_start'
|
date_hierarchy = "real_start"
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
return super(ContestParticipationAdmin, self).get_queryset(request).only(
|
return (
|
||||||
'contest__name', 'contest__format_name', 'contest__format_config',
|
super(ContestParticipationAdmin, self)
|
||||||
'user__user__username', 'real_start', 'score', 'cumtime', 'tiebreaker', 'virtual',
|
.get_queryset(request)
|
||||||
|
.only(
|
||||||
|
"contest__name",
|
||||||
|
"contest__format_name",
|
||||||
|
"contest__format_config",
|
||||||
|
"user__user__username",
|
||||||
|
"real_start",
|
||||||
|
"score",
|
||||||
|
"cumtime",
|
||||||
|
"tiebreaker",
|
||||||
|
"virtual",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
super().save_model(request, obj, form, change)
|
super().save_model(request, obj, form, change)
|
||||||
if form.changed_data and 'is_disqualified' in form.changed_data:
|
if form.changed_data and "is_disqualified" in form.changed_data:
|
||||||
obj.set_disqualified(obj.is_disqualified)
|
obj.set_disqualified(obj.is_disqualified)
|
||||||
|
|
||||||
def recalculate_results(self, request, queryset):
|
def recalculate_results(self, request, queryset):
|
||||||
|
@ -305,17 +487,48 @@ class ContestParticipationAdmin(admin.ModelAdmin):
|
||||||
for participation in queryset:
|
for participation in queryset:
|
||||||
participation.recompute_results()
|
participation.recompute_results()
|
||||||
count += 1
|
count += 1
|
||||||
self.message_user(request, ungettext('%d participation recalculated.',
|
self.message_user(
|
||||||
'%d participations recalculated.',
|
request,
|
||||||
count) % count)
|
ungettext(
|
||||||
recalculate_results.short_description = _('Recalculate results')
|
"%d participation recalculated.",
|
||||||
|
"%d participations recalculated.",
|
||||||
|
count,
|
||||||
|
)
|
||||||
|
% count,
|
||||||
|
)
|
||||||
|
|
||||||
|
recalculate_results.short_description = _("Recalculate results")
|
||||||
|
|
||||||
def username(self, obj):
|
def username(self, obj):
|
||||||
return obj.user.username
|
return obj.user.username
|
||||||
username.short_description = _('username')
|
|
||||||
username.admin_order_field = 'user__user__username'
|
username.short_description = _("username")
|
||||||
|
username.admin_order_field = "user__user__username"
|
||||||
|
|
||||||
def show_virtual(self, obj):
|
def show_virtual(self, obj):
|
||||||
return obj.virtual or '-'
|
return obj.virtual or "-"
|
||||||
show_virtual.short_description = _('virtual')
|
|
||||||
show_virtual.admin_order_field = 'virtual'
|
show_virtual.short_description = _("virtual")
|
||||||
|
show_virtual.admin_order_field = "virtual"
|
||||||
|
|
||||||
|
|
||||||
|
class ContestsSummaryForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
widgets = {
|
||||||
|
"contests": AdminHeavySelect2MultipleWidget(
|
||||||
|
data_view="contest_select2", attrs={"style": "width: 100%"}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ContestsSummaryAdmin(admin.ModelAdmin):
|
||||||
|
fields = ("key", "contests", "scores")
|
||||||
|
list_display = ("key",)
|
||||||
|
search_fields = ("key", "contests__key")
|
||||||
|
form = ContestsSummaryForm
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
super(ContestsSummaryAdmin, self).save_model(request, obj, form, change)
|
||||||
|
obj.refresh_from_db()
|
||||||
|
obj.results = recalculate_contest_summary_result(request, obj)
|
||||||
|
obj.save()
|
||||||
|
|
52
judge/admin/course.py
Normal file
52
judge/admin/course.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.urls import reverse, reverse_lazy
|
||||||
|
from django.utils.translation import gettext, gettext_lazy as _, ungettext
|
||||||
|
from django.forms import ModelForm
|
||||||
|
|
||||||
|
from judge.models import Course, CourseRole
|
||||||
|
from judge.widgets import AdminSelect2MultipleWidget
|
||||||
|
from judge.widgets import (
|
||||||
|
AdminHeavySelect2MultipleWidget,
|
||||||
|
AdminHeavySelect2Widget,
|
||||||
|
HeavyPreviewAdminPageDownWidget,
|
||||||
|
AdminSelect2Widget,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CourseRoleInlineForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
widgets = {
|
||||||
|
"user": AdminHeavySelect2Widget(
|
||||||
|
data_view="profile_select2", attrs={"style": "width: 100%"}
|
||||||
|
),
|
||||||
|
"role": AdminSelect2Widget,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CourseRoleInline(admin.TabularInline):
|
||||||
|
model = CourseRole
|
||||||
|
extra = 1
|
||||||
|
form = CourseRoleInlineForm
|
||||||
|
|
||||||
|
|
||||||
|
class CourseForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
widgets = {
|
||||||
|
"organizations": AdminHeavySelect2MultipleWidget(
|
||||||
|
data_view="organization_select2"
|
||||||
|
),
|
||||||
|
"about": HeavyPreviewAdminPageDownWidget(
|
||||||
|
preview=reverse_lazy("blog_preview")
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CourseAdmin(admin.ModelAdmin):
|
||||||
|
prepopulated_fields = {"slug": ("name",)}
|
||||||
|
inlines = [
|
||||||
|
CourseRoleInline,
|
||||||
|
]
|
||||||
|
list_display = ("name", "is_public", "is_open")
|
||||||
|
search_fields = ("name",)
|
||||||
|
form = CourseForm
|
|
@ -6,26 +6,33 @@ from django.utils.html import format_html
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from mptt.admin import DraggableMPTTAdmin
|
from mptt.admin import DraggableMPTTAdmin
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
|
from reversion_compare.admin import CompareVersionAdmin
|
||||||
|
|
||||||
|
|
||||||
from judge.dblock import LockModel
|
from judge.dblock import LockModel
|
||||||
from judge.models import NavigationBar
|
from judge.models import NavigationBar
|
||||||
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
|
from judge.widgets import (
|
||||||
|
AdminHeavySelect2MultipleWidget,
|
||||||
|
AdminHeavySelect2Widget,
|
||||||
|
HeavyPreviewAdminPageDownWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NavigationBarAdmin(DraggableMPTTAdmin):
|
class NavigationBarAdmin(DraggableMPTTAdmin):
|
||||||
list_display = DraggableMPTTAdmin.list_display + ('key', 'linked_path')
|
list_display = DraggableMPTTAdmin.list_display + ("key", "linked_path")
|
||||||
fields = ('key', 'label', 'path', 'order', 'regex', 'parent')
|
fields = ("key", "label", "path", "order", "regex", "parent")
|
||||||
list_editable = () # Bug in SortableModelAdmin: 500 without list_editable being set
|
list_editable = () # Bug in SortableModelAdmin: 500 without list_editable being set
|
||||||
mptt_level_indent = 20
|
mptt_level_indent = 20
|
||||||
sortable = 'order'
|
sortable = "order"
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(NavigationBarAdmin, self).__init__(*args, **kwargs)
|
super(NavigationBarAdmin, self).__init__(*args, **kwargs)
|
||||||
self.__save_model_calls = 0
|
self.__save_model_calls = 0
|
||||||
|
|
||||||
def linked_path(self, obj):
|
def linked_path(self, obj):
|
||||||
return format_html(u'<a href="{0}" target="_blank">{0}</a>', obj.path)
|
return format_html('<a href="{0}" target="_blank">{0}</a>', obj.path)
|
||||||
linked_path.short_description = _('link path')
|
|
||||||
|
linked_path.short_description = _("link path")
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
self.__save_model_calls += 1
|
self.__save_model_calls += 1
|
||||||
|
@ -34,7 +41,9 @@ class NavigationBarAdmin(DraggableMPTTAdmin):
|
||||||
def changelist_view(self, request, extra_context=None):
|
def changelist_view(self, request, extra_context=None):
|
||||||
self.__save_model_calls = 0
|
self.__save_model_calls = 0
|
||||||
with NavigationBar.objects.disable_mptt_updates():
|
with NavigationBar.objects.disable_mptt_updates():
|
||||||
result = super(NavigationBarAdmin, self).changelist_view(request, extra_context)
|
result = super(NavigationBarAdmin, self).changelist_view(
|
||||||
|
request, extra_context
|
||||||
|
)
|
||||||
if self.__save_model_calls:
|
if self.__save_model_calls:
|
||||||
with LockModel(write=(NavigationBar,)):
|
with LockModel(write=(NavigationBar,)):
|
||||||
NavigationBar.objects.rebuild()
|
NavigationBar.objects.rebuild()
|
||||||
|
@ -44,74 +53,106 @@ class NavigationBarAdmin(DraggableMPTTAdmin):
|
||||||
class BlogPostForm(ModelForm):
|
class BlogPostForm(ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(BlogPostForm, self).__init__(*args, **kwargs)
|
super(BlogPostForm, self).__init__(*args, **kwargs)
|
||||||
self.fields['authors'].widget.can_add_related = False
|
if "authors" in self.fields:
|
||||||
|
self.fields["authors"].widget.can_add_related = False
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
"authors": AdminHeavySelect2MultipleWidget(
|
||||||
'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2',
|
data_view="profile_select2", attrs={"style": "width: 100%"}
|
||||||
attrs={'style': 'width: 100%'}),
|
),
|
||||||
|
"organizations": AdminHeavySelect2MultipleWidget(
|
||||||
|
data_view="organization_select2", attrs={"style": "width: 100%"}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('blog_preview'))
|
widgets["content"] = HeavyPreviewAdminPageDownWidget(
|
||||||
widgets['summary'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('blog_preview'))
|
preview=reverse_lazy("blog_preview")
|
||||||
|
|
||||||
|
|
||||||
class BlogPostAdmin(VersionAdmin):
|
|
||||||
fieldsets = (
|
|
||||||
(None, {'fields': ('title', 'slug', 'authors', 'visible', 'sticky', 'publish_on',
|
|
||||||
'is_organization_private', 'organizations')}),
|
|
||||||
(_('Content'), {'fields': ('content', 'og_image')}),
|
|
||||||
(_('Summary'), {'classes': ('collapse',), 'fields': ('summary',)}),
|
|
||||||
)
|
)
|
||||||
prepopulated_fields = {'slug': ('title',)}
|
widgets["summary"] = HeavyPreviewAdminPageDownWidget(
|
||||||
list_display = ('id', 'title', 'visible', 'sticky', 'publish_on')
|
preview=reverse_lazy("blog_preview")
|
||||||
list_display_links = ('id', 'title')
|
)
|
||||||
ordering = ('-publish_on',)
|
|
||||||
|
|
||||||
|
class BlogPostAdmin(CompareVersionAdmin):
|
||||||
|
fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"title",
|
||||||
|
"slug",
|
||||||
|
"authors",
|
||||||
|
"visible",
|
||||||
|
"sticky",
|
||||||
|
"publish_on",
|
||||||
|
"is_organization_private",
|
||||||
|
"organizations",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(_("Content"), {"fields": ("content", "og_image")}),
|
||||||
|
(_("Summary"), {"classes": ("collapse",), "fields": ("summary",)}),
|
||||||
|
)
|
||||||
|
prepopulated_fields = {"slug": ("title",)}
|
||||||
|
list_display = ("id", "title", "visible", "sticky", "publish_on")
|
||||||
|
list_display_links = ("id", "title")
|
||||||
|
ordering = ("-publish_on",)
|
||||||
form = BlogPostForm
|
form = BlogPostForm
|
||||||
date_hierarchy = 'publish_on'
|
date_hierarchy = "publish_on"
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
def has_change_permission(self, request, obj=None):
|
||||||
return (request.user.has_perm('judge.edit_all_post') or
|
return (
|
||||||
request.user.has_perm('judge.change_blogpost') and (
|
request.user.has_perm("judge.edit_all_post")
|
||||||
obj is None or
|
or request.user.has_perm("judge.change_blogpost")
|
||||||
obj.authors.filter(id=request.profile.id).exists()))
|
and (obj is None or obj.authors.filter(id=request.profile.id).exists())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SolutionForm(ModelForm):
|
class SolutionForm(ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(SolutionForm, self).__init__(*args, **kwargs)
|
super(SolutionForm, self).__init__(*args, **kwargs)
|
||||||
self.fields['authors'].widget.can_add_related = False
|
self.fields["authors"].widget.can_add_related = False
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
"authors": AdminHeavySelect2MultipleWidget(
|
||||||
'problem': AdminHeavySelect2Widget(data_view='problem_select2', attrs={'style': 'width: 250px'}),
|
data_view="profile_select2", attrs={"style": "width: 100%"}
|
||||||
|
),
|
||||||
|
"problem": AdminHeavySelect2Widget(
|
||||||
|
data_view="problem_select2", attrs={"style": "width: 250px"}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('solution_preview'))
|
widgets["content"] = HeavyPreviewAdminPageDownWidget(
|
||||||
|
preview=reverse_lazy("solution_preview")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class LicenseForm(ModelForm):
|
class LicenseForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets = {'text': HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('license_preview'))}
|
widgets = {
|
||||||
|
"text": HeavyPreviewAdminPageDownWidget(
|
||||||
|
preview=reverse_lazy("license_preview")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class LicenseAdmin(admin.ModelAdmin):
|
class LicenseAdmin(admin.ModelAdmin):
|
||||||
fields = ('key', 'link', 'name', 'display', 'icon', 'text')
|
fields = ("key", "link", "name", "display", "icon", "text")
|
||||||
list_display = ('name', 'key')
|
list_display = ("name", "key")
|
||||||
form = LicenseForm
|
form = LicenseForm
|
||||||
|
|
||||||
|
|
||||||
class UserListFilter(admin.SimpleListFilter):
|
class UserListFilter(admin.SimpleListFilter):
|
||||||
title = _('user')
|
title = _("user")
|
||||||
parameter_name = 'user'
|
parameter_name = "user"
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
return User.objects.filter(is_staff=True).values_list('id', 'username')
|
return User.objects.filter(is_staff=True).values_list("id", "username")
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value():
|
if self.value():
|
||||||
|
@ -120,10 +161,29 @@ class UserListFilter(admin.SimpleListFilter):
|
||||||
|
|
||||||
|
|
||||||
class LogEntryAdmin(admin.ModelAdmin):
|
class LogEntryAdmin(admin.ModelAdmin):
|
||||||
readonly_fields = ('user', 'content_type', 'object_id', 'object_repr', 'action_flag', 'change_message')
|
readonly_fields = (
|
||||||
list_display = ('__str__', 'action_time', 'user', 'content_type', 'object_link')
|
"user",
|
||||||
search_fields = ('object_repr', 'change_message')
|
"content_type",
|
||||||
list_filter = (UserListFilter, 'content_type')
|
"object_id",
|
||||||
|
"object_repr",
|
||||||
|
"action_flag",
|
||||||
|
"change_message",
|
||||||
|
)
|
||||||
|
list_display = (
|
||||||
|
"__str__",
|
||||||
|
"action_time",
|
||||||
|
"user",
|
||||||
|
"content_type",
|
||||||
|
"object_link",
|
||||||
|
"diff_link",
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"object_repr",
|
||||||
|
"change_message",
|
||||||
|
"user__username",
|
||||||
|
"content_type__model",
|
||||||
|
)
|
||||||
|
list_filter = (UserListFilter, "content_type")
|
||||||
list_display_links = None
|
list_display_links = None
|
||||||
actions = None
|
actions = None
|
||||||
|
|
||||||
|
@ -142,13 +202,35 @@ class LogEntryAdmin(admin.ModelAdmin):
|
||||||
else:
|
else:
|
||||||
ct = obj.content_type
|
ct = obj.content_type
|
||||||
try:
|
try:
|
||||||
link = format_html('<a href="{1}">{0}</a>', obj.object_repr,
|
link = format_html(
|
||||||
reverse('admin:%s_%s_change' % (ct.app_label, ct.model), args=(obj.object_id,)))
|
'<a href="{1}">{0}</a>',
|
||||||
|
obj.object_repr,
|
||||||
|
reverse(
|
||||||
|
"admin:%s_%s_change" % (ct.app_label, ct.model),
|
||||||
|
args=(obj.object_id,),
|
||||||
|
),
|
||||||
|
)
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
link = obj.object_repr
|
link = obj.object_repr
|
||||||
return link
|
return link
|
||||||
object_link.admin_order_field = 'object_repr'
|
|
||||||
object_link.short_description = _('object')
|
object_link.admin_order_field = "object_repr"
|
||||||
|
object_link.short_description = _("object")
|
||||||
|
|
||||||
|
def diff_link(self, obj):
|
||||||
|
if obj.is_deletion():
|
||||||
|
return None
|
||||||
|
ct = obj.content_type
|
||||||
|
try:
|
||||||
|
url = reverse(
|
||||||
|
"admin:%s_%s_history" % (ct.app_label, ct.model), args=(obj.object_id,)
|
||||||
|
)
|
||||||
|
link = format_html('<a href="{1}">{0}</a>', _("Diff"), url)
|
||||||
|
except NoReverseMatch:
|
||||||
|
link = None
|
||||||
|
return link
|
||||||
|
|
||||||
|
diff_link.short_description = _("diff")
|
||||||
|
|
||||||
def queryset(self, request):
|
def queryset(self, request):
|
||||||
return super().queryset(request).prefetch_related('content_type')
|
return super().queryset(request).prefetch_related("content_type")
|
||||||
|
|
|
@ -6,61 +6,94 @@ from django.utils.translation import gettext, gettext_lazy as _
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
|
|
||||||
from judge.models import Organization
|
from judge.models import Organization
|
||||||
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
|
from judge.widgets import (
|
||||||
|
AdminHeavySelect2MultipleWidget,
|
||||||
|
AdminHeavySelect2Widget,
|
||||||
|
HeavyPreviewAdminPageDownWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class OrganizationForm(ModelForm):
|
class OrganizationForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
'admins': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
|
"admins": AdminHeavySelect2MultipleWidget(data_view="profile_select2"),
|
||||||
'registrant': AdminHeavySelect2Widget(data_view='profile_select2'),
|
"registrant": AdminHeavySelect2Widget(data_view="profile_select2"),
|
||||||
}
|
}
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets['about'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('organization_preview'))
|
widgets["about"] = HeavyPreviewAdminPageDownWidget(
|
||||||
|
preview=reverse_lazy("organization_preview")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class OrganizationAdmin(VersionAdmin):
|
class OrganizationAdmin(VersionAdmin):
|
||||||
readonly_fields = ('creation_date',)
|
readonly_fields = ("creation_date",)
|
||||||
fields = ('name', 'slug', 'short_name', 'is_open', 'about', 'logo_override_image', 'slots', 'registrant',
|
fields = (
|
||||||
'creation_date', 'admins')
|
"name",
|
||||||
list_display = ('name', 'short_name', 'is_open', 'slots', 'registrant', 'show_public')
|
"slug",
|
||||||
prepopulated_fields = {'slug': ('name',)}
|
"short_name",
|
||||||
|
"is_open",
|
||||||
|
"about",
|
||||||
|
"slots",
|
||||||
|
"registrant",
|
||||||
|
"creation_date",
|
||||||
|
"admins",
|
||||||
|
)
|
||||||
|
list_display = (
|
||||||
|
"name",
|
||||||
|
"short_name",
|
||||||
|
"is_open",
|
||||||
|
"creation_date",
|
||||||
|
"registrant",
|
||||||
|
"show_public",
|
||||||
|
)
|
||||||
|
search_fields = ("name", "short_name", "registrant__user__username")
|
||||||
|
prepopulated_fields = {"slug": ("name",)}
|
||||||
actions_on_top = True
|
actions_on_top = True
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
form = OrganizationForm
|
form = OrganizationForm
|
||||||
|
ordering = ["-creation_date"]
|
||||||
|
|
||||||
def show_public(self, obj):
|
def show_public(self, obj):
|
||||||
return format_html('<a href="{0}" style="white-space:nowrap;">{1}</a>',
|
return format_html(
|
||||||
obj.get_absolute_url(), gettext('View on site'))
|
'<a href="{0}" style="white-space:nowrap;">{1}</a>',
|
||||||
|
obj.get_absolute_url(),
|
||||||
|
gettext("View on site"),
|
||||||
|
)
|
||||||
|
|
||||||
show_public.short_description = ''
|
show_public.short_description = ""
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
fields = self.readonly_fields
|
fields = self.readonly_fields
|
||||||
if not request.user.has_perm('judge.organization_admin'):
|
if not request.user.has_perm("judge.organization_admin"):
|
||||||
return fields + ('registrant', 'admins', 'is_open', 'slots')
|
return fields + ("registrant", "admins", "is_open", "slots")
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
queryset = Organization.objects.all()
|
queryset = Organization.objects.all()
|
||||||
if request.user.has_perm('judge.edit_all_organization'):
|
if request.user.has_perm("judge.edit_all_organization"):
|
||||||
return queryset
|
return queryset
|
||||||
else:
|
else:
|
||||||
return queryset.filter(admins=request.profile.id)
|
return queryset.filter(admins=request.profile.id)
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
def has_change_permission(self, request, obj=None):
|
||||||
if not request.user.has_perm('judge.change_organization'):
|
if not request.user.has_perm("judge.change_organization"):
|
||||||
return False
|
return False
|
||||||
if request.user.has_perm('judge.edit_all_organization') or obj is None:
|
if request.user.has_perm("judge.edit_all_organization") or obj is None:
|
||||||
return True
|
return True
|
||||||
return obj.admins.filter(id=request.profile.id).exists()
|
return obj.admins.filter(id=request.profile.id).exists()
|
||||||
|
|
||||||
|
def save_related(self, request, form, formsets, change):
|
||||||
|
super().save_related(request, form, formsets, change)
|
||||||
|
obj = form.instance
|
||||||
|
obj.members.add(*obj.admins.all())
|
||||||
|
|
||||||
|
|
||||||
class OrganizationRequestAdmin(admin.ModelAdmin):
|
class OrganizationRequestAdmin(admin.ModelAdmin):
|
||||||
list_display = ('username', 'organization', 'state', 'time')
|
list_display = ("username", "organization", "state", "time")
|
||||||
readonly_fields = ('user', 'organization')
|
readonly_fields = ("user", "organization")
|
||||||
|
|
||||||
def username(self, obj):
|
def username(self, obj):
|
||||||
return obj.user.user.username
|
return obj.user.user.username
|
||||||
username.short_description = _('username')
|
|
||||||
username.admin_order_field = 'user__user__username'
|
username.short_description = _("username")
|
||||||
|
username.admin_order_field = "user__user__username"
|
||||||
|
|
|
@ -1,54 +1,115 @@
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin, messages
|
||||||
from django.db import transaction
|
from django.db import transaction, IntegrityError
|
||||||
from django.db.models import Q
|
from django.db.models import Q, Avg, Count
|
||||||
from django.forms import ModelForm
|
from django.db.models.aggregates import StdDev
|
||||||
|
from django.forms import ModelForm, TextInput
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.translation import gettext, gettext_lazy as _, ungettext
|
from django.utils.translation import gettext, gettext_lazy as _, ungettext
|
||||||
from reversion.admin import VersionAdmin
|
from django_ace import AceWidget
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from judge.models import LanguageLimit, Problem, ProblemClarification, ProblemTranslation, Profile, Solution
|
from reversion.admin import VersionAdmin
|
||||||
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminSelect2MultipleWidget, AdminSelect2Widget, \
|
from reversion_compare.admin import CompareVersionAdmin
|
||||||
CheckboxSelectMultipleWithSelectAll, HeavyPreviewAdminPageDownWidget, HeavyPreviewPageDownWidget
|
|
||||||
|
|
||||||
|
from judge.models import (
|
||||||
|
LanguageLimit,
|
||||||
|
LanguageTemplate,
|
||||||
|
Problem,
|
||||||
|
ProblemTranslation,
|
||||||
|
Profile,
|
||||||
|
Solution,
|
||||||
|
Notification,
|
||||||
|
)
|
||||||
|
from judge.models.notification import make_notification
|
||||||
|
from judge.widgets import (
|
||||||
|
AdminHeavySelect2MultipleWidget,
|
||||||
|
AdminSelect2MultipleWidget,
|
||||||
|
AdminSelect2Widget,
|
||||||
|
CheckboxSelectMultipleWithSelectAll,
|
||||||
|
HeavyPreviewAdminPageDownWidget,
|
||||||
|
)
|
||||||
|
from judge.utils.problems import user_editable_ids, user_tester_ids
|
||||||
|
|
||||||
|
MEMORY_UNITS = (("KB", "KB"), ("MB", "MB"))
|
||||||
|
|
||||||
|
|
||||||
class ProblemForm(ModelForm):
|
class ProblemForm(ModelForm):
|
||||||
change_message = forms.CharField(max_length=256, label='Edit reason', required=False)
|
change_message = forms.CharField(
|
||||||
|
max_length=256, label="Edit reason", required=False
|
||||||
|
)
|
||||||
|
memory_unit = forms.ChoiceField(choices=MEMORY_UNITS)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(ProblemForm, self).__init__(*args, **kwargs)
|
super(ProblemForm, self).__init__(*args, **kwargs)
|
||||||
self.fields['authors'].widget.can_add_related = False
|
self.fields["authors"].widget.can_add_related = False
|
||||||
self.fields['curators'].widget.can_add_related = False
|
self.fields["curators"].widget.can_add_related = False
|
||||||
self.fields['testers'].widget.can_add_related = False
|
self.fields["testers"].widget.can_add_related = False
|
||||||
self.fields['banned_users'].widget.can_add_related = False
|
self.fields["banned_users"].widget.can_add_related = False
|
||||||
self.fields['change_message'].widget.attrs.update({
|
self.fields["change_message"].widget.attrs.update(
|
||||||
'placeholder': gettext('Describe the changes you made (optional)'),
|
{
|
||||||
})
|
"placeholder": gettext("Describe the changes you made (optional)"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean_code(self):
|
||||||
|
code = self.cleaned_data.get("code")
|
||||||
|
if self.instance.pk:
|
||||||
|
return code
|
||||||
|
|
||||||
|
if Problem.objects.filter(code=code).exists():
|
||||||
|
raise ValidationError(_("A problem with this code already exists."))
|
||||||
|
|
||||||
|
return code
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
memory_unit = self.cleaned_data.get("memory_unit", "KB")
|
||||||
|
if memory_unit == "MB":
|
||||||
|
self.cleaned_data["memory_limit"] *= 1024
|
||||||
|
date = self.cleaned_data.get("date")
|
||||||
|
if not date or date > timezone.now():
|
||||||
|
self.cleaned_data["date"] = timezone.now()
|
||||||
|
return self.cleaned_data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
"authors": AdminHeavySelect2MultipleWidget(
|
||||||
'curators': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
data_view="profile_select2", attrs={"style": "width: 100%"}
|
||||||
'testers': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
),
|
||||||
'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
|
"curators": AdminHeavySelect2MultipleWidget(
|
||||||
attrs={'style': 'width: 100%'}),
|
data_view="profile_select2", attrs={"style": "width: 100%"}
|
||||||
'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2',
|
),
|
||||||
attrs={'style': 'width: 100%'}),
|
"testers": AdminHeavySelect2MultipleWidget(
|
||||||
'types': AdminSelect2MultipleWidget,
|
data_view="profile_select2", attrs={"style": "width: 100%"}
|
||||||
'group': AdminSelect2Widget,
|
),
|
||||||
|
"banned_users": AdminHeavySelect2MultipleWidget(
|
||||||
|
data_view="profile_select2", attrs={"style": "width: 100%"}
|
||||||
|
),
|
||||||
|
"organizations": AdminHeavySelect2MultipleWidget(
|
||||||
|
data_view="organization_select2", attrs={"style": "width: 100%"}
|
||||||
|
),
|
||||||
|
"types": AdminSelect2MultipleWidget,
|
||||||
|
"group": AdminSelect2Widget,
|
||||||
|
"memory_limit": TextInput(attrs={"size": "20"}),
|
||||||
}
|
}
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets['description'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('problem_preview'))
|
widgets["description"] = HeavyPreviewAdminPageDownWidget(
|
||||||
|
preview=reverse_lazy("problem_preview")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ProblemCreatorListFilter(admin.SimpleListFilter):
|
class ProblemCreatorListFilter(admin.SimpleListFilter):
|
||||||
title = parameter_name = 'creator'
|
title = parameter_name = "creator"
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
queryset = Profile.objects.exclude(authored_problems=None).values_list('user__username', flat=True)
|
queryset = Profile.objects.exclude(authored_problems=None).values_list(
|
||||||
|
"user__username", flat=True
|
||||||
|
)
|
||||||
return [(name, name) for name in queryset]
|
return [(name, name) for name in queryset]
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
|
@ -58,46 +119,68 @@ class ProblemCreatorListFilter(admin.SimpleListFilter):
|
||||||
|
|
||||||
|
|
||||||
class LanguageLimitInlineForm(ModelForm):
|
class LanguageLimitInlineForm(ModelForm):
|
||||||
|
memory_unit = forms.ChoiceField(choices=MEMORY_UNITS, label=_("Memory unit"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {'language': AdminSelect2Widget}
|
widgets = {
|
||||||
|
"language": AdminSelect2Widget,
|
||||||
|
"memory_limit": TextInput(attrs={"size": "10"}),
|
||||||
|
}
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if not self.cleaned_data.get("language"):
|
||||||
|
self.cleaned_data["DELETE"] = True
|
||||||
|
if (
|
||||||
|
self.cleaned_data.get("memory_limit")
|
||||||
|
and self.cleaned_data.get("memory_unit") == "MB"
|
||||||
|
):
|
||||||
|
self.cleaned_data["memory_limit"] *= 1024
|
||||||
|
return self.cleaned_data
|
||||||
|
|
||||||
|
|
||||||
class LanguageLimitInline(admin.TabularInline):
|
class LanguageLimitInline(admin.TabularInline):
|
||||||
model = LanguageLimit
|
model = LanguageLimit
|
||||||
fields = ('language', 'time_limit', 'memory_limit')
|
fields = ("language", "time_limit", "memory_limit", "memory_unit")
|
||||||
form = LanguageLimitInlineForm
|
form = LanguageLimitInlineForm
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
class ProblemClarificationForm(ModelForm):
|
class LanguageTemplateInlineForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
if HeavyPreviewPageDownWidget is not None:
|
widgets = {
|
||||||
widgets = {'description': HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'))}
|
"language": AdminSelect2Widget,
|
||||||
|
"source": AceWidget(width="600px", height="200px", toolbar=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ProblemClarificationInline(admin.StackedInline):
|
class LanguageTemplateInline(admin.TabularInline):
|
||||||
model = ProblemClarification
|
model = LanguageTemplate
|
||||||
fields = ('description',)
|
fields = ("language", "source")
|
||||||
form = ProblemClarificationForm
|
form = LanguageTemplateInlineForm
|
||||||
extra = 0
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
class ProblemSolutionForm(ModelForm):
|
class ProblemSolutionForm(ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(ProblemSolutionForm, self).__init__(*args, **kwargs)
|
super(ProblemSolutionForm, self).__init__(*args, **kwargs)
|
||||||
self.fields['authors'].widget.can_add_related = False
|
self.fields["authors"].widget.can_add_related = False
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
"authors": AdminHeavySelect2MultipleWidget(
|
||||||
|
data_view="profile_select2", attrs={"style": "width: 100%"}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('solution_preview'))
|
widgets["content"] = HeavyPreviewAdminPageDownWidget(
|
||||||
|
preview=reverse_lazy("solution_preview")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ProblemSolutionInline(admin.StackedInline):
|
class ProblemSolutionInline(admin.StackedInline):
|
||||||
model = Solution
|
model = Solution
|
||||||
fields = ('is_public', 'publish_on', 'authors', 'content')
|
fields = ("is_public", "publish_on", "authors", "content")
|
||||||
form = ProblemSolutionForm
|
form = ProblemSolutionForm
|
||||||
extra = 0
|
extra = 0
|
||||||
|
|
||||||
|
@ -105,134 +188,300 @@ class ProblemSolutionInline(admin.StackedInline):
|
||||||
class ProblemTranslationForm(ModelForm):
|
class ProblemTranslationForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets = {'description': HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('problem_preview'))}
|
widgets = {
|
||||||
|
"description": HeavyPreviewAdminPageDownWidget(
|
||||||
|
preview=reverse_lazy("problem_preview")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ProblemTranslationInline(admin.StackedInline):
|
class ProblemTranslationInline(admin.StackedInline):
|
||||||
model = ProblemTranslation
|
model = ProblemTranslation
|
||||||
fields = ('language', 'name', 'description')
|
fields = ("language", "name", "description")
|
||||||
form = ProblemTranslationForm
|
form = ProblemTranslationForm
|
||||||
extra = 0
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
class ProblemAdmin(VersionAdmin):
|
class ProblemAdmin(CompareVersionAdmin):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {
|
(
|
||||||
'fields': (
|
None,
|
||||||
'code', 'name', 'is_public', 'is_manually_managed', 'date', 'authors', 'curators', 'testers',
|
{
|
||||||
'is_organization_private', 'organizations', 'description', 'license',
|
"fields": (
|
||||||
|
"code",
|
||||||
|
"name",
|
||||||
|
"is_public",
|
||||||
|
"organizations",
|
||||||
|
"date",
|
||||||
|
"authors",
|
||||||
|
"curators",
|
||||||
|
"testers",
|
||||||
|
"description",
|
||||||
|
"pdf_description",
|
||||||
|
"license",
|
||||||
),
|
),
|
||||||
}),
|
},
|
||||||
(_('Social Media'), {'classes': ('collapse',), 'fields': ('og_image', 'summary')}),
|
),
|
||||||
(_('Taxonomy'), {'fields': ('types', 'group')}),
|
(
|
||||||
(_('Points'), {'fields': (('points', 'partial'), 'short_circuit')}),
|
_("Social Media"),
|
||||||
(_('Limits'), {'fields': ('time_limit', 'memory_limit')}),
|
{"classes": ("collapse",), "fields": ("og_image", "summary")},
|
||||||
(_('Language'), {'fields': ('allowed_languages',)}),
|
),
|
||||||
(_('Justice'), {'fields': ('banned_users',)}),
|
(_("Taxonomy"), {"fields": ("types", "group")}),
|
||||||
(_('History'), {'fields': ('change_message',)}),
|
(_("Points"), {"fields": (("points", "partial"), "short_circuit")}),
|
||||||
|
(_("Limits"), {"fields": ("time_limit", ("memory_limit", "memory_unit"))}),
|
||||||
|
(_("Language"), {"fields": ("allowed_languages",)}),
|
||||||
|
(_("Justice"), {"fields": ("banned_users",)}),
|
||||||
|
(_("History"), {"fields": ("change_message",)}),
|
||||||
)
|
)
|
||||||
list_display = ['code', 'name', 'show_authors', 'points', 'is_public', 'show_public']
|
list_display = [
|
||||||
ordering = ['code']
|
"code",
|
||||||
search_fields = ('code', 'name', 'authors__user__username', 'curators__user__username')
|
"name",
|
||||||
inlines = [LanguageLimitInline, ProblemClarificationInline, ProblemSolutionInline, ProblemTranslationInline]
|
"show_authors",
|
||||||
|
"date",
|
||||||
|
"points",
|
||||||
|
"is_public",
|
||||||
|
"show_public",
|
||||||
|
]
|
||||||
|
ordering = ["-date"]
|
||||||
|
search_fields = (
|
||||||
|
"code",
|
||||||
|
"name",
|
||||||
|
"authors__user__username",
|
||||||
|
"curators__user__username",
|
||||||
|
)
|
||||||
|
inlines = [
|
||||||
|
LanguageLimitInline,
|
||||||
|
LanguageTemplateInline,
|
||||||
|
ProblemSolutionInline,
|
||||||
|
ProblemTranslationInline,
|
||||||
|
]
|
||||||
list_max_show_all = 1000
|
list_max_show_all = 1000
|
||||||
actions_on_top = True
|
actions_on_top = True
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
list_filter = ('is_public', ProblemCreatorListFilter)
|
list_filter = ("is_public", ProblemCreatorListFilter)
|
||||||
form = ProblemForm
|
form = ProblemForm
|
||||||
date_hierarchy = 'date'
|
date_hierarchy = "date"
|
||||||
|
|
||||||
def get_actions(self, request):
|
def get_actions(self, request):
|
||||||
actions = super(ProblemAdmin, self).get_actions(request)
|
actions = super(ProblemAdmin, self).get_actions(request)
|
||||||
|
|
||||||
if request.user.has_perm('judge.change_public_visibility'):
|
if request.user.has_perm("judge.change_public_visibility"):
|
||||||
func, name, desc = self.get_action('make_public')
|
func, name, desc = self.get_action("make_public")
|
||||||
actions[name] = (func, name, desc)
|
actions[name] = (func, name, desc)
|
||||||
|
|
||||||
func, name, desc = self.get_action('make_private')
|
func, name, desc = self.get_action("make_private")
|
||||||
actions[name] = (func, name, desc)
|
actions[name] = (func, name, desc)
|
||||||
|
|
||||||
return actions
|
return actions
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
fields = self.readonly_fields
|
fields = self.readonly_fields
|
||||||
if not request.user.has_perm('judge.change_public_visibility'):
|
if not request.user.has_perm("judge.change_public_visibility"):
|
||||||
fields += ('is_public',)
|
fields += ("is_public",)
|
||||||
if not request.user.has_perm('judge.change_manually_managed'):
|
|
||||||
fields += ('is_manually_managed',)
|
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
def show_authors(self, obj):
|
def show_authors(self, obj):
|
||||||
return ', '.join(map(attrgetter('user.username'), obj.authors.all()))
|
return ", ".join(map(attrgetter("user.username"), obj.authors.all()))
|
||||||
|
|
||||||
show_authors.short_description = _('Authors')
|
show_authors.short_description = _("Authors")
|
||||||
|
|
||||||
def show_public(self, obj):
|
def show_public(self, obj):
|
||||||
return format_html('<a href="{1}">{0}</a>', gettext('View on site'), obj.get_absolute_url())
|
return format_html(
|
||||||
|
'<a href="{1}">{0}</a>', gettext("View on site"), obj.get_absolute_url()
|
||||||
|
)
|
||||||
|
|
||||||
show_public.short_description = ''
|
show_public.short_description = ""
|
||||||
|
|
||||||
def _rescore(self, request, problem_id):
|
def _rescore(self, request, problem_id):
|
||||||
from judge.tasks import rescore_problem
|
from judge.tasks import rescore_problem
|
||||||
|
|
||||||
transaction.on_commit(rescore_problem.s(problem_id).delay)
|
transaction.on_commit(rescore_problem.s(problem_id).delay)
|
||||||
|
|
||||||
def make_public(self, request, queryset):
|
def make_public(self, request, queryset):
|
||||||
count = queryset.update(is_public=True)
|
count = queryset.update(is_public=True)
|
||||||
for problem_id in queryset.values_list('id', flat=True):
|
for problem_id in queryset.values_list("id", flat=True):
|
||||||
self._rescore(request, problem_id)
|
self._rescore(request, problem_id)
|
||||||
self.message_user(request, ungettext('%d problem successfully marked as public.',
|
self.message_user(
|
||||||
'%d problems successfully marked as public.',
|
request,
|
||||||
count) % count)
|
ungettext(
|
||||||
|
"%d problem successfully marked as public.",
|
||||||
|
"%d problems successfully marked as public.",
|
||||||
|
count,
|
||||||
|
)
|
||||||
|
% count,
|
||||||
|
)
|
||||||
|
|
||||||
make_public.short_description = _('Mark problems as public')
|
make_public.short_description = _("Mark problems as public")
|
||||||
|
|
||||||
def make_private(self, request, queryset):
|
def make_private(self, request, queryset):
|
||||||
count = queryset.update(is_public=False)
|
count = queryset.update(is_public=False)
|
||||||
for problem_id in queryset.values_list('id', flat=True):
|
for problem_id in queryset.values_list("id", flat=True):
|
||||||
self._rescore(request, problem_id)
|
self._rescore(request, problem_id)
|
||||||
self.message_user(request, ungettext('%d problem successfully marked as private.',
|
self.message_user(
|
||||||
'%d problems successfully marked as private.',
|
request,
|
||||||
count) % count)
|
ungettext(
|
||||||
|
"%d problem successfully marked as private.",
|
||||||
|
"%d problems successfully marked as private.",
|
||||||
|
count,
|
||||||
|
)
|
||||||
|
% count,
|
||||||
|
)
|
||||||
|
|
||||||
make_private.short_description = _('Mark problems as private')
|
make_private.short_description = _("Mark problems as private")
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
queryset = Problem.objects.prefetch_related('authors__user')
|
queryset = Problem.objects.prefetch_related("authors__user")
|
||||||
if request.user.has_perm('judge.edit_all_problem'):
|
if request.user.has_perm("judge.edit_all_problem"):
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
access = Q()
|
access = Q()
|
||||||
if request.user.has_perm('judge.edit_public_problem'):
|
if request.user.has_perm("judge.edit_public_problem"):
|
||||||
access |= Q(is_public=True)
|
access |= Q(is_public=True)
|
||||||
if request.user.has_perm('judge.edit_own_problem'):
|
if request.user.has_perm("judge.edit_own_problem"):
|
||||||
access |= Q(authors__id=request.profile.id) | Q(curators__id=request.profile.id)
|
access |= Q(authors__id=request.profile.id) | Q(
|
||||||
|
curators__id=request.profile.id
|
||||||
|
)
|
||||||
return queryset.filter(access).distinct() if access else queryset.none()
|
return queryset.filter(access).distinct() if access else queryset.none()
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
def has_change_permission(self, request, obj=None):
|
||||||
if request.user.has_perm('judge.edit_all_problem') or obj is None:
|
if request.user.has_perm("judge.edit_all_problem") or obj is None:
|
||||||
return True
|
return True
|
||||||
if request.user.has_perm('judge.edit_public_problem') and obj.is_public:
|
if request.user.has_perm("judge.edit_public_problem") and obj.is_public:
|
||||||
return True
|
return True
|
||||||
if not request.user.has_perm('judge.edit_own_problem'):
|
if not request.user.has_perm("judge.edit_own_problem"):
|
||||||
return False
|
return False
|
||||||
return obj.is_editor(request.profile)
|
return obj.is_editor(request.profile)
|
||||||
|
|
||||||
def formfield_for_manytomany(self, db_field, request=None, **kwargs):
|
def formfield_for_manytomany(self, db_field, request=None, **kwargs):
|
||||||
if db_field.name == 'allowed_languages':
|
if db_field.name == "allowed_languages":
|
||||||
kwargs['widget'] = CheckboxSelectMultipleWithSelectAll()
|
kwargs["widget"] = CheckboxSelectMultipleWithSelectAll()
|
||||||
return super(ProblemAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)
|
return super(ProblemAdmin, self).formfield_for_manytomany(
|
||||||
|
db_field, request, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
def get_form(self, *args, **kwargs):
|
def get_form(self, *args, **kwargs):
|
||||||
form = super(ProblemAdmin, self).get_form(*args, **kwargs)
|
form = super(ProblemAdmin, self).get_form(*args, **kwargs)
|
||||||
form.base_fields['authors'].queryset = Profile.objects.all()
|
form.base_fields["authors"].queryset = Profile.objects.all()
|
||||||
return form
|
return form
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
|
form.changed_data.remove("memory_unit")
|
||||||
super().save_model(request, obj, form, change)
|
super().save_model(request, obj, form, change)
|
||||||
if form.changed_data and any(f in form.changed_data for f in ('is_public', 'points', 'partial')):
|
if form.changed_data and any(
|
||||||
|
f in form.changed_data for f in ("is_public", "points", "partial")
|
||||||
|
):
|
||||||
self._rescore(request, obj.id)
|
self._rescore(request, obj.id)
|
||||||
|
|
||||||
|
def save_related(self, request, form, formsets, change):
|
||||||
|
editors = set()
|
||||||
|
testers = set()
|
||||||
|
if "curators" in form.changed_data or "authors" in form.changed_data:
|
||||||
|
editors = set(form.instance.editor_ids)
|
||||||
|
if "testers" in form.changed_data:
|
||||||
|
testers = set(form.instance.tester_ids)
|
||||||
|
|
||||||
|
super().save_related(request, form, formsets, change)
|
||||||
|
obj = form.instance
|
||||||
|
obj.curators.add(request.profile)
|
||||||
|
|
||||||
|
if "curators" in form.changed_data or "authors" in form.changed_data:
|
||||||
|
del obj.editor_ids
|
||||||
|
editors = editors.union(set(obj.editor_ids))
|
||||||
|
if "testers" in form.changed_data:
|
||||||
|
del obj.tester_ids
|
||||||
|
testers = testers.union(set(obj.tester_ids))
|
||||||
|
|
||||||
|
for editor in editors:
|
||||||
|
user_editable_ids.dirty(editor)
|
||||||
|
for tester in testers:
|
||||||
|
user_tester_ids.dirty(tester)
|
||||||
|
|
||||||
|
# Create notification
|
||||||
|
if "is_public" in form.changed_data or "organizations" in form.changed_data:
|
||||||
|
users = set(obj.authors.all())
|
||||||
|
users = users.union(users, set(obj.curators.all()))
|
||||||
|
orgs = []
|
||||||
|
if obj.organizations.count() > 0:
|
||||||
|
for org in obj.organizations.all():
|
||||||
|
users = users.union(users, set(org.admins.all()))
|
||||||
|
orgs.append(org.name)
|
||||||
|
else:
|
||||||
|
admins = Profile.objects.filter(user__is_superuser=True).all()
|
||||||
|
users = users.union(users, admins)
|
||||||
|
link = reverse_lazy("admin:judge_problem_change", args=(obj.id,))
|
||||||
|
html = f'<a href="{link}">{obj.name}</a>'
|
||||||
|
category = "Problem public: " + str(obj.is_public)
|
||||||
|
if orgs:
|
||||||
|
category += " (" + ", ".join(orgs) + ")"
|
||||||
|
make_notification(users, category, html, request.profile)
|
||||||
|
|
||||||
def construct_change_message(self, request, form, *args, **kwargs):
|
def construct_change_message(self, request, form, *args, **kwargs):
|
||||||
if form.cleaned_data.get('change_message'):
|
if form.cleaned_data.get("change_message"):
|
||||||
return form.cleaned_data['change_message']
|
return form.cleaned_data["change_message"]
|
||||||
return super(ProblemAdmin, self).construct_change_message(request, form, *args, **kwargs)
|
return super(ProblemAdmin, self).construct_change_message(
|
||||||
|
request, form, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProblemPointsVoteAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"vote_points",
|
||||||
|
"voter",
|
||||||
|
"voter_rating",
|
||||||
|
"voter_point",
|
||||||
|
"problem_name",
|
||||||
|
"problem_code",
|
||||||
|
"problem_points",
|
||||||
|
)
|
||||||
|
search_fields = ("voter__user__username", "problem__code", "problem__name")
|
||||||
|
readonly_fields = (
|
||||||
|
"voter",
|
||||||
|
"problem",
|
||||||
|
"problem_code",
|
||||||
|
"problem_points",
|
||||||
|
"voter_rating",
|
||||||
|
"voter_point",
|
||||||
|
)
|
||||||
|
|
||||||
|
def has_change_permission(self, request, obj=None):
|
||||||
|
if obj is None:
|
||||||
|
return request.user.has_perm("judge.edit_own_problem")
|
||||||
|
return obj.problem.is_editable_by(request.user)
|
||||||
|
|
||||||
|
def lookup_allowed(self, key, value):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def problem_code(self, obj):
|
||||||
|
return obj.problem.code
|
||||||
|
|
||||||
|
problem_code.short_description = _("Problem code")
|
||||||
|
problem_code.admin_order_field = "problem__code"
|
||||||
|
|
||||||
|
def problem_points(self, obj):
|
||||||
|
return obj.problem.points
|
||||||
|
|
||||||
|
problem_points.short_description = _("Points")
|
||||||
|
problem_points.admin_order_field = "problem__points"
|
||||||
|
|
||||||
|
def problem_name(self, obj):
|
||||||
|
return obj.problem.name
|
||||||
|
|
||||||
|
problem_name.short_description = _("Problem name")
|
||||||
|
problem_name.admin_order_field = "problem__name"
|
||||||
|
|
||||||
|
def voter_rating(self, obj):
|
||||||
|
return obj.voter.rating
|
||||||
|
|
||||||
|
voter_rating.short_description = _("Voter rating")
|
||||||
|
voter_rating.admin_order_field = "voter__rating"
|
||||||
|
|
||||||
|
def voter_point(self, obj):
|
||||||
|
return round(obj.voter.performance_points)
|
||||||
|
|
||||||
|
voter_point.short_description = _("Voter point")
|
||||||
|
voter_point.admin_order_field = "voter__performance_points"
|
||||||
|
|
||||||
|
def vote_points(self, obj):
|
||||||
|
return obj.points
|
||||||
|
|
||||||
|
vote_points.short_description = _("Vote")
|
||||||
|
|
|
@ -1,41 +1,58 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.forms import ModelForm
|
from django.forms import ModelForm, CharField, TextInput
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.translation import gettext, gettext_lazy as _, ungettext
|
from django.utils.translation import gettext, gettext_lazy as _, ungettext
|
||||||
from reversion.admin import VersionAdmin
|
from django.contrib.auth.admin import UserAdmin as OldUserAdmin
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.contrib.auth.forms import UserChangeForm
|
||||||
|
|
||||||
from django_ace import AceWidget
|
from django_ace import AceWidget
|
||||||
from judge.models import Profile
|
|
||||||
|
from judge.models import Profile, ProfileInfo
|
||||||
from judge.widgets import AdminPagedownWidget, AdminSelect2Widget
|
from judge.widgets import AdminPagedownWidget, AdminSelect2Widget
|
||||||
|
|
||||||
|
from reversion.admin import VersionAdmin
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
class ProfileForm(ModelForm):
|
class ProfileForm(ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(ProfileForm, self).__init__(*args, **kwargs)
|
super(ProfileForm, self).__init__(*args, **kwargs)
|
||||||
if 'current_contest' in self.base_fields:
|
if "current_contest" in self.base_fields:
|
||||||
# form.fields['current_contest'] does not exist when the user has only view permission on the model.
|
# form.fields['current_contest'] does not exist when the user has only view permission on the model.
|
||||||
self.fields['current_contest'].queryset = self.instance.contest_history.select_related('contest') \
|
self.fields[
|
||||||
.only('contest__name', 'user_id', 'virtual')
|
"current_contest"
|
||||||
self.fields['current_contest'].label_from_instance = \
|
].queryset = self.instance.contest_history.select_related("contest").only(
|
||||||
lambda obj: '%s v%d' % (obj.contest.name, obj.virtual) if obj.virtual else obj.contest.name
|
"contest__name", "user_id", "virtual"
|
||||||
|
)
|
||||||
|
self.fields["current_contest"].label_from_instance = (
|
||||||
|
lambda obj: "%s v%d" % (obj.contest.name, obj.virtual)
|
||||||
|
if obj.virtual
|
||||||
|
else obj.contest.name
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
'timezone': AdminSelect2Widget,
|
"timezone": AdminSelect2Widget,
|
||||||
'language': AdminSelect2Widget,
|
"language": AdminSelect2Widget,
|
||||||
'ace_theme': AdminSelect2Widget,
|
"ace_theme": AdminSelect2Widget,
|
||||||
'current_contest': AdminSelect2Widget,
|
"current_contest": AdminSelect2Widget,
|
||||||
}
|
}
|
||||||
if AdminPagedownWidget is not None:
|
if AdminPagedownWidget is not None:
|
||||||
widgets['about'] = AdminPagedownWidget
|
widgets["about"] = AdminPagedownWidget
|
||||||
|
|
||||||
|
|
||||||
class TimezoneFilter(admin.SimpleListFilter):
|
class TimezoneFilter(admin.SimpleListFilter):
|
||||||
title = _('timezone')
|
title = _("timezone")
|
||||||
parameter_name = 'timezone'
|
parameter_name = "timezone"
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
return Profile.objects.values_list('timezone', 'timezone').distinct().order_by('timezone')
|
return (
|
||||||
|
Profile.objects.values_list("timezone", "timezone")
|
||||||
|
.distinct()
|
||||||
|
.order_by("timezone")
|
||||||
|
)
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value() is None:
|
if self.value() is None:
|
||||||
|
@ -43,76 +60,168 @@ class TimezoneFilter(admin.SimpleListFilter):
|
||||||
return queryset.filter(timezone=self.value())
|
return queryset.filter(timezone=self.value())
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileInfoInline(admin.StackedInline):
|
||||||
|
model = ProfileInfo
|
||||||
|
can_delete = False
|
||||||
|
verbose_name_plural = "profile info"
|
||||||
|
fk_name = "profile"
|
||||||
|
|
||||||
|
|
||||||
class ProfileAdmin(VersionAdmin):
|
class ProfileAdmin(VersionAdmin):
|
||||||
fields = ('user', 'display_rank', 'about', 'organizations', 'timezone', 'language', 'ace_theme',
|
fields = (
|
||||||
'math_engine', 'last_access', 'ip', 'mute', 'is_unlisted', 'notes', 'is_totp_enabled', 'user_script',
|
"user",
|
||||||
'current_contest')
|
"display_rank",
|
||||||
readonly_fields = ('user',)
|
"about",
|
||||||
list_display = ('admin_user_admin', 'email', 'is_totp_enabled', 'timezone_full',
|
"organizations",
|
||||||
'date_joined', 'last_access', 'ip', 'show_public')
|
"timezone",
|
||||||
ordering = ('user__username',)
|
"language",
|
||||||
search_fields = ('user__username', 'ip', 'user__email')
|
"ace_theme",
|
||||||
list_filter = ('language', TimezoneFilter)
|
"last_access",
|
||||||
actions = ('recalculate_points',)
|
"ip",
|
||||||
|
"mute",
|
||||||
|
"is_unlisted",
|
||||||
|
"notes",
|
||||||
|
"is_totp_enabled",
|
||||||
|
"current_contest",
|
||||||
|
)
|
||||||
|
readonly_fields = ("user",)
|
||||||
|
list_display = (
|
||||||
|
"admin_user_admin",
|
||||||
|
"email",
|
||||||
|
"is_totp_enabled",
|
||||||
|
"timezone_full",
|
||||||
|
"date_joined",
|
||||||
|
"last_access",
|
||||||
|
"ip",
|
||||||
|
"show_public",
|
||||||
|
)
|
||||||
|
ordering = ("user__username",)
|
||||||
|
search_fields = ("user__username", "ip", "user__email")
|
||||||
|
list_filter = ("language", TimezoneFilter)
|
||||||
|
actions = ("recalculate_points",)
|
||||||
actions_on_top = True
|
actions_on_top = True
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
form = ProfileForm
|
form = ProfileForm
|
||||||
|
inlines = (ProfileInfoInline,)
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
return super(ProfileAdmin, self).get_queryset(request).select_related('user')
|
return super(ProfileAdmin, self).get_queryset(request).select_related("user")
|
||||||
|
|
||||||
def get_fields(self, request, obj=None):
|
def get_fields(self, request, obj=None):
|
||||||
if request.user.has_perm('judge.totp'):
|
if request.user.has_perm("judge.totp"):
|
||||||
fields = list(self.fields)
|
fields = list(self.fields)
|
||||||
fields.insert(fields.index('is_totp_enabled') + 1, 'totp_key')
|
fields.insert(fields.index("is_totp_enabled") + 1, "totp_key")
|
||||||
return tuple(fields)
|
return tuple(fields)
|
||||||
else:
|
else:
|
||||||
return self.fields
|
return self.fields
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
fields = self.readonly_fields
|
fields = self.readonly_fields
|
||||||
if not request.user.has_perm('judge.totp'):
|
if not request.user.has_perm("judge.totp"):
|
||||||
fields += ('is_totp_enabled',)
|
fields += ("is_totp_enabled",)
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
def show_public(self, obj):
|
def show_public(self, obj):
|
||||||
return format_html('<a href="{0}" style="white-space:nowrap;">{1}</a>',
|
return format_html(
|
||||||
obj.get_absolute_url(), gettext('View on site'))
|
'<a href="{0}" style="white-space:nowrap;">{1}</a>',
|
||||||
show_public.short_description = ''
|
obj.get_absolute_url(),
|
||||||
|
gettext("View on site"),
|
||||||
|
)
|
||||||
|
|
||||||
|
show_public.short_description = ""
|
||||||
|
|
||||||
def admin_user_admin(self, obj):
|
def admin_user_admin(self, obj):
|
||||||
return obj.username
|
return obj.username
|
||||||
admin_user_admin.admin_order_field = 'user__username'
|
|
||||||
admin_user_admin.short_description = _('User')
|
admin_user_admin.admin_order_field = "user__username"
|
||||||
|
admin_user_admin.short_description = _("User")
|
||||||
|
|
||||||
def email(self, obj):
|
def email(self, obj):
|
||||||
return obj.user.email
|
return obj.email
|
||||||
email.admin_order_field = 'user__email'
|
|
||||||
email.short_description = _('Email')
|
email.admin_order_field = "user__email"
|
||||||
|
email.short_description = _("Email")
|
||||||
|
|
||||||
def timezone_full(self, obj):
|
def timezone_full(self, obj):
|
||||||
return obj.timezone
|
return obj.timezone
|
||||||
timezone_full.admin_order_field = 'timezone'
|
|
||||||
timezone_full.short_description = _('Timezone')
|
timezone_full.admin_order_field = "timezone"
|
||||||
|
timezone_full.short_description = _("Timezone")
|
||||||
|
|
||||||
def date_joined(self, obj):
|
def date_joined(self, obj):
|
||||||
return obj.user.date_joined
|
return obj.user.date_joined
|
||||||
date_joined.admin_order_field = 'user__date_joined'
|
|
||||||
date_joined.short_description = _('date joined')
|
date_joined.admin_order_field = "user__date_joined"
|
||||||
|
date_joined.short_description = _("date joined")
|
||||||
|
|
||||||
def recalculate_points(self, request, queryset):
|
def recalculate_points(self, request, queryset):
|
||||||
count = 0
|
count = 0
|
||||||
for profile in queryset:
|
for profile in queryset:
|
||||||
profile.calculate_points()
|
profile.calculate_points()
|
||||||
count += 1
|
count += 1
|
||||||
self.message_user(request, ungettext('%d user have scores recalculated.',
|
self.message_user(
|
||||||
'%d users have scores recalculated.',
|
request,
|
||||||
count) % count)
|
ungettext(
|
||||||
recalculate_points.short_description = _('Recalculate scores')
|
"%d user have scores recalculated.",
|
||||||
|
"%d users have scores recalculated.",
|
||||||
|
count,
|
||||||
|
)
|
||||||
|
% count,
|
||||||
|
)
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
recalculate_points.short_description = _("Recalculate scores")
|
||||||
form = super(ProfileAdmin, self).get_form(request, obj, **kwargs)
|
|
||||||
if 'user_script' in form.base_fields:
|
|
||||||
# form.base_fields['user_script'] does not exist when the user has only view permission on the model.
|
class UserForm(UserChangeForm):
|
||||||
form.base_fields['user_script'].widget = AceWidget('javascript', request.profile.ace_theme)
|
def __init__(self, *args, **kwargs):
|
||||||
return form
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields["username"].help_text = _(
|
||||||
|
"Username can only contain letters, digits, and underscores."
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean_username(self):
|
||||||
|
username = self.cleaned_data.get("username")
|
||||||
|
if not re.match(r"^\w+$", username):
|
||||||
|
raise ValidationError(
|
||||||
|
_("Username can only contain letters, digits, and underscores.")
|
||||||
|
)
|
||||||
|
return username
|
||||||
|
|
||||||
|
|
||||||
|
class UserAdmin(OldUserAdmin):
|
||||||
|
# Customize the fieldsets for adding and editing users
|
||||||
|
form = UserForm
|
||||||
|
fieldsets = (
|
||||||
|
(None, {"fields": ("username", "password")}),
|
||||||
|
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
|
||||||
|
(
|
||||||
|
"Permissions",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"is_active",
|
||||||
|
"is_staff",
|
||||||
|
"is_superuser",
|
||||||
|
"groups",
|
||||||
|
"user_permissions",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
("Important dates", {"fields": ("last_login", "date_joined")}),
|
||||||
|
)
|
||||||
|
|
||||||
|
readonly_fields = ("last_login", "date_joined")
|
||||||
|
|
||||||
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
fields = self.readonly_fields
|
||||||
|
if not request.user.is_superuser:
|
||||||
|
fields += (
|
||||||
|
"is_staff",
|
||||||
|
"is_active",
|
||||||
|
"is_superuser",
|
||||||
|
"groups",
|
||||||
|
"user_permissions",
|
||||||
|
)
|
||||||
|
return fields
|
||||||
|
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
return False
|
||||||
|
|
|
@ -16,41 +16,63 @@ from judge.widgets import AdminHeavySelect2MultipleWidget, AdminPagedownWidget
|
||||||
|
|
||||||
class LanguageForm(ModelForm):
|
class LanguageForm(ModelForm):
|
||||||
problems = ModelMultipleChoiceField(
|
problems = ModelMultipleChoiceField(
|
||||||
label=_('Disallowed problems'),
|
label=_("Disallowed problems"),
|
||||||
queryset=Problem.objects.all(),
|
queryset=Problem.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_('These problems are NOT allowed to be submitted in this language'),
|
help_text=_("These problems are NOT allowed to be submitted in this language"),
|
||||||
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'))
|
widget=AdminHeavySelect2MultipleWidget(data_view="problem_select2"),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
if AdminPagedownWidget is not None:
|
if AdminPagedownWidget is not None:
|
||||||
widgets = {'description': AdminPagedownWidget}
|
widgets = {"description": AdminPagedownWidget}
|
||||||
|
|
||||||
|
|
||||||
class LanguageAdmin(VersionAdmin):
|
class LanguageAdmin(VersionAdmin):
|
||||||
fields = ('key', 'name', 'short_name', 'common_name', 'ace', 'pygments', 'info', 'description',
|
fields = (
|
||||||
'template', 'problems')
|
"key",
|
||||||
list_display = ('key', 'name', 'common_name', 'info')
|
"name",
|
||||||
|
"short_name",
|
||||||
|
"common_name",
|
||||||
|
"ace",
|
||||||
|
"pygments",
|
||||||
|
"info",
|
||||||
|
"description",
|
||||||
|
"template",
|
||||||
|
"problems",
|
||||||
|
)
|
||||||
|
list_display = ("key", "name", "common_name", "info")
|
||||||
form = LanguageForm
|
form = LanguageForm
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
super(LanguageAdmin, self).save_model(request, obj, form, change)
|
super(LanguageAdmin, self).save_model(request, obj, form, change)
|
||||||
obj.problem_set.set(Problem.objects.exclude(id__in=form.cleaned_data['problems'].values('id')))
|
obj.problem_set.set(
|
||||||
|
Problem.objects.exclude(id__in=form.cleaned_data["problems"].values("id"))
|
||||||
|
)
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
self.form.base_fields['problems'].initial = \
|
self.form.base_fields["problems"].initial = (
|
||||||
Problem.objects.exclude(id__in=obj.problem_set.values('id')).values_list('pk', flat=True) if obj else []
|
Problem.objects.exclude(id__in=obj.problem_set.values("id")).values_list(
|
||||||
|
"pk", flat=True
|
||||||
|
)
|
||||||
|
if obj
|
||||||
|
else []
|
||||||
|
)
|
||||||
form = super(LanguageAdmin, self).get_form(request, obj, **kwargs)
|
form = super(LanguageAdmin, self).get_form(request, obj, **kwargs)
|
||||||
if obj is not None:
|
if obj is not None:
|
||||||
form.base_fields['template'].widget = AceWidget(obj.ace, request.profile.ace_theme)
|
form.base_fields["template"].widget = AceWidget(
|
||||||
|
obj.ace, request.profile.ace_theme
|
||||||
|
)
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
|
||||||
class GenerateKeyTextInput(TextInput):
|
class GenerateKeyTextInput(TextInput):
|
||||||
def render(self, name, value, attrs=None, renderer=None):
|
def render(self, name, value, attrs=None, renderer=None):
|
||||||
text = super(TextInput, self).render(name, value, attrs)
|
text = super(TextInput, self).render(name, value, attrs)
|
||||||
return mark_safe(text + format_html(
|
return mark_safe(
|
||||||
'''\
|
text
|
||||||
|
+ format_html(
|
||||||
|
"""\
|
||||||
<a href="#" onclick="return false;" class="button" id="id_{0}_regen">Regenerate</a>
|
<a href="#" onclick="return false;" class="button" id="id_{0}_regen">Regenerate</a>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
django.jQuery(document).ready(function ($) {{
|
django.jQuery(document).ready(function ($) {{
|
||||||
|
@ -65,37 +87,59 @@ django.jQuery(document).ready(function ($) {{
|
||||||
}});
|
}});
|
||||||
}});
|
}});
|
||||||
</script>
|
</script>
|
||||||
''', name))
|
""",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class JudgeAdminForm(ModelForm):
|
class JudgeAdminForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {'auth_key': GenerateKeyTextInput}
|
widgets = {"auth_key": GenerateKeyTextInput}
|
||||||
if AdminPagedownWidget is not None:
|
if AdminPagedownWidget is not None:
|
||||||
widgets['description'] = AdminPagedownWidget
|
widgets["description"] = AdminPagedownWidget
|
||||||
|
|
||||||
|
|
||||||
class JudgeAdmin(VersionAdmin):
|
class JudgeAdmin(VersionAdmin):
|
||||||
form = JudgeAdminForm
|
form = JudgeAdminForm
|
||||||
readonly_fields = ('created', 'online', 'start_time', 'ping', 'load', 'last_ip', 'runtimes', 'problems')
|
readonly_fields = (
|
||||||
fieldsets = (
|
"created",
|
||||||
(None, {'fields': ('name', 'auth_key', 'is_blocked')}),
|
"online",
|
||||||
(_('Description'), {'fields': ('description',)}),
|
"start_time",
|
||||||
(_('Information'), {'fields': ('created', 'online', 'last_ip', 'start_time', 'ping', 'load')}),
|
"ping",
|
||||||
(_('Capabilities'), {'fields': ('runtimes', 'problems')}),
|
"load",
|
||||||
|
"last_ip",
|
||||||
|
"runtimes",
|
||||||
|
"problems",
|
||||||
)
|
)
|
||||||
list_display = ('name', 'online', 'start_time', 'ping', 'load', 'last_ip')
|
fieldsets = (
|
||||||
ordering = ['-online', 'name']
|
(None, {"fields": ("name", "auth_key", "is_blocked")}),
|
||||||
|
(_("Description"), {"fields": ("description",)}),
|
||||||
|
(
|
||||||
|
_("Information"),
|
||||||
|
{"fields": ("created", "online", "last_ip", "start_time", "ping", "load")},
|
||||||
|
),
|
||||||
|
(_("Capabilities"), {"fields": ("runtimes", "problems")}),
|
||||||
|
)
|
||||||
|
list_display = ("name", "online", "start_time", "ping", "load", "last_ip")
|
||||||
|
ordering = ["-online", "name"]
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
return ([url(r'^(\d+)/disconnect/$', self.disconnect_view, name='judge_judge_disconnect'),
|
return [
|
||||||
url(r'^(\d+)/terminate/$', self.terminate_view, name='judge_judge_terminate')] +
|
url(
|
||||||
super(JudgeAdmin, self).get_urls())
|
r"^(\d+)/disconnect/$",
|
||||||
|
self.disconnect_view,
|
||||||
|
name="judge_judge_disconnect",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^(\d+)/terminate/$", self.terminate_view, name="judge_judge_terminate"
|
||||||
|
),
|
||||||
|
] + super(JudgeAdmin, self).get_urls()
|
||||||
|
|
||||||
def disconnect_judge(self, id, force=False):
|
def disconnect_judge(self, id, force=False):
|
||||||
judge = get_object_or_404(Judge, id=id)
|
judge = get_object_or_404(Judge, id=id)
|
||||||
judge.disconnect(force=force)
|
judge.disconnect(force=force)
|
||||||
return HttpResponseRedirect(reverse('admin:judge_judge_changelist'))
|
return HttpResponseRedirect(reverse("admin:judge_judge_changelist"))
|
||||||
|
|
||||||
def disconnect_view(self, request, id):
|
def disconnect_view(self, request, id):
|
||||||
return self.disconnect_judge(id)
|
return self.disconnect_judge(id)
|
||||||
|
@ -105,7 +149,7 @@ class JudgeAdmin(VersionAdmin):
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
if obj is not None and obj.online:
|
if obj is not None and obj.online:
|
||||||
return self.readonly_fields + ('name',)
|
return self.readonly_fields + ("name",)
|
||||||
return self.readonly_fields
|
return self.readonly_fields
|
||||||
|
|
||||||
def has_delete_permission(self, request, obj=None):
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
@ -116,5 +160,5 @@ class JudgeAdmin(VersionAdmin):
|
||||||
|
|
||||||
if AdminPagedownWidget is not None:
|
if AdminPagedownWidget is not None:
|
||||||
formfield_overrides = {
|
formfield_overrides = {
|
||||||
TextField: {'widget': AdminPagedownWidget},
|
TextField: {"widget": AdminPagedownWidget},
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,239 +13,365 @@ from django.utils.html import format_html
|
||||||
from django.utils.translation import gettext, gettext_lazy as _, pgettext, ungettext
|
from django.utils.translation import gettext, gettext_lazy as _, pgettext, ungettext
|
||||||
|
|
||||||
from django_ace import AceWidget
|
from django_ace import AceWidget
|
||||||
from judge.models import ContestParticipation, ContestProblem, ContestSubmission, Profile, Submission, \
|
from judge.models import (
|
||||||
SubmissionSource, SubmissionTestCase
|
ContestParticipation,
|
||||||
|
ContestProblem,
|
||||||
|
ContestSubmission,
|
||||||
|
Profile,
|
||||||
|
Submission,
|
||||||
|
SubmissionSource,
|
||||||
|
SubmissionTestCase,
|
||||||
|
)
|
||||||
from judge.utils.raw_sql import use_straight_join
|
from judge.utils.raw_sql import use_straight_join
|
||||||
|
|
||||||
|
|
||||||
class SubmissionStatusFilter(admin.SimpleListFilter):
|
class SubmissionStatusFilter(admin.SimpleListFilter):
|
||||||
parameter_name = title = 'status'
|
parameter_name = title = "status"
|
||||||
__lookups = (('None', _('None')), ('NotDone', _('Not done')), ('EX', _('Exceptional'))) + Submission.STATUS
|
__lookups = (
|
||||||
|
("None", _("None")),
|
||||||
|
("NotDone", _("Not done")),
|
||||||
|
("EX", _("Exceptional")),
|
||||||
|
) + Submission.STATUS
|
||||||
__handles = set(map(itemgetter(0), Submission.STATUS))
|
__handles = set(map(itemgetter(0), Submission.STATUS))
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
return self.__lookups
|
return self.__lookups
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value() == 'None':
|
if self.value() == "None":
|
||||||
return queryset.filter(status=None)
|
return queryset.filter(status=None)
|
||||||
elif self.value() == 'NotDone':
|
elif self.value() == "NotDone":
|
||||||
return queryset.exclude(status__in=['D', 'IE', 'CE', 'AB'])
|
return queryset.exclude(status__in=["D", "IE", "CE", "AB"])
|
||||||
elif self.value() == 'EX':
|
elif self.value() == "EX":
|
||||||
return queryset.exclude(status__in=['D', 'CE', 'G', 'AB'])
|
return queryset.exclude(status__in=["D", "CE", "G", "AB"])
|
||||||
elif self.value() in self.__handles:
|
elif self.value() in self.__handles:
|
||||||
return queryset.filter(status=self.value())
|
return queryset.filter(status=self.value())
|
||||||
|
|
||||||
|
|
||||||
class SubmissionResultFilter(admin.SimpleListFilter):
|
class SubmissionResultFilter(admin.SimpleListFilter):
|
||||||
parameter_name = title = 'result'
|
parameter_name = title = "result"
|
||||||
__lookups = (('None', _('None')), ('BAD', _('Unaccepted'))) + Submission.RESULT
|
__lookups = (("None", _("None")), ("BAD", _("Unaccepted"))) + Submission.RESULT
|
||||||
__handles = set(map(itemgetter(0), Submission.RESULT))
|
__handles = set(map(itemgetter(0), Submission.RESULT))
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
return self.__lookups
|
return self.__lookups
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value() == 'None':
|
if self.value() == "None":
|
||||||
return queryset.filter(result=None)
|
return queryset.filter(result=None)
|
||||||
elif self.value() == 'BAD':
|
elif self.value() == "BAD":
|
||||||
return queryset.exclude(result='AC')
|
return queryset.exclude(result="AC")
|
||||||
elif self.value() in self.__handles:
|
elif self.value() in self.__handles:
|
||||||
return queryset.filter(result=self.value())
|
return queryset.filter(result=self.value())
|
||||||
|
|
||||||
|
|
||||||
class SubmissionTestCaseInline(admin.TabularInline):
|
class SubmissionTestCaseInline(admin.TabularInline):
|
||||||
fields = ('case', 'batch', 'status', 'time', 'memory', 'points', 'total')
|
fields = ("case", "batch", "status", "time", "memory", "points", "total")
|
||||||
readonly_fields = ('case', 'batch', 'total')
|
readonly_fields = ("case", "batch", "total")
|
||||||
model = SubmissionTestCase
|
model = SubmissionTestCase
|
||||||
can_delete = False
|
can_delete = False
|
||||||
max_num = 0
|
max_num = 0
|
||||||
|
|
||||||
|
|
||||||
class ContestSubmissionInline(admin.StackedInline):
|
class ContestSubmissionInline(admin.StackedInline):
|
||||||
fields = ('problem', 'participation', 'points')
|
fields = ("problem", "participation", "points")
|
||||||
model = ContestSubmission
|
model = ContestSubmission
|
||||||
|
|
||||||
def get_formset(self, request, obj=None, **kwargs):
|
def get_formset(self, request, obj=None, **kwargs):
|
||||||
kwargs['formfield_callback'] = partial(self.formfield_for_dbfield, request=request, obj=obj)
|
kwargs["formfield_callback"] = partial(
|
||||||
|
self.formfield_for_dbfield, request=request, obj=obj
|
||||||
|
)
|
||||||
return super(ContestSubmissionInline, self).get_formset(request, obj, **kwargs)
|
return super(ContestSubmissionInline, self).get_formset(request, obj, **kwargs)
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
submission = kwargs.pop('obj', None)
|
submission = kwargs.pop("obj", None)
|
||||||
label = None
|
label = None
|
||||||
if submission:
|
if submission:
|
||||||
if db_field.name == 'participation':
|
if db_field.name == "participation":
|
||||||
kwargs['queryset'] = ContestParticipation.objects.filter(user=submission.user,
|
kwargs["queryset"] = ContestParticipation.objects.filter(
|
||||||
contest__problems=submission.problem) \
|
user=submission.user, contest__problems=submission.problem
|
||||||
.only('id', 'contest__name')
|
).only("id", "contest__name")
|
||||||
|
|
||||||
def label(obj):
|
def label(obj):
|
||||||
return obj.contest.name
|
return obj.contest.name
|
||||||
elif db_field.name == 'problem':
|
|
||||||
kwargs['queryset'] = ContestProblem.objects.filter(problem=submission.problem) \
|
elif db_field.name == "problem":
|
||||||
.only('id', 'problem__name', 'contest__name')
|
kwargs["queryset"] = ContestProblem.objects.filter(
|
||||||
|
problem=submission.problem
|
||||||
|
).only("id", "problem__name", "contest__name")
|
||||||
|
|
||||||
def label(obj):
|
def label(obj):
|
||||||
return pgettext('contest problem', '%(problem)s in %(contest)s') % {
|
return pgettext("contest problem", "%(problem)s in %(contest)s") % {
|
||||||
'problem': obj.problem.name, 'contest': obj.contest.name,
|
"problem": obj.problem.name,
|
||||||
|
"contest": obj.contest.name,
|
||||||
}
|
}
|
||||||
field = super(ContestSubmissionInline, self).formfield_for_dbfield(db_field, **kwargs)
|
|
||||||
|
field = super(ContestSubmissionInline, self).formfield_for_dbfield(
|
||||||
|
db_field, **kwargs
|
||||||
|
)
|
||||||
if label is not None:
|
if label is not None:
|
||||||
field.label_from_instance = label
|
field.label_from_instance = label
|
||||||
return field
|
return field
|
||||||
|
|
||||||
|
|
||||||
class SubmissionSourceInline(admin.StackedInline):
|
class SubmissionSourceInline(admin.StackedInline):
|
||||||
fields = ('source',)
|
fields = ("source",)
|
||||||
model = SubmissionSource
|
model = SubmissionSource
|
||||||
can_delete = False
|
can_delete = False
|
||||||
extra = 0
|
extra = 0
|
||||||
|
|
||||||
def get_formset(self, request, obj=None, **kwargs):
|
def get_formset(self, request, obj=None, **kwargs):
|
||||||
kwargs.setdefault('widgets', {})['source'] = AceWidget(mode=obj and obj.language.ace,
|
kwargs.setdefault("widgets", {})["source"] = AceWidget(
|
||||||
theme=request.profile.ace_theme)
|
mode=obj and obj.language.ace, theme=request.profile.ace_theme
|
||||||
|
)
|
||||||
return super().get_formset(request, obj, **kwargs)
|
return super().get_formset(request, obj, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class SubmissionAdmin(admin.ModelAdmin):
|
class SubmissionAdmin(admin.ModelAdmin):
|
||||||
readonly_fields = ('user', 'problem', 'date', 'judged_date')
|
readonly_fields = ("user", "problem", "date", "judged_date")
|
||||||
fields = ('user', 'problem', 'date', 'judged_date', 'time', 'memory', 'points', 'language', 'status', 'result',
|
fields = (
|
||||||
'case_points', 'case_total', 'judged_on', 'error')
|
"user",
|
||||||
actions = ('judge', 'recalculate_score')
|
"problem",
|
||||||
list_display = ('id', 'problem_code', 'problem_name', 'user_column', 'execution_time', 'pretty_memory',
|
"date",
|
||||||
'points', 'language_column', 'status', 'result', 'judge_column')
|
"judged_date",
|
||||||
list_filter = ('language', SubmissionStatusFilter, SubmissionResultFilter)
|
"time",
|
||||||
search_fields = ('problem__code', 'problem__name', 'user__user__username')
|
"memory",
|
||||||
|
"points",
|
||||||
|
"language",
|
||||||
|
"status",
|
||||||
|
"result",
|
||||||
|
"case_points",
|
||||||
|
"case_total",
|
||||||
|
"judged_on",
|
||||||
|
"error",
|
||||||
|
)
|
||||||
|
actions = ("judge", "recalculate_score")
|
||||||
|
list_display = (
|
||||||
|
"id",
|
||||||
|
"problem_code",
|
||||||
|
"problem_name",
|
||||||
|
"user_column",
|
||||||
|
"execution_time",
|
||||||
|
"pretty_memory",
|
||||||
|
"points",
|
||||||
|
"language_column",
|
||||||
|
"status",
|
||||||
|
"result",
|
||||||
|
"judge_column",
|
||||||
|
)
|
||||||
|
list_filter = ("language", SubmissionStatusFilter, SubmissionResultFilter)
|
||||||
|
search_fields = ("problem__code", "problem__name", "user__user__username")
|
||||||
actions_on_top = True
|
actions_on_top = True
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
inlines = [SubmissionSourceInline, SubmissionTestCaseInline, ContestSubmissionInline]
|
inlines = [
|
||||||
|
SubmissionSourceInline,
|
||||||
|
SubmissionTestCaseInline,
|
||||||
|
ContestSubmissionInline,
|
||||||
|
]
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
queryset = Submission.objects.select_related('problem', 'user__user', 'language').only(
|
queryset = Submission.objects.select_related(
|
||||||
'problem__code', 'problem__name', 'user__user__username', 'language__name',
|
"problem", "user__user", "language"
|
||||||
'time', 'memory', 'points', 'status', 'result',
|
).only(
|
||||||
|
"problem__code",
|
||||||
|
"problem__name",
|
||||||
|
"user__user__username",
|
||||||
|
"language__name",
|
||||||
|
"time",
|
||||||
|
"memory",
|
||||||
|
"points",
|
||||||
|
"status",
|
||||||
|
"result",
|
||||||
)
|
)
|
||||||
use_straight_join(queryset)
|
use_straight_join(queryset)
|
||||||
if not request.user.has_perm('judge.edit_all_problem'):
|
if not request.user.has_perm("judge.edit_all_problem"):
|
||||||
id = request.profile.id
|
id = request.profile.id
|
||||||
queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id)).distinct()
|
queryset = queryset.filter(
|
||||||
|
Q(problem__authors__id=id) | Q(problem__curators__id=id)
|
||||||
|
).distinct()
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
def has_add_permission(self, request):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
|
||||||
if not request.user.has_perm('judge.edit_own_problem'):
|
|
||||||
return False
|
|
||||||
if request.user.has_perm('judge.edit_all_problem') or obj is None:
|
|
||||||
return True
|
|
||||||
return obj.problem.is_editor(request.profile)
|
|
||||||
|
|
||||||
def lookup_allowed(self, key, value):
|
def lookup_allowed(self, key, value):
|
||||||
return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in ('problem__code',)
|
return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in (
|
||||||
|
"problem__code",
|
||||||
|
)
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
|
if "case_points" in form.changed_data or "case_total" in form.changed_data:
|
||||||
|
obj.update_contest()
|
||||||
|
|
||||||
def judge(self, request, queryset):
|
def judge(self, request, queryset):
|
||||||
if not request.user.has_perm('judge.rejudge_submission') or not request.user.has_perm('judge.edit_own_problem'):
|
if not request.user.has_perm(
|
||||||
self.message_user(request, gettext('You do not have the permission to rejudge submissions.'),
|
"judge.rejudge_submission"
|
||||||
level=messages.ERROR)
|
) or not request.user.has_perm("judge.edit_own_problem"):
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
gettext("You do not have the permission to rejudge submissions."),
|
||||||
|
level=messages.ERROR,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
queryset = queryset.order_by('id')
|
queryset = queryset.order_by("id")
|
||||||
if not request.user.has_perm('judge.rejudge_submission_lot') and \
|
if (
|
||||||
queryset.count() > settings.DMOJ_SUBMISSIONS_REJUDGE_LIMIT:
|
not request.user.has_perm("judge.rejudge_submission_lot")
|
||||||
self.message_user(request, gettext('You do not have the permission to rejudge THAT many submissions.'),
|
and queryset.count() > settings.DMOJ_SUBMISSIONS_REJUDGE_LIMIT
|
||||||
level=messages.ERROR)
|
):
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
gettext(
|
||||||
|
"You do not have the permission to rejudge THAT many submissions."
|
||||||
|
),
|
||||||
|
level=messages.ERROR,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
if not request.user.has_perm('judge.edit_all_problem'):
|
if not request.user.has_perm("judge.edit_all_problem"):
|
||||||
id = request.profile.id
|
id = request.profile.id
|
||||||
queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id))
|
queryset = queryset.filter(
|
||||||
|
Q(problem__authors__id=id) | Q(problem__curators__id=id)
|
||||||
|
)
|
||||||
judged = len(queryset)
|
judged = len(queryset)
|
||||||
for model in queryset:
|
for model in queryset:
|
||||||
model.judge(rejudge=True, batch_rejudge=True)
|
model.judge(rejudge=True, batch_rejudge=True)
|
||||||
self.message_user(request, ungettext('%d submission was successfully scheduled for rejudging.',
|
self.message_user(
|
||||||
'%d submissions were successfully scheduled for rejudging.',
|
request,
|
||||||
judged) % judged)
|
ungettext(
|
||||||
judge.short_description = _('Rejudge the selected submissions')
|
"%d submission was successfully scheduled for rejudging.",
|
||||||
|
"%d submissions were successfully scheduled for rejudging.",
|
||||||
|
judged,
|
||||||
|
)
|
||||||
|
% judged,
|
||||||
|
)
|
||||||
|
|
||||||
|
judge.short_description = _("Rejudge the selected submissions")
|
||||||
|
|
||||||
def recalculate_score(self, request, queryset):
|
def recalculate_score(self, request, queryset):
|
||||||
if not request.user.has_perm('judge.rejudge_submission'):
|
if not request.user.has_perm("judge.rejudge_submission"):
|
||||||
self.message_user(request, gettext('You do not have the permission to rejudge submissions.'),
|
self.message_user(
|
||||||
level=messages.ERROR)
|
request,
|
||||||
|
gettext("You do not have the permission to rejudge submissions."),
|
||||||
|
level=messages.ERROR,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
submissions = list(queryset.defer(None).select_related(None).select_related('problem')
|
submissions = list(
|
||||||
.only('points', 'case_points', 'case_total', 'problem__partial', 'problem__points'))
|
queryset.defer(None)
|
||||||
|
.select_related(None)
|
||||||
|
.select_related("problem")
|
||||||
|
.only(
|
||||||
|
"points",
|
||||||
|
"case_points",
|
||||||
|
"case_total",
|
||||||
|
"problem__partial",
|
||||||
|
"problem__points",
|
||||||
|
)
|
||||||
|
)
|
||||||
for submission in submissions:
|
for submission in submissions:
|
||||||
submission.points = round(submission.case_points / submission.case_total * submission.problem.points
|
submission.points = round(
|
||||||
if submission.case_total else 0, 1)
|
submission.case_points
|
||||||
if not submission.problem.partial and submission.points < submission.problem.points:
|
/ submission.case_total
|
||||||
|
* submission.problem.points
|
||||||
|
if submission.case_total
|
||||||
|
else 0,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
not submission.problem.partial
|
||||||
|
and submission.points < submission.problem.points
|
||||||
|
):
|
||||||
submission.points = 0
|
submission.points = 0
|
||||||
submission.save()
|
submission.save()
|
||||||
submission.update_contest()
|
submission.update_contest()
|
||||||
|
|
||||||
for profile in Profile.objects.filter(id__in=queryset.values_list('user_id', flat=True).distinct()):
|
for profile in Profile.objects.filter(
|
||||||
|
id__in=queryset.values_list("user_id", flat=True).distinct()
|
||||||
|
):
|
||||||
profile.calculate_points()
|
profile.calculate_points()
|
||||||
cache.delete('user_complete:%d' % profile.id)
|
cache.delete("user_complete:%d" % profile.id)
|
||||||
cache.delete('user_attempted:%d' % profile.id)
|
cache.delete("user_attempted:%d" % profile.id)
|
||||||
|
|
||||||
for participation in ContestParticipation.objects.filter(
|
for participation in ContestParticipation.objects.filter(
|
||||||
id__in=queryset.values_list('contest__participation_id')).prefetch_related('contest'):
|
id__in=queryset.values_list("contest__participation_id")
|
||||||
|
).prefetch_related("contest"):
|
||||||
participation.recompute_results()
|
participation.recompute_results()
|
||||||
|
|
||||||
self.message_user(request, ungettext('%d submission were successfully rescored.',
|
self.message_user(
|
||||||
'%d submissions were successfully rescored.',
|
request,
|
||||||
len(submissions)) % len(submissions))
|
ungettext(
|
||||||
recalculate_score.short_description = _('Rescore the selected submissions')
|
"%d submission were successfully rescored.",
|
||||||
|
"%d submissions were successfully rescored.",
|
||||||
|
len(submissions),
|
||||||
|
)
|
||||||
|
% len(submissions),
|
||||||
|
)
|
||||||
|
|
||||||
|
recalculate_score.short_description = _("Rescore the selected submissions")
|
||||||
|
|
||||||
def problem_code(self, obj):
|
def problem_code(self, obj):
|
||||||
return obj.problem.code
|
return obj.problem.code
|
||||||
problem_code.short_description = _('Problem code')
|
|
||||||
problem_code.admin_order_field = 'problem__code'
|
problem_code.short_description = _("Problem code")
|
||||||
|
problem_code.admin_order_field = "problem__code"
|
||||||
|
|
||||||
def problem_name(self, obj):
|
def problem_name(self, obj):
|
||||||
return obj.problem.name
|
return obj.problem.name
|
||||||
problem_name.short_description = _('Problem name')
|
|
||||||
problem_name.admin_order_field = 'problem__name'
|
problem_name.short_description = _("Problem name")
|
||||||
|
problem_name.admin_order_field = "problem__name"
|
||||||
|
|
||||||
def user_column(self, obj):
|
def user_column(self, obj):
|
||||||
return obj.user.user.username
|
return obj.user.user.username
|
||||||
user_column.admin_order_field = 'user__user__username'
|
|
||||||
user_column.short_description = _('User')
|
user_column.admin_order_field = "user__user__username"
|
||||||
|
user_column.short_description = _("User")
|
||||||
|
|
||||||
def execution_time(self, obj):
|
def execution_time(self, obj):
|
||||||
return round(obj.time, 2) if obj.time is not None else 'None'
|
return round(obj.time, 2) if obj.time is not None else "None"
|
||||||
execution_time.short_description = _('Time')
|
|
||||||
execution_time.admin_order_field = 'time'
|
execution_time.short_description = _("Time")
|
||||||
|
execution_time.admin_order_field = "time"
|
||||||
|
|
||||||
def pretty_memory(self, obj):
|
def pretty_memory(self, obj):
|
||||||
memory = obj.memory
|
memory = obj.memory
|
||||||
if memory is None:
|
if memory is None:
|
||||||
return gettext('None')
|
return gettext("None")
|
||||||
if memory < 1000:
|
if memory < 1000:
|
||||||
return gettext('%d KB') % memory
|
return gettext("%d KB") % memory
|
||||||
else:
|
else:
|
||||||
return gettext('%.2f MB') % (memory / 1024)
|
return gettext("%.2f MB") % (memory / 1024)
|
||||||
pretty_memory.admin_order_field = 'memory'
|
|
||||||
pretty_memory.short_description = _('Memory')
|
pretty_memory.admin_order_field = "memory"
|
||||||
|
pretty_memory.short_description = _("Memory")
|
||||||
|
|
||||||
def language_column(self, obj):
|
def language_column(self, obj):
|
||||||
return obj.language.name
|
return obj.language.name
|
||||||
language_column.admin_order_field = 'language__name'
|
|
||||||
language_column.short_description = _('Language')
|
language_column.admin_order_field = "language__name"
|
||||||
|
language_column.short_description = _("Language")
|
||||||
|
|
||||||
def judge_column(self, obj):
|
def judge_column(self, obj):
|
||||||
return format_html('<input type="button" value="Rejudge" onclick="location.href=\'{}/judge/\'" />', obj.id)
|
return format_html(
|
||||||
judge_column.short_description = ''
|
'<input type="button" value="Rejudge" onclick="location.href=\'{}/judge/\'" />',
|
||||||
|
obj.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
judge_column.short_description = ""
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
return [
|
return [
|
||||||
url(r'^(\d+)/judge/$', self.judge_view, name='judge_submission_rejudge'),
|
url(r"^(\d+)/judge/$", self.judge_view, name="judge_submission_rejudge"),
|
||||||
] + super(SubmissionAdmin, self).get_urls()
|
] + super(SubmissionAdmin, self).get_urls()
|
||||||
|
|
||||||
def judge_view(self, request, id):
|
def judge_view(self, request, id):
|
||||||
if not request.user.has_perm('judge.rejudge_submission') or not request.user.has_perm('judge.edit_own_problem'):
|
if not request.user.has_perm(
|
||||||
|
"judge.rejudge_submission"
|
||||||
|
) or not request.user.has_perm("judge.edit_own_problem"):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
submission = get_object_or_404(Submission, id=id)
|
submission = get_object_or_404(Submission, id=id)
|
||||||
if not request.user.has_perm('judge.edit_all_problem') and \
|
if not request.user.has_perm(
|
||||||
not submission.problem.is_editor(request.profile):
|
"judge.edit_all_problem"
|
||||||
|
) and not submission.problem.is_editor(request.profile):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
submission.judge(rejudge=True)
|
submission.judge(rejudge=True)
|
||||||
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
|
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
|
||||||
|
|
|
@ -8,45 +8,59 @@ from judge.widgets import AdminHeavySelect2MultipleWidget
|
||||||
|
|
||||||
class ProblemGroupForm(ModelForm):
|
class ProblemGroupForm(ModelForm):
|
||||||
problems = ModelMultipleChoiceField(
|
problems = ModelMultipleChoiceField(
|
||||||
label=_('Included problems'),
|
label=_("Included problems"),
|
||||||
queryset=Problem.objects.all(),
|
queryset=Problem.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_('These problems are included in this group of problems'),
|
help_text=_("These problems are included in this group of problems"),
|
||||||
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'))
|
widget=AdminHeavySelect2MultipleWidget(data_view="problem_select2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ProblemGroupAdmin(admin.ModelAdmin):
|
class ProblemGroupAdmin(admin.ModelAdmin):
|
||||||
fields = ('name', 'full_name', 'problems')
|
fields = ("name", "full_name", "problems")
|
||||||
form = ProblemGroupForm
|
form = ProblemGroupForm
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
super(ProblemGroupAdmin, self).save_model(request, obj, form, change)
|
super(ProblemGroupAdmin, self).save_model(request, obj, form, change)
|
||||||
obj.problem_set.set(form.cleaned_data['problems'])
|
obj.problem_set.set(form.cleaned_data["problems"])
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
self.form.base_fields['problems'].initial = [o.pk for o in obj.problem_set.all()] if obj else []
|
self.form.base_fields["problems"].initial = (
|
||||||
|
[o.pk for o in obj.problem_set.all()] if obj else []
|
||||||
|
)
|
||||||
return super(ProblemGroupAdmin, self).get_form(request, obj, **kwargs)
|
return super(ProblemGroupAdmin, self).get_form(request, obj, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ProblemTypeForm(ModelForm):
|
class ProblemTypeForm(ModelForm):
|
||||||
problems = ModelMultipleChoiceField(
|
problems = ModelMultipleChoiceField(
|
||||||
label=_('Included problems'),
|
label=_("Included problems"),
|
||||||
queryset=Problem.objects.all(),
|
queryset=Problem.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_('These problems are included in this type of problems'),
|
help_text=_("These problems are included in this type of problems"),
|
||||||
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'))
|
widget=AdminHeavySelect2MultipleWidget(data_view="problem_select2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ProblemTypeAdmin(admin.ModelAdmin):
|
class ProblemTypeAdmin(admin.ModelAdmin):
|
||||||
fields = ('name', 'full_name', 'problems')
|
fields = ("name", "full_name", "problems")
|
||||||
form = ProblemTypeForm
|
form = ProblemTypeForm
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
super(ProblemTypeAdmin, self).save_model(request, obj, form, change)
|
super(ProblemTypeAdmin, self).save_model(request, obj, form, change)
|
||||||
obj.problem_set.set(form.cleaned_data['problems'])
|
obj.problem_set.set(form.cleaned_data["problems"])
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
self.form.base_fields['problems'].initial = [o.pk for o in obj.problem_set.all()] if obj else []
|
self.form.base_fields["problems"].initial = (
|
||||||
|
[o.pk for o in obj.problem_set.all()] if obj else []
|
||||||
|
)
|
||||||
return super(ProblemTypeAdmin, self).get_form(request, obj, **kwargs)
|
return super(ProblemTypeAdmin, self).get_form(request, obj, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class OfficialContestCategoryAdmin(admin.ModelAdmin):
|
||||||
|
fields = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
class OfficialContestLocationAdmin(admin.ModelAdmin):
|
||||||
|
fields = ("name",)
|
||||||
|
|
|
@ -4,36 +4,56 @@ from django.forms import ModelForm
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
|
||||||
from judge.models import TicketMessage
|
from judge.models import TicketMessage
|
||||||
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
|
from judge.widgets import (
|
||||||
|
AdminHeavySelect2MultipleWidget,
|
||||||
|
AdminHeavySelect2Widget,
|
||||||
|
HeavyPreviewAdminPageDownWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TicketMessageForm(ModelForm):
|
class TicketMessageForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
"user": AdminHeavySelect2Widget(
|
||||||
|
data_view="profile_select2", attrs={"style": "width: 100%"}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets['body'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('ticket_preview'))
|
widgets["body"] = HeavyPreviewAdminPageDownWidget(
|
||||||
|
preview=reverse_lazy("ticket_preview")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TicketMessageInline(StackedInline):
|
class TicketMessageInline(StackedInline):
|
||||||
model = TicketMessage
|
model = TicketMessage
|
||||||
form = TicketMessageForm
|
form = TicketMessageForm
|
||||||
fields = ('user', 'body')
|
fields = ("user", "body")
|
||||||
|
|
||||||
|
|
||||||
class TicketForm(ModelForm):
|
class TicketForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
"user": AdminHeavySelect2Widget(
|
||||||
'assignees': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
data_view="profile_select2", attrs={"style": "width: 100%"}
|
||||||
|
),
|
||||||
|
"assignees": AdminHeavySelect2MultipleWidget(
|
||||||
|
data_view="profile_select2", attrs={"style": "width: 100%"}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TicketAdmin(ModelAdmin):
|
class TicketAdmin(ModelAdmin):
|
||||||
fields = ('title', 'time', 'user', 'assignees', 'content_type', 'object_id', 'notes')
|
fields = (
|
||||||
readonly_fields = ('time',)
|
"title",
|
||||||
list_display = ('title', 'user', 'time', 'linked_item')
|
"time",
|
||||||
|
"user",
|
||||||
|
"assignees",
|
||||||
|
"content_type",
|
||||||
|
"object_id",
|
||||||
|
"notes",
|
||||||
|
)
|
||||||
|
readonly_fields = ("time",)
|
||||||
|
list_display = ("title", "user", "time", "linked_item")
|
||||||
inlines = [TicketMessageInline]
|
inlines = [TicketMessageInline]
|
||||||
form = TicketForm
|
form = TicketForm
|
||||||
date_hierarchy = 'time'
|
date_hierarchy = "time"
|
||||||
|
|
67
judge/admin/volunteer.py
Normal file
67
judge/admin/volunteer.py
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext, gettext_lazy as _, ungettext
|
||||||
|
from django.forms import ModelForm
|
||||||
|
|
||||||
|
from judge.models import VolunteerProblemVote
|
||||||
|
from judge.widgets import AdminSelect2MultipleWidget
|
||||||
|
|
||||||
|
|
||||||
|
class VolunteerProblemVoteForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
widgets = {
|
||||||
|
"types": AdminSelect2MultipleWidget,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class VolunteerProblemVoteAdmin(admin.ModelAdmin):
|
||||||
|
form = VolunteerProblemVoteForm
|
||||||
|
fields = (
|
||||||
|
"voter",
|
||||||
|
"problem_link",
|
||||||
|
"time",
|
||||||
|
"thinking_points",
|
||||||
|
"knowledge_points",
|
||||||
|
"types",
|
||||||
|
"feedback",
|
||||||
|
)
|
||||||
|
readonly_fields = ("time", "problem_link", "voter")
|
||||||
|
list_display = (
|
||||||
|
"voter",
|
||||||
|
"problem_link",
|
||||||
|
"thinking_points",
|
||||||
|
"knowledge_points",
|
||||||
|
"show_types",
|
||||||
|
"feedback",
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"voter__user__username",
|
||||||
|
"problem__code",
|
||||||
|
"problem__name",
|
||||||
|
)
|
||||||
|
date_hierarchy = "time"
|
||||||
|
|
||||||
|
def problem_link(self, obj):
|
||||||
|
if self.request.user.is_superuser:
|
||||||
|
url = reverse("admin:judge_problem_change", args=(obj.problem.id,))
|
||||||
|
else:
|
||||||
|
url = reverse("problem_detail", args=(obj.problem.code,))
|
||||||
|
return format_html(f"<a href='{url}'>{obj.problem}</a>")
|
||||||
|
|
||||||
|
problem_link.short_description = _("Problem")
|
||||||
|
problem_link.admin_order_field = "problem__code"
|
||||||
|
|
||||||
|
def show_types(self, obj):
|
||||||
|
return ", ".join(map(attrgetter("name"), obj.types.all()))
|
||||||
|
|
||||||
|
show_types.short_description = _("Types")
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
self.request = request
|
||||||
|
if request.user.is_superuser:
|
||||||
|
return super().get_queryset(request)
|
||||||
|
queryset = VolunteerProblemVote.objects.prefetch_related("voter")
|
||||||
|
return queryset.filter(voter=request.profile).distinct()
|
|
@ -4,15 +4,15 @@ from django.utils.translation import gettext_lazy
|
||||||
|
|
||||||
|
|
||||||
class JudgeAppConfig(AppConfig):
|
class JudgeAppConfig(AppConfig):
|
||||||
name = 'judge'
|
name = "judge"
|
||||||
verbose_name = gettext_lazy('Online Judge')
|
verbose_name = gettext_lazy("Online Judge")
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
# WARNING: AS THIS IS NOT A FUNCTIONAL PROGRAMMING LANGUAGE,
|
# WARNING: AS THIS IS NOT A FUNCTIONAL PROGRAMMING LANGUAGE,
|
||||||
# OPERATIONS MAY HAVE SIDE EFFECTS.
|
# OPERATIONS MAY HAVE SIDE EFFECTS.
|
||||||
# DO NOT REMOVE THINKING THE IMPORT IS UNUSED.
|
# DO NOT REMOVE THINKING THE IMPORT IS UNUSED.
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
from . import signals, jinja2 # noqa: F401, imported for side effects
|
from . import models, signals, jinja2 # noqa: F401, imported for side effects
|
||||||
|
|
||||||
from django.contrib.flatpages.models import FlatPage
|
from django.contrib.flatpages.models import FlatPage
|
||||||
from django.contrib.flatpages.admin import FlatPageAdmin
|
from django.contrib.flatpages.admin import FlatPageAdmin
|
||||||
|
|
48
judge/authentication.py
Normal file
48
judge/authentication.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
from django.contrib.auth.backends import ModelBackend
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.auth.forms import PasswordChangeForm
|
||||||
|
from django.contrib.auth.views import PasswordChangeView
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class CustomModelBackend(ModelBackend):
|
||||||
|
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||||
|
try:
|
||||||
|
# Check if the username is an email
|
||||||
|
user = User.objects.get(username=username)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
# If the username is not an email, try authenticating with the username field
|
||||||
|
user = User.objects.filter(email=username).first()
|
||||||
|
|
||||||
|
if user and user.check_password(password):
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
class CustomPasswordChangeForm(PasswordChangeForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(CustomPasswordChangeForm, self).__init__(*args, **kwargs)
|
||||||
|
if not self.user.has_usable_password():
|
||||||
|
self.fields.pop("old_password")
|
||||||
|
|
||||||
|
def clean_old_password(self):
|
||||||
|
if "old_password" not in self.cleaned_data:
|
||||||
|
return
|
||||||
|
return super(CustomPasswordChangeForm, self).clean_old_password()
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super(CustomPasswordChangeForm, self).clean()
|
||||||
|
if "old_password" not in self.cleaned_data and not self.errors:
|
||||||
|
cleaned_data["old_password"] = ""
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class CustomPasswordChangeView(PasswordChangeView):
|
||||||
|
form_class = CustomPasswordChangeForm
|
||||||
|
success_url = reverse_lazy("password_change_done")
|
||||||
|
template_name = "registration/password_change_form.html"
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super(CustomPasswordChangeView, self).get_form_kwargs()
|
||||||
|
kwargs["user"] = self.request.user
|
||||||
|
return kwargs
|
|
@ -8,9 +8,9 @@ from netaddr import IPGlob, IPSet
|
||||||
|
|
||||||
from judge.utils.unicode import utf8text
|
from judge.utils.unicode import utf8text
|
||||||
|
|
||||||
logger = logging.getLogger('judge.bridge')
|
logger = logging.getLogger("judge.bridge")
|
||||||
|
|
||||||
size_pack = struct.Struct('!I')
|
size_pack = struct.Struct("!I")
|
||||||
assert size_pack.size == 4
|
assert size_pack.size == 4
|
||||||
|
|
||||||
MAX_ALLOWED_PACKET_SIZE = 8 * 1024 * 1024
|
MAX_ALLOWED_PACKET_SIZE = 8 * 1024 * 1024
|
||||||
|
@ -20,7 +20,7 @@ def proxy_list(human_readable):
|
||||||
globs = []
|
globs = []
|
||||||
addrs = []
|
addrs = []
|
||||||
for item in human_readable:
|
for item in human_readable:
|
||||||
if '*' in item or '-' in item:
|
if "*" in item or "-" in item:
|
||||||
globs.append(IPGlob(item))
|
globs.append(IPGlob(item))
|
||||||
else:
|
else:
|
||||||
addrs.append(item)
|
addrs.append(item)
|
||||||
|
@ -43,7 +43,7 @@ class RequestHandlerMeta(type):
|
||||||
try:
|
try:
|
||||||
handler.handle()
|
handler.handle()
|
||||||
except BaseException:
|
except BaseException:
|
||||||
logger.exception('Error in base packet handling')
|
logger.exception("Error in base packet handling")
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
handler.on_disconnect()
|
handler.on_disconnect()
|
||||||
|
@ -70,8 +70,12 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta):
|
||||||
|
|
||||||
def read_sized_packet(self, size, initial=None):
|
def read_sized_packet(self, size, initial=None):
|
||||||
if size > MAX_ALLOWED_PACKET_SIZE:
|
if size > MAX_ALLOWED_PACKET_SIZE:
|
||||||
logger.log(logging.WARNING if self._got_packet else logging.INFO,
|
logger.log(
|
||||||
'Disconnecting client due to too-large message size (%d bytes): %s', size, self.client_address)
|
logging.WARNING if self._got_packet else logging.INFO,
|
||||||
|
"Disconnecting client due to too-large message size (%d bytes): %s",
|
||||||
|
size,
|
||||||
|
self.client_address,
|
||||||
|
)
|
||||||
raise Disconnect()
|
raise Disconnect()
|
||||||
|
|
||||||
buffer = []
|
buffer = []
|
||||||
|
@ -86,7 +90,7 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta):
|
||||||
data = self.request.recv(remainder)
|
data = self.request.recv(remainder)
|
||||||
remainder -= len(data)
|
remainder -= len(data)
|
||||||
buffer.append(data)
|
buffer.append(data)
|
||||||
self._on_packet(b''.join(buffer))
|
self._on_packet(b"".join(buffer))
|
||||||
|
|
||||||
def parse_proxy_protocol(self, line):
|
def parse_proxy_protocol(self, line):
|
||||||
words = line.split()
|
words = line.split()
|
||||||
|
@ -94,18 +98,18 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta):
|
||||||
if len(words) < 2:
|
if len(words) < 2:
|
||||||
raise Disconnect()
|
raise Disconnect()
|
||||||
|
|
||||||
if words[1] == b'TCP4':
|
if words[1] == b"TCP4":
|
||||||
if len(words) != 6:
|
if len(words) != 6:
|
||||||
raise Disconnect()
|
raise Disconnect()
|
||||||
self.client_address = (utf8text(words[2]), utf8text(words[4]))
|
self.client_address = (utf8text(words[2]), utf8text(words[4]))
|
||||||
self.server_address = (utf8text(words[3]), utf8text(words[5]))
|
self.server_address = (utf8text(words[3]), utf8text(words[5]))
|
||||||
elif words[1] == b'TCP6':
|
elif words[1] == b"TCP6":
|
||||||
self.client_address = (utf8text(words[2]), utf8text(words[4]), 0, 0)
|
self.client_address = (utf8text(words[2]), utf8text(words[4]), 0, 0)
|
||||||
self.server_address = (utf8text(words[3]), utf8text(words[5]), 0, 0)
|
self.server_address = (utf8text(words[3]), utf8text(words[5]), 0, 0)
|
||||||
elif words[1] != b'UNKNOWN':
|
elif words[1] != b"UNKNOWN":
|
||||||
raise Disconnect()
|
raise Disconnect()
|
||||||
|
|
||||||
def read_size(self, buffer=b''):
|
def read_size(self, buffer=b""):
|
||||||
while len(buffer) < size_pack.size:
|
while len(buffer) < size_pack.size:
|
||||||
recv = self.request.recv(size_pack.size - len(buffer))
|
recv = self.request.recv(size_pack.size - len(buffer))
|
||||||
if not recv:
|
if not recv:
|
||||||
|
@ -113,9 +117,9 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta):
|
||||||
buffer += recv
|
buffer += recv
|
||||||
return size_pack.unpack(buffer)[0]
|
return size_pack.unpack(buffer)[0]
|
||||||
|
|
||||||
def read_proxy_header(self, buffer=b''):
|
def read_proxy_header(self, buffer=b""):
|
||||||
# Max line length for PROXY protocol is 107, and we received 4 already.
|
# Max line length for PROXY protocol is 107, and we received 4 already.
|
||||||
while b'\r\n' not in buffer:
|
while b"\r\n" not in buffer:
|
||||||
if len(buffer) > 107:
|
if len(buffer) > 107:
|
||||||
raise Disconnect()
|
raise Disconnect()
|
||||||
data = self.request.recv(107)
|
data = self.request.recv(107)
|
||||||
|
@ -125,7 +129,7 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta):
|
||||||
return buffer
|
return buffer
|
||||||
|
|
||||||
def _on_packet(self, data):
|
def _on_packet(self, data):
|
||||||
decompressed = zlib.decompress(data).decode('utf-8')
|
decompressed = zlib.decompress(data).decode("utf-8")
|
||||||
self._got_packet = True
|
self._got_packet = True
|
||||||
self.on_packet(decompressed)
|
self.on_packet(decompressed)
|
||||||
|
|
||||||
|
@ -141,12 +145,17 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta):
|
||||||
def on_timeout(self):
|
def on_timeout(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def on_cleanup(self):
|
||||||
|
pass
|
||||||
|
|
||||||
def handle(self):
|
def handle(self):
|
||||||
try:
|
try:
|
||||||
tag = self.read_size()
|
tag = self.read_size()
|
||||||
self._initial_tag = size_pack.pack(tag)
|
self._initial_tag = size_pack.pack(tag)
|
||||||
if self.client_address[0] in self.proxies and self._initial_tag == b'PROX':
|
if self.client_address[0] in self.proxies and self._initial_tag == b"PROX":
|
||||||
proxy, _, remainder = self.read_proxy_header(self._initial_tag).partition(b'\r\n')
|
proxy, _, remainder = self.read_proxy_header(
|
||||||
|
self._initial_tag
|
||||||
|
).partition(b"\r\n")
|
||||||
self.parse_proxy_protocol(proxy)
|
self.parse_proxy_protocol(proxy)
|
||||||
|
|
||||||
while remainder:
|
while remainder:
|
||||||
|
@ -171,25 +180,38 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta):
|
||||||
return
|
return
|
||||||
except zlib.error:
|
except zlib.error:
|
||||||
if self._got_packet:
|
if self._got_packet:
|
||||||
logger.warning('Encountered zlib error during packet handling, disconnecting client: %s',
|
logger.warning(
|
||||||
self.client_address, exc_info=True)
|
"Encountered zlib error during packet handling, disconnecting client: %s",
|
||||||
|
self.client_address,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.info('Potentially wrong protocol (zlib error): %s: %r', self.client_address, self._initial_tag,
|
logger.info(
|
||||||
exc_info=True)
|
"Potentially wrong protocol (zlib error): %s: %r",
|
||||||
|
self.client_address,
|
||||||
|
self._initial_tag,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
except socket.timeout:
|
except socket.timeout:
|
||||||
if self._got_packet:
|
if self._got_packet:
|
||||||
logger.info('Socket timed out: %s', self.client_address)
|
logger.info("Socket timed out: %s", self.client_address)
|
||||||
self.on_timeout()
|
self.on_timeout()
|
||||||
else:
|
else:
|
||||||
logger.info('Potentially wrong protocol: %s: %r', self.client_address, self._initial_tag)
|
logger.info(
|
||||||
|
"Potentially wrong protocol: %s: %r",
|
||||||
|
self.client_address,
|
||||||
|
self._initial_tag,
|
||||||
|
)
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
# When a gevent socket is shutdown, gevent cancels all waits, causing recv to raise cancel_wait_ex.
|
# When a gevent socket is shutdown, gevent cancels all waits, causing recv to raise cancel_wait_ex.
|
||||||
if e.__class__.__name__ == 'cancel_wait_ex':
|
if e.__class__.__name__ == "cancel_wait_ex":
|
||||||
return
|
return
|
||||||
raise
|
raise
|
||||||
|
finally:
|
||||||
|
self.on_cleanup()
|
||||||
|
|
||||||
def send(self, data):
|
def send(self, data):
|
||||||
compressed = zlib.compress(data.encode('utf-8'))
|
compressed = zlib.compress(data.encode("utf-8"))
|
||||||
self.request.sendall(size_pack.pack(len(compressed)) + compressed)
|
self.request.sendall(size_pack.pack(len(compressed)) + compressed)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
|
|
@ -11,7 +11,7 @@ from judge.bridge.judge_list import JudgeList
|
||||||
from judge.bridge.server import Server
|
from judge.bridge.server import Server
|
||||||
from judge.models import Judge, Submission
|
from judge.models import Judge, Submission
|
||||||
|
|
||||||
logger = logging.getLogger('judge.bridge')
|
logger = logging.getLogger("judge.bridge")
|
||||||
|
|
||||||
|
|
||||||
def reset_judges():
|
def reset_judges():
|
||||||
|
@ -20,12 +20,17 @@ def reset_judges():
|
||||||
|
|
||||||
def judge_daemon():
|
def judge_daemon():
|
||||||
reset_judges()
|
reset_judges()
|
||||||
Submission.objects.filter(status__in=Submission.IN_PROGRESS_GRADING_STATUS) \
|
Submission.objects.filter(status__in=Submission.IN_PROGRESS_GRADING_STATUS).update(
|
||||||
.update(status='IE', result='IE', error=None)
|
status="IE", result="IE", error=None
|
||||||
|
)
|
||||||
judges = JudgeList()
|
judges = JudgeList()
|
||||||
|
|
||||||
judge_server = Server(settings.BRIDGED_JUDGE_ADDRESS, partial(JudgeHandler, judges=judges))
|
judge_server = Server(
|
||||||
django_server = Server(settings.BRIDGED_DJANGO_ADDRESS, partial(DjangoHandler, judges=judges))
|
settings.BRIDGED_JUDGE_ADDRESS, partial(JudgeHandler, judges=judges)
|
||||||
|
)
|
||||||
|
django_server = Server(
|
||||||
|
settings.BRIDGED_DJANGO_ADDRESS, partial(DjangoHandler, judges=judges)
|
||||||
|
)
|
||||||
|
|
||||||
threading.Thread(target=django_server.serve_forever).start()
|
threading.Thread(target=django_server.serve_forever).start()
|
||||||
threading.Thread(target=judge_server.serve_forever).start()
|
threading.Thread(target=judge_server.serve_forever).start()
|
||||||
|
@ -33,7 +38,7 @@ def judge_daemon():
|
||||||
stop = threading.Event()
|
stop = threading.Event()
|
||||||
|
|
||||||
def signal_handler(signum, _):
|
def signal_handler(signum, _):
|
||||||
logger.info('Exiting due to %s', signal.Signals(signum).name)
|
logger.info("Exiting due to %s", signal.Signals(signum).name)
|
||||||
stop.set()
|
stop.set()
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
|
|
@ -2,10 +2,12 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
|
from django import db
|
||||||
|
|
||||||
from judge.bridge.base_handler import Disconnect, ZlibPacketHandler
|
from judge.bridge.base_handler import Disconnect, ZlibPacketHandler
|
||||||
|
|
||||||
logger = logging.getLogger('judge.bridge')
|
logger = logging.getLogger("judge.bridge")
|
||||||
size_pack = struct.Struct('!I')
|
size_pack = struct.Struct("!I")
|
||||||
|
|
||||||
|
|
||||||
class DjangoHandler(ZlibPacketHandler):
|
class DjangoHandler(ZlibPacketHandler):
|
||||||
|
@ -13,47 +15,52 @@ class DjangoHandler(ZlibPacketHandler):
|
||||||
super().__init__(request, client_address, server)
|
super().__init__(request, client_address, server)
|
||||||
|
|
||||||
self.handlers = {
|
self.handlers = {
|
||||||
'submission-request': self.on_submission,
|
"submission-request": self.on_submission,
|
||||||
'terminate-submission': self.on_termination,
|
"terminate-submission": self.on_termination,
|
||||||
'disconnect-judge': self.on_disconnect_request,
|
"disconnect-judge": self.on_disconnect_request,
|
||||||
}
|
}
|
||||||
self.judges = judges
|
self.judges = judges
|
||||||
|
|
||||||
def send(self, data):
|
def send(self, data):
|
||||||
super().send(json.dumps(data, separators=(',', ':')))
|
super().send(json.dumps(data, separators=(",", ":")))
|
||||||
|
|
||||||
def on_packet(self, packet):
|
def on_packet(self, packet):
|
||||||
packet = json.loads(packet)
|
packet = json.loads(packet)
|
||||||
try:
|
try:
|
||||||
result = self.handlers.get(packet.get('name', None), self.on_malformed)(packet)
|
result = self.handlers.get(packet.get("name", None), self.on_malformed)(
|
||||||
|
packet
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception('Error in packet handling (Django-facing)')
|
logger.exception("Error in packet handling (Django-facing)")
|
||||||
result = {'name': 'bad-request'}
|
result = {"name": "bad-request"}
|
||||||
self.send(result)
|
self.send(result)
|
||||||
raise Disconnect()
|
raise Disconnect()
|
||||||
|
|
||||||
def on_submission(self, data):
|
def on_submission(self, data):
|
||||||
id = data['submission-id']
|
id = data["submission-id"]
|
||||||
problem = data['problem-id']
|
problem = data["problem-id"]
|
||||||
language = data['language']
|
language = data["language"]
|
||||||
source = data['source']
|
source = data["source"]
|
||||||
judge_id = data['judge-id']
|
judge_id = data["judge-id"]
|
||||||
priority = data['priority']
|
priority = data["priority"]
|
||||||
if not self.judges.check_priority(priority):
|
if not self.judges.check_priority(priority):
|
||||||
return {'name': 'bad-request'}
|
return {"name": "bad-request"}
|
||||||
self.judges.judge(id, problem, language, source, judge_id, priority)
|
self.judges.judge(id, problem, language, source, judge_id, priority)
|
||||||
return {'name': 'submission-received', 'submission-id': id}
|
return {"name": "submission-received", "submission-id": id}
|
||||||
|
|
||||||
def on_termination(self, data):
|
def on_termination(self, data):
|
||||||
return {'name': 'submission-received', 'judge-aborted': self.judges.abort(data['submission-id'])}
|
return {
|
||||||
|
"name": "submission-received",
|
||||||
|
"judge-aborted": self.judges.abort(data["submission-id"]),
|
||||||
|
}
|
||||||
|
|
||||||
def on_disconnect_request(self, data):
|
def on_disconnect_request(self, data):
|
||||||
judge_id = data['judge-id']
|
judge_id = data["judge-id"]
|
||||||
force = data['force']
|
force = data["force"]
|
||||||
self.judges.disconnect(judge_id, force=force)
|
self.judges.disconnect(judge_id, force=force)
|
||||||
|
|
||||||
def on_malformed(self, packet):
|
def on_malformed(self, packet):
|
||||||
logger.error('Malformed packet: %s', packet)
|
logger.error("Malformed packet: %s", packet)
|
||||||
|
|
||||||
def on_close(self):
|
def on_cleanup(self):
|
||||||
self._to_kill = False
|
db.connection.close()
|
||||||
|
|
|
@ -4,7 +4,7 @@ import struct
|
||||||
import time
|
import time
|
||||||
import zlib
|
import zlib
|
||||||
|
|
||||||
size_pack = struct.Struct('!I')
|
size_pack = struct.Struct("!I")
|
||||||
|
|
||||||
|
|
||||||
def open_connection():
|
def open_connection():
|
||||||
|
@ -13,69 +13,70 @@ def open_connection():
|
||||||
|
|
||||||
|
|
||||||
def zlibify(data):
|
def zlibify(data):
|
||||||
data = zlib.compress(data.encode('utf-8'))
|
data = zlib.compress(data.encode("utf-8"))
|
||||||
return size_pack.pack(len(data)) + data
|
return size_pack.pack(len(data)) + data
|
||||||
|
|
||||||
|
|
||||||
def dezlibify(data, skip_head=True):
|
def dezlibify(data, skip_head=True):
|
||||||
if skip_head:
|
if skip_head:
|
||||||
data = data[size_pack.size :]
|
data = data[size_pack.size :]
|
||||||
return zlib.decompress(data).decode('utf-8')
|
return zlib.decompress(data).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
global host, port
|
global host, port
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('-l', '--host', default='localhost')
|
parser.add_argument("-l", "--host", default="localhost")
|
||||||
parser.add_argument('-p', '--port', default=9999, type=int)
|
parser.add_argument("-p", "--port", default=9999, type=int)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
host, port = args.host, args.port
|
host, port = args.host, args.port
|
||||||
|
|
||||||
print('Opening idle connection:', end=' ')
|
print("Opening idle connection:", end=" ")
|
||||||
s1 = open_connection()
|
s1 = open_connection()
|
||||||
print('Success')
|
print("Success")
|
||||||
print('Opening hello world connection:', end=' ')
|
print("Opening hello world connection:", end=" ")
|
||||||
s2 = open_connection()
|
s2 = open_connection()
|
||||||
print('Success')
|
print("Success")
|
||||||
print('Sending Hello, World!', end=' ')
|
print("Sending Hello, World!", end=" ")
|
||||||
s2.sendall(zlibify('Hello, World!'))
|
s2.sendall(zlibify("Hello, World!"))
|
||||||
print('Success')
|
print("Success")
|
||||||
print('Testing blank connection:', end=' ')
|
print("Testing blank connection:", end=" ")
|
||||||
s3 = open_connection()
|
s3 = open_connection()
|
||||||
s3.close()
|
s3.close()
|
||||||
print('Success')
|
print("Success")
|
||||||
result = dezlibify(s2.recv(1024))
|
result = dezlibify(s2.recv(1024))
|
||||||
assert result == 'Hello, World!'
|
assert result == "Hello, World!"
|
||||||
print(result)
|
print(result)
|
||||||
s2.close()
|
s2.close()
|
||||||
print('Large random data test:', end=' ')
|
print("Large random data test:", end=" ")
|
||||||
s4 = open_connection()
|
s4 = open_connection()
|
||||||
data = os.urandom(1000000).decode('iso-8859-1')
|
data = os.urandom(1000000).decode("iso-8859-1")
|
||||||
print('Generated', end=' ')
|
print("Generated", end=" ")
|
||||||
s4.sendall(zlibify(data))
|
s4.sendall(zlibify(data))
|
||||||
print('Sent', end=' ')
|
print("Sent", end=" ")
|
||||||
result = b''
|
result = b""
|
||||||
while len(result) < size_pack.size:
|
while len(result) < size_pack.size:
|
||||||
result += s4.recv(1024)
|
result += s4.recv(1024)
|
||||||
size = size_pack.unpack(result[: size_pack.size])[0]
|
size = size_pack.unpack(result[: size_pack.size])[0]
|
||||||
result = result[size_pack.size :]
|
result = result[size_pack.size :]
|
||||||
while len(result) < size:
|
while len(result) < size:
|
||||||
result += s4.recv(1024)
|
result += s4.recv(1024)
|
||||||
print('Received', end=' ')
|
print("Received", end=" ")
|
||||||
assert dezlibify(result, False) == data
|
assert dezlibify(result, False) == data
|
||||||
print('Success')
|
print("Success")
|
||||||
s4.close()
|
s4.close()
|
||||||
print('Test malformed connection:', end=' ')
|
print("Test malformed connection:", end=" ")
|
||||||
s5 = open_connection()
|
s5 = open_connection()
|
||||||
s5.sendall(data[:100000].encode('utf-8'))
|
s5.sendall(data[:100000].encode("utf-8"))
|
||||||
s5.close()
|
s5.close()
|
||||||
print('Success')
|
print("Success")
|
||||||
print('Waiting for timeout to close idle connection:', end=' ')
|
print("Waiting for timeout to close idle connection:", end=" ")
|
||||||
time.sleep(6)
|
time.sleep(6)
|
||||||
print('Done')
|
print("Done")
|
||||||
s1.close()
|
s1.close()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -3,19 +3,22 @@ from judge.bridge.base_handler import ZlibPacketHandler
|
||||||
|
|
||||||
class EchoPacketHandler(ZlibPacketHandler):
|
class EchoPacketHandler(ZlibPacketHandler):
|
||||||
def on_connect(self):
|
def on_connect(self):
|
||||||
print('New client:', self.client_address)
|
print("New client:", self.client_address)
|
||||||
self.timeout = 5
|
self.timeout = 5
|
||||||
|
|
||||||
def on_timeout(self):
|
def on_timeout(self):
|
||||||
print('Inactive client:', self.client_address)
|
print("Inactive client:", self.client_address)
|
||||||
|
|
||||||
def on_packet(self, data):
|
def on_packet(self, data):
|
||||||
self.timeout = None
|
self.timeout = None
|
||||||
print('Data from %s: %r' % (self.client_address, data[:30] if len(data) > 30 else data))
|
print(
|
||||||
|
"Data from %s: %r"
|
||||||
|
% (self.client_address, data[:30] if len(data) > 30 else data)
|
||||||
|
)
|
||||||
self.send(data)
|
self.send(data)
|
||||||
|
|
||||||
def on_disconnect(self):
|
def on_disconnect(self):
|
||||||
print('Closed client:', self.client_address)
|
print("Closed client:", self.client_address)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
@ -23,9 +26,9 @@ def main():
|
||||||
from judge.bridge.server import Server
|
from judge.bridge.server import Server
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('-l', '--host', action='append')
|
parser.add_argument("-l", "--host", action="append")
|
||||||
parser.add_argument('-p', '--port', type=int, action='append')
|
parser.add_argument("-p", "--port", type=int, action="append")
|
||||||
parser.add_argument('-P', '--proxy', action='append')
|
parser.add_argument("-P", "--proxy", action="append")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
class Handler(EchoPacketHandler):
|
class Handler(EchoPacketHandler):
|
||||||
|
@ -35,5 +38,5 @@ def main():
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,14 +3,16 @@ from collections import namedtuple
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from threading import RLock
|
from threading import RLock
|
||||||
|
|
||||||
|
from judge.bridge.utils import VanishedSubmission
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from llist import dllist
|
from llist import dllist
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from pyllist import dllist
|
from pyllist import dllist
|
||||||
|
|
||||||
logger = logging.getLogger('judge.bridge')
|
logger = logging.getLogger("judge.bridge")
|
||||||
|
|
||||||
PriorityMarker = namedtuple('PriorityMarker', 'priority')
|
PriorityMarker = namedtuple("PriorityMarker", "priority")
|
||||||
|
|
||||||
|
|
||||||
class JudgeList(object):
|
class JudgeList(object):
|
||||||
|
@ -18,7 +20,9 @@ class JudgeList(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.queue = dllist()
|
self.queue = dllist()
|
||||||
self.priority = [self.queue.append(PriorityMarker(i)) for i in range(self.priorities)]
|
self.priority = [
|
||||||
|
self.queue.append(PriorityMarker(i)) for i in range(self.priorities)
|
||||||
|
]
|
||||||
self.judges = set()
|
self.judges = set()
|
||||||
self.node_map = {}
|
self.node_map = {}
|
||||||
self.submission_map = {}
|
self.submission_map = {}
|
||||||
|
@ -32,11 +36,21 @@ class JudgeList(object):
|
||||||
id, problem, language, source, judge_id = node.value
|
id, problem, language, source, judge_id = node.value
|
||||||
if judge.can_judge(problem, language, judge_id):
|
if judge.can_judge(problem, language, judge_id):
|
||||||
self.submission_map[id] = judge
|
self.submission_map[id] = judge
|
||||||
logger.info('Dispatched queued submission %d: %s', id, judge.name)
|
logger.info(
|
||||||
|
"Dispatched queued submission %d: %s", id, judge.name
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
judge.submit(id, problem, language, source)
|
judge.submit(id, problem, language, source)
|
||||||
|
except VanishedSubmission:
|
||||||
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name)
|
logger.exception(
|
||||||
|
"Failed to dispatch %d (%s, %s) to %s",
|
||||||
|
id,
|
||||||
|
problem,
|
||||||
|
language,
|
||||||
|
judge.name,
|
||||||
|
)
|
||||||
self.judges.remove(judge)
|
self.judges.remove(judge)
|
||||||
return
|
return
|
||||||
self.queue.remove(node)
|
self.queue.remove(node)
|
||||||
|
@ -76,14 +90,15 @@ class JudgeList(object):
|
||||||
|
|
||||||
def on_judge_free(self, judge, submission):
|
def on_judge_free(self, judge, submission):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
logger.info('Judge available after grading %d: %s', submission, judge.name)
|
logger.info("Judge available after grading %d: %s", submission, judge.name)
|
||||||
del self.submission_map[submission]
|
del self.submission_map[submission]
|
||||||
judge._working = False
|
judge._working = False
|
||||||
|
judge._working_data = {}
|
||||||
self._handle_free_judge(judge)
|
self._handle_free_judge(judge)
|
||||||
|
|
||||||
def abort(self, submission):
|
def abort(self, submission):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
logger.info('Abort request: %d', submission)
|
logger.info("Abort request: %d", submission)
|
||||||
try:
|
try:
|
||||||
self.submission_map[submission].abort()
|
self.submission_map[submission].abort()
|
||||||
return True
|
return True
|
||||||
|
@ -108,21 +123,33 @@ class JudgeList(object):
|
||||||
return
|
return
|
||||||
|
|
||||||
candidates = [
|
candidates = [
|
||||||
judge for judge in self.judges if not judge.working and judge.can_judge(problem, language, judge_id)
|
judge
|
||||||
|
for judge in self.judges
|
||||||
|
if not judge.working and judge.can_judge(problem, language, judge_id)
|
||||||
]
|
]
|
||||||
if judge_id:
|
if judge_id:
|
||||||
logger.info('Specified judge %s is%savailable', judge_id, ' ' if candidates else ' not ')
|
logger.info(
|
||||||
|
"Specified judge %s is%savailable",
|
||||||
|
judge_id,
|
||||||
|
" " if candidates else " not ",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.info('Free judges: %d', len(candidates))
|
logger.info("Free judges: %d", len(candidates))
|
||||||
if candidates:
|
if candidates:
|
||||||
# Schedule the submission on the judge reporting least load.
|
# Schedule the submission on the judge reporting least load.
|
||||||
judge = min(candidates, key=attrgetter('load'))
|
judge = min(candidates, key=attrgetter("load"))
|
||||||
logger.info('Dispatched submission %d to: %s', id, judge.name)
|
logger.info("Dispatched submission %d to: %s", id, judge.name)
|
||||||
self.submission_map[id] = judge
|
self.submission_map[id] = judge
|
||||||
try:
|
try:
|
||||||
judge.submit(id, problem, language, source)
|
judge.submit(id, problem, language, source)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name)
|
logger.exception(
|
||||||
|
"Failed to dispatch %d (%s, %s) to %s",
|
||||||
|
id,
|
||||||
|
problem,
|
||||||
|
language,
|
||||||
|
judge.name,
|
||||||
|
)
|
||||||
self.judges.discard(judge)
|
self.judges.discard(judge)
|
||||||
return self.judge(id, problem, language, source, judge_id, priority)
|
return self.judge(id, problem, language, source, judge_id, priority)
|
||||||
else:
|
else:
|
||||||
|
@ -130,4 +157,4 @@ class JudgeList(object):
|
||||||
(id, problem, language, source, judge_id),
|
(id, problem, language, source, judge_id),
|
||||||
self.priority[priority],
|
self.priority[priority],
|
||||||
)
|
)
|
||||||
logger.info('Queued submission: %d', id)
|
logger.info("Queued submission: %d", id)
|
||||||
|
|
|
@ -12,7 +12,9 @@ class Server:
|
||||||
self._shutdown = threading.Event()
|
self._shutdown = threading.Event()
|
||||||
|
|
||||||
def serve_forever(self):
|
def serve_forever(self):
|
||||||
threads = [threading.Thread(target=server.serve_forever) for server in self.servers]
|
threads = [
|
||||||
|
threading.Thread(target=server.serve_forever) for server in self.servers
|
||||||
|
]
|
||||||
for thread in threads:
|
for thread in threads:
|
||||||
thread.daemon = True
|
thread.daemon = True
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
2
judge/bridge/utils.py
Normal file
2
judge/bridge/utils.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
class VanishedSubmission(Exception):
|
||||||
|
pass
|
121
judge/caching.py
121
judge/caching.py
|
@ -1,10 +1,117 @@
|
||||||
from django.core.cache import cache
|
from inspect import signature
|
||||||
|
from django.core.cache import cache, caches
|
||||||
|
from django.db.models.query import QuerySet
|
||||||
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from judge.logging import log_debug
|
||||||
|
|
||||||
|
MAX_NUM_CHAR = 50
|
||||||
|
NONE_RESULT = "__None__"
|
||||||
|
|
||||||
|
|
||||||
def finished_submission(sub):
|
def arg_to_str(arg):
|
||||||
keys = ['user_complete:%d' % sub.user_id, 'user_attempted:%s' % sub.user_id]
|
if hasattr(arg, "id"):
|
||||||
if hasattr(sub, 'contest'):
|
return str(arg.id)
|
||||||
participation = sub.contest.participation
|
if isinstance(arg, list) or isinstance(arg, QuerySet):
|
||||||
keys += ['contest_complete:%d' % participation.id]
|
return hashlib.sha1(str(list(arg)).encode()).hexdigest()[:MAX_NUM_CHAR]
|
||||||
keys += ['contest_attempted:%d' % participation.id]
|
if len(str(arg)) > MAX_NUM_CHAR:
|
||||||
|
return str(arg)[:MAX_NUM_CHAR]
|
||||||
|
return str(arg)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_args(args_list):
|
||||||
|
return [x for x in args_list if not isinstance(x, WSGIRequest)]
|
||||||
|
|
||||||
|
|
||||||
|
l0_cache = caches["l0"] if "l0" in caches else None
|
||||||
|
|
||||||
|
|
||||||
|
def cache_wrapper(prefix, timeout=None, expected_type=None):
|
||||||
|
def get_key(func, *args, **kwargs):
|
||||||
|
args_list = list(args)
|
||||||
|
signature_args = list(signature(func).parameters.keys())
|
||||||
|
args_list += [kwargs.get(k) for k in signature_args[len(args) :]]
|
||||||
|
args_list = filter_args(args_list)
|
||||||
|
args_list = [arg_to_str(i) for i in args_list]
|
||||||
|
key = prefix + ":" + ":".join(args_list)
|
||||||
|
key = key.replace(" ", "_")
|
||||||
|
return key
|
||||||
|
|
||||||
|
def _get(key):
|
||||||
|
if not l0_cache:
|
||||||
|
return cache.get(key)
|
||||||
|
result = l0_cache.get(key)
|
||||||
|
if result is None:
|
||||||
|
result = cache.get(key)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _set_l0(key, value):
|
||||||
|
if l0_cache:
|
||||||
|
l0_cache.set(key, value, 30)
|
||||||
|
|
||||||
|
def _set(key, value, timeout):
|
||||||
|
_set_l0(key, value)
|
||||||
|
cache.set(key, value, timeout)
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
def _validate_type(cache_key, result):
|
||||||
|
if expected_type and not isinstance(result, expected_type):
|
||||||
|
data = {
|
||||||
|
"function": f"{func.__module__}.{func.__qualname__}",
|
||||||
|
"result": str(result)[:30],
|
||||||
|
"expected_type": expected_type,
|
||||||
|
"type": type(result),
|
||||||
|
"key": cache_key,
|
||||||
|
}
|
||||||
|
log_debug("invalid_key", data)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
cache_key = get_key(func, *args, **kwargs)
|
||||||
|
result = _get(cache_key)
|
||||||
|
if result is not None and _validate_type(cache_key, result):
|
||||||
|
_set_l0(cache_key, result)
|
||||||
|
if type(result) == str and result == NONE_RESULT:
|
||||||
|
result = None
|
||||||
|
return result
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
if result is None:
|
||||||
|
cache_result = NONE_RESULT
|
||||||
|
else:
|
||||||
|
cache_result = result
|
||||||
|
_set(cache_key, cache_result, timeout)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def dirty(*args, **kwargs):
|
||||||
|
cache_key = get_key(func, *args, **kwargs)
|
||||||
|
cache.delete(cache_key)
|
||||||
|
if l0_cache:
|
||||||
|
l0_cache.delete(cache_key)
|
||||||
|
|
||||||
|
def prefetch_multi(args_list):
|
||||||
|
keys = []
|
||||||
|
for args in args_list:
|
||||||
|
keys.append(get_key(func, *args))
|
||||||
|
results = cache.get_many(keys)
|
||||||
|
for key, result in results.items():
|
||||||
|
if result is not None:
|
||||||
|
_set_l0(key, result)
|
||||||
|
|
||||||
|
def dirty_multi(args_list):
|
||||||
|
keys = []
|
||||||
|
for args in args_list:
|
||||||
|
keys.append(get_key(func, *args))
|
||||||
cache.delete_many(keys)
|
cache.delete_many(keys)
|
||||||
|
if l0_cache:
|
||||||
|
l0_cache.delete_many(keys)
|
||||||
|
|
||||||
|
wrapper.dirty = dirty
|
||||||
|
wrapper.prefetch_multi = prefetch_multi
|
||||||
|
wrapper.dirty_multi = dirty_multi
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
|
@ -1,155 +0,0 @@
|
||||||
from django import forms
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.db.models import Count
|
|
||||||
from django.db.models.expressions import F, Value
|
|
||||||
from django.db.models.functions import Coalesce
|
|
||||||
from django.forms import ModelForm
|
|
||||||
from django.http import HttpResponseForbidden, HttpResponseNotFound, HttpResponseRedirect
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.decorators import method_decorator
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import View
|
|
||||||
from django.views.generic.base import TemplateResponseMixin
|
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
|
||||||
from reversion import revisions
|
|
||||||
from reversion.models import Revision, Version
|
|
||||||
|
|
||||||
from judge.dblock import LockModel
|
|
||||||
from judge.models import Comment, CommentLock, CommentVote, Notification
|
|
||||||
from judge.utils.raw_sql import RawSQLColumn, unique_together_left_join
|
|
||||||
from judge.widgets import HeavyPreviewPageDownWidget
|
|
||||||
from judge.jinja2.reference import get_user_from_text
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def add_mention_notifications(comment):
|
|
||||||
user_referred = get_user_from_text(comment.body).exclude(id=comment.author.id)
|
|
||||||
for user in user_referred:
|
|
||||||
notification_ref = Notification(owner=user,
|
|
||||||
comment=comment,
|
|
||||||
category='Mention')
|
|
||||||
notification_ref.save()
|
|
||||||
|
|
||||||
def del_mention_notifications(comment):
|
|
||||||
query = {
|
|
||||||
'comment': comment,
|
|
||||||
'category': 'Mention'
|
|
||||||
}
|
|
||||||
Notification.objects.filter(**query).delete()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CommentForm(ModelForm):
|
|
||||||
class Meta:
|
|
||||||
model = Comment
|
|
||||||
fields = ['body', 'parent']
|
|
||||||
widgets = {
|
|
||||||
'parent': forms.HiddenInput(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if HeavyPreviewPageDownWidget is not None:
|
|
||||||
widgets['body'] = HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'),
|
|
||||||
preview_timeout=1000, hide_preview_button=True)
|
|
||||||
|
|
||||||
def __init__(self, request, *args, **kwargs):
|
|
||||||
self.request = request
|
|
||||||
super(CommentForm, self).__init__(*args, **kwargs)
|
|
||||||
self.fields['body'].widget.attrs.update({'placeholder': _('Comment body')})
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if self.request is not None and self.request.user.is_authenticated:
|
|
||||||
profile = self.request.profile
|
|
||||||
if profile.mute:
|
|
||||||
raise ValidationError(_('Your part is silent, little toad.'))
|
|
||||||
elif (not self.request.user.is_staff and
|
|
||||||
not profile.submission_set.filter(points=F('problem__points')).exists()):
|
|
||||||
raise ValidationError(_('You need to have solved at least one problem '
|
|
||||||
'before your voice can be heard.'))
|
|
||||||
return super(CommentForm, self).clean()
|
|
||||||
|
|
||||||
|
|
||||||
class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
|
|
||||||
comment_page = None
|
|
||||||
|
|
||||||
def get_comment_page(self):
|
|
||||||
if self.comment_page is None:
|
|
||||||
raise NotImplementedError()
|
|
||||||
return self.comment_page
|
|
||||||
|
|
||||||
def is_comment_locked(self):
|
|
||||||
if self.request.user.has_perm('judge.override_comment_lock'):
|
|
||||||
return False
|
|
||||||
return (CommentLock.objects.filter(page=self.get_comment_page()).exists()
|
|
||||||
or (self.request.in_contest and self.request.participation.contest.use_clarifications))
|
|
||||||
|
|
||||||
@method_decorator(login_required)
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
self.object = self.get_object()
|
|
||||||
page = self.get_comment_page()
|
|
||||||
|
|
||||||
if self.is_comment_locked():
|
|
||||||
return HttpResponseForbidden()
|
|
||||||
|
|
||||||
parent = request.POST.get('parent')
|
|
||||||
if parent:
|
|
||||||
try:
|
|
||||||
parent = int(parent)
|
|
||||||
except ValueError:
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
else:
|
|
||||||
if not Comment.objects.filter(hidden=False, id=parent, page=page).exists():
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
|
|
||||||
form = CommentForm(request, request.POST)
|
|
||||||
if form.is_valid():
|
|
||||||
comment = form.save(commit=False)
|
|
||||||
comment.author = request.profile
|
|
||||||
comment.page = page
|
|
||||||
|
|
||||||
|
|
||||||
with LockModel(write=(Comment, Revision, Version), read=(ContentType,)), revisions.create_revision():
|
|
||||||
revisions.set_user(request.user)
|
|
||||||
revisions.set_comment(_('Posted comment'))
|
|
||||||
comment.save()
|
|
||||||
|
|
||||||
# add notification for reply
|
|
||||||
if comment.parent and comment.parent.author != comment.author:
|
|
||||||
notification_rep = Notification(owner=comment.parent.author,
|
|
||||||
comment=comment,
|
|
||||||
category='Reply')
|
|
||||||
notification_rep.save()
|
|
||||||
|
|
||||||
add_mention_notifications(comment)
|
|
||||||
|
|
||||||
return HttpResponseRedirect(request.path)
|
|
||||||
|
|
||||||
context = self.get_context_data(object=self.object, comment_form=form)
|
|
||||||
return self.render_to_response(context)
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
self.object = self.get_object()
|
|
||||||
return self.render_to_response(self.get_context_data(
|
|
||||||
object=self.object,
|
|
||||||
comment_form=CommentForm(request, initial={'page': self.get_comment_page(), 'parent': None}),
|
|
||||||
))
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(CommentedDetailView, self).get_context_data(**kwargs)
|
|
||||||
queryset = Comment.objects.filter(hidden=False, page=self.get_comment_page())
|
|
||||||
context['has_comments'] = queryset.exists()
|
|
||||||
context['comment_lock'] = self.is_comment_locked()
|
|
||||||
queryset = queryset.select_related('author__user').defer('author__about').annotate(revisions=Count('versions'))
|
|
||||||
|
|
||||||
if self.request.user.is_authenticated:
|
|
||||||
queryset = queryset.annotate(vote_score=Coalesce(RawSQLColumn(CommentVote, 'score'), Value(0)))
|
|
||||||
profile = self.request.profile
|
|
||||||
unique_together_left_join(queryset, CommentVote, 'comment', 'voter', profile.id)
|
|
||||||
context['is_new_user'] = (not self.request.user.is_staff and
|
|
||||||
not profile.submission_set.filter(points=F('problem__points')).exists())
|
|
||||||
context['comment_list'] = queryset
|
|
||||||
context['vote_hide_threshold'] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD
|
|
||||||
return context
|
|
||||||
|
|
|
@ -3,4 +3,6 @@ from judge.contest_format.default import DefaultContestFormat
|
||||||
from judge.contest_format.ecoo import ECOOContestFormat
|
from judge.contest_format.ecoo import ECOOContestFormat
|
||||||
from judge.contest_format.icpc import ICPCContestFormat
|
from judge.contest_format.icpc import ICPCContestFormat
|
||||||
from judge.contest_format.ioi import IOIContestFormat
|
from judge.contest_format.ioi import IOIContestFormat
|
||||||
|
from judge.contest_format.new_ioi import NewIOIContestFormat
|
||||||
|
from judge.contest_format.ultimate import UltimateContestFormat
|
||||||
from judge.contest_format.registry import choices, formats
|
from judge.contest_format.registry import choices, formats
|
||||||
|
|
|
@ -10,18 +10,18 @@ from django.utils.translation import gettext_lazy
|
||||||
|
|
||||||
from judge.contest_format.default import DefaultContestFormat
|
from judge.contest_format.default import DefaultContestFormat
|
||||||
from judge.contest_format.registry import register_contest_format
|
from judge.contest_format.registry import register_contest_format
|
||||||
from judge.timezone import from_database_time
|
from judge.timezone import from_database_time, to_database_time
|
||||||
from judge.utils.timedelta import nice_repr
|
from judge.utils.timedelta import nice_repr
|
||||||
|
|
||||||
|
|
||||||
@register_contest_format('atcoder')
|
@register_contest_format("atcoder")
|
||||||
class AtCoderContestFormat(DefaultContestFormat):
|
class AtCoderContestFormat(DefaultContestFormat):
|
||||||
name = gettext_lazy('AtCoder')
|
name = gettext_lazy("AtCoder")
|
||||||
config_defaults = {'penalty': 5}
|
config_defaults = {"penalty": 5}
|
||||||
config_validators = {'penalty': lambda x: x >= 0}
|
config_validators = {"penalty": lambda x: x >= 0}
|
||||||
'''
|
"""
|
||||||
penalty: Number of penalty minutes each incorrect submission adds. Defaults to 5.
|
penalty: Number of penalty minutes each incorrect submission adds. Defaults to 5.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config):
|
def validate(cls, config):
|
||||||
|
@ -29,7 +29,9 @@ class AtCoderContestFormat(DefaultContestFormat):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not isinstance(config, dict):
|
if not isinstance(config, dict):
|
||||||
raise ValidationError('AtCoder-styled contest expects no config or dict as config')
|
raise ValidationError(
|
||||||
|
"AtCoder-styled contest expects no config or dict as config"
|
||||||
|
)
|
||||||
|
|
||||||
for key, value in config.items():
|
for key, value in config.items():
|
||||||
if key not in cls.config_defaults:
|
if key not in cls.config_defaults:
|
||||||
|
@ -37,7 +39,9 @@ class AtCoderContestFormat(DefaultContestFormat):
|
||||||
if not isinstance(value, type(cls.config_defaults[key])):
|
if not isinstance(value, type(cls.config_defaults[key])):
|
||||||
raise ValidationError('invalid type for config key "%s"' % key)
|
raise ValidationError('invalid type for config key "%s"' % key)
|
||||||
if not cls.config_validators[key](value):
|
if not cls.config_validators[key](value):
|
||||||
raise ValidationError('invalid value "%s" for config key "%s"' % (value, key))
|
raise ValidationError(
|
||||||
|
'invalid value "%s" for config key "%s"' % (value, key)
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, contest, config):
|
def __init__(self, contest, config):
|
||||||
self.config = self.config_defaults.copy()
|
self.config = self.config_defaults.copy()
|
||||||
|
@ -50,8 +54,13 @@ class AtCoderContestFormat(DefaultContestFormat):
|
||||||
points = 0
|
points = 0
|
||||||
format_data = {}
|
format_data = {}
|
||||||
|
|
||||||
|
frozen_time = self.contest.end_time
|
||||||
|
if self.contest.freeze_after:
|
||||||
|
frozen_time = participation.start + self.contest.freeze_after
|
||||||
|
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
SELECT MAX(cs.points) as `score`, (
|
SELECT MAX(cs.points) as `score`, (
|
||||||
SELECT MIN(csub.date)
|
SELECT MIN(csub.date)
|
||||||
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
||||||
|
@ -61,22 +70,29 @@ class AtCoderContestFormat(DefaultContestFormat):
|
||||||
FROM judge_contestproblem cp INNER JOIN
|
FROM judge_contestproblem cp INNER JOIN
|
||||||
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
||||||
judge_submission sub ON (sub.id = cs.submission_id)
|
judge_submission sub ON (sub.id = cs.submission_id)
|
||||||
|
WHERE sub.date < %s
|
||||||
GROUP BY cp.id
|
GROUP BY cp.id
|
||||||
''', (participation.id, participation.id))
|
""",
|
||||||
|
(participation.id, participation.id, to_database_time(frozen_time)),
|
||||||
|
)
|
||||||
|
|
||||||
for score, time, prob in cursor.fetchall():
|
for score, time, prob in cursor.fetchall():
|
||||||
time = from_database_time(time)
|
time = from_database_time(time)
|
||||||
dt = (time - participation.start).total_seconds()
|
dt = (time - participation.start).total_seconds()
|
||||||
|
|
||||||
# Compute penalty
|
# Compute penalty
|
||||||
if self.config['penalty']:
|
if self.config["penalty"]:
|
||||||
# An IE can have a submission result of `None`
|
# An IE can have a submission result of `None`
|
||||||
subs = participation.submissions.exclude(submission__result__isnull=True) \
|
subs = (
|
||||||
.exclude(submission__result__in=['IE', 'CE']) \
|
participation.submissions.exclude(
|
||||||
|
submission__result__isnull=True
|
||||||
|
)
|
||||||
|
.exclude(submission__result__in=["IE", "CE"])
|
||||||
.filter(problem_id=prob)
|
.filter(problem_id=prob)
|
||||||
|
)
|
||||||
if score:
|
if score:
|
||||||
prev = subs.filter(submission__date__lte=time).count() - 1
|
prev = subs.filter(submission__date__lte=time).count() - 1
|
||||||
penalty += prev * self.config['penalty'] * 60
|
penalty += prev * self.config["penalty"] * 60
|
||||||
else:
|
else:
|
||||||
# We should always display the penalty, even if the user has a score of 0
|
# We should always display the penalty, even if the user has a score of 0
|
||||||
prev = subs.count()
|
prev = subs.count()
|
||||||
|
@ -86,29 +102,52 @@ class AtCoderContestFormat(DefaultContestFormat):
|
||||||
if score:
|
if score:
|
||||||
cumtime = max(cumtime, dt)
|
cumtime = max(cumtime, dt)
|
||||||
|
|
||||||
format_data[str(prob)] = {'time': dt, 'points': score, 'penalty': prev}
|
format_data[str(prob)] = {"time": dt, "points": score, "penalty": prev}
|
||||||
points += score
|
points += score
|
||||||
|
|
||||||
|
self.handle_frozen_state(participation, format_data)
|
||||||
participation.cumtime = cumtime + penalty
|
participation.cumtime = cumtime + penalty
|
||||||
participation.score = points
|
participation.score = round(points, self.contest.points_precision)
|
||||||
participation.tiebreaker = 0
|
participation.tiebreaker = 0
|
||||||
participation.format_data = format_data
|
participation.format_data = format_data
|
||||||
participation.save()
|
participation.save()
|
||||||
|
|
||||||
def display_user_problem(self, participation, contest_problem):
|
def display_user_problem(self, participation, contest_problem, show_final=False):
|
||||||
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
||||||
if format_data:
|
if format_data:
|
||||||
penalty = format_html('<small style="color:red"> ({penalty})</small>',
|
penalty = (
|
||||||
penalty=floatformat(format_data['penalty'])) if format_data['penalty'] else ''
|
format_html(
|
||||||
|
'<small style="color:red"> ({penalty})</small>',
|
||||||
|
penalty=floatformat(format_data["penalty"]),
|
||||||
|
)
|
||||||
|
if format_data.get("penalty")
|
||||||
|
else ""
|
||||||
|
)
|
||||||
return format_html(
|
return format_html(
|
||||||
'<td class="{state} problem-score-col"><a href="{url}">{points}{penalty}<div class="solving-time">{time}</div></a></td>',
|
'<td class="{state} problem-score-col"><a data-featherlight="{url}" href="#">{points}{penalty}<div class="solving-time">{time}</div></a></td>',
|
||||||
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
|
state=(
|
||||||
self.best_solution_state(format_data['points'], contest_problem.points)),
|
(
|
||||||
url=reverse('contest_user_submissions',
|
"pretest-"
|
||||||
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
|
if self.contest.run_pretests_only
|
||||||
points=floatformat(format_data['points']),
|
and contest_problem.is_pretested
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
+ self.best_solution_state(
|
||||||
|
format_data["points"], contest_problem.points
|
||||||
|
)
|
||||||
|
+ (" frozen" if format_data.get("frozen") else "")
|
||||||
|
),
|
||||||
|
url=reverse(
|
||||||
|
"contest_user_submissions_ajax",
|
||||||
|
args=[
|
||||||
|
self.contest.key,
|
||||||
|
participation.id,
|
||||||
|
contest_problem.problem.code,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
points=floatformat(format_data["points"]),
|
||||||
penalty=penalty,
|
penalty=penalty,
|
||||||
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'),
|
time=nice_repr(timedelta(seconds=format_data["time"]), "noday"),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return mark_safe('<td class="problem-score-col"></td>')
|
return mark_safe('<td class="problem-score-col"></td>')
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from abc import ABCMeta, abstractmethod, abstractproperty
|
from abc import ABCMeta, abstractmethod, abstractproperty
|
||||||
|
from django.db.models import Max
|
||||||
from django.utils import six
|
|
||||||
|
|
||||||
|
|
||||||
class abstractclassmethod(classmethod):
|
class abstractclassmethod(classmethod):
|
||||||
|
@ -11,7 +10,9 @@ class abstractclassmethod(classmethod):
|
||||||
super(abstractclassmethod, self).__init__(callable)
|
super(abstractclassmethod, self).__init__(callable)
|
||||||
|
|
||||||
|
|
||||||
class BaseContestFormat(six.with_metaclass(ABCMeta)):
|
class BaseContestFormat(metaclass=ABCMeta):
|
||||||
|
has_hidden_subtasks = False
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __init__(self, contest, config):
|
def __init__(self, contest, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
@ -49,7 +50,7 @@ class BaseContestFormat(six.with_metaclass(ABCMeta)):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def display_user_problem(self, participation, contest_problem):
|
def display_user_problem(self, participation, contest_problem, show_final):
|
||||||
"""
|
"""
|
||||||
Returns the HTML fragment to show a user's performance on an individual problem. This is expected to use
|
Returns the HTML fragment to show a user's performance on an individual problem. This is expected to use
|
||||||
information from the format_data field instead of computing it from scratch.
|
information from the format_data field instead of computing it from scratch.
|
||||||
|
@ -61,7 +62,7 @@ class BaseContestFormat(six.with_metaclass(ABCMeta)):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def display_participation_result(self, participation):
|
def display_participation_result(self, participation, show_final):
|
||||||
"""
|
"""
|
||||||
Returns the HTML fragment to show a user's performance on the whole contest. This is expected to use
|
Returns the HTML fragment to show a user's performance on the whole contest. This is expected to use
|
||||||
information from the format_data field instead of computing it from scratch.
|
information from the format_data field instead of computing it from scratch.
|
||||||
|
@ -93,7 +94,30 @@ class BaseContestFormat(six.with_metaclass(ABCMeta)):
|
||||||
@classmethod
|
@classmethod
|
||||||
def best_solution_state(cls, points, total):
|
def best_solution_state(cls, points, total):
|
||||||
if not points:
|
if not points:
|
||||||
return 'failed-score'
|
return "failed-score"
|
||||||
if points == total:
|
if points == total:
|
||||||
return 'full-score'
|
return "full-score"
|
||||||
return 'partial-score'
|
return "partial-score"
|
||||||
|
|
||||||
|
def handle_frozen_state(self, participation, format_data):
|
||||||
|
hidden_subtasks = {}
|
||||||
|
if hasattr(self, "get_hidden_subtasks"):
|
||||||
|
hidden_subtasks = self.get_hidden_subtasks()
|
||||||
|
|
||||||
|
queryset = participation.submissions.values("problem_id").annotate(
|
||||||
|
time=Max("submission__date")
|
||||||
|
)
|
||||||
|
for result in queryset:
|
||||||
|
problem = str(result["problem_id"])
|
||||||
|
if not (self.contest.freeze_after or hidden_subtasks.get(problem)):
|
||||||
|
continue
|
||||||
|
if format_data.get(problem):
|
||||||
|
is_after_freeze = (
|
||||||
|
self.contest.freeze_after
|
||||||
|
and result["time"]
|
||||||
|
>= self.contest.freeze_after + participation.start
|
||||||
|
)
|
||||||
|
if is_after_freeze or hidden_subtasks.get(problem):
|
||||||
|
format_data[problem]["frozen"] = True
|
||||||
|
else:
|
||||||
|
format_data[problem] = {"time": 0, "points": 0, "frozen": True}
|
||||||
|
|
|
@ -13,14 +13,16 @@ from judge.contest_format.registry import register_contest_format
|
||||||
from judge.utils.timedelta import nice_repr
|
from judge.utils.timedelta import nice_repr
|
||||||
|
|
||||||
|
|
||||||
@register_contest_format('default')
|
@register_contest_format("default")
|
||||||
class DefaultContestFormat(BaseContestFormat):
|
class DefaultContestFormat(BaseContestFormat):
|
||||||
name = gettext_lazy('Default')
|
name = gettext_lazy("Default")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config):
|
def validate(cls, config):
|
||||||
if config is not None and (not isinstance(config, dict) or config):
|
if config is not None and (not isinstance(config, dict) or config):
|
||||||
raise ValidationError('default contest expects no config or empty dict as config')
|
raise ValidationError(
|
||||||
|
"default contest expects no config or empty dict as config"
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, contest, config):
|
def __init__(self, contest, config):
|
||||||
super(DefaultContestFormat, self).__init__(contest, config)
|
super(DefaultContestFormat, self).__init__(contest, config)
|
||||||
|
@ -30,49 +32,84 @@ class DefaultContestFormat(BaseContestFormat):
|
||||||
points = 0
|
points = 0
|
||||||
format_data = {}
|
format_data = {}
|
||||||
|
|
||||||
for result in participation.submissions.values('problem_id').annotate(
|
queryset = participation.submissions
|
||||||
time=Max('submission__date'), points=Max('points'),
|
|
||||||
):
|
|
||||||
dt = (result['time'] - participation.start).total_seconds()
|
|
||||||
if result['points']:
|
|
||||||
cumtime += dt
|
|
||||||
format_data[str(result['problem_id'])] = {'time': dt, 'points': result['points']}
|
|
||||||
points += result['points']
|
|
||||||
|
|
||||||
|
if self.contest.freeze_after:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
submission__date__lt=participation.start + self.contest.freeze_after
|
||||||
|
)
|
||||||
|
|
||||||
|
queryset = queryset.values("problem_id").annotate(
|
||||||
|
time=Max("submission__date"),
|
||||||
|
points=Max("points"),
|
||||||
|
)
|
||||||
|
|
||||||
|
for result in queryset:
|
||||||
|
dt = (result["time"] - participation.start).total_seconds()
|
||||||
|
if result["points"]:
|
||||||
|
cumtime += dt
|
||||||
|
format_data[str(result["problem_id"])] = {
|
||||||
|
"time": dt,
|
||||||
|
"points": result["points"],
|
||||||
|
}
|
||||||
|
points += result["points"]
|
||||||
|
|
||||||
|
self.handle_frozen_state(participation, format_data)
|
||||||
participation.cumtime = max(cumtime, 0)
|
participation.cumtime = max(cumtime, 0)
|
||||||
participation.score = points
|
participation.score = round(points, self.contest.points_precision)
|
||||||
participation.tiebreaker = 0
|
participation.tiebreaker = 0
|
||||||
participation.format_data = format_data
|
participation.format_data = format_data
|
||||||
participation.save()
|
participation.save()
|
||||||
|
|
||||||
def display_user_problem(self, participation, contest_problem):
|
def display_user_problem(self, participation, contest_problem, show_final=False):
|
||||||
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
||||||
if format_data:
|
if format_data:
|
||||||
return format_html(
|
return format_html(
|
||||||
u'<td class="{state} problem-score-col"><a href="{url}">{points}<div class="solving-time">{time}</div></a></td>',
|
'<td class="{state} problem-score-col"><a data-featherlight="{url}" href="#">{points}<div class="solving-time">{time}</div></a></td>',
|
||||||
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
|
state=(
|
||||||
self.best_solution_state(format_data['points'], contest_problem.points)),
|
(
|
||||||
url=reverse('contest_user_submissions',
|
"pretest-"
|
||||||
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
|
if self.contest.run_pretests_only
|
||||||
points=floatformat(format_data['points'], -self.contest.points_precision),
|
and contest_problem.is_pretested
|
||||||
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'),
|
else ""
|
||||||
|
)
|
||||||
|
+ self.best_solution_state(
|
||||||
|
format_data["points"], contest_problem.points
|
||||||
|
)
|
||||||
|
+ (" frozen" if format_data.get("frozen") else "")
|
||||||
|
),
|
||||||
|
url=reverse(
|
||||||
|
"contest_user_submissions_ajax",
|
||||||
|
args=[
|
||||||
|
self.contest.key,
|
||||||
|
participation.id,
|
||||||
|
contest_problem.problem.code,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
points=floatformat(
|
||||||
|
format_data["points"], -self.contest.points_precision
|
||||||
|
),
|
||||||
|
time=nice_repr(timedelta(seconds=format_data["time"]), "noday"),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return mark_safe('<td class="problem-score-col"></td>')
|
return mark_safe('<td class="problem-score-col"></td>')
|
||||||
|
|
||||||
def display_participation_result(self, participation):
|
def display_participation_result(self, participation, show_final=False):
|
||||||
return format_html(
|
return format_html(
|
||||||
u'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
|
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
|
||||||
points=floatformat(participation.score, -self.contest.points_precision),
|
points=floatformat(participation.score, -self.contest.points_precision),
|
||||||
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday'),
|
cumtime=nice_repr(timedelta(seconds=participation.cumtime), "noday"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_problem_breakdown(self, participation, contest_problems):
|
def get_problem_breakdown(self, participation, contest_problems):
|
||||||
return [(participation.format_data or {}).get(str(contest_problem.id)) for contest_problem in contest_problems]
|
return [
|
||||||
|
(participation.format_data or {}).get(str(contest_problem.id))
|
||||||
|
for contest_problem in contest_problems
|
||||||
|
]
|
||||||
|
|
||||||
def get_contest_problem_label_script(self):
|
def get_contest_problem_label_script(self):
|
||||||
return '''
|
return """
|
||||||
function(n)
|
function(n)
|
||||||
return tostring(math.floor(n + 1))
|
return tostring(math.floor(n + 1))
|
||||||
end
|
end
|
||||||
'''
|
"""
|
||||||
|
|
|
@ -10,21 +10,25 @@ from django.utils.translation import gettext_lazy
|
||||||
|
|
||||||
from judge.contest_format.default import DefaultContestFormat
|
from judge.contest_format.default import DefaultContestFormat
|
||||||
from judge.contest_format.registry import register_contest_format
|
from judge.contest_format.registry import register_contest_format
|
||||||
from judge.timezone import from_database_time
|
from judge.timezone import from_database_time, to_database_time
|
||||||
from judge.utils.timedelta import nice_repr
|
from judge.utils.timedelta import nice_repr
|
||||||
|
|
||||||
|
|
||||||
@register_contest_format('ecoo')
|
@register_contest_format("ecoo")
|
||||||
class ECOOContestFormat(DefaultContestFormat):
|
class ECOOContestFormat(DefaultContestFormat):
|
||||||
name = gettext_lazy('ECOO')
|
name = gettext_lazy("ECOO")
|
||||||
config_defaults = {'cumtime': False, 'first_ac_bonus': 10, 'time_bonus': 5}
|
config_defaults = {"cumtime": False, "first_ac_bonus": 10, "time_bonus": 5}
|
||||||
config_validators = {'cumtime': lambda x: True, 'first_ac_bonus': lambda x: x >= 0, 'time_bonus': lambda x: x >= 0}
|
config_validators = {
|
||||||
'''
|
"cumtime": lambda x: True,
|
||||||
|
"first_ac_bonus": lambda x: x >= 0,
|
||||||
|
"time_bonus": lambda x: x >= 0,
|
||||||
|
}
|
||||||
|
"""
|
||||||
cumtime: Specify True if cumulative time is to be used in breaking ties. Defaults to False.
|
cumtime: Specify True if cumulative time is to be used in breaking ties. Defaults to False.
|
||||||
first_ac_bonus: The number of points to award if a solution gets AC on its first non-IE/CE run. Defaults to 10.
|
first_ac_bonus: The number of points to award if a solution gets AC on its first non-IE/CE run. Defaults to 10.
|
||||||
time_bonus: Number of minutes to award an extra point for submitting before the contest end.
|
time_bonus: Number of minutes to award an extra point for submitting before the contest end.
|
||||||
Specify 0 to disable. Defaults to 5.
|
Specify 0 to disable. Defaults to 5.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config):
|
def validate(cls, config):
|
||||||
|
@ -32,7 +36,9 @@ class ECOOContestFormat(DefaultContestFormat):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not isinstance(config, dict):
|
if not isinstance(config, dict):
|
||||||
raise ValidationError('ECOO-styled contest expects no config or dict as config')
|
raise ValidationError(
|
||||||
|
"ECOO-styled contest expects no config or dict as config"
|
||||||
|
)
|
||||||
|
|
||||||
for key, value in config.items():
|
for key, value in config.items():
|
||||||
if key not in cls.config_defaults:
|
if key not in cls.config_defaults:
|
||||||
|
@ -40,7 +46,9 @@ class ECOOContestFormat(DefaultContestFormat):
|
||||||
if not isinstance(value, type(cls.config_defaults[key])):
|
if not isinstance(value, type(cls.config_defaults[key])):
|
||||||
raise ValidationError('invalid type for config key "%s"' % key)
|
raise ValidationError('invalid type for config key "%s"' % key)
|
||||||
if not cls.config_validators[key](value):
|
if not cls.config_validators[key](value):
|
||||||
raise ValidationError('invalid value "%s" for config key "%s"' % (value, key))
|
raise ValidationError(
|
||||||
|
'invalid value "%s" for config key "%s"' % (value, key)
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, contest, config):
|
def __init__(self, contest, config):
|
||||||
self.config = self.config_defaults.copy()
|
self.config = self.config_defaults.copy()
|
||||||
|
@ -52,8 +60,13 @@ class ECOOContestFormat(DefaultContestFormat):
|
||||||
points = 0
|
points = 0
|
||||||
format_data = {}
|
format_data = {}
|
||||||
|
|
||||||
|
frozen_time = self.contest.end_time
|
||||||
|
if self.contest.freeze_after:
|
||||||
|
frozen_time = participation.start + self.contest.freeze_after
|
||||||
|
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
SELECT (
|
SELECT (
|
||||||
SELECT MAX(ccs.points)
|
SELECT MAX(ccs.points)
|
||||||
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
||||||
|
@ -68,56 +81,92 @@ class ECOOContestFormat(DefaultContestFormat):
|
||||||
FROM judge_contestproblem cp INNER JOIN
|
FROM judge_contestproblem cp INNER JOIN
|
||||||
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
||||||
judge_submission sub ON (sub.id = cs.submission_id)
|
judge_submission sub ON (sub.id = cs.submission_id)
|
||||||
|
WHERE sub.date < %s
|
||||||
GROUP BY cp.id
|
GROUP BY cp.id
|
||||||
''', (participation.id, participation.id, participation.id))
|
""",
|
||||||
|
(
|
||||||
|
participation.id,
|
||||||
|
participation.id,
|
||||||
|
participation.id,
|
||||||
|
to_database_time(frozen_time),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
for score, time, prob, subs, max_score in cursor.fetchall():
|
for score, time, prob, subs, max_score in cursor.fetchall():
|
||||||
time = from_database_time(time)
|
time = from_database_time(time)
|
||||||
dt = (time - participation.start).total_seconds()
|
dt = (time - participation.start).total_seconds()
|
||||||
if self.config['cumtime']:
|
if self.config["cumtime"]:
|
||||||
cumtime += dt
|
cumtime += dt
|
||||||
|
|
||||||
bonus = 0
|
bonus = 0
|
||||||
if score > 0:
|
if score > 0:
|
||||||
# First AC bonus
|
# First AC bonus
|
||||||
if subs == 1 and score == max_score:
|
if subs == 1 and score == max_score:
|
||||||
bonus += self.config['first_ac_bonus']
|
bonus += self.config["first_ac_bonus"]
|
||||||
# Time bonus
|
# Time bonus
|
||||||
if self.config['time_bonus']:
|
if self.config["time_bonus"]:
|
||||||
bonus += (participation.end_time - time).total_seconds() // 60 // self.config['time_bonus']
|
bonus += (
|
||||||
|
(participation.end_time - time).total_seconds()
|
||||||
|
// 60
|
||||||
|
// self.config["time_bonus"]
|
||||||
|
)
|
||||||
points += bonus
|
points += bonus
|
||||||
|
|
||||||
format_data[str(prob)] = {'time': dt, 'points': score, 'bonus': bonus}
|
format_data[str(prob)] = {"time": dt, "points": score, "bonus": bonus}
|
||||||
points += score
|
points += score
|
||||||
|
|
||||||
|
self.handle_frozen_state(participation, format_data)
|
||||||
participation.cumtime = cumtime
|
participation.cumtime = cumtime
|
||||||
participation.score = points
|
participation.score = round(points, self.contest.points_precision)
|
||||||
participation.tiebreaker = 0
|
participation.tiebreaker = 0
|
||||||
participation.format_data = format_data
|
participation.format_data = format_data
|
||||||
participation.save()
|
participation.save()
|
||||||
|
|
||||||
def display_user_problem(self, participation, contest_problem):
|
def display_user_problem(self, participation, contest_problem, show_final=False):
|
||||||
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
||||||
if format_data:
|
if format_data:
|
||||||
bonus = format_html('<small> +{bonus}</small>',
|
bonus = (
|
||||||
bonus=floatformat(format_data['bonus'])) if format_data['bonus'] else ''
|
format_html(
|
||||||
|
"<small> +{bonus}</small>", bonus=floatformat(format_data["bonus"])
|
||||||
|
)
|
||||||
|
if format_data.get("bonus")
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
return format_html(
|
return format_html(
|
||||||
'<td class="{state}"><a href="{url}">{points}{bonus}<div class="solving-time">{time}</div></a></td>',
|
'<td class="{state}"><a data-featherlight="{url}" href="#">{points}{bonus}<div class="solving-time">{time}</div></a></td>',
|
||||||
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
|
state=(
|
||||||
self.best_solution_state(format_data['points'], contest_problem.points)),
|
(
|
||||||
url=reverse('contest_user_submissions',
|
"pretest-"
|
||||||
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
|
if self.contest.run_pretests_only
|
||||||
points=floatformat(format_data['points']),
|
and contest_problem.is_pretested
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
+ self.best_solution_state(
|
||||||
|
format_data["points"], contest_problem.points
|
||||||
|
)
|
||||||
|
+ (" frozen" if format_data.get("frozen") else "")
|
||||||
|
),
|
||||||
|
url=reverse(
|
||||||
|
"contest_user_submissions_ajax",
|
||||||
|
args=[
|
||||||
|
self.contest.key,
|
||||||
|
participation.id,
|
||||||
|
contest_problem.problem.code,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
points=floatformat(format_data["points"]),
|
||||||
bonus=bonus,
|
bonus=bonus,
|
||||||
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'),
|
time=nice_repr(timedelta(seconds=format_data["time"]), "noday"),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return mark_safe('<td></td>')
|
return mark_safe("<td></td>")
|
||||||
|
|
||||||
def display_participation_result(self, participation):
|
def display_participation_result(self, participation, show_final=False):
|
||||||
return format_html(
|
return format_html(
|
||||||
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
|
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
|
||||||
points=floatformat(participation.score),
|
points=floatformat(participation.score),
|
||||||
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') if self.config['cumtime'] else '',
|
cumtime=nice_repr(timedelta(seconds=participation.cumtime), "noday")
|
||||||
|
if self.config["cumtime"]
|
||||||
|
else "",
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,18 +10,18 @@ from django.utils.translation import gettext_lazy
|
||||||
|
|
||||||
from judge.contest_format.default import DefaultContestFormat
|
from judge.contest_format.default import DefaultContestFormat
|
||||||
from judge.contest_format.registry import register_contest_format
|
from judge.contest_format.registry import register_contest_format
|
||||||
from judge.timezone import from_database_time
|
from judge.timezone import from_database_time, to_database_time
|
||||||
from judge.utils.timedelta import nice_repr
|
from judge.utils.timedelta import nice_repr
|
||||||
|
|
||||||
|
|
||||||
@register_contest_format('icpc')
|
@register_contest_format("icpc")
|
||||||
class ICPCContestFormat(DefaultContestFormat):
|
class ICPCContestFormat(DefaultContestFormat):
|
||||||
name = gettext_lazy('ICPC')
|
name = gettext_lazy("ICPC")
|
||||||
config_defaults = {'penalty': 20}
|
config_defaults = {"penalty": 20}
|
||||||
config_validators = {'penalty': lambda x: x >= 0}
|
config_validators = {"penalty": lambda x: x >= 0}
|
||||||
'''
|
"""
|
||||||
penalty: Number of penalty minutes each incorrect submission adds. Defaults to 20.
|
penalty: Number of penalty minutes each incorrect submission adds. Defaults to 20.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config):
|
def validate(cls, config):
|
||||||
|
@ -29,7 +29,9 @@ class ICPCContestFormat(DefaultContestFormat):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not isinstance(config, dict):
|
if not isinstance(config, dict):
|
||||||
raise ValidationError('ICPC-styled contest expects no config or dict as config')
|
raise ValidationError(
|
||||||
|
"ICPC-styled contest expects no config or dict as config"
|
||||||
|
)
|
||||||
|
|
||||||
for key, value in config.items():
|
for key, value in config.items():
|
||||||
if key not in cls.config_defaults:
|
if key not in cls.config_defaults:
|
||||||
|
@ -37,7 +39,9 @@ class ICPCContestFormat(DefaultContestFormat):
|
||||||
if not isinstance(value, type(cls.config_defaults[key])):
|
if not isinstance(value, type(cls.config_defaults[key])):
|
||||||
raise ValidationError('invalid type for config key "%s"' % key)
|
raise ValidationError('invalid type for config key "%s"' % key)
|
||||||
if not cls.config_validators[key](value):
|
if not cls.config_validators[key](value):
|
||||||
raise ValidationError('invalid value "%s" for config key "%s"' % (value, key))
|
raise ValidationError(
|
||||||
|
'invalid value "%s" for config key "%s"' % (value, key)
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, contest, config):
|
def __init__(self, contest, config):
|
||||||
self.config = self.config_defaults.copy()
|
self.config = self.config_defaults.copy()
|
||||||
|
@ -51,8 +55,13 @@ class ICPCContestFormat(DefaultContestFormat):
|
||||||
score = 0
|
score = 0
|
||||||
format_data = {}
|
format_data = {}
|
||||||
|
|
||||||
|
frozen_time = self.contest.end_time
|
||||||
|
if self.contest.freeze_after:
|
||||||
|
frozen_time = participation.start + self.contest.freeze_after
|
||||||
|
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
SELECT MAX(cs.points) as `points`, (
|
SELECT MAX(cs.points) as `points`, (
|
||||||
SELECT MIN(csub.date)
|
SELECT MIN(csub.date)
|
||||||
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
||||||
|
@ -62,22 +71,29 @@ class ICPCContestFormat(DefaultContestFormat):
|
||||||
FROM judge_contestproblem cp INNER JOIN
|
FROM judge_contestproblem cp INNER JOIN
|
||||||
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
||||||
judge_submission sub ON (sub.id = cs.submission_id)
|
judge_submission sub ON (sub.id = cs.submission_id)
|
||||||
|
WHERE sub.date < %s
|
||||||
GROUP BY cp.id
|
GROUP BY cp.id
|
||||||
''', (participation.id, participation.id))
|
""",
|
||||||
|
(participation.id, participation.id, to_database_time(frozen_time)),
|
||||||
|
)
|
||||||
|
|
||||||
for points, time, prob in cursor.fetchall():
|
for points, time, prob in cursor.fetchall():
|
||||||
time = from_database_time(time)
|
time = from_database_time(time)
|
||||||
dt = (time - participation.start).total_seconds()
|
dt = (time - participation.start).total_seconds()
|
||||||
|
|
||||||
# Compute penalty
|
# Compute penalty
|
||||||
if self.config['penalty']:
|
if self.config["penalty"]:
|
||||||
# An IE can have a submission result of `None`
|
# An IE can have a submission result of `None`
|
||||||
subs = participation.submissions.exclude(submission__result__isnull=True) \
|
subs = (
|
||||||
.exclude(submission__result__in=['IE', 'CE']) \
|
participation.submissions.exclude(
|
||||||
|
submission__result__isnull=True
|
||||||
|
)
|
||||||
|
.exclude(submission__result__in=["IE", "CE"])
|
||||||
.filter(problem_id=prob)
|
.filter(problem_id=prob)
|
||||||
|
)
|
||||||
if points:
|
if points:
|
||||||
prev = subs.filter(submission__date__lte=time).count() - 1
|
prev = subs.filter(submission__date__lte=time).count() - 1
|
||||||
penalty += prev * self.config['penalty'] * 60
|
penalty += prev * self.config["penalty"] * 60
|
||||||
else:
|
else:
|
||||||
# We should always display the penalty, even if the user has a score of 0
|
# We should always display the penalty, even if the user has a score of 0
|
||||||
prev = subs.count()
|
prev = subs.count()
|
||||||
|
@ -88,35 +104,58 @@ class ICPCContestFormat(DefaultContestFormat):
|
||||||
cumtime += dt
|
cumtime += dt
|
||||||
last = max(last, dt)
|
last = max(last, dt)
|
||||||
|
|
||||||
format_data[str(prob)] = {'time': dt, 'points': points, 'penalty': prev}
|
format_data[str(prob)] = {"time": dt, "points": points, "penalty": prev}
|
||||||
score += points
|
score += points
|
||||||
|
|
||||||
|
self.handle_frozen_state(participation, format_data)
|
||||||
participation.cumtime = max(0, cumtime + penalty)
|
participation.cumtime = max(0, cumtime + penalty)
|
||||||
participation.score = score
|
participation.score = round(score, self.contest.points_precision)
|
||||||
participation.tiebreaker = last # field is sorted from least to greatest
|
participation.tiebreaker = last # field is sorted from least to greatest
|
||||||
participation.format_data = format_data
|
participation.format_data = format_data
|
||||||
participation.save()
|
participation.save()
|
||||||
|
|
||||||
def display_user_problem(self, participation, contest_problem):
|
def display_user_problem(self, participation, contest_problem, show_final=False):
|
||||||
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
||||||
if format_data:
|
if format_data:
|
||||||
penalty = format_html('<small style="color:red"> ({penalty})</small>',
|
penalty = (
|
||||||
penalty=floatformat(format_data['penalty'])) if format_data['penalty'] else ''
|
format_html(
|
||||||
|
'<small style="color:red"> +{penalty}</small>',
|
||||||
|
penalty=floatformat(format_data["penalty"]),
|
||||||
|
)
|
||||||
|
if format_data.get("penalty")
|
||||||
|
else ""
|
||||||
|
)
|
||||||
return format_html(
|
return format_html(
|
||||||
'<td class="{state}"><a href="{url}">{points}{penalty}<div class="solving-time">{time}</div></a></td>',
|
'<td class="{state}"><a data-featherlight="{url}" href="#">{points}{penalty}<div class="solving-time">{time}</div></a></td>',
|
||||||
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
|
state=(
|
||||||
self.best_solution_state(format_data['points'], contest_problem.points)),
|
(
|
||||||
url=reverse('contest_user_submissions',
|
"pretest-"
|
||||||
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
|
if self.contest.run_pretests_only
|
||||||
points=floatformat(format_data['points']),
|
and contest_problem.is_pretested
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
+ self.best_solution_state(
|
||||||
|
format_data["points"], contest_problem.points
|
||||||
|
)
|
||||||
|
+ (" frozen" if format_data.get("frozen") else "")
|
||||||
|
),
|
||||||
|
url=reverse(
|
||||||
|
"contest_user_submissions_ajax",
|
||||||
|
args=[
|
||||||
|
self.contest.key,
|
||||||
|
participation.id,
|
||||||
|
contest_problem.problem.code,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
points=floatformat(format_data["points"]),
|
||||||
penalty=penalty,
|
penalty=penalty,
|
||||||
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'),
|
time=nice_repr(timedelta(seconds=format_data["time"]), "noday"),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return mark_safe('<td></td>')
|
return mark_safe("<td></td>")
|
||||||
|
|
||||||
def get_contest_problem_label_script(self):
|
def get_contest_problem_label_script(self):
|
||||||
return '''
|
return """
|
||||||
function(n)
|
function(n)
|
||||||
n = n + 1
|
n = n + 1
|
||||||
ret = ""
|
ret = ""
|
||||||
|
@ -126,4 +165,4 @@ class ICPCContestFormat(DefaultContestFormat):
|
||||||
end
|
end
|
||||||
return ret
|
return ret
|
||||||
end
|
end
|
||||||
'''
|
"""
|
||||||
|
|
|
@ -12,15 +12,16 @@ from judge.contest_format.default import DefaultContestFormat
|
||||||
from judge.contest_format.registry import register_contest_format
|
from judge.contest_format.registry import register_contest_format
|
||||||
from judge.timezone import from_database_time
|
from judge.timezone import from_database_time
|
||||||
from judge.utils.timedelta import nice_repr
|
from judge.utils.timedelta import nice_repr
|
||||||
|
from django.db.models import Min, OuterRef, Subquery
|
||||||
|
|
||||||
|
|
||||||
@register_contest_format('ioi')
|
@register_contest_format("ioi")
|
||||||
class IOIContestFormat(DefaultContestFormat):
|
class IOIContestFormat(DefaultContestFormat):
|
||||||
name = gettext_lazy('IOI')
|
name = gettext_lazy("IOI")
|
||||||
config_defaults = {'cumtime': False}
|
config_defaults = {"cumtime": False}
|
||||||
'''
|
"""
|
||||||
cumtime: Specify True if time penalties are to be computed. Defaults to False.
|
cumtime: Specify True if time penalties are to be computed. Defaults to False.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config):
|
def validate(cls, config):
|
||||||
|
@ -28,7 +29,9 @@ class IOIContestFormat(DefaultContestFormat):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not isinstance(config, dict):
|
if not isinstance(config, dict):
|
||||||
raise ValidationError('IOI-styled contest expects no config or dict as config')
|
raise ValidationError(
|
||||||
|
"IOI-styled contest expects no config or dict as config"
|
||||||
|
)
|
||||||
|
|
||||||
for key, value in config.items():
|
for key, value in config.items():
|
||||||
if key not in cls.config_defaults:
|
if key not in cls.config_defaults:
|
||||||
|
@ -43,58 +46,97 @@ class IOIContestFormat(DefaultContestFormat):
|
||||||
|
|
||||||
def update_participation(self, participation):
|
def update_participation(self, participation):
|
||||||
cumtime = 0
|
cumtime = 0
|
||||||
points = 0
|
score = 0
|
||||||
format_data = {}
|
format_data = {}
|
||||||
|
|
||||||
with connection.cursor() as cursor:
|
queryset = participation.submissions
|
||||||
cursor.execute('''
|
if self.contest.freeze_after:
|
||||||
SELECT MAX(cs.points) as `score`, (
|
queryset = queryset.filter(
|
||||||
SELECT MIN(csub.date)
|
submission__date__lt=participation.start + self.contest.freeze_after
|
||||||
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
)
|
||||||
judge_submission csub ON (csub.id = ccs.submission_id)
|
|
||||||
WHERE ccs.problem_id = cp.id AND ccs.participation_id = %s AND ccs.points = MAX(cs.points)
|
|
||||||
) AS `time`, cp.id AS `prob`
|
|
||||||
FROM judge_contestproblem cp INNER JOIN
|
|
||||||
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
|
||||||
judge_submission sub ON (sub.id = cs.submission_id)
|
|
||||||
GROUP BY cp.id
|
|
||||||
''', (participation.id, participation.id))
|
|
||||||
|
|
||||||
for score, time, prob in cursor.fetchall():
|
queryset = (
|
||||||
if self.config['cumtime']:
|
queryset.values("problem_id")
|
||||||
dt = (from_database_time(time) - participation.start).total_seconds()
|
.filter(
|
||||||
if score:
|
points=Subquery(
|
||||||
|
queryset.filter(problem_id=OuterRef("problem_id"))
|
||||||
|
.order_by("-points")
|
||||||
|
.values("points")[:1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(time=Min("submission__date"))
|
||||||
|
.values_list("problem_id", "time", "points")
|
||||||
|
)
|
||||||
|
|
||||||
|
for problem_id, time, points in queryset:
|
||||||
|
if self.config["cumtime"]:
|
||||||
|
dt = (time - participation.start).total_seconds()
|
||||||
|
if points:
|
||||||
cumtime += dt
|
cumtime += dt
|
||||||
else:
|
else:
|
||||||
dt = 0
|
dt = 0
|
||||||
|
|
||||||
format_data[str(prob)] = {'time': dt, 'points': score}
|
format_data[str(problem_id)] = {"points": points, "time": dt}
|
||||||
points += score
|
score += points
|
||||||
|
|
||||||
|
self.handle_frozen_state(participation, format_data)
|
||||||
participation.cumtime = max(cumtime, 0)
|
participation.cumtime = max(cumtime, 0)
|
||||||
participation.score = points
|
participation.score = round(score, self.contest.points_precision)
|
||||||
participation.tiebreaker = 0
|
participation.tiebreaker = 0
|
||||||
participation.format_data = format_data
|
participation.format_data = format_data
|
||||||
participation.save()
|
participation.save()
|
||||||
|
|
||||||
def display_user_problem(self, participation, contest_problem):
|
def display_user_problem(self, participation, contest_problem, show_final=False):
|
||||||
|
if show_final:
|
||||||
|
format_data = (participation.format_data_final or {}).get(
|
||||||
|
str(contest_problem.id)
|
||||||
|
)
|
||||||
|
else:
|
||||||
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
||||||
if format_data:
|
if format_data:
|
||||||
return format_html(
|
return format_html(
|
||||||
'<td class="{state} problem-score-col"><a href="{url}">{points}<div class="solving-time">{time}</div></a></td>',
|
'<td class="{state} problem-score-col"><a data-featherlight="{url}" href="#">{points}<div class="solving-time">{time}</div></a></td>',
|
||||||
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
|
state=(
|
||||||
self.best_solution_state(format_data['points'], contest_problem.points)),
|
(
|
||||||
url=reverse('contest_user_submissions',
|
"pretest-"
|
||||||
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
|
if self.contest.run_pretests_only
|
||||||
points=floatformat(format_data['points']),
|
and contest_problem.is_pretested
|
||||||
time=nice_repr(timedelta(seconds=format_data['time']), 'noday') if self.config['cumtime'] else '',
|
else ""
|
||||||
|
)
|
||||||
|
+ self.best_solution_state(
|
||||||
|
format_data["points"], contest_problem.points
|
||||||
|
)
|
||||||
|
+ (" frozen" if format_data.get("frozen") else "")
|
||||||
|
),
|
||||||
|
url=reverse(
|
||||||
|
"contest_user_submissions_ajax",
|
||||||
|
args=[
|
||||||
|
self.contest.key,
|
||||||
|
participation.id,
|
||||||
|
contest_problem.problem.code,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
points=floatformat(
|
||||||
|
format_data["points"], -self.contest.points_precision
|
||||||
|
),
|
||||||
|
time=nice_repr(timedelta(seconds=format_data["time"]), "noday")
|
||||||
|
if self.config["cumtime"]
|
||||||
|
else "",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return mark_safe('<td class="problem-score-col"></td>')
|
return mark_safe('<td class="problem-score-col"></td>')
|
||||||
|
|
||||||
def display_participation_result(self, participation):
|
def display_participation_result(self, participation, show_final=False):
|
||||||
|
if show_final:
|
||||||
|
score = participation.score_final
|
||||||
|
cumtime = participation.cumtime_final
|
||||||
|
else:
|
||||||
|
score = participation.score
|
||||||
|
cumtime = participation.cumtime
|
||||||
return format_html(
|
return format_html(
|
||||||
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
|
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
|
||||||
points=floatformat(participation.score),
|
points=floatformat(score, -self.contest.points_precision),
|
||||||
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') if self.config['cumtime'] else '',
|
cumtime=nice_repr(timedelta(seconds=cumtime), "noday")
|
||||||
|
if self.config["cumtime"]
|
||||||
|
else "",
|
||||||
)
|
)
|
||||||
|
|
173
judge/contest_format/new_ioi.py
Normal file
173
judge/contest_format/new_ioi.py
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
from django.db import connection
|
||||||
|
from django.utils.translation import gettext as _, gettext_lazy
|
||||||
|
|
||||||
|
from judge.contest_format.ioi import IOIContestFormat
|
||||||
|
from judge.contest_format.registry import register_contest_format
|
||||||
|
from judge.timezone import from_database_time, to_database_time
|
||||||
|
|
||||||
|
|
||||||
|
@register_contest_format("ioi16")
|
||||||
|
class NewIOIContestFormat(IOIContestFormat):
|
||||||
|
name = gettext_lazy("New IOI")
|
||||||
|
config_defaults = {"cumtime": False}
|
||||||
|
has_hidden_subtasks = True
|
||||||
|
"""
|
||||||
|
cumtime: Specify True if time penalties are to be computed. Defaults to False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_hidden_subtasks(self):
|
||||||
|
queryset = self.contest.contest_problems.values_list("id", "hidden_subtasks")
|
||||||
|
res = {}
|
||||||
|
for problem_id, hidden_subtasks in queryset:
|
||||||
|
subtasks = set()
|
||||||
|
if hidden_subtasks:
|
||||||
|
hidden_subtasks = hidden_subtasks.split(",")
|
||||||
|
for i in hidden_subtasks:
|
||||||
|
try:
|
||||||
|
subtasks.add(int(i))
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
res[str(problem_id)] = subtasks
|
||||||
|
return res
|
||||||
|
|
||||||
|
def get_results_by_subtask(self, participation, include_frozen=False):
|
||||||
|
frozen_time = self.contest.end_time
|
||||||
|
if self.contest.freeze_after and not include_frozen:
|
||||||
|
frozen_time = participation.start + self.contest.freeze_after
|
||||||
|
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT q.prob,
|
||||||
|
q.prob_points,
|
||||||
|
MIN(q.date) as `date`,
|
||||||
|
q.batch_points,
|
||||||
|
q.total_batch_points,
|
||||||
|
q.batch,
|
||||||
|
q.subid
|
||||||
|
FROM (
|
||||||
|
SELECT cp.id as `prob`,
|
||||||
|
cp.points as `prob_points`,
|
||||||
|
sub.id as `subid`,
|
||||||
|
sub.date as `date`,
|
||||||
|
tc.points as `points`,
|
||||||
|
tc.batch as `batch`,
|
||||||
|
SUM(tc.points) as `batch_points`,
|
||||||
|
SUM(tc.total) as `total_batch_points`
|
||||||
|
FROM judge_contestproblem cp
|
||||||
|
INNER JOIN
|
||||||
|
judge_contestsubmission cs
|
||||||
|
ON (cs.problem_id = cp.id AND cs.participation_id = %s)
|
||||||
|
LEFT OUTER JOIN
|
||||||
|
judge_submission sub
|
||||||
|
ON (sub.id = cs.submission_id AND sub.status = 'D')
|
||||||
|
INNER JOIN judge_submissiontestcase tc
|
||||||
|
ON sub.id = tc.submission_id
|
||||||
|
WHERE sub.date < %s
|
||||||
|
GROUP BY cp.id, tc.batch, sub.id
|
||||||
|
) q
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT prob, batch, MAX(r.batch_points) as max_batch_points
|
||||||
|
FROM (
|
||||||
|
SELECT cp.id as `prob`,
|
||||||
|
tc.batch as `batch`,
|
||||||
|
SUM(tc.points) as `batch_points`
|
||||||
|
FROM judge_contestproblem cp
|
||||||
|
INNER JOIN
|
||||||
|
judge_contestsubmission cs
|
||||||
|
ON (cs.problem_id = cp.id AND cs.participation_id = %s)
|
||||||
|
LEFT OUTER JOIN
|
||||||
|
judge_submission sub
|
||||||
|
ON (sub.id = cs.submission_id AND sub.status = 'D')
|
||||||
|
INNER JOIN judge_submissiontestcase tc
|
||||||
|
ON sub.id = tc.submission_id
|
||||||
|
WHERE sub.date < %s
|
||||||
|
GROUP BY cp.id, tc.batch, sub.id
|
||||||
|
) r
|
||||||
|
GROUP BY prob, batch
|
||||||
|
) p
|
||||||
|
ON p.prob = q.prob AND (p.batch = q.batch OR p.batch is NULL AND q.batch is NULL)
|
||||||
|
WHERE p.max_batch_points = q.batch_points
|
||||||
|
GROUP BY q.prob, q.batch
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
participation.id,
|
||||||
|
to_database_time(frozen_time),
|
||||||
|
participation.id,
|
||||||
|
to_database_time(frozen_time),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
|
def update_participation(self, participation):
|
||||||
|
hidden_subtasks = self.get_hidden_subtasks()
|
||||||
|
|
||||||
|
def calculate_format_data(participation, include_frozen):
|
||||||
|
format_data = {}
|
||||||
|
for (
|
||||||
|
problem_id,
|
||||||
|
problem_points,
|
||||||
|
time,
|
||||||
|
subtask_points,
|
||||||
|
total_subtask_points,
|
||||||
|
subtask,
|
||||||
|
sub_id,
|
||||||
|
) in self.get_results_by_subtask(participation, include_frozen):
|
||||||
|
problem_id = str(problem_id)
|
||||||
|
time = from_database_time(time)
|
||||||
|
if self.config["cumtime"]:
|
||||||
|
dt = (time - participation.start).total_seconds()
|
||||||
|
else:
|
||||||
|
dt = 0
|
||||||
|
|
||||||
|
if format_data.get(problem_id) is None:
|
||||||
|
format_data[problem_id] = {
|
||||||
|
"points": 0,
|
||||||
|
"time": 0,
|
||||||
|
"total_points": 0,
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
subtask not in hidden_subtasks.get(problem_id, set())
|
||||||
|
or include_frozen
|
||||||
|
):
|
||||||
|
format_data[problem_id]["points"] += subtask_points
|
||||||
|
format_data[problem_id]["total_points"] += total_subtask_points
|
||||||
|
format_data[problem_id]["time"] = max(
|
||||||
|
dt, format_data[problem_id]["time"]
|
||||||
|
)
|
||||||
|
format_data[problem_id]["problem_points"] = problem_points
|
||||||
|
|
||||||
|
return format_data
|
||||||
|
|
||||||
|
def recalculate_results(format_data):
|
||||||
|
cumtime = 0
|
||||||
|
score = 0
|
||||||
|
for problem_data in format_data.values():
|
||||||
|
if not problem_data["total_points"]:
|
||||||
|
continue
|
||||||
|
penalty = problem_data["time"]
|
||||||
|
problem_data["points"] = (
|
||||||
|
problem_data["points"]
|
||||||
|
/ problem_data["total_points"]
|
||||||
|
* problem_data["problem_points"]
|
||||||
|
)
|
||||||
|
if self.config["cumtime"] and problem_data["points"]:
|
||||||
|
cumtime += penalty
|
||||||
|
score += problem_data["points"]
|
||||||
|
return score, cumtime
|
||||||
|
|
||||||
|
format_data = calculate_format_data(participation, False)
|
||||||
|
score, cumtime = recalculate_results(format_data)
|
||||||
|
self.handle_frozen_state(participation, format_data)
|
||||||
|
participation.cumtime = max(cumtime, 0)
|
||||||
|
participation.score = round(score, self.contest.points_precision)
|
||||||
|
participation.tiebreaker = 0
|
||||||
|
participation.format_data = format_data
|
||||||
|
|
||||||
|
format_data_final = calculate_format_data(participation, True)
|
||||||
|
score_final, cumtime_final = recalculate_results(format_data_final)
|
||||||
|
participation.cumtime_final = max(cumtime_final, 0)
|
||||||
|
participation.score_final = round(score_final, self.contest.points_precision)
|
||||||
|
participation.format_data_final = format_data_final
|
||||||
|
participation.save()
|
|
@ -1,5 +1,3 @@
|
||||||
from django.utils import six
|
|
||||||
|
|
||||||
formats = {}
|
formats = {}
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,4 +11,4 @@ def register_contest_format(name):
|
||||||
|
|
||||||
|
|
||||||
def choices():
|
def choices():
|
||||||
return [(key, value.name) for key, value in sorted(six.iteritems(formats))]
|
return [(key, value.name) for key, value in sorted(formats.items())]
|
||||||
|
|
55
judge/contest_format/ultimate.py
Normal file
55
judge/contest_format/ultimate.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
from django.utils.translation import gettext_lazy
|
||||||
|
|
||||||
|
from judge.contest_format.ioi import IOIContestFormat
|
||||||
|
from judge.contest_format.registry import register_contest_format
|
||||||
|
from django.db.models import Min, OuterRef, Subquery
|
||||||
|
|
||||||
|
# This contest format only counts last submission for each problem.
|
||||||
|
|
||||||
|
|
||||||
|
@register_contest_format("ultimate")
|
||||||
|
class UltimateContestFormat(IOIContestFormat):
|
||||||
|
name = gettext_lazy("Ultimate")
|
||||||
|
|
||||||
|
def update_participation(self, participation):
|
||||||
|
cumtime = 0
|
||||||
|
score = 0
|
||||||
|
format_data = {}
|
||||||
|
|
||||||
|
queryset = participation.submissions
|
||||||
|
if self.contest.freeze_after:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
submission__date__lt=participation.start + self.contest.freeze_after
|
||||||
|
)
|
||||||
|
|
||||||
|
queryset = (
|
||||||
|
queryset.values("problem_id")
|
||||||
|
.filter(
|
||||||
|
id=Subquery(
|
||||||
|
queryset.filter(problem_id=OuterRef("problem_id"))
|
||||||
|
.order_by("-id")
|
||||||
|
.values("id")[:1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values_list("problem_id", "submission__date", "points")
|
||||||
|
)
|
||||||
|
|
||||||
|
for problem_id, time, points in queryset:
|
||||||
|
if self.config["cumtime"]:
|
||||||
|
dt = (time - participation.start).total_seconds()
|
||||||
|
if points:
|
||||||
|
cumtime += dt
|
||||||
|
else:
|
||||||
|
dt = 0
|
||||||
|
format_data[str(problem_id)] = {
|
||||||
|
"time": dt,
|
||||||
|
"points": points,
|
||||||
|
}
|
||||||
|
score += points
|
||||||
|
|
||||||
|
self.handle_frozen_state(participation, format_data)
|
||||||
|
participation.cumtime = max(cumtime, 0)
|
||||||
|
participation.score = round(score, self.contest.points_precision)
|
||||||
|
participation.tiebreaker = 0
|
||||||
|
participation.format_data = format_data
|
||||||
|
participation.save()
|
22
judge/custom_translations.py
Normal file
22
judge/custom_translations.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from django.utils.translation import gettext_lazy as _, ngettext
|
||||||
|
|
||||||
|
|
||||||
|
def custom_trans():
|
||||||
|
return [
|
||||||
|
# Password reset
|
||||||
|
ngettext(
|
||||||
|
"This password is too short. It must contain at least %(min_length)d character.",
|
||||||
|
"This password is too short. It must contain at least %(min_length)d characters.",
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
ngettext(
|
||||||
|
"Your password must contain at least %(min_length)d character.",
|
||||||
|
"Your password must contain at least %(min_length)d characters.",
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
_("The two password fields didn’t match."),
|
||||||
|
_("Your password can’t be entirely numeric."),
|
||||||
|
# Navbar
|
||||||
|
_("Bug Report"),
|
||||||
|
_("Courses"),
|
||||||
|
]
|
|
@ -5,19 +5,21 @@ from django.db import connection, transaction
|
||||||
|
|
||||||
class LockModel(object):
|
class LockModel(object):
|
||||||
def __init__(self, write, read=()):
|
def __init__(self, write, read=()):
|
||||||
self.tables = ', '.join(chain(
|
self.tables = ", ".join(
|
||||||
('`%s` WRITE' % model._meta.db_table for model in write),
|
chain(
|
||||||
('`%s` READ' % model._meta.db_table for model in read),
|
("`%s` WRITE" % model._meta.db_table for model in write),
|
||||||
))
|
("`%s` READ" % model._meta.db_table for model in read),
|
||||||
|
)
|
||||||
|
)
|
||||||
self.cursor = connection.cursor()
|
self.cursor = connection.cursor()
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.cursor.execute('LOCK TABLES ' + self.tables)
|
self.cursor.execute("LOCK TABLES " + self.tables)
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
if exc_type is None:
|
if exc_type is None:
|
||||||
transaction.commit()
|
transaction.commit()
|
||||||
else:
|
else:
|
||||||
transaction.rollback()
|
transaction.rollback()
|
||||||
self.cursor.execute('UNLOCK TABLES')
|
self.cursor.execute("UNLOCK TABLES")
|
||||||
self.cursor.close()
|
self.cursor.close()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
__all__ = ['last', 'post']
|
__all__ = ["last", "post"]
|
||||||
|
|
||||||
if not settings.EVENT_DAEMON_USE:
|
if not settings.EVENT_DAEMON_USE:
|
||||||
real = False
|
real = False
|
||||||
|
@ -10,9 +10,12 @@ if not settings.EVENT_DAEMON_USE:
|
||||||
|
|
||||||
def last():
|
def last():
|
||||||
return 0
|
return 0
|
||||||
elif hasattr(settings, 'EVENT_DAEMON_AMQP'):
|
|
||||||
|
elif hasattr(settings, "EVENT_DAEMON_AMQP"):
|
||||||
from .event_poster_amqp import last, post
|
from .event_poster_amqp import last, post
|
||||||
|
|
||||||
real = True
|
real = True
|
||||||
else:
|
else:
|
||||||
from .event_poster_ws import last, post
|
from .event_poster_ws import last, post
|
||||||
|
|
||||||
real = True
|
real = True
|
||||||
|
|
|
@ -6,7 +6,7 @@ import pika
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from pika.exceptions import AMQPError
|
from pika.exceptions import AMQPError
|
||||||
|
|
||||||
__all__ = ['EventPoster', 'post', 'last']
|
__all__ = ["EventPoster", "post", "last"]
|
||||||
|
|
||||||
|
|
||||||
class EventPoster(object):
|
class EventPoster(object):
|
||||||
|
@ -15,14 +15,19 @@ class EventPoster(object):
|
||||||
self._exchange = settings.EVENT_DAEMON_AMQP_EXCHANGE
|
self._exchange = settings.EVENT_DAEMON_AMQP_EXCHANGE
|
||||||
|
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
self._conn = pika.BlockingConnection(pika.URLParameters(settings.EVENT_DAEMON_AMQP))
|
self._conn = pika.BlockingConnection(
|
||||||
|
pika.URLParameters(settings.EVENT_DAEMON_AMQP),
|
||||||
|
)
|
||||||
self._chan = self._conn.channel()
|
self._chan = self._conn.channel()
|
||||||
|
|
||||||
def post(self, channel, message, tries=0):
|
def post(self, channel, message, tries=0):
|
||||||
try:
|
try:
|
||||||
id = int(time() * 1000000)
|
id = int(time() * 1000000)
|
||||||
self._chan.basic_publish(self._exchange, '',
|
self._chan.basic_publish(
|
||||||
json.dumps({'id': id, 'channel': channel, 'message': message}))
|
self._exchange,
|
||||||
|
"#",
|
||||||
|
json.dumps({"id": id, "channel": channel, "message": message}),
|
||||||
|
)
|
||||||
return id
|
return id
|
||||||
except AMQPError:
|
except AMQPError:
|
||||||
if tries > 10:
|
if tries > 10:
|
||||||
|
@ -35,7 +40,7 @@ _local = threading.local()
|
||||||
|
|
||||||
|
|
||||||
def _get_poster():
|
def _get_poster():
|
||||||
if 'poster' not in _local.__dict__:
|
if "poster" not in _local.__dict__:
|
||||||
_local.poster = EventPoster()
|
_local.poster = EventPoster()
|
||||||
return _local.poster
|
return _local.poster
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import threading
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from websocket import WebSocketException, create_connection
|
from websocket import WebSocketException, create_connection
|
||||||
|
|
||||||
__all__ = ['EventPostingError', 'EventPoster', 'post', 'last']
|
__all__ = ["EventPostingError", "EventPoster", "post", "last"]
|
||||||
_local = threading.local()
|
_local = threading.local()
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,19 +20,23 @@ class EventPoster(object):
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
self._conn = create_connection(settings.EVENT_DAEMON_POST)
|
self._conn = create_connection(settings.EVENT_DAEMON_POST)
|
||||||
if settings.EVENT_DAEMON_KEY is not None:
|
if settings.EVENT_DAEMON_KEY is not None:
|
||||||
self._conn.send(json.dumps({'command': 'auth', 'key': settings.EVENT_DAEMON_KEY}))
|
self._conn.send(
|
||||||
|
json.dumps({"command": "auth", "key": settings.EVENT_DAEMON_KEY})
|
||||||
|
)
|
||||||
resp = json.loads(self._conn.recv())
|
resp = json.loads(self._conn.recv())
|
||||||
if resp['status'] == 'error':
|
if resp["status"] == "error":
|
||||||
raise EventPostingError(resp['code'])
|
raise EventPostingError(resp["code"])
|
||||||
|
|
||||||
def post(self, channel, message, tries=0):
|
def post(self, channel, message, tries=0):
|
||||||
try:
|
try:
|
||||||
self._conn.send(json.dumps({'command': 'post', 'channel': channel, 'message': message}))
|
self._conn.send(
|
||||||
|
json.dumps({"command": "post", "channel": channel, "message": message})
|
||||||
|
)
|
||||||
resp = json.loads(self._conn.recv())
|
resp = json.loads(self._conn.recv())
|
||||||
if resp['status'] == 'error':
|
if resp["status"] == "error":
|
||||||
raise EventPostingError(resp['code'])
|
raise EventPostingError(resp["code"])
|
||||||
else:
|
else:
|
||||||
return resp['id']
|
return resp["id"]
|
||||||
except WebSocketException:
|
except WebSocketException:
|
||||||
if tries > 10:
|
if tries > 10:
|
||||||
raise
|
raise
|
||||||
|
@ -43,10 +47,10 @@ class EventPoster(object):
|
||||||
try:
|
try:
|
||||||
self._conn.send('{"command": "last-msg"}')
|
self._conn.send('{"command": "last-msg"}')
|
||||||
resp = json.loads(self._conn.recv())
|
resp = json.loads(self._conn.recv())
|
||||||
if resp['status'] == 'error':
|
if resp["status"] == "error":
|
||||||
raise EventPostingError(resp['code'])
|
raise EventPostingError(resp["code"])
|
||||||
else:
|
else:
|
||||||
return resp['id']
|
return resp["id"]
|
||||||
except WebSocketException:
|
except WebSocketException:
|
||||||
if tries > 10:
|
if tries > 10:
|
||||||
raise
|
raise
|
||||||
|
@ -55,7 +59,7 @@ class EventPoster(object):
|
||||||
|
|
||||||
|
|
||||||
def _get_poster():
|
def _get_poster():
|
||||||
if 'poster' not in _local.__dict__:
|
if "poster" not in _local.__dict__:
|
||||||
_local.poster = EventPoster()
|
_local.poster = EventPoster()
|
||||||
return _local.poster
|
return _local.poster
|
||||||
|
|
||||||
|
|
110
judge/feed.py
110
judge/feed.py
|
@ -1,110 +0,0 @@
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.models import AnonymousUser
|
|
||||||
from django.contrib.syndication.views import Feed
|
|
||||||
from django.core.cache import cache
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.feedgenerator import Atom1Feed
|
|
||||||
|
|
||||||
from judge.jinja2.markdown import markdown
|
|
||||||
from judge.models import BlogPost, Comment, Problem
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
# https://lsimons.wordpress.com/2011/03/17/stripping-illegal-characters-out-of-xml-in-python/
|
|
||||||
def escape_xml_illegal_chars(val, replacement='?'):
|
|
||||||
_illegal_xml_chars_RE = re.compile(u'[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]')
|
|
||||||
return _illegal_xml_chars_RE.sub(replacement, val)
|
|
||||||
|
|
||||||
class ProblemFeed(Feed):
|
|
||||||
title = 'Recently Added %s Problems' % settings.SITE_NAME
|
|
||||||
link = '/'
|
|
||||||
description = 'The latest problems added on the %s website' % settings.SITE_LONG_NAME
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
return Problem.objects.filter(is_public=True, is_organization_private=False).defer('description')\
|
|
||||||
.order_by('-date', '-id')[:25]
|
|
||||||
|
|
||||||
def item_title(self, problem):
|
|
||||||
return problem.name
|
|
||||||
|
|
||||||
def item_description(self, problem):
|
|
||||||
key = 'problem_feed:%d' % problem.id
|
|
||||||
desc = cache.get(key)
|
|
||||||
if desc is None:
|
|
||||||
desc = str(markdown(problem.description, 'problem'))[:500] + '...'
|
|
||||||
desc = escape_xml_illegal_chars(desc)
|
|
||||||
cache.set(key, desc, 86400)
|
|
||||||
return desc
|
|
||||||
|
|
||||||
def item_pubdate(self, problem):
|
|
||||||
return problem.date
|
|
||||||
|
|
||||||
item_updateddate = item_pubdate
|
|
||||||
|
|
||||||
|
|
||||||
class AtomProblemFeed(ProblemFeed):
|
|
||||||
feed_type = Atom1Feed
|
|
||||||
subtitle = ProblemFeed.description
|
|
||||||
|
|
||||||
|
|
||||||
class CommentFeed(Feed):
|
|
||||||
title = 'Latest %s Comments' % settings.SITE_NAME
|
|
||||||
link = '/'
|
|
||||||
description = 'The latest comments on the %s website' % settings.SITE_LONG_NAME
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
return Comment.most_recent(AnonymousUser(), 25)
|
|
||||||
|
|
||||||
def item_title(self, comment):
|
|
||||||
return '%s -> %s' % (comment.author.user.username, comment.page_title)
|
|
||||||
|
|
||||||
def item_description(self, comment):
|
|
||||||
key = 'comment_feed:%d' % comment.id
|
|
||||||
desc = cache.get(key)
|
|
||||||
if desc is None:
|
|
||||||
desc = str(markdown(comment.body, 'comment'))
|
|
||||||
desc = escape_xml_illegal_chars(desc)
|
|
||||||
cache.set(key, desc, 86400)
|
|
||||||
return desc
|
|
||||||
|
|
||||||
def item_pubdate(self, comment):
|
|
||||||
return comment.time
|
|
||||||
|
|
||||||
item_updateddate = item_pubdate
|
|
||||||
|
|
||||||
|
|
||||||
class AtomCommentFeed(CommentFeed):
|
|
||||||
feed_type = Atom1Feed
|
|
||||||
subtitle = CommentFeed.description
|
|
||||||
|
|
||||||
|
|
||||||
class BlogFeed(Feed):
|
|
||||||
title = 'Latest %s Blog Posts' % settings.SITE_NAME
|
|
||||||
link = '/'
|
|
||||||
description = 'The latest blog posts from the %s' % settings.SITE_LONG_NAME
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
return BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()).order_by('-sticky', '-publish_on')
|
|
||||||
|
|
||||||
def item_title(self, post):
|
|
||||||
return post.title
|
|
||||||
|
|
||||||
def item_description(self, post):
|
|
||||||
key = 'blog_feed:%d' % post.id
|
|
||||||
summary = cache.get(key)
|
|
||||||
if summary is None:
|
|
||||||
summary = str(markdown(post.summary or post.content, 'blog'))
|
|
||||||
summary = escape_xml_illegal_chars(summary)
|
|
||||||
cache.set(key, summary, 86400)
|
|
||||||
return summary
|
|
||||||
|
|
||||||
def item_pubdate(self, post):
|
|
||||||
return post.publish_on
|
|
||||||
|
|
||||||
item_updateddate = item_pubdate
|
|
||||||
|
|
||||||
|
|
||||||
class AtomBlogFeed(BlogFeed):
|
|
||||||
feed_type = Atom1Feed
|
|
||||||
subtitle = BlogFeed.description
|
|
|
@ -8,7 +8,6 @@
|
||||||
"ip": "10.0.2.2",
|
"ip": "10.0.2.2",
|
||||||
"language": 1,
|
"language": 1,
|
||||||
"last_access": "2017-12-02T08:57:10.093Z",
|
"last_access": "2017-12-02T08:57:10.093Z",
|
||||||
"math_engine": "auto",
|
|
||||||
"mute": false,
|
"mute": false,
|
||||||
"organizations": [
|
"organizations": [
|
||||||
1
|
1
|
||||||
|
@ -18,8 +17,7 @@
|
||||||
"problem_count": 0,
|
"problem_count": 0,
|
||||||
"rating": null,
|
"rating": null,
|
||||||
"timezone": "America/Toronto",
|
"timezone": "America/Toronto",
|
||||||
"user": 1,
|
"user": 1
|
||||||
"user_script": ""
|
|
||||||
},
|
},
|
||||||
"model": "judge.profile",
|
"model": "judge.profile",
|
||||||
"pk": 1
|
"pk": 1
|
||||||
|
@ -147,25 +145,8 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fields": {
|
"fields": {
|
||||||
"author": 1,
|
"domain": "localhost:8000",
|
||||||
"body": "This is your first comment!",
|
"name": "LQDOJ"
|
||||||
"hidden": false,
|
|
||||||
"level": 0,
|
|
||||||
"lft": 1,
|
|
||||||
"page": "b:1",
|
|
||||||
"parent": null,
|
|
||||||
"rght": 2,
|
|
||||||
"score": 0,
|
|
||||||
"time": "2017-12-02T08:46:54.007Z",
|
|
||||||
"tree_id": 1
|
|
||||||
},
|
|
||||||
"model": "judge.comment",
|
|
||||||
"pk": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fields": {
|
|
||||||
"domain": "localhost:8081",
|
|
||||||
"name": "DMOJ: Modern Online Judge"
|
|
||||||
},
|
},
|
||||||
"model": "sites.site",
|
"model": "sites.site",
|
||||||
"pk": 1
|
"pk": 1
|
||||||
|
|
585
judge/forms.py
585
judge/forms.py
|
@ -1,164 +1,599 @@
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
import pyotp
|
import pyotp
|
||||||
|
import time
|
||||||
|
import datetime
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.auth.forms import AuthenticationForm
|
from django.contrib.auth.forms import AuthenticationForm
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.forms import CharField, ChoiceField, Form, ModelForm
|
from django.forms import (
|
||||||
from django.urls import reverse_lazy
|
CharField,
|
||||||
|
ChoiceField,
|
||||||
|
Form,
|
||||||
|
ModelForm,
|
||||||
|
formset_factory,
|
||||||
|
BaseModelFormSet,
|
||||||
|
FileField,
|
||||||
|
)
|
||||||
|
from django.urls import reverse_lazy, reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from django_ace import AceWidget
|
from django_ace import AceWidget
|
||||||
from judge.models import Contest, Language, Organization, PrivateMessage, Problem, Profile, Submission
|
from judge.models import (
|
||||||
from judge.utils.subscription import newsletter_id
|
Contest,
|
||||||
from judge.widgets import HeavyPreviewPageDownWidget, MathJaxPagedownWidget, PagedownWidget, Select2MultipleWidget, \
|
Language,
|
||||||
Select2Widget
|
TestFormatterModel,
|
||||||
|
Organization,
|
||||||
|
PrivateMessage,
|
||||||
|
Problem,
|
||||||
|
ProblemPointsVote,
|
||||||
|
Profile,
|
||||||
|
Submission,
|
||||||
|
BlogPost,
|
||||||
|
ContestProblem,
|
||||||
|
TestFormatterModel,
|
||||||
|
ProfileInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
from judge.widgets import (
|
||||||
|
HeavyPreviewPageDownWidget,
|
||||||
|
PagedownWidget,
|
||||||
|
Select2MultipleWidget,
|
||||||
|
Select2Widget,
|
||||||
|
HeavySelect2MultipleWidget,
|
||||||
|
HeavySelect2Widget,
|
||||||
|
Select2MultipleWidget,
|
||||||
|
DateTimePickerWidget,
|
||||||
|
ImageWidget,
|
||||||
|
DatePickerWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def fix_unicode(string, unsafe=tuple('\u202a\u202b\u202d\u202e')):
|
def fix_unicode(string, unsafe=tuple("\u202a\u202b\u202d\u202e")):
|
||||||
return string + (sum(k in unsafe for k in string) - string.count('\u202c')) * '\u202c'
|
return (
|
||||||
|
string + (sum(k in unsafe for k in string) - string.count("\u202c")) * "\u202c"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = [
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileInfoForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = ProfileInfo
|
||||||
|
fields = ["tshirt_size", "date_of_birth", "address"]
|
||||||
|
widgets = {
|
||||||
|
"tshirt_size": Select2Widget(attrs={"style": "width:100%"}),
|
||||||
|
"date_of_birth": DatePickerWidget,
|
||||||
|
"address": forms.TextInput(attrs={"style": "width:100%"}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ProfileForm(ModelForm):
|
class ProfileForm(ModelForm):
|
||||||
if newsletter_id is not None:
|
|
||||||
newsletter = forms.BooleanField(label=_('Subscribe to contest updates'), initial=False, required=False)
|
|
||||||
test_site = forms.BooleanField(label=_('Enable experimental features'), initial=False, required=False)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Profile
|
model = Profile
|
||||||
fields = ['about', 'organizations', 'timezone', 'language', 'ace_theme', 'user_script']
|
fields = [
|
||||||
|
"about",
|
||||||
|
"timezone",
|
||||||
|
"language",
|
||||||
|
"ace_theme",
|
||||||
|
"profile_image",
|
||||||
|
"css_background",
|
||||||
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'user_script': AceWidget(theme='github'),
|
"timezone": Select2Widget(attrs={"style": "width:200px"}),
|
||||||
'timezone': Select2Widget(attrs={'style': 'width:200px'}),
|
"language": Select2Widget(attrs={"style": "width:200px"}),
|
||||||
'language': Select2Widget(attrs={'style': 'width:200px'}),
|
"ace_theme": Select2Widget(attrs={"style": "width:200px"}),
|
||||||
'ace_theme': Select2Widget(attrs={'style': 'width:200px'}),
|
"profile_image": ImageWidget,
|
||||||
|
"css_background": forms.TextInput(),
|
||||||
}
|
}
|
||||||
|
|
||||||
has_math_config = bool(settings.MATHOID_URL)
|
|
||||||
if has_math_config:
|
|
||||||
fields.append('math_engine')
|
|
||||||
widgets['math_engine'] = Select2Widget(attrs={'style': 'width:200px'})
|
|
||||||
|
|
||||||
if HeavyPreviewPageDownWidget is not None:
|
if HeavyPreviewPageDownWidget is not None:
|
||||||
widgets['about'] = HeavyPreviewPageDownWidget(
|
widgets["about"] = HeavyPreviewPageDownWidget(
|
||||||
preview=reverse_lazy('profile_preview'),
|
preview=reverse_lazy("profile_preview"),
|
||||||
attrs={'style': 'max-width:700px;min-width:700px;width:700px'},
|
attrs={"style": "max-width:700px;min-width:700px;width:700px"},
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
organizations = self.cleaned_data.get('organizations') or []
|
|
||||||
max_orgs = settings.DMOJ_USER_MAX_ORGANIZATION_COUNT
|
|
||||||
|
|
||||||
if sum(org.is_open for org in organizations) > max_orgs:
|
|
||||||
raise ValidationError(
|
|
||||||
_('You may not be part of more than {count} public organizations.').format(count=max_orgs))
|
|
||||||
|
|
||||||
return self.cleaned_data
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
user = kwargs.pop('user', None)
|
user = kwargs.pop("user", None)
|
||||||
super(ProfileForm, self).__init__(*args, **kwargs)
|
super(ProfileForm, self).__init__(*args, **kwargs)
|
||||||
if not user.has_perm('judge.edit_all_organization'):
|
self.fields["profile_image"].required = False
|
||||||
self.fields['organizations'].queryset = Organization.objects.filter(
|
|
||||||
Q(is_open=True) | Q(id__in=user.profile.organizations.all()),
|
def clean_profile_image(self):
|
||||||
|
profile_image = self.cleaned_data.get("profile_image")
|
||||||
|
if profile_image:
|
||||||
|
if profile_image.size > 5 * 1024 * 1024:
|
||||||
|
raise ValidationError(
|
||||||
|
_("File size exceeds the maximum allowed limit of 5MB.")
|
||||||
)
|
)
|
||||||
|
return profile_image
|
||||||
|
|
||||||
|
|
||||||
|
def file_size_validator(file):
|
||||||
|
limit = 10 * 1024 * 1024
|
||||||
|
if file.size > limit:
|
||||||
|
raise ValidationError("File too large. Size should not exceed 10MB.")
|
||||||
|
|
||||||
|
|
||||||
class ProblemSubmitForm(ModelForm):
|
class ProblemSubmitForm(ModelForm):
|
||||||
source = CharField(max_length=65536, widget=AceWidget(theme='twilight', no_ace_media=True))
|
source = CharField(
|
||||||
|
max_length=65536, widget=AceWidget(theme="twilight", no_ace_media=True)
|
||||||
|
)
|
||||||
judge = ChoiceField(choices=(), widget=forms.HiddenInput(), required=False)
|
judge = ChoiceField(choices=(), widget=forms.HiddenInput(), required=False)
|
||||||
|
source_file = FileField(required=False, validators=[file_size_validator])
|
||||||
|
|
||||||
def __init__(self, *args, judge_choices=(), **kwargs):
|
def __init__(self, *args, judge_choices=(), request=None, problem=None, **kwargs):
|
||||||
super(ProblemSubmitForm, self).__init__(*args, **kwargs)
|
super(ProblemSubmitForm, self).__init__(*args, **kwargs)
|
||||||
self.fields['language'].empty_label = None
|
self.source_file_name = None
|
||||||
self.fields['language'].label_from_instance = attrgetter('display_name')
|
self.request = request
|
||||||
self.fields['language'].queryset = Language.objects.filter(judges__online=True).distinct()
|
self.problem = problem
|
||||||
|
self.fields["language"].empty_label = None
|
||||||
|
self.fields["language"].label_from_instance = attrgetter("display_name")
|
||||||
|
self.fields["language"].queryset = Language.objects.filter(
|
||||||
|
judges__online=True
|
||||||
|
).distinct()
|
||||||
|
|
||||||
if judge_choices:
|
if judge_choices:
|
||||||
self.fields['judge'].widget = Select2Widget(
|
self.fields["judge"].widget = Select2Widget(
|
||||||
attrs={'style': 'width: 150px', 'data-placeholder': _('Any judge')},
|
attrs={"style": "width: 150px", "data-placeholder": _("Any judge")},
|
||||||
)
|
)
|
||||||
self.fields['judge'].choices = judge_choices
|
self.fields["judge"].choices = judge_choices
|
||||||
|
|
||||||
|
def allow_url_as_source(self):
|
||||||
|
key = self.cleaned_data["language"].key
|
||||||
|
filename = self.files["source_file"].name
|
||||||
|
if key == "OUTPUT" and self.problem.data_files.output_only:
|
||||||
|
return filename.endswith(".zip")
|
||||||
|
if key == "SCAT":
|
||||||
|
return filename.endswith(".sb3")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if "source_file" in self.files:
|
||||||
|
if self.allow_url_as_source():
|
||||||
|
filename = self.files["source_file"].name
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
timestamp = str(int(time.mktime(now.timetuple())))
|
||||||
|
self.source_file_name = (
|
||||||
|
timestamp + secrets.token_hex(5) + "." + filename.split(".")[-1]
|
||||||
|
)
|
||||||
|
filepath = os.path.join(
|
||||||
|
settings.DMOJ_SUBMISSION_ROOT, self.source_file_name
|
||||||
|
)
|
||||||
|
with open(filepath, "wb+") as destination:
|
||||||
|
for chunk in self.files["source_file"].chunks():
|
||||||
|
destination.write(chunk)
|
||||||
|
self.cleaned_data["source"] = self.request.build_absolute_uri(
|
||||||
|
reverse("submission_source_file", args=(self.source_file_name,))
|
||||||
|
)
|
||||||
|
del self.files["source_file"]
|
||||||
|
return self.cleaned_data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Submission
|
model = Submission
|
||||||
fields = ['language']
|
fields = ["language"]
|
||||||
|
|
||||||
|
|
||||||
class EditOrganizationForm(ModelForm):
|
class EditOrganizationForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Organization
|
model = Organization
|
||||||
fields = ['about', 'logo_override_image', 'admins']
|
fields = [
|
||||||
widgets = {'admins': Select2MultipleWidget()}
|
"name",
|
||||||
|
"slug",
|
||||||
|
"short_name",
|
||||||
|
"about",
|
||||||
|
"organization_image",
|
||||||
|
"admins",
|
||||||
|
"is_open",
|
||||||
|
]
|
||||||
|
widgets = {
|
||||||
|
"admins": Select2MultipleWidget(),
|
||||||
|
"organization_image": ImageWidget,
|
||||||
|
}
|
||||||
if HeavyPreviewPageDownWidget is not None:
|
if HeavyPreviewPageDownWidget is not None:
|
||||||
widgets['about'] = HeavyPreviewPageDownWidget(preview=reverse_lazy('organization_preview'))
|
widgets["about"] = HeavyPreviewPageDownWidget(
|
||||||
|
preview=reverse_lazy("organization_preview")
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(EditOrganizationForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields["organization_image"].required = False
|
||||||
|
|
||||||
|
def clean_organization_image(self):
|
||||||
|
organization_image = self.cleaned_data.get("organization_image")
|
||||||
|
if organization_image:
|
||||||
|
if organization_image.size > 5 * 1024 * 1024:
|
||||||
|
raise ValidationError(
|
||||||
|
_("File size exceeds the maximum allowed limit of 5MB.")
|
||||||
|
)
|
||||||
|
return organization_image
|
||||||
|
|
||||||
|
|
||||||
|
class AddOrganizationForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Organization
|
||||||
|
fields = [
|
||||||
|
"name",
|
||||||
|
"slug",
|
||||||
|
"short_name",
|
||||||
|
"about",
|
||||||
|
"organization_image",
|
||||||
|
"is_open",
|
||||||
|
]
|
||||||
|
widgets = {}
|
||||||
|
if HeavyPreviewPageDownWidget is not None:
|
||||||
|
widgets["about"] = HeavyPreviewPageDownWidget(
|
||||||
|
preview=reverse_lazy("organization_preview")
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.request = kwargs.pop("request", None)
|
||||||
|
super(AddOrganizationForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields["organization_image"].required = False
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
res = super(AddOrganizationForm, self).save(commit=False)
|
||||||
|
res.registrant = self.request.profile
|
||||||
|
if commit:
|
||||||
|
res.save()
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class AddOrganizationContestForm(ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.request = kwargs.pop("request", None)
|
||||||
|
super(AddOrganizationContestForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
contest = super(AddOrganizationContestForm, self).save(commit=False)
|
||||||
|
old_save_m2m = self.save_m2m
|
||||||
|
|
||||||
|
def save_m2m():
|
||||||
|
for i, problem in enumerate(self.cleaned_data["problems"]):
|
||||||
|
contest_problem = ContestProblem(
|
||||||
|
contest=contest, problem=problem, points=100, order=i + 1
|
||||||
|
)
|
||||||
|
contest_problem.save()
|
||||||
|
contest.contest_problems.add(contest_problem)
|
||||||
|
old_save_m2m()
|
||||||
|
|
||||||
|
self.save_m2m = save_m2m
|
||||||
|
contest.save()
|
||||||
|
self.save_m2m()
|
||||||
|
return contest
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Contest
|
||||||
|
fields = (
|
||||||
|
"key",
|
||||||
|
"name",
|
||||||
|
"start_time",
|
||||||
|
"end_time",
|
||||||
|
"problems",
|
||||||
|
)
|
||||||
|
widgets = {
|
||||||
|
"start_time": DateTimePickerWidget(),
|
||||||
|
"end_time": DateTimePickerWidget(),
|
||||||
|
"problems": HeavySelect2MultipleWidget(data_view="problem_select2"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class EditOrganizationContestForm(ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.org_id = kwargs.pop("org_id", 0)
|
||||||
|
super(EditOrganizationContestForm, self).__init__(*args, **kwargs)
|
||||||
|
for field in [
|
||||||
|
"authors",
|
||||||
|
"curators",
|
||||||
|
"testers",
|
||||||
|
"private_contestants",
|
||||||
|
"banned_users",
|
||||||
|
"view_contest_scoreboard",
|
||||||
|
]:
|
||||||
|
self.fields[field].widget.data_url = (
|
||||||
|
self.fields[field].widget.get_url() + f"?org_id={self.org_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Contest
|
||||||
|
fields = (
|
||||||
|
"is_visible",
|
||||||
|
"key",
|
||||||
|
"name",
|
||||||
|
"start_time",
|
||||||
|
"end_time",
|
||||||
|
"format_name",
|
||||||
|
"authors",
|
||||||
|
"curators",
|
||||||
|
"testers",
|
||||||
|
"time_limit",
|
||||||
|
"freeze_after",
|
||||||
|
"use_clarifications",
|
||||||
|
"hide_problem_tags",
|
||||||
|
"public_scoreboard",
|
||||||
|
"scoreboard_visibility",
|
||||||
|
"points_precision",
|
||||||
|
"rate_limit",
|
||||||
|
"description",
|
||||||
|
"og_image",
|
||||||
|
"logo_override_image",
|
||||||
|
"summary",
|
||||||
|
"access_code",
|
||||||
|
"private_contestants",
|
||||||
|
"view_contest_scoreboard",
|
||||||
|
"banned_users",
|
||||||
|
)
|
||||||
|
widgets = {
|
||||||
|
"authors": HeavySelect2MultipleWidget(data_view="profile_select2"),
|
||||||
|
"curators": HeavySelect2MultipleWidget(data_view="profile_select2"),
|
||||||
|
"testers": HeavySelect2MultipleWidget(data_view="profile_select2"),
|
||||||
|
"private_contestants": HeavySelect2MultipleWidget(
|
||||||
|
data_view="profile_select2"
|
||||||
|
),
|
||||||
|
"banned_users": HeavySelect2MultipleWidget(data_view="profile_select2"),
|
||||||
|
"view_contest_scoreboard": HeavySelect2MultipleWidget(
|
||||||
|
data_view="profile_select2"
|
||||||
|
),
|
||||||
|
"organizations": HeavySelect2MultipleWidget(
|
||||||
|
data_view="organization_select2"
|
||||||
|
),
|
||||||
|
"tags": Select2MultipleWidget,
|
||||||
|
"description": HeavyPreviewPageDownWidget(
|
||||||
|
preview=reverse_lazy("contest_preview")
|
||||||
|
),
|
||||||
|
"start_time": DateTimePickerWidget(),
|
||||||
|
"end_time": DateTimePickerWidget(),
|
||||||
|
"format_name": Select2Widget(),
|
||||||
|
"scoreboard_visibility": Select2Widget(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AddOrganizationMemberForm(ModelForm):
|
||||||
|
new_users = CharField(
|
||||||
|
max_length=65536,
|
||||||
|
widget=forms.Textarea,
|
||||||
|
help_text=_("Enter usernames separating by space"),
|
||||||
|
label=_("New users"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean_new_users(self):
|
||||||
|
new_users = self.cleaned_data.get("new_users") or ""
|
||||||
|
usernames = new_users.split()
|
||||||
|
invalid_usernames = []
|
||||||
|
valid_usernames = []
|
||||||
|
|
||||||
|
for username in usernames:
|
||||||
|
try:
|
||||||
|
valid_usernames.append(Profile.objects.get(user__username=username))
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
invalid_usernames.append(username)
|
||||||
|
|
||||||
|
if invalid_usernames:
|
||||||
|
raise ValidationError(
|
||||||
|
_("These usernames don't exist: {usernames}").format(
|
||||||
|
usernames=str(invalid_usernames)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return valid_usernames
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Organization
|
||||||
|
fields = ()
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationBlogForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = BlogPost
|
||||||
|
fields = ("title", "content", "publish_on")
|
||||||
|
widgets = {
|
||||||
|
"publish_on": forms.HiddenInput,
|
||||||
|
}
|
||||||
|
if HeavyPreviewPageDownWidget is not None:
|
||||||
|
widgets["content"] = HeavyPreviewPageDownWidget(
|
||||||
|
preview=reverse_lazy("organization_preview")
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(OrganizationBlogForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields["publish_on"].required = False
|
||||||
|
self.fields["publish_on"].is_hidden = True
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
self.cleaned_data["publish_on"] = timezone.now()
|
||||||
|
return self.cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationAdminBlogForm(OrganizationBlogForm):
|
||||||
|
class Meta:
|
||||||
|
model = BlogPost
|
||||||
|
fields = ("visible", "sticky", "title", "content", "publish_on")
|
||||||
|
widgets = {
|
||||||
|
"publish_on": forms.HiddenInput,
|
||||||
|
}
|
||||||
|
if HeavyPreviewPageDownWidget is not None:
|
||||||
|
widgets["content"] = HeavyPreviewPageDownWidget(
|
||||||
|
preview=reverse_lazy("organization_preview")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NewMessageForm(ModelForm):
|
class NewMessageForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PrivateMessage
|
model = PrivateMessage
|
||||||
fields = ['title', 'content']
|
fields = ["title", "content"]
|
||||||
widgets = {}
|
widgets = {}
|
||||||
if PagedownWidget is not None:
|
if PagedownWidget is not None:
|
||||||
widgets['content'] = MathJaxPagedownWidget()
|
widgets["content"] = PagedownWidget()
|
||||||
|
|
||||||
|
|
||||||
class CustomAuthenticationForm(AuthenticationForm):
|
class CustomAuthenticationForm(AuthenticationForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(CustomAuthenticationForm, self).__init__(*args, **kwargs)
|
super(CustomAuthenticationForm, self).__init__(*args, **kwargs)
|
||||||
self.fields['username'].widget.attrs.update({'placeholder': _('Username')})
|
self.fields["username"].widget.attrs.update(
|
||||||
self.fields['password'].widget.attrs.update({'placeholder': _('Password')})
|
{"placeholder": _("Username/Email")}
|
||||||
|
)
|
||||||
|
self.fields["password"].widget.attrs.update({"placeholder": _("Password")})
|
||||||
|
|
||||||
self.has_google_auth = self._has_social_auth('GOOGLE_OAUTH2')
|
self.has_google_auth = self._has_social_auth("GOOGLE_OAUTH2")
|
||||||
self.has_facebook_auth = self._has_social_auth('FACEBOOK')
|
self.has_facebook_auth = self._has_social_auth("FACEBOOK")
|
||||||
self.has_github_auth = self._has_social_auth('GITHUB_SECURE')
|
self.has_github_auth = self._has_social_auth("GITHUB_SECURE")
|
||||||
|
|
||||||
def _has_social_auth(self, key):
|
def _has_social_auth(self, key):
|
||||||
return (getattr(settings, 'SOCIAL_AUTH_%s_KEY' % key, None) and
|
return getattr(settings, "SOCIAL_AUTH_%s_KEY" % key, None) and getattr(
|
||||||
getattr(settings, 'SOCIAL_AUTH_%s_SECRET' % key, None))
|
settings, "SOCIAL_AUTH_%s_SECRET" % key, None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NoAutoCompleteCharField(forms.CharField):
|
class NoAutoCompleteCharField(forms.CharField):
|
||||||
def widget_attrs(self, widget):
|
def widget_attrs(self, widget):
|
||||||
attrs = super(NoAutoCompleteCharField, self).widget_attrs(widget)
|
attrs = super(NoAutoCompleteCharField, self).widget_attrs(widget)
|
||||||
attrs['autocomplete'] = 'off'
|
attrs["autocomplete"] = "off"
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class TOTPForm(Form):
|
class TOTPForm(Form):
|
||||||
TOLERANCE = settings.DMOJ_TOTP_TOLERANCE_HALF_MINUTES
|
TOLERANCE = settings.DMOJ_TOTP_TOLERANCE_HALF_MINUTES
|
||||||
|
|
||||||
totp_token = NoAutoCompleteCharField(validators=[
|
totp_token = NoAutoCompleteCharField(
|
||||||
RegexValidator('^[0-9]{6}$', _('Two Factor Authentication tokens must be 6 decimal digits.')),
|
validators=[
|
||||||
])
|
RegexValidator(
|
||||||
|
"^[0-9]{6}$",
|
||||||
|
_("Two Factor Authentication tokens must be 6 decimal digits."),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.totp_key = kwargs.pop('totp_key')
|
self.totp_key = kwargs.pop("totp_key")
|
||||||
super(TOTPForm, self).__init__(*args, **kwargs)
|
super(TOTPForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def clean_totp_token(self):
|
def clean_totp_token(self):
|
||||||
if not pyotp.TOTP(self.totp_key).verify(self.cleaned_data['totp_token'], valid_window=self.TOLERANCE):
|
if not pyotp.TOTP(self.totp_key).verify(
|
||||||
raise ValidationError(_('Invalid Two Factor Authentication token.'))
|
self.cleaned_data["totp_token"], valid_window=self.TOLERANCE
|
||||||
|
):
|
||||||
|
raise ValidationError(_("Invalid Two Factor Authentication token."))
|
||||||
|
|
||||||
|
|
||||||
class ProblemCloneForm(Form):
|
class ProblemCloneForm(Form):
|
||||||
code = CharField(max_length=20, validators=[RegexValidator('^[a-z0-9]+$', _('Problem code must be ^[a-z0-9]+$'))])
|
code = CharField(
|
||||||
|
max_length=20,
|
||||||
|
validators=[
|
||||||
|
RegexValidator("^[a-z0-9]+$", _("Problem code must be ^[a-z0-9]+$"))
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def clean_code(self):
|
def clean_code(self):
|
||||||
code = self.cleaned_data['code']
|
code = self.cleaned_data["code"]
|
||||||
if Problem.objects.filter(code=code).exists():
|
if Problem.objects.filter(code=code).exists():
|
||||||
raise ValidationError(_('Problem with code already exists.'))
|
raise ValidationError(_("Problem with code already exists."))
|
||||||
return code
|
return code
|
||||||
|
|
||||||
|
|
||||||
class ContestCloneForm(Form):
|
class ContestCloneForm(Form):
|
||||||
key = CharField(max_length=20, validators=[RegexValidator('^[a-z0-9]+$', _('Contest id must be ^[a-z0-9]+$'))])
|
key = CharField(
|
||||||
|
max_length=20,
|
||||||
|
validators=[RegexValidator("^[a-z0-9]+$", _("Contest id must be ^[a-z0-9]+$"))],
|
||||||
|
)
|
||||||
|
organization = ChoiceField(choices=(), required=True)
|
||||||
|
|
||||||
|
def __init__(self, *args, org_choices=(), profile=None, **kwargs):
|
||||||
|
super(ContestCloneForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields["organization"].widget = Select2Widget(
|
||||||
|
attrs={"style": "width: 100%", "data-placeholder": _("Group")},
|
||||||
|
)
|
||||||
|
self.fields["organization"].choices = org_choices
|
||||||
|
self.profile = profile
|
||||||
|
|
||||||
def clean_key(self):
|
def clean_key(self):
|
||||||
key = self.cleaned_data['key']
|
key = self.cleaned_data["key"]
|
||||||
if Contest.objects.filter(key=key).exists():
|
if Contest.objects.filter(key=key).exists():
|
||||||
raise ValidationError(_('Contest with key already exists.'))
|
raise ValidationError(_("Contest with key already exists."))
|
||||||
return key
|
return key
|
||||||
|
|
||||||
|
def clean_organization(self):
|
||||||
|
organization_id = self.cleaned_data["organization"]
|
||||||
|
try:
|
||||||
|
organization = Organization.objects.get(id=organization_id)
|
||||||
|
except Exception:
|
||||||
|
raise ValidationError(_("Group doesn't exist."))
|
||||||
|
if not organization.admins.filter(id=self.profile.id).exists():
|
||||||
|
raise ValidationError(_("You don't have permission in this group."))
|
||||||
|
return organization
|
||||||
|
|
||||||
|
|
||||||
|
class ProblemPointsVoteForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = ProblemPointsVote
|
||||||
|
fields = ["points"]
|
||||||
|
|
||||||
|
|
||||||
|
class ContestProblemForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = ContestProblem
|
||||||
|
fields = (
|
||||||
|
"order",
|
||||||
|
"problem",
|
||||||
|
"points",
|
||||||
|
"partial",
|
||||||
|
"show_testcases",
|
||||||
|
"max_submissions",
|
||||||
|
)
|
||||||
|
widgets = {
|
||||||
|
"problem": HeavySelect2Widget(
|
||||||
|
data_view="problem_select2", attrs={"style": "width: 100%"}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ContestProblemModelFormSet(BaseModelFormSet):
|
||||||
|
def is_valid(self):
|
||||||
|
valid = super().is_valid()
|
||||||
|
|
||||||
|
if not valid:
|
||||||
|
return valid
|
||||||
|
|
||||||
|
problems = set()
|
||||||
|
duplicates = []
|
||||||
|
|
||||||
|
for form in self.forms:
|
||||||
|
if form.cleaned_data and not form.cleaned_data.get("DELETE", False):
|
||||||
|
problem = form.cleaned_data.get("problem")
|
||||||
|
if problem in problems:
|
||||||
|
duplicates.append(problem)
|
||||||
|
else:
|
||||||
|
problems.add(problem)
|
||||||
|
|
||||||
|
if duplicates:
|
||||||
|
for form in self.forms:
|
||||||
|
problem = form.cleaned_data.get("problem")
|
||||||
|
if problem in duplicates:
|
||||||
|
form.add_error("problem", _("This problem is duplicated."))
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class ContestProblemFormSet(
|
||||||
|
formset_factory(
|
||||||
|
ContestProblemForm, formset=ContestProblemModelFormSet, extra=6, can_delete=True
|
||||||
|
)
|
||||||
|
):
|
||||||
|
model = ContestProblem
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormatterForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = TestFormatterModel
|
||||||
|
fields = ["file"]
|
||||||
|
|
|
@ -5,10 +5,10 @@ from django.db.models.query import QuerySet
|
||||||
|
|
||||||
|
|
||||||
class SearchQuerySet(QuerySet):
|
class SearchQuerySet(QuerySet):
|
||||||
DEFAULT = ''
|
DEFAULT = ""
|
||||||
BOOLEAN = ' IN BOOLEAN MODE'
|
BOOLEAN = " IN BOOLEAN MODE"
|
||||||
NATURAL_LANGUAGE = ' IN NATURAL LANGUAGE MODE'
|
NATURAL_LANGUAGE = " IN NATURAL LANGUAGE MODE"
|
||||||
QUERY_EXPANSION = ' WITH QUERY EXPANSION'
|
QUERY_EXPANSION = " WITH QUERY EXPANSION"
|
||||||
|
|
||||||
def __init__(self, fields=None, **kwargs):
|
def __init__(self, fields=None, **kwargs):
|
||||||
super(SearchQuerySet, self).__init__(**kwargs)
|
super(SearchQuerySet, self).__init__(**kwargs)
|
||||||
|
@ -25,20 +25,26 @@ class SearchQuerySet(QuerySet):
|
||||||
# Get the table name and column names from the model
|
# Get the table name and column names from the model
|
||||||
# in `table_name`.`column_name` style
|
# in `table_name`.`column_name` style
|
||||||
columns = [meta.get_field(name).column for name in self._search_fields]
|
columns = [meta.get_field(name).column for name in self._search_fields]
|
||||||
full_names = ['%s.%s' %
|
full_names = [
|
||||||
(connection.ops.quote_name(meta.db_table),
|
"%s.%s"
|
||||||
connection.ops.quote_name(column))
|
% (
|
||||||
for column in columns]
|
connection.ops.quote_name(meta.db_table),
|
||||||
|
connection.ops.quote_name(column),
|
||||||
|
)
|
||||||
|
for column in columns
|
||||||
|
]
|
||||||
|
|
||||||
# Create the MATCH...AGAINST expressions
|
# Create the MATCH...AGAINST expressions
|
||||||
fulltext_columns = ', '.join(full_names)
|
fulltext_columns = ", ".join(full_names)
|
||||||
match_expr = ('MATCH(%s) AGAINST (%%s%s)' % (fulltext_columns, mode))
|
match_expr = "MATCH(%s) AGAINST (%%s%s)" % (fulltext_columns, mode)
|
||||||
|
|
||||||
# Add the extra SELECT and WHERE options
|
# Add the extra SELECT and WHERE options
|
||||||
return self.extra(select={'relevance': match_expr},
|
return self.extra(
|
||||||
|
select={"relevance": match_expr},
|
||||||
select_params=[query],
|
select_params=[query],
|
||||||
where=[match_expr],
|
where=[match_expr],
|
||||||
params=[query])
|
params=[query],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SearchManager(models.Manager):
|
class SearchManager(models.Manager):
|
||||||
|
|
|
@ -1,38 +1,13 @@
|
||||||
from django.utils.html import escape, mark_safe
|
from django.utils.html import escape, mark_safe
|
||||||
|
from judge.markdown import markdown
|
||||||
|
|
||||||
__all__ = ['highlight_code']
|
__all__ = ["highlight_code"]
|
||||||
|
|
||||||
|
|
||||||
def _make_pre_code(code):
|
def highlight_code(code, language, linenos=True, title=None):
|
||||||
return mark_safe('<pre>' + escape(code) + '</pre>')
|
linenos_option = 'linenums="1"' if linenos else ""
|
||||||
|
title_option = f'title="{title}"' if title else ""
|
||||||
|
options = f"{{.{language} {linenos_option} {title_option}}}"
|
||||||
|
|
||||||
|
value = f"```{options}\n{code}\n```\n"
|
||||||
def _wrap_code(inner):
|
return mark_safe(markdown(value))
|
||||||
yield 0, "<code>"
|
|
||||||
for tup in inner:
|
|
||||||
yield tup
|
|
||||||
yield 0, "</code>"
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
import pygments
|
|
||||||
import pygments.lexers
|
|
||||||
import pygments.formatters.html
|
|
||||||
import pygments.util
|
|
||||||
except ImportError:
|
|
||||||
def highlight_code(code, language, cssclass=None):
|
|
||||||
return _make_pre_code(code)
|
|
||||||
else:
|
|
||||||
class HtmlCodeFormatter(pygments.formatters.HtmlFormatter):
|
|
||||||
def wrap(self, source, outfile):
|
|
||||||
return self._wrap_div(self._wrap_pre(_wrap_code(source)))
|
|
||||||
|
|
||||||
def highlight_code(code, language, cssclass='codehilite', linenos=True):
|
|
||||||
try:
|
|
||||||
lexer = pygments.lexers.get_lexer_by_name(language)
|
|
||||||
except pygments.util.ClassNotFound:
|
|
||||||
return _make_pre_code(code)
|
|
||||||
|
|
||||||
if linenos:
|
|
||||||
return mark_safe(pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass, linenos='table')))
|
|
||||||
return mark_safe(pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass)))
|
|
||||||
|
|
|
@ -8,19 +8,33 @@ from statici18n.templatetags.statici18n import inlinei18n
|
||||||
|
|
||||||
from judge.highlight_code import highlight_code
|
from judge.highlight_code import highlight_code
|
||||||
from judge.user_translations import gettext
|
from judge.user_translations import gettext
|
||||||
from . import (camo, chat, datetime, filesize, gravatar, language, markdown, rating, reference, render, social,
|
from . import (
|
||||||
spaceless, submission, timedelta)
|
camo,
|
||||||
|
chat,
|
||||||
|
datetime,
|
||||||
|
filesize,
|
||||||
|
gravatar,
|
||||||
|
language,
|
||||||
|
markdown,
|
||||||
|
rating,
|
||||||
|
reference,
|
||||||
|
render,
|
||||||
|
social,
|
||||||
|
spaceless,
|
||||||
|
timedelta,
|
||||||
|
comment,
|
||||||
|
)
|
||||||
from . import registry
|
from . import registry
|
||||||
|
|
||||||
registry.function('str', str)
|
registry.function("str", str)
|
||||||
registry.filter('str', str)
|
registry.filter("str", str)
|
||||||
registry.filter('json', json.dumps)
|
registry.filter("json", json.dumps)
|
||||||
registry.filter('highlight', highlight_code)
|
registry.filter("highlight", highlight_code)
|
||||||
registry.filter('urlquote', urlquote)
|
registry.filter("urlquote", urlquote)
|
||||||
registry.filter('roundfloat', round)
|
registry.filter("roundfloat", round)
|
||||||
registry.function('inlinei18n', inlinei18n)
|
registry.function("inlinei18n", inlinei18n)
|
||||||
registry.function('mptt_tree', get_cached_trees)
|
registry.function("mptt_tree", get_cached_trees)
|
||||||
registry.function('user_trans', gettext)
|
registry.function("user_trans", gettext)
|
||||||
|
|
||||||
|
|
||||||
@registry.function
|
@registry.function
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from judge.utils.camo import client as camo_client
|
from judge.utils.camo import client as camo_client
|
||||||
from . import registry
|
from . import registry
|
||||||
|
|
||||||
|
|
||||||
@registry.filter
|
@registry.filter
|
||||||
def camo(url):
|
def camo(url):
|
||||||
if camo_client is None:
|
if camo_client is None:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from . import registry
|
from . import registry
|
||||||
from chat_box.utils import encrypt_url
|
from chat_box.utils import encrypt_url
|
||||||
|
|
||||||
|
|
||||||
@registry.function
|
@registry.function
|
||||||
def chat_param(request_profile, profile):
|
def chat_param(request_profile, profile):
|
||||||
return encrypt_url(request_profile.id, profile.id)
|
return encrypt_url(request_profile.id, profile.id)
|
12
judge/jinja2/comment.py
Normal file
12
judge/jinja2/comment.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from . import registry
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
from judge.models.comment import get_visible_comment_count
|
||||||
|
from judge.caching import cache_wrapper
|
||||||
|
|
||||||
|
|
||||||
|
@registry.function
|
||||||
|
def comment_count(obj):
|
||||||
|
content_type = ContentType.objects.get_for_model(obj)
|
||||||
|
return get_visible_comment_count(content_type, obj.pk)
|
|
@ -10,7 +10,7 @@ from . import registry
|
||||||
def localtime_wrapper(func):
|
def localtime_wrapper(func):
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def wrapper(datetime, *args, **kwargs):
|
def wrapper(datetime, *args, **kwargs):
|
||||||
if getattr(datetime, 'convert_to_local_time', True):
|
if getattr(datetime, "convert_to_local_time", True):
|
||||||
datetime = localtime(datetime)
|
datetime = localtime(datetime)
|
||||||
return func(datetime, *args, **kwargs)
|
return func(datetime, *args, **kwargs)
|
||||||
|
|
||||||
|
@ -22,6 +22,6 @@ registry.filter(localtime_wrapper(time))
|
||||||
|
|
||||||
|
|
||||||
@registry.function
|
@registry.function
|
||||||
@registry.render_with('widgets/relative-time.html')
|
@registry.render_with("widgets/relative-time.html")
|
||||||
def relative_time(time, format=_('N j, Y, g:i a'), rel=_('{time}'), abs=_('on {time}')):
|
def relative_time(time, format=_("N j, Y, g:i a"), rel=_("{time}"), abs=_("{time}")):
|
||||||
return {'time': time, 'format': format, 'rel_format': rel, 'abs_format': abs}
|
return {"time": time, "format": format, "rel_format": rel, "abs_format": abs}
|
||||||
|
|
|
@ -13,24 +13,28 @@ def _format_size(bytes, callback):
|
||||||
PB = 1 << 50
|
PB = 1 << 50
|
||||||
|
|
||||||
if bytes < KB:
|
if bytes < KB:
|
||||||
return callback('', bytes)
|
return callback("", bytes)
|
||||||
elif bytes < MB:
|
elif bytes < MB:
|
||||||
return callback('K', bytes / KB)
|
return callback("K", bytes / KB)
|
||||||
elif bytes < GB:
|
elif bytes < GB:
|
||||||
return callback('M', bytes / MB)
|
return callback("M", bytes / MB)
|
||||||
elif bytes < TB:
|
elif bytes < TB:
|
||||||
return callback('G', bytes / GB)
|
return callback("G", bytes / GB)
|
||||||
elif bytes < PB:
|
elif bytes < PB:
|
||||||
return callback('T', bytes / TB)
|
return callback("T", bytes / TB)
|
||||||
else:
|
else:
|
||||||
return callback('P', bytes / PB)
|
return callback("P", bytes / PB)
|
||||||
|
|
||||||
|
|
||||||
@registry.filter
|
@registry.filter
|
||||||
def kbdetailformat(bytes):
|
def kbdetailformat(bytes):
|
||||||
return avoid_wrapping(_format_size(bytes * 1024, lambda x, y: ['%d %sB', '%.2f %sB'][bool(x)] % (y, x)))
|
return avoid_wrapping(
|
||||||
|
_format_size(
|
||||||
|
bytes * 1024, lambda x, y: ["%d %sB", "%.2f %sB"][bool(x)] % (y, x)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@registry.filter
|
@registry.filter
|
||||||
def kbsimpleformat(kb):
|
def kbsimpleformat(kb):
|
||||||
return _format_size(kb * 1024, lambda x, y: '%.0f%s' % (y, x or 'B'))
|
return _format_size(kb * 1024, lambda x, y: "%.0f%s" % (y, x or "B"))
|
||||||
|
|
|
@ -9,17 +9,23 @@ from . import registry
|
||||||
|
|
||||||
|
|
||||||
@registry.function
|
@registry.function
|
||||||
def gravatar(email, size=80, default=None):
|
def gravatar(profile, size=80, default=None, profile_image=None, email=None):
|
||||||
if isinstance(email, Profile):
|
if profile and not profile.is_muted:
|
||||||
|
if profile_image:
|
||||||
|
return profile_image
|
||||||
|
if profile and profile.profile_image_url:
|
||||||
|
return profile.profile_image_url
|
||||||
|
if profile:
|
||||||
|
email = email or profile.email
|
||||||
if default is None:
|
if default is None:
|
||||||
default = email.mute
|
default = profile.is_muted
|
||||||
email = email.user.email
|
gravatar_url = (
|
||||||
elif isinstance(email, AbstractUser):
|
"//www.gravatar.com/avatar/"
|
||||||
email = email.email
|
+ hashlib.md5(utf8bytes(email.strip().lower())).hexdigest()
|
||||||
|
+ "?"
|
||||||
gravatar_url = '//www.gravatar.com/avatar/' + hashlib.md5(utf8bytes(email.strip().lower())).hexdigest() + '?'
|
)
|
||||||
args = {'d': 'identicon', 's': str(size)}
|
args = {"d": "identicon", "s": str(size)}
|
||||||
if default:
|
if default:
|
||||||
args['f'] = 'y'
|
args["f"] = "y"
|
||||||
gravatar_url += urlencode(args)
|
gravatar_url += urlencode(args)
|
||||||
return gravatar_url
|
return gravatar_url
|
||||||
|
|
|
@ -3,7 +3,7 @@ from django.utils import translation
|
||||||
from . import registry
|
from . import registry
|
||||||
|
|
||||||
|
|
||||||
@registry.function('language_info')
|
@registry.function("language_info")
|
||||||
def get_language_info(language):
|
def get_language_info(language):
|
||||||
# ``language`` is either a language code string or a sequence
|
# ``language`` is either a language code string or a sequence
|
||||||
# with the language code as its first item
|
# with the language code as its first item
|
||||||
|
@ -13,6 +13,6 @@ def get_language_info(language):
|
||||||
return translation.get_language_info(str(language))
|
return translation.get_language_info(str(language))
|
||||||
|
|
||||||
|
|
||||||
@registry.function('language_info_list')
|
@registry.function("language_info_list")
|
||||||
def get_language_info_list(langs):
|
def get_language_info_list(langs):
|
||||||
return [get_language_info(lang) for lang in langs]
|
return [get_language_info(lang) for lang in langs]
|
||||||
|
|
|
@ -1,142 +1,7 @@
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
from html.parser import HTMLParser
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
import mistune
|
|
||||||
from django.conf import settings
|
|
||||||
from jinja2 import Markup
|
|
||||||
from lxml import html
|
|
||||||
from lxml.etree import ParserError, XMLSyntaxError
|
|
||||||
|
|
||||||
from judge.highlight_code import highlight_code
|
|
||||||
from judge.jinja2.markdown.lazy_load import lazy_load as lazy_load_processor
|
|
||||||
from judge.jinja2.markdown.math import MathInlineGrammar, MathInlineLexer, MathRenderer
|
|
||||||
from judge.jinja2.markdown.spoiler import SpoilerInlineGrammar, SpoilerInlineLexer, SpoilerRenderer
|
|
||||||
from judge.utils.camo import client as camo_client
|
|
||||||
from judge.utils.texoid import TEXOID_ENABLED, TexoidRenderer
|
|
||||||
from .. import registry
|
from .. import registry
|
||||||
|
from judge.markdown import markdown as _markdown
|
||||||
logger = logging.getLogger('judge.html')
|
|
||||||
|
|
||||||
NOFOLLOW_WHITELIST = settings.NOFOLLOW_EXCLUDED
|
|
||||||
|
|
||||||
|
|
||||||
class CodeSafeInlineGrammar(mistune.InlineGrammar):
|
|
||||||
double_emphasis = re.compile(r'^\*{2}([\s\S]+?)()\*{2}(?!\*)') # **word**
|
|
||||||
emphasis = re.compile(r'^\*((?:\*\*|[^\*])+?)()\*(?!\*)') # *word*
|
|
||||||
|
|
||||||
|
|
||||||
class AwesomeInlineGrammar(MathInlineGrammar, SpoilerInlineGrammar, CodeSafeInlineGrammar):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AwesomeInlineLexer(MathInlineLexer, SpoilerInlineLexer, mistune.InlineLexer):
|
|
||||||
grammar_class = AwesomeInlineGrammar
|
|
||||||
|
|
||||||
|
|
||||||
class AwesomeRenderer(MathRenderer, SpoilerRenderer, mistune.Renderer):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.nofollow = kwargs.pop('nofollow', True)
|
|
||||||
self.texoid = TexoidRenderer() if kwargs.pop('texoid', False) else None
|
|
||||||
self.parser = HTMLParser()
|
|
||||||
super(AwesomeRenderer, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def _link_rel(self, href):
|
|
||||||
if href:
|
|
||||||
try:
|
|
||||||
url = urlparse(href)
|
|
||||||
except ValueError:
|
|
||||||
return ' rel="nofollow"'
|
|
||||||
else:
|
|
||||||
if url.netloc and url.netloc not in NOFOLLOW_WHITELIST:
|
|
||||||
return ' rel="nofollow"'
|
|
||||||
return ''
|
|
||||||
|
|
||||||
def autolink(self, link, is_email=False):
|
|
||||||
text = link = mistune.escape(link)
|
|
||||||
if is_email:
|
|
||||||
link = 'mailto:%s' % link
|
|
||||||
return '<a href="%s"%s>%s</a>' % (link, self._link_rel(link), text)
|
|
||||||
|
|
||||||
def table(self, header, body):
|
|
||||||
return (
|
|
||||||
'<table class="table">\n<thead>%s</thead>\n'
|
|
||||||
'<tbody>\n%s</tbody>\n</table>\n'
|
|
||||||
) % (header, body)
|
|
||||||
|
|
||||||
def link(self, link, title, text):
|
|
||||||
link = mistune.escape_link(link)
|
|
||||||
if not title:
|
|
||||||
return '<a href="%s"%s>%s</a>' % (link, self._link_rel(link), text)
|
|
||||||
title = mistune.escape(title, quote=True)
|
|
||||||
return '<a href="%s" title="%s"%s>%s</a>' % (link, title, self._link_rel(link), text)
|
|
||||||
|
|
||||||
def block_code(self, code, lang=None):
|
|
||||||
if not lang:
|
|
||||||
return '\n<pre><code>%s</code></pre>\n' % mistune.escape(code).rstrip()
|
|
||||||
return highlight_code(code, lang)
|
|
||||||
|
|
||||||
def block_html(self, html):
|
|
||||||
if self.texoid and html.startswith('<latex'):
|
|
||||||
attr = html[6:html.index('>')]
|
|
||||||
latex = html[html.index('>') + 1:html.rindex('<')]
|
|
||||||
latex = self.parser.unescape(latex)
|
|
||||||
result = self.texoid.get_result(latex)
|
|
||||||
if not result:
|
|
||||||
return '<pre>%s</pre>' % mistune.escape(latex, smart_amp=False)
|
|
||||||
elif 'error' not in result:
|
|
||||||
img = ('''<img src="%(svg)s" onerror="this.src='%(png)s';this.onerror=null"'''
|
|
||||||
'width="%(width)s" height="%(height)s"%(tail)s>') % {
|
|
||||||
'svg': result['svg'], 'png': result['png'],
|
|
||||||
'width': result['meta']['width'], 'height': result['meta']['height'],
|
|
||||||
'tail': ' /' if self.options.get('use_xhtml') else '',
|
|
||||||
}
|
|
||||||
style = ['max-width: 100%',
|
|
||||||
'height: %s' % result['meta']['height'],
|
|
||||||
'max-height: %s' % result['meta']['height'],
|
|
||||||
'width: %s' % result['meta']['height']]
|
|
||||||
if 'inline' in attr:
|
|
||||||
tag = 'span'
|
|
||||||
else:
|
|
||||||
tag = 'div'
|
|
||||||
style += ['text-align: center']
|
|
||||||
return '<%s style="%s">%s</%s>' % (tag, ';'.join(style), img, tag)
|
|
||||||
else:
|
|
||||||
return '<pre>%s</pre>' % mistune.escape(result['error'], smart_amp=False)
|
|
||||||
return super(AwesomeRenderer, self).block_html(html)
|
|
||||||
|
|
||||||
def header(self, text, level, *args, **kwargs):
|
|
||||||
return super(AwesomeRenderer, self).header(text, level + 2, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
@registry.filter
|
@registry.filter
|
||||||
def markdown(value, style, math_engine=None, lazy_load=False):
|
def markdown(value, lazy_load=False):
|
||||||
styles = settings.MARKDOWN_STYLES.get(style, settings.MARKDOWN_DEFAULT_STYLE)
|
return _markdown(value, lazy_load)
|
||||||
escape = styles.get('safe_mode', True)
|
|
||||||
nofollow = styles.get('nofollow', True)
|
|
||||||
texoid = TEXOID_ENABLED and styles.get('texoid', False)
|
|
||||||
math = hasattr(settings, 'MATHOID_URL') and styles.get('math', False)
|
|
||||||
|
|
||||||
post_processors = []
|
|
||||||
if styles.get('use_camo', False) and camo_client is not None:
|
|
||||||
post_processors.append(camo_client.update_tree)
|
|
||||||
if lazy_load:
|
|
||||||
post_processors.append(lazy_load_processor)
|
|
||||||
|
|
||||||
renderer = AwesomeRenderer(escape=escape, nofollow=nofollow, texoid=texoid,
|
|
||||||
math=math and math_engine is not None, math_engine=math_engine)
|
|
||||||
markdown = mistune.Markdown(renderer=renderer, inline=AwesomeInlineLexer,
|
|
||||||
parse_block_html=1, parse_inline_html=1)
|
|
||||||
result = markdown(value)
|
|
||||||
if post_processors:
|
|
||||||
try:
|
|
||||||
tree = html.fromstring(result, parser=html.HTMLParser(recover=True))
|
|
||||||
except (XMLSyntaxError, ParserError) as e:
|
|
||||||
if result and (not isinstance(e, ParserError) or e.args[0] != 'Document is empty'):
|
|
||||||
logger.exception('Failed to parse HTML string')
|
|
||||||
tree = html.Element('div')
|
|
||||||
for processor in post_processors:
|
|
||||||
processor(tree)
|
|
||||||
result = html.tostring(tree, encoding='unicode')
|
|
||||||
return Markup(result)
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
from django.templatetags.static import static
|
|
||||||
from lxml import html
|
|
||||||
|
|
||||||
|
|
||||||
def lazy_load(tree):
|
|
||||||
blank = static('blank.gif')
|
|
||||||
for img in tree.xpath('.//img'):
|
|
||||||
src = img.get('src', '')
|
|
||||||
if src.startswith('data') or '-math' in img.get('class', ''):
|
|
||||||
continue
|
|
||||||
noscript = html.Element('noscript')
|
|
||||||
copy = deepcopy(img)
|
|
||||||
copy.tail = ''
|
|
||||||
noscript.append(copy)
|
|
||||||
img.addprevious(noscript)
|
|
||||||
img.set('data-src', src)
|
|
||||||
img.set('src', blank)
|
|
||||||
img.set('class', img.get('class') + ' unveil' if img.get('class') else 'unveil')
|
|
|
@ -1,67 +0,0 @@
|
||||||
import re
|
|
||||||
|
|
||||||
import mistune
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from judge.utils.mathoid import MathoidMathParser
|
|
||||||
|
|
||||||
mistune._pre_tags.append('latex')
|
|
||||||
|
|
||||||
|
|
||||||
class MathInlineGrammar(mistune.InlineGrammar):
|
|
||||||
block_math = re.compile(r'^\$\$(.*?)\$\$|^\\\[(.*?)\\\]', re.DOTALL)
|
|
||||||
math = re.compile(r'^~(.*?)~|^\\\((.*?)\\\)', re.DOTALL)
|
|
||||||
text = re.compile(r'^[\s\S]+?(?=[\\<!\[_*`~$]|\\[\[(]|https?://| {2,}\n|$)')
|
|
||||||
|
|
||||||
|
|
||||||
class MathInlineLexer(mistune.InlineLexer):
|
|
||||||
grammar_class = MathInlineGrammar
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.default_rules = self.default_rules[:]
|
|
||||||
self.inline_html_rules = self.default_rules
|
|
||||||
self.default_rules.insert(self.default_rules.index('strikethrough') + 1, 'math')
|
|
||||||
self.default_rules.insert(self.default_rules.index('strikethrough') + 1, 'block_math')
|
|
||||||
super(MathInlineLexer, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def output_block_math(self, m):
|
|
||||||
return self.renderer.block_math(m.group(1) or m.group(2))
|
|
||||||
|
|
||||||
def output_math(self, m):
|
|
||||||
return self.renderer.math(m.group(1) or m.group(2))
|
|
||||||
|
|
||||||
def output_inline_html(self, m):
|
|
||||||
tag = m.group(1)
|
|
||||||
text = m.group(3)
|
|
||||||
if self._parse_inline_html and text:
|
|
||||||
if tag == 'a':
|
|
||||||
self._in_link = True
|
|
||||||
text = self.output(text)
|
|
||||||
self._in_link = False
|
|
||||||
else:
|
|
||||||
text = self.output(text)
|
|
||||||
extra = m.group(2) or ''
|
|
||||||
html = '<%s%s>%s</%s>' % (tag, extra, text, tag)
|
|
||||||
else:
|
|
||||||
html = m.group(0)
|
|
||||||
return self.renderer.inline_html(html)
|
|
||||||
|
|
||||||
|
|
||||||
class MathRenderer(mistune.Renderer):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
if kwargs.pop('math', False) and settings.MATHOID_URL != False:
|
|
||||||
self.mathoid = MathoidMathParser(kwargs.pop('math_engine', None) or 'svg')
|
|
||||||
else:
|
|
||||||
self.mathoid = None
|
|
||||||
super(MathRenderer, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def block_math(self, math):
|
|
||||||
if self.mathoid is None or not math:
|
|
||||||
return r'\[%s\]' % mistune.escape(str(math))
|
|
||||||
return self.mathoid.display_math(math)
|
|
||||||
|
|
||||||
def math(self, math):
|
|
||||||
if self.mathoid is None or not math:
|
|
||||||
return r'\(%s\)' % mistune.escape(str(math))
|
|
||||||
return self.mathoid.inline_math(math)
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue