Compare commits
389 commits
revert-68-
...
master
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 |
2483 changed files with 39714 additions and 17401 deletions
|
@ -11,3 +11,7 @@ repos:
|
||||||
rev: 22.12.0
|
rev: 22.12.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- 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>
|
||||||
|
|
191
README.md
191
README.md
|
@ -31,6 +31,197 @@ 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`.
|
||||||
|
|
||||||
|
|
||||||
|
- Bước 1: cài các thư viện cần thiết
|
||||||
|
- $ ở đâ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:
|
||||||
|
|
||||||
1. Missing the `local_settings.py`. You need to copy the `local_settings.py` in order to pass the check.
|
1. Missing the `local_settings.py`. You need to copy the `local_settings.py` in order to pass the check.
|
||||||
|
|
|
@ -3,3 +3,6 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
class ChatBoxConfig(AppConfig):
|
class ChatBoxConfig(AppConfig):
|
||||||
name = "chat_box"
|
name = "chat_box"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from . import models
|
||||||
|
|
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,9 +1,11 @@
|
||||||
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", "Room", "UserRoom", "Ignore"]
|
__all__ = ["Message", "Room", "UserRoom", "Ignore"]
|
||||||
|
@ -16,20 +18,44 @@ class Room(models.Model):
|
||||||
user_two = models.ForeignKey(
|
user_two = models.ForeignKey(
|
||||||
Profile, related_name="user_two", verbose_name="user 2", on_delete=CASCADE
|
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(
|
||||||
|
verbose_name=_("posted time"), auto_now_add=True, db_index=True
|
||||||
|
)
|
||||||
body = models.TextField(verbose_name=_("body of comment"), max_length=8192)
|
body = models.TextField(verbose_name=_("body of comment"), max_length=8192)
|
||||||
hidden = models.BooleanField(verbose_name="is hidden", default=False)
|
hidden = models.BooleanField(verbose_name="is hidden", default=False)
|
||||||
room = models.ForeignKey(
|
room = models.ForeignKey(
|
||||||
|
@ -37,7 +63,6 @@ class Message(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
@ -48,6 +73,7 @@ class Message(models.Model):
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["hidden", "room", "-id"]),
|
models.Index(fields=["hidden", "room", "-id"]),
|
||||||
]
|
]
|
||||||
|
app_label = "chat_box"
|
||||||
|
|
||||||
|
|
||||||
class UserRoom(models.Model):
|
class UserRoom(models.Model):
|
||||||
|
@ -56,9 +82,11 @@ class UserRoom(models.Model):
|
||||||
Room, verbose_name="room id", on_delete=CASCADE, default=None, null=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)
|
last_seen = models.DateTimeField(verbose_name=_("last seen"), auto_now_add=True)
|
||||||
|
unread_count = models.IntegerField(default=0, db_index=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ("user", "room")
|
unique_together = ("user", "room")
|
||||||
|
app_label = "chat_box"
|
||||||
|
|
||||||
|
|
||||||
class Ignore(models.Model):
|
class Ignore(models.Model):
|
||||||
|
@ -71,14 +99,15 @@ class Ignore(models.Model):
|
||||||
)
|
)
|
||||||
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 (
|
return current_user.ignored_chat_users.ignored_users.filter(
|
||||||
current_user.ignored_chat_users.get()
|
id=new_friend.id
|
||||||
.ignored_users.filter(id=new_friend.id)
|
).exists()
|
||||||
.exists()
|
|
||||||
)
|
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -89,6 +118,16 @@ class Ignore(models.Model):
|
||||||
except:
|
except:
|
||||||
return Profile.objects.none()
|
return Profile.objects.none()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_ignored_rooms(self, user):
|
||||||
|
try:
|
||||||
|
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
|
@classmethod
|
||||||
def add_ignore(self, current_user, friend):
|
def add_ignore(self, current_user, friend):
|
||||||
ignore, created = self.objects.get_or_create(user=current_user)
|
ignore, created = self.objects.get_or_create(user=current_user)
|
||||||
|
@ -105,3 +144,11 @@ class Ignore(models.Model):
|
||||||
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,10 +1,14 @@
|
||||||
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
|
from django.db.models import OuterRef, Count, Subquery, IntegerField, Q
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
|
|
||||||
from chat_box.models import Ignore, Message, UserRoom
|
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)
|
||||||
|
@ -24,25 +28,23 @@ def decrypt_url(message_encrypted):
|
||||||
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):
|
def get_unread_boxes(profile):
|
||||||
ignored_users = Ignore.get_ignored_users(profile)
|
ignored_rooms = Ignore.get_ignored_rooms(profile)
|
||||||
|
|
||||||
mess = (
|
|
||||||
Message.objects.filter(room=OuterRef("room"), time__gte=OuterRef("last_seen"))
|
|
||||||
.exclude(author=profile)
|
|
||||||
.exclude(author__in=ignored_users)
|
|
||||||
.order_by()
|
|
||||||
.values("room")
|
|
||||||
.annotate(unread_count=Count("pk"))
|
|
||||||
.values("unread_count")
|
|
||||||
)
|
|
||||||
|
|
||||||
unread_boxes = (
|
unread_boxes = (
|
||||||
UserRoom.objects.filter(user=profile, room__isnull=False)
|
UserRoom.objects.filter(user=profile, unread_count__gt=0)
|
||||||
.annotate(
|
.exclude(room__in=ignored_rooms)
|
||||||
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
|
|
||||||
)
|
|
||||||
.filter(unread_count__gte=1)
|
|
||||||
.count()
|
.count()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -21,22 +21,23 @@ from django.db.models import (
|
||||||
Exists,
|
Exists,
|
||||||
Count,
|
Count,
|
||||||
IntegerField,
|
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):
|
||||||
|
@ -49,7 +50,8 @@ class ChatView(ListView):
|
||||||
self.room_id = None
|
self.room_id = None
|
||||||
self.room = None
|
self.room = None
|
||||||
self.messages = None
|
self.messages = None
|
||||||
self.page_size = 20
|
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
|
||||||
|
@ -63,10 +65,12 @@ class ChatView(ListView):
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
request_room = kwargs["room_id"]
|
request_room = kwargs["room_id"]
|
||||||
|
page_size = self.follow_up_page_size
|
||||||
try:
|
try:
|
||||||
last_id = int(request.GET.get("last_id"))
|
last_id = int(request.GET.get("last_id"))
|
||||||
except Exception:
|
except Exception:
|
||||||
last_id = 1e15
|
last_id = 1e15
|
||||||
|
page_size = self.first_page_size
|
||||||
only_messages = request.GET.get("only_messages")
|
only_messages = request.GET.get("only_messages")
|
||||||
|
|
||||||
if request_room:
|
if request_room:
|
||||||
|
@ -80,11 +84,12 @@ class ChatView(ListView):
|
||||||
request_room = None
|
request_room = None
|
||||||
|
|
||||||
self.room_id = request_room
|
self.room_id = request_room
|
||||||
self.messages = Message.objects.filter(
|
self.messages = (
|
||||||
hidden=False, room=self.room_id, id__lt=last_id
|
Message.objects.filter(hidden=False, room=self.room_id, id__lt=last_id)
|
||||||
)[: self.page_size]
|
.select_related("author")
|
||||||
|
.only("body", "time", "author__rating", "author__display_rank")[:page_size]
|
||||||
|
)
|
||||||
if not only_messages:
|
if not only_messages:
|
||||||
update_last_seen(request, **kwargs)
|
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
|
@ -101,10 +106,14 @@ class ChatView(ListView):
|
||||||
|
|
||||||
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["has_next"] = self.has_next()
|
context["has_next"] = self.has_next()
|
||||||
context["unread_count_lobby"] = get_unread_count(None, self.request.profile)
|
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)
|
||||||
|
@ -130,15 +139,15 @@ def delete_message(request):
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
if not request.user.is_staff:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
|
@ -160,26 +169,57 @@ def mute_message(request):
|
||||||
except:
|
except:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
with revisions.create_revision():
|
||||||
|
revisions.set_comment(_("Mute chat") + ": " + mess.body)
|
||||||
|
revisions.set_user(request.user)
|
||||||
mess.author.mute = True
|
mess.author.mute = True
|
||||||
mess.author.save()
|
mess.author.save()
|
||||||
|
|
||||||
Message.objects.filter(room=None, author=mess.author).update(hidden=True)
|
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()
|
return HttpResponseBadRequest()
|
||||||
if len(request.POST["body"]) > 5000:
|
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, body=request.POST["body"], room=room)
|
new_message = Message(author=request.profile, body=request.POST["body"], room=room)
|
||||||
|
@ -187,7 +227,7 @@ def post_message(request):
|
||||||
|
|
||||||
if not room:
|
if not room:
|
||||||
event.post(
|
event.post(
|
||||||
"chat_lobby",
|
encrypt_channel("chat_lobby"),
|
||||||
{
|
{
|
||||||
"type": "lobby",
|
"type": "lobby",
|
||||||
"author_id": request.profile.id,
|
"author_id": request.profile.id,
|
||||||
|
@ -197,9 +237,13 @@ def post_message(request):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
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(
|
event.post(
|
||||||
"chat_" + str(user.id),
|
encrypt_channel("chat_" + str(user.id)),
|
||||||
{
|
{
|
||||||
"type": "private",
|
"type": "private",
|
||||||
"author_id": request.profile.id,
|
"author_id": request.profile.id,
|
||||||
|
@ -208,14 +252,17 @@ def post_message(request):
|
||||||
"tmp_id": request.POST.get("tmp_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 (
|
return not room or room.contain(request.profile)
|
||||||
not room or room.user_one == request.profile or room.user_two == request.profile
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -231,7 +278,7 @@ def chat_message_ajax(request):
|
||||||
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()
|
||||||
|
@ -254,35 +301,35 @@ def update_last_seen(request, **kwargs):
|
||||||
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()
|
||||||
|
|
||||||
|
get_unread_boxes.dirty(profile)
|
||||||
|
|
||||||
return JsonResponse({"msg": "updated"})
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@ -319,47 +366,66 @@ def user_online_status_ajax(request):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
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 = (
|
recent_profile = (
|
||||||
Room.objects.filter(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",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -369,50 +435,24 @@ def get_status_context(request, include_ignored=False):
|
||||||
.values("other_user", "id")[:20]
|
.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]
|
||||||
recent_list = None
|
Room.prefetch_room_cache(recent_rooms)
|
||||||
if joined_id:
|
|
||||||
recent_list = Profile.objects.raw(
|
|
||||||
f"SELECT * from judge_profile where id in ({joined_id}) order by field(id,{joined_id})"
|
|
||||||
)
|
|
||||||
friend_list = (
|
|
||||||
Friend.get_friend_profiles(request.profile)
|
|
||||||
.exclude(id__in=recent_profile_id)
|
|
||||||
.exclude(id__in=ignored_users)
|
|
||||||
.order_by("-last_access")
|
|
||||||
)
|
|
||||||
admin_list = (
|
admin_list = (
|
||||||
queryset.filter(display_rank="admin")
|
queryset.filter(display_rank="admin")
|
||||||
.exclude(id__in=friend_list)
|
.exclude(id__in=recent_profile_ids)
|
||||||
.exclude(id__in=recent_profile_id)
|
.values_list("id", flat=True)
|
||||||
)
|
|
||||||
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),
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -423,7 +463,7 @@ def online_status_ajax(request):
|
||||||
request,
|
request,
|
||||||
"chat/online_status.html",
|
"chat/online_status.html",
|
||||||
{
|
{
|
||||||
"status_sections": get_status_context(request),
|
"status_sections": get_status_context(request.profile),
|
||||||
"unread_count_lobby": get_unread_count(None, request.profile),
|
"unread_count_lobby": get_unread_count(None, request.profile),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -447,7 +487,6 @@ def get_or_create_room(request):
|
||||||
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()
|
||||||
|
|
||||||
|
@ -468,55 +507,36 @@ def get_or_create_room(request):
|
||||||
user_room.last_seen = timezone.now()
|
user_room.last_seen = timezone.now()
|
||||||
user_room.save()
|
user_room.save()
|
||||||
|
|
||||||
|
room_url = reverse("chat", kwargs={"room_id": room.id})
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
return JsonResponse({"room": room.id, "other_user_id": other_user.id})
|
return JsonResponse(
|
||||||
return HttpResponseRedirect(reverse("chat", kwargs={"room_id": room.id}))
|
{
|
||||||
|
"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 = (
|
return UserRoom.objects.filter(
|
||||||
Message.objects.filter(
|
user=user, room__in=rooms, unread_count__gt=0
|
||||||
room=OuterRef("room"), time__gte=OuterRef("last_seen")
|
).values("unread_count", "room")
|
||||||
)
|
|
||||||
.exclude(author=user)
|
|
||||||
.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 = (
|
user_room = UserRoom.objects.filter(user=user, room__isnull=True).first()
|
||||||
Message.objects.filter(room__isnull=True, time__gte=OuterRef("last_seen"))
|
if not user_room:
|
||||||
.exclude(author=user)
|
return 0
|
||||||
.order_by()
|
last_seen = user_room.last_seen
|
||||||
.values("room")
|
|
||||||
.annotate(unread_count=Count("pk"))
|
|
||||||
.values("unread_count")
|
|
||||||
)
|
|
||||||
|
|
||||||
res = (
|
res = (
|
||||||
UserRoom.objects.filter(user=user, room__isnull=True)
|
Message.objects.filter(room__isnull=True, time__gte=last_seen)
|
||||||
.annotate(
|
.exclude(author=user)
|
||||||
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
|
.exclude(hidden=True)
|
||||||
)
|
.count()
|
||||||
.values_list("unread_count", flat=True)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return res[0] if len(res) else 0
|
return res
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
|
|
@ -66,7 +66,7 @@ class AceWidget(forms.Textarea):
|
||||||
if self.toolbar:
|
if self.toolbar:
|
||||||
toolbar = (
|
toolbar = (
|
||||||
'<div style="width: {}" class="django-ace-toolbar">'
|
'<div style="width: {}" class="django-ace-toolbar">'
|
||||||
'<a href="./" class="django-ace-max_min"></a>'
|
'<a href="#" class="django-ace-max_min"></a>'
|
||||||
"</div>"
|
"</div>"
|
||||||
).format(self.width)
|
).format(self.width)
|
||||||
html = toolbar + html
|
html = toolbar + html
|
||||||
|
|
|
@ -33,6 +33,7 @@ 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
|
||||||
|
|
||||||
|
@ -83,6 +84,9 @@ DMOJ_STATS_SUBMISSION_RESULT_COLORS = {
|
||||||
"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 = {}
|
||||||
|
@ -128,13 +132,10 @@ 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 = (
|
FONTAWESOME_CSS = "//cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
|
||||||
"//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
|
|
||||||
)
|
|
||||||
DMOJ_CANONICAL = ""
|
DMOJ_CANONICAL = ""
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
@ -168,7 +169,7 @@ else:
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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",
|
||||||
|
@ -276,8 +277,11 @@ LANGUAGE_COOKIE_AGE = 8640000
|
||||||
|
|
||||||
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
|
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
|
||||||
|
|
||||||
IMPERSONATE_REQUIRE_SUPERUSER = True
|
IMPERSONATE = {
|
||||||
IMPERSONATE_DISABLE_LOGGING = True
|
"REQUIRE_SUPERUSER": True,
|
||||||
|
"DISABLE_LOGGING": True,
|
||||||
|
"ADMIN_DELETE_PERMISSION": True,
|
||||||
|
}
|
||||||
|
|
||||||
ACCOUNT_ACTIVATION_DAYS = 7
|
ACCOUNT_ACTIVATION_DAYS = 7
|
||||||
|
|
||||||
|
@ -321,7 +325,6 @@ TEMPLATES = [
|
||||||
"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",
|
||||||
],
|
],
|
||||||
|
@ -381,6 +384,7 @@ 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
|
||||||
|
@ -428,7 +432,7 @@ 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 = (
|
||||||
|
@ -474,10 +478,24 @@ ML_OUTPUT_PATH = None
|
||||||
# Use subdomain for organizations
|
# Use subdomain for organizations
|
||||||
USE_SUBDOMAIN = False
|
USE_SUBDOMAIN = False
|
||||||
|
|
||||||
|
# Chat
|
||||||
|
CHAT_SECRET_KEY = "QUdVFsxk6f5-Hd8g9BXv81xMqvIZFRqMl-KbRzztW-U="
|
||||||
|
|
||||||
|
# Nginx
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
|
||||||
|
|
168
dmoj/urls.py
168
dmoj/urls.py
|
@ -15,14 +15,6 @@ from django.contrib.auth.decorators import login_required
|
||||||
from django.conf.urls.static import static as url_static
|
from django.conf.urls.static import static as url_static
|
||||||
|
|
||||||
|
|
||||||
from judge.feed import (
|
|
||||||
AtomBlogFeed,
|
|
||||||
AtomCommentFeed,
|
|
||||||
AtomProblemFeed,
|
|
||||||
BlogFeed,
|
|
||||||
CommentFeed,
|
|
||||||
ProblemFeed,
|
|
||||||
)
|
|
||||||
from judge.forms import CustomAuthenticationForm
|
from judge.forms import CustomAuthenticationForm
|
||||||
from judge.sitemap import (
|
from judge.sitemap import (
|
||||||
BlogPostSitemap,
|
BlogPostSitemap,
|
||||||
|
@ -44,6 +36,8 @@ from judge.views import (
|
||||||
language,
|
language,
|
||||||
license,
|
license,
|
||||||
mailgun,
|
mailgun,
|
||||||
|
markdown_editor,
|
||||||
|
test_formatter,
|
||||||
notification,
|
notification,
|
||||||
organization,
|
organization,
|
||||||
preview,
|
preview,
|
||||||
|
@ -65,7 +59,13 @@ from judge.views import (
|
||||||
internal,
|
internal,
|
||||||
resolver,
|
resolver,
|
||||||
course,
|
course,
|
||||||
|
email,
|
||||||
|
custom_file_upload,
|
||||||
)
|
)
|
||||||
|
from judge import authentication
|
||||||
|
|
||||||
|
from judge.views.test_formatter import test_formatter
|
||||||
|
|
||||||
from judge.views.problem_data import (
|
from judge.views.problem_data import (
|
||||||
ProblemDataView,
|
ProblemDataView,
|
||||||
ProblemSubmissionDiff,
|
ProblemSubmissionDiff,
|
||||||
|
@ -77,7 +77,6 @@ from judge.views.register import ActivationView, RegistrationView
|
||||||
from judge.views.select2 import (
|
from judge.views.select2 import (
|
||||||
AssigneeSelect2View,
|
AssigneeSelect2View,
|
||||||
ChatUserSearchSelect2View,
|
ChatUserSearchSelect2View,
|
||||||
CommentSelect2View,
|
|
||||||
ContestSelect2View,
|
ContestSelect2View,
|
||||||
ContestUserSearchSelect2View,
|
ContestUserSearchSelect2View,
|
||||||
OrganizationSelect2View,
|
OrganizationSelect2View,
|
||||||
|
@ -85,6 +84,7 @@ from judge.views.select2 import (
|
||||||
TicketUserSelect2View,
|
TicketUserSelect2View,
|
||||||
UserSearchSelect2View,
|
UserSearchSelect2View,
|
||||||
UserSelect2View,
|
UserSelect2View,
|
||||||
|
ProblemAuthorSearchSelect2View,
|
||||||
)
|
)
|
||||||
|
|
||||||
admin.autodiscover()
|
admin.autodiscover()
|
||||||
|
@ -104,19 +104,19 @@ register_patterns = [
|
||||||
# confusing 404.
|
# confusing 404.
|
||||||
url(
|
url(
|
||||||
r"^activate/(?P<activation_key>\w+)/$",
|
r"^activate/(?P<activation_key>\w+)/$",
|
||||||
ActivationView.as_view(title="Activation key invalid"),
|
ActivationView.as_view(title=_("Activation key invalid")),
|
||||||
name="registration_activate",
|
name="registration_activate",
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r"^register/$",
|
r"^register/$",
|
||||||
RegistrationView.as_view(title="Register"),
|
RegistrationView.as_view(title=_("Register")),
|
||||||
name="registration_register",
|
name="registration_register",
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r"^register/complete/$",
|
r"^register/complete/$",
|
||||||
TitledTemplateView.as_view(
|
TitledTemplateView.as_view(
|
||||||
template_name="registration/registration_complete.html",
|
template_name="registration/registration_complete.html",
|
||||||
title="Registration Completed",
|
title=_("Registration Completed"),
|
||||||
),
|
),
|
||||||
name="registration_complete",
|
name="registration_complete",
|
||||||
),
|
),
|
||||||
|
@ -124,7 +124,7 @@ register_patterns = [
|
||||||
r"^register/closed/$",
|
r"^register/closed/$",
|
||||||
TitledTemplateView.as_view(
|
TitledTemplateView.as_view(
|
||||||
template_name="registration/registration_closed.html",
|
template_name="registration/registration_closed.html",
|
||||||
title="Registration not allowed",
|
title=_("Registration not allowed"),
|
||||||
),
|
),
|
||||||
name="registration_disallowed",
|
name="registration_disallowed",
|
||||||
),
|
),
|
||||||
|
@ -141,9 +141,7 @@ register_patterns = [
|
||||||
url(r"^logout/$", user.UserLogoutView.as_view(), name="auth_logout"),
|
url(r"^logout/$", user.UserLogoutView.as_view(), name="auth_logout"),
|
||||||
url(
|
url(
|
||||||
r"^password/change/$",
|
r"^password/change/$",
|
||||||
auth_views.PasswordChangeView.as_view(
|
authentication.CustomPasswordChangeView.as_view(),
|
||||||
template_name="registration/password_change_form.html",
|
|
||||||
),
|
|
||||||
name="password_change",
|
name="password_change",
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
|
@ -183,6 +181,17 @@ register_patterns = [
|
||||||
),
|
),
|
||||||
name="password_reset_done",
|
name="password_reset_done",
|
||||||
),
|
),
|
||||||
|
url(r"^email/change/$", email.email_change_view, name="email_change"),
|
||||||
|
url(
|
||||||
|
r"^email/change/verify/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$",
|
||||||
|
email.verify_email_view,
|
||||||
|
name="email_change_verify",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^email/change/pending$",
|
||||||
|
email.email_change_pending_view,
|
||||||
|
name="email_change_pending",
|
||||||
|
),
|
||||||
url(r"^social/error/$", register.social_auth_error, name="social_auth_error"),
|
url(r"^social/error/$", register.social_auth_error, name="social_auth_error"),
|
||||||
url(r"^2fa/$", totp.TOTPLoginView.as_view(), name="login_2fa"),
|
url(r"^2fa/$", totp.TOTPLoginView.as_view(), name="login_2fa"),
|
||||||
url(r"^2fa/enable/$", totp.TOTPEnableView.as_view(), name="enable_2fa"),
|
url(r"^2fa/enable/$", totp.TOTPEnableView.as_view(), name="enable_2fa"),
|
||||||
|
@ -389,10 +398,36 @@ urlpatterns = [
|
||||||
name="submission_status",
|
name="submission_status",
|
||||||
),
|
),
|
||||||
url(r"^/abort$", submission.abort_submission, name="submission_abort"),
|
url(r"^/abort$", submission.abort_submission, name="submission_abort"),
|
||||||
url(r"^/html$", submission.single_submission),
|
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
url(
|
||||||
|
r"^test_formatter/",
|
||||||
|
include(
|
||||||
|
[
|
||||||
|
url(
|
||||||
|
r"^$",
|
||||||
|
login_required(test_formatter.TestFormatter.as_view()),
|
||||||
|
name="test_formatter",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^edit_page$",
|
||||||
|
login_required(test_formatter.EditTestFormatter.as_view()),
|
||||||
|
name="test_formatter_edit",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^download_page$",
|
||||||
|
login_required(test_formatter.DownloadTestFormatter.as_view()),
|
||||||
|
name="test_formatter_download",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^markdown_editor/",
|
||||||
|
markdown_editor.MarkdownEditor.as_view(),
|
||||||
|
name="markdown_editor",
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r"^submission_source_file/(?P<filename>(\w|\.)+)",
|
r"^submission_source_file/(?P<filename>(\w|\.)+)",
|
||||||
submission.SubmissionSourceFileView.as_view(),
|
submission.SubmissionSourceFileView.as_view(),
|
||||||
|
@ -452,6 +487,7 @@ urlpatterns = [
|
||||||
reverse("all_user_submissions", args=[user])
|
reverse("all_user_submissions", args=[user])
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
url(r"^/toggle_follow/", user.toggle_follow, name="user_toggle_follow"),
|
||||||
url(
|
url(
|
||||||
r"^/$",
|
r"^/$",
|
||||||
lambda _, user: HttpResponsePermanentRedirect(
|
lambda _, user: HttpResponsePermanentRedirect(
|
||||||
|
@ -468,6 +504,8 @@ urlpatterns = [
|
||||||
url(r"^comments/upvote/$", comment.upvote_comment, name="comment_upvote"),
|
url(r"^comments/upvote/$", comment.upvote_comment, name="comment_upvote"),
|
||||||
url(r"^comments/downvote/$", comment.downvote_comment, name="comment_downvote"),
|
url(r"^comments/downvote/$", comment.downvote_comment, name="comment_downvote"),
|
||||||
url(r"^comments/hide/$", comment.comment_hide, name="comment_hide"),
|
url(r"^comments/hide/$", comment.comment_hide, name="comment_hide"),
|
||||||
|
url(r"^comments/get_replies/$", comment.get_replies, name="comment_get_replies"),
|
||||||
|
url(r"^comments/show_more/$", comment.get_show_more, name="comment_show_more"),
|
||||||
url(
|
url(
|
||||||
r"^comments/(?P<id>\d+)/",
|
r"^comments/(?P<id>\d+)/",
|
||||||
include(
|
include(
|
||||||
|
@ -497,7 +535,58 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")),
|
url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")),
|
||||||
url(r"^course/", paged_list_view(course.CourseList, "course_list")),
|
url(
|
||||||
|
r"^contests/summary/(?P<key>\w+)/",
|
||||||
|
paged_list_view(contests.ContestsSummaryView, "contests_summary"),
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^contests/official",
|
||||||
|
paged_list_view(contests.OfficialContestList, "official_contest_list"),
|
||||||
|
),
|
||||||
|
url(r"^courses/", paged_list_view(course.CourseList, "course_list")),
|
||||||
|
url(
|
||||||
|
r"^course/(?P<slug>[\w-]*)",
|
||||||
|
include(
|
||||||
|
[
|
||||||
|
url(r"^$", course.CourseDetail.as_view(), name="course_detail"),
|
||||||
|
url(
|
||||||
|
r"^/lesson/(?P<id>\d+)$",
|
||||||
|
course.CourseLessonDetail.as_view(),
|
||||||
|
name="course_lesson_detail",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^/edit_lessons$",
|
||||||
|
course.EditCourseLessonsView.as_view(),
|
||||||
|
name="edit_course_lessons",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^/grades$",
|
||||||
|
course.CourseStudentResults.as_view(),
|
||||||
|
name="course_grades",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^/grades/lesson/(?P<id>\d+)$",
|
||||||
|
course.CourseStudentResultsLesson.as_view(),
|
||||||
|
name="course_grades_lesson",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^/add_contest$",
|
||||||
|
course.AddCourseContest.as_view(),
|
||||||
|
name="add_course_contest",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^/edit_contest/(?P<contest>\w+)$",
|
||||||
|
course.EditCourseContest.as_view(),
|
||||||
|
name="edit_course_contest",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^/contests$",
|
||||||
|
course.CourseContestList.as_view(),
|
||||||
|
name="course_contest_list",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r"^contests/(?P<year>\d+)/(?P<month>\d+)/$",
|
r"^contests/(?P<year>\d+)/(?P<month>\d+)/$",
|
||||||
contests.ContestCalendar.as_view(),
|
contests.ContestCalendar.as_view(),
|
||||||
|
@ -540,11 +629,6 @@ urlpatterns = [
|
||||||
contests.ContestFinalRanking.as_view(),
|
contests.ContestFinalRanking.as_view(),
|
||||||
name="contest_final_ranking",
|
name="contest_final_ranking",
|
||||||
),
|
),
|
||||||
url(
|
|
||||||
r"^/ranking/ajax$",
|
|
||||||
contests.contest_ranking_ajax,
|
|
||||||
name="contest_ranking_ajax",
|
|
||||||
),
|
|
||||||
url(r"^/join$", contests.ContestJoin.as_view(), name="contest_join"),
|
url(r"^/join$", contests.ContestJoin.as_view(), name="contest_join"),
|
||||||
url(r"^/leave$", contests.ContestLeave.as_view(), name="contest_leave"),
|
url(r"^/leave$", contests.ContestLeave.as_view(), name="contest_leave"),
|
||||||
url(r"^/stats$", contests.ContestStats.as_view(), name="contest_stats"),
|
url(r"^/stats$", contests.ContestStats.as_view(), name="contest_stats"),
|
||||||
|
@ -561,6 +645,13 @@ urlpatterns = [
|
||||||
"contest_user_submissions_ajax",
|
"contest_user_submissions_ajax",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
url(
|
||||||
|
r"^/submissions",
|
||||||
|
paged_list_view(
|
||||||
|
submission.ContestSubmissions,
|
||||||
|
"contest_submissions",
|
||||||
|
),
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r"^/participations$",
|
r"^/participations$",
|
||||||
contests.ContestParticipationList.as_view(),
|
contests.ContestParticipationList.as_view(),
|
||||||
|
@ -826,6 +917,11 @@ urlpatterns = [
|
||||||
AssigneeSelect2View.as_view(),
|
AssigneeSelect2View.as_view(),
|
||||||
name="ticket_assignee_select2_ajax",
|
name="ticket_assignee_select2_ajax",
|
||||||
),
|
),
|
||||||
|
url(
|
||||||
|
r"^problem_authors$",
|
||||||
|
ProblemAuthorSearchSelect2View.as_view(),
|
||||||
|
name="problem_authors_select2_ajax",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -884,19 +980,6 @@ urlpatterns = [
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
url(
|
|
||||||
r"^feed/",
|
|
||||||
include(
|
|
||||||
[
|
|
||||||
url(r"^problems/rss/$", ProblemFeed(), name="problem_rss"),
|
|
||||||
url(r"^problems/atom/$", AtomProblemFeed(), name="problem_atom"),
|
|
||||||
url(r"^comment/rss/$", CommentFeed(), name="comment_rss"),
|
|
||||||
url(r"^comment/atom/$", AtomCommentFeed(), name="comment_atom"),
|
|
||||||
url(r"^blog/rss/$", BlogFeed(), name="blog_rss"),
|
|
||||||
url(r"^blog/atom/$", AtomBlogFeed(), name="blog_atom"),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
url(
|
url(
|
||||||
r"^stats/",
|
r"^stats/",
|
||||||
include(
|
include(
|
||||||
|
@ -997,9 +1080,6 @@ urlpatterns = [
|
||||||
url(
|
url(
|
||||||
r"^contest/$", ContestSelect2View.as_view(), name="contest_select2"
|
r"^contest/$", ContestSelect2View.as_view(), name="contest_select2"
|
||||||
),
|
),
|
||||||
url(
|
|
||||||
r"^comment/$", CommentSelect2View.as_view(), name="comment_select2"
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1075,6 +1155,11 @@ urlpatterns = [
|
||||||
internal.InternalProblem.as_view(),
|
internal.InternalProblem.as_view(),
|
||||||
name="internal_problem",
|
name="internal_problem",
|
||||||
),
|
),
|
||||||
|
url(
|
||||||
|
r"^problem_votes$",
|
||||||
|
internal.get_problem_votes,
|
||||||
|
name="internal_problem_votes",
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r"^request_time$",
|
r"^request_time$",
|
||||||
internal.InternalRequestTime.as_view(),
|
internal.InternalRequestTime.as_view(),
|
||||||
|
@ -1100,8 +1185,7 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r"^notifications/",
|
r"^notifications/",
|
||||||
login_required(notification.NotificationList.as_view()),
|
paged_list_view(notification.NotificationList, "notification"),
|
||||||
name="notification",
|
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r"^import_users/",
|
r"^import_users/",
|
||||||
|
@ -1131,6 +1215,7 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
url(r"^resolver/(?P<contest>\w+)", resolver.Resolver.as_view(), name="resolver"),
|
url(r"^resolver/(?P<contest>\w+)", resolver.Resolver.as_view(), name="resolver"),
|
||||||
|
url(r"^upload/$", custom_file_upload.file_upload, name="custom_file_upload"),
|
||||||
] + url_static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + url_static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
# if hasattr(settings, "INTERNAL_IPS"):
|
# if hasattr(settings, "INTERNAL_IPS"):
|
||||||
|
@ -1158,6 +1243,7 @@ favicon_paths = [
|
||||||
"favicon-32x32.png",
|
"favicon-32x32.png",
|
||||||
"favicon-16x16.png",
|
"favicon-16x16.png",
|
||||||
"android-chrome-192x192.png",
|
"android-chrome-192x192.png",
|
||||||
|
"android-chrome-512x512.png",
|
||||||
"android-chrome-48x48.png",
|
"android-chrome-48x48.png",
|
||||||
"mstile-310x150.png",
|
"mstile-310x150.png",
|
||||||
"apple-touch-icon-144x144.png",
|
"apple-touch-icon-144x144.png",
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
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 (
|
||||||
|
ContestAdmin,
|
||||||
|
ContestParticipationAdmin,
|
||||||
|
ContestTagAdmin,
|
||||||
|
ContestsSummaryAdmin,
|
||||||
|
)
|
||||||
from judge.admin.interface import (
|
from judge.admin.interface import (
|
||||||
BlogPostAdmin,
|
BlogPostAdmin,
|
||||||
LicenseAdmin,
|
LicenseAdmin,
|
||||||
|
@ -11,12 +17,18 @@ from judge.admin.interface import (
|
||||||
)
|
)
|
||||||
from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin
|
from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin
|
||||||
from judge.admin.problem import ProblemAdmin, ProblemPointsVoteAdmin
|
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.admin.volunteer import VolunteerProblemVoteAdmin
|
from judge.admin.volunteer import VolunteerProblemVoteAdmin
|
||||||
|
from judge.admin.course import CourseAdmin
|
||||||
from judge.models import (
|
from judge.models import (
|
||||||
BlogPost,
|
BlogPost,
|
||||||
Comment,
|
Comment,
|
||||||
|
@ -40,6 +52,9 @@ from judge.models import (
|
||||||
Ticket,
|
Ticket,
|
||||||
VolunteerProblemVote,
|
VolunteerProblemVote,
|
||||||
Course,
|
Course,
|
||||||
|
ContestsSummary,
|
||||||
|
OfficialContestCategory,
|
||||||
|
OfficialContestLocation,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -65,4 +80,9 @@ 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(VolunteerProblemVote, VolunteerProblemVoteAdmin)
|
||||||
admin.site.register(Course)
|
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)
|
||||||
|
|
|
@ -12,7 +12,6 @@ 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(
|
widgets["body"] = HeavyPreviewAdminPageDownWidget(
|
||||||
|
@ -39,7 +38,7 @@ class CommentAdmin(VersionAdmin):
|
||||||
)
|
)
|
||||||
list_display = ["author", "linked_object", "time"]
|
list_display = ["author", "linked_object", "time"]
|
||||||
search_fields = ["author__user__username", "body"]
|
search_fields = ["author__user__username", "body"]
|
||||||
readonly_fields = ["score"]
|
readonly_fields = ["score", "parent"]
|
||||||
actions = ["hide_comment", "unhide_comment"]
|
actions = ["hide_comment", "unhide_comment"]
|
||||||
list_filter = ["hidden"]
|
list_filter = ["hidden"]
|
||||||
actions_on_top = True
|
actions_on_top = True
|
||||||
|
|
|
@ -14,7 +14,14 @@ from reversion.admin import VersionAdmin
|
||||||
from reversion_compare.admin import CompareVersionAdmin
|
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 (
|
from judge.widgets import (
|
||||||
AdminHeavySelect2MultipleWidget,
|
AdminHeavySelect2MultipleWidget,
|
||||||
|
@ -24,6 +31,8 @@ from judge.widgets import (
|
||||||
AdminSelect2Widget,
|
AdminSelect2Widget,
|
||||||
HeavyPreviewAdminPageDownWidget,
|
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):
|
||||||
|
@ -71,7 +80,6 @@ class ContestProblemInlineForm(ModelForm):
|
||||||
"hidden_subtasks": TextInput(attrs={"size": "3"}),
|
"hidden_subtasks": TextInput(attrs={"size": "3"}),
|
||||||
"points": TextInput(attrs={"size": "1"}),
|
"points": TextInput(attrs={"size": "1"}),
|
||||||
"order": TextInput(attrs={"size": "1"}),
|
"order": TextInput(attrs={"size": "1"}),
|
||||||
"output_prefix_override": TextInput(attrs={"size": "1"}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -86,7 +94,7 @@ class ContestProblemInline(admin.TabularInline):
|
||||||
"is_pretested",
|
"is_pretested",
|
||||||
"max_submissions",
|
"max_submissions",
|
||||||
"hidden_subtasks",
|
"hidden_subtasks",
|
||||||
"output_prefix_override",
|
"show_testcases",
|
||||||
"order",
|
"order",
|
||||||
"rejudge_column",
|
"rejudge_column",
|
||||||
)
|
)
|
||||||
|
@ -149,6 +157,26 @@ class ContestForm(ModelForm):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
class ContestAdmin(CompareVersionAdmin):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {"fields": ("key", "name", "authors", "curators", "testers")}),
|
(None, {"fields": ("key", "name", "authors", "curators", "testers")}),
|
||||||
|
@ -159,9 +187,11 @@ class ContestAdmin(CompareVersionAdmin):
|
||||||
"is_visible",
|
"is_visible",
|
||||||
"use_clarifications",
|
"use_clarifications",
|
||||||
"hide_problem_tags",
|
"hide_problem_tags",
|
||||||
|
"public_scoreboard",
|
||||||
"scoreboard_visibility",
|
"scoreboard_visibility",
|
||||||
"run_pretests_only",
|
"run_pretests_only",
|
||||||
"points_precision",
|
"points_precision",
|
||||||
|
"rate_limit",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -221,7 +251,7 @@ class ContestAdmin(CompareVersionAdmin):
|
||||||
"user_count",
|
"user_count",
|
||||||
)
|
)
|
||||||
search_fields = ("key", "name")
|
search_fields = ("key", "name")
|
||||||
inlines = [ContestProblemInline]
|
inlines = [ContestProblemInline, OfficialContestInline]
|
||||||
actions_on_top = True
|
actions_on_top = True
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
form = ContestForm
|
form = ContestForm
|
||||||
|
@ -281,31 +311,14 @@ class ContestAdmin(CompareVersionAdmin):
|
||||||
|
|
||||||
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 (
|
|
||||||
"start_time",
|
|
||||||
"end_time",
|
|
||||||
"time_limit",
|
|
||||||
"format_config",
|
|
||||||
"format_name",
|
|
||||||
"freeze_after",
|
|
||||||
)
|
|
||||||
):
|
|
||||||
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):
|
||||||
obj = form.instance
|
formset_changed = True
|
||||||
obj.is_organization_private = obj.organizations.count() > 0
|
|
||||||
obj.is_private = obj.private_contestants.count() > 0
|
maybe_trigger_contest_rescore(form, form.instance, formset_changed)
|
||||||
obj.save()
|
|
||||||
|
|
||||||
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"):
|
||||||
|
@ -314,11 +327,6 @@ class ContestAdmin(CompareVersionAdmin):
|
||||||
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(
|
queryset = queryset.filter(
|
||||||
|
@ -502,3 +510,25 @@ class ContestParticipationAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
show_virtual.short_description = _("virtual")
|
show_virtual.short_description = _("virtual")
|
||||||
show_virtual.admin_order_field = "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
|
|
@ -53,6 +53,7 @@ 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)
|
||||||
|
if "authors" in self.fields:
|
||||||
self.fields["authors"].widget.can_add_related = False
|
self.fields["authors"].widget.can_add_related = False
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -33,7 +33,6 @@ class OrganizationAdmin(VersionAdmin):
|
||||||
"short_name",
|
"short_name",
|
||||||
"is_open",
|
"is_open",
|
||||||
"about",
|
"about",
|
||||||
"logo_override_image",
|
|
||||||
"slots",
|
"slots",
|
||||||
"registrant",
|
"registrant",
|
||||||
"creation_date",
|
"creation_date",
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
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, Avg, Count
|
from django.db.models import Q, Avg, Count
|
||||||
from django.db.models.aggregates import StdDev
|
from django.db.models.aggregates import StdDev
|
||||||
from django.forms import ModelForm, TextInput
|
from django.forms import ModelForm, TextInput
|
||||||
|
@ -11,6 +11,7 @@ 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 django_ace import AceWidget
|
from django_ace import AceWidget
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
from reversion_compare.admin import CompareVersionAdmin
|
from reversion_compare.admin import CompareVersionAdmin
|
||||||
|
@ -25,6 +26,7 @@ from judge.models import (
|
||||||
Solution,
|
Solution,
|
||||||
Notification,
|
Notification,
|
||||||
)
|
)
|
||||||
|
from judge.models.notification import make_notification
|
||||||
from judge.widgets import (
|
from judge.widgets import (
|
||||||
AdminHeavySelect2MultipleWidget,
|
AdminHeavySelect2MultipleWidget,
|
||||||
AdminSelect2MultipleWidget,
|
AdminSelect2MultipleWidget,
|
||||||
|
@ -32,6 +34,7 @@ from judge.widgets import (
|
||||||
CheckboxSelectMultipleWithSelectAll,
|
CheckboxSelectMultipleWithSelectAll,
|
||||||
HeavyPreviewAdminPageDownWidget,
|
HeavyPreviewAdminPageDownWidget,
|
||||||
)
|
)
|
||||||
|
from judge.utils.problems import user_editable_ids, user_tester_ids
|
||||||
|
|
||||||
MEMORY_UNITS = (("KB", "KB"), ("MB", "MB"))
|
MEMORY_UNITS = (("KB", "KB"), ("MB", "MB"))
|
||||||
|
|
||||||
|
@ -54,6 +57,16 @@ class ProblemForm(ModelForm):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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):
|
def clean(self):
|
||||||
memory_unit = self.cleaned_data.get("memory_unit", "KB")
|
memory_unit = self.cleaned_data.get("memory_unit", "KB")
|
||||||
if memory_unit == "MB":
|
if memory_unit == "MB":
|
||||||
|
@ -129,6 +142,7 @@ class LanguageLimitInline(admin.TabularInline):
|
||||||
model = LanguageLimit
|
model = LanguageLimit
|
||||||
fields = ("language", "time_limit", "memory_limit", "memory_unit")
|
fields = ("language", "time_limit", "memory_limit", "memory_unit")
|
||||||
form = LanguageLimitInlineForm
|
form = LanguageLimitInlineForm
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
class LanguageTemplateInlineForm(ModelForm):
|
class LanguageTemplateInlineForm(ModelForm):
|
||||||
|
@ -143,6 +157,7 @@ class LanguageTemplateInline(admin.TabularInline):
|
||||||
model = LanguageTemplate
|
model = LanguageTemplate
|
||||||
fields = ("language", "source")
|
fields = ("language", "source")
|
||||||
form = LanguageTemplateInlineForm
|
form = LanguageTemplateInlineForm
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
class ProblemSolutionForm(ModelForm):
|
class ProblemSolutionForm(ModelForm):
|
||||||
|
@ -358,12 +373,29 @@ class ProblemAdmin(CompareVersionAdmin):
|
||||||
self._rescore(request, obj.id)
|
self._rescore(request, obj.id)
|
||||||
|
|
||||||
def save_related(self, request, form, formsets, change):
|
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)
|
super().save_related(request, form, formsets, change)
|
||||||
# Only rescored if we did not already do so in `save_model`
|
|
||||||
obj = form.instance
|
obj = form.instance
|
||||||
obj.curators.add(request.profile)
|
obj.curators.add(request.profile)
|
||||||
obj.is_organization_private = obj.organizations.count() > 0
|
|
||||||
obj.save()
|
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
|
# Create notification
|
||||||
if "is_public" in form.changed_data or "organizations" in form.changed_data:
|
if "is_public" in form.changed_data or "organizations" in form.changed_data:
|
||||||
users = set(obj.authors.all())
|
users = set(obj.authors.all())
|
||||||
|
@ -381,14 +413,7 @@ class ProblemAdmin(CompareVersionAdmin):
|
||||||
category = "Problem public: " + str(obj.is_public)
|
category = "Problem public: " + str(obj.is_public)
|
||||||
if orgs:
|
if orgs:
|
||||||
category += " (" + ", ".join(orgs) + ")"
|
category += " (" + ", ".join(orgs) + ")"
|
||||||
for user in users:
|
make_notification(users, category, html, request.profile)
|
||||||
notification = Notification(
|
|
||||||
owner=user,
|
|
||||||
html_link=html,
|
|
||||||
category=category,
|
|
||||||
author=request.profile,
|
|
||||||
)
|
|
||||||
notification.save()
|
|
||||||
|
|
||||||
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"):
|
||||||
|
|
|
@ -1,13 +1,20 @@
|
||||||
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):
|
||||||
|
@ -53,6 +60,13 @@ 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 = (
|
fields = (
|
||||||
"user",
|
"user",
|
||||||
|
@ -62,15 +76,12 @@ class ProfileAdmin(VersionAdmin):
|
||||||
"timezone",
|
"timezone",
|
||||||
"language",
|
"language",
|
||||||
"ace_theme",
|
"ace_theme",
|
||||||
"math_engine",
|
|
||||||
"last_access",
|
"last_access",
|
||||||
"ip",
|
"ip",
|
||||||
"mute",
|
"mute",
|
||||||
"is_unlisted",
|
"is_unlisted",
|
||||||
"is_banned_problem_voting",
|
|
||||||
"notes",
|
"notes",
|
||||||
"is_totp_enabled",
|
"is_totp_enabled",
|
||||||
"user_script",
|
|
||||||
"current_contest",
|
"current_contest",
|
||||||
)
|
)
|
||||||
readonly_fields = ("user",)
|
readonly_fields = ("user",)
|
||||||
|
@ -91,6 +102,7 @@ class ProfileAdmin(VersionAdmin):
|
||||||
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")
|
||||||
|
@ -125,7 +137,7 @@ class ProfileAdmin(VersionAdmin):
|
||||||
admin_user_admin.short_description = _("User")
|
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.admin_order_field = "user__email"
|
||||||
email.short_description = _("Email")
|
email.short_description = _("Email")
|
||||||
|
@ -159,11 +171,57 @@ class ProfileAdmin(VersionAdmin):
|
||||||
|
|
||||||
recalculate_points.short_description = _("Recalculate scores")
|
recalculate_points.short_description = _("Recalculate scores")
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
|
||||||
form = super(ProfileAdmin, self).get_form(request, obj, **kwargs)
|
class UserForm(UserChangeForm):
|
||||||
if "user_script" in form.base_fields:
|
def __init__(self, *args, **kwargs):
|
||||||
# form.base_fields['user_script'] does not exist when the user has only view permission on the model.
|
super().__init__(*args, **kwargs)
|
||||||
form.base_fields["user_script"].widget = AceWidget(
|
self.fields["username"].help_text = _(
|
||||||
"javascript", request.profile.ace_theme
|
"Username can only contain letters, digits, and underscores."
|
||||||
)
|
)
|
||||||
return form
|
|
||||||
|
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
|
||||||
|
|
|
@ -194,13 +194,6 @@ class SubmissionAdmin(admin.ModelAdmin):
|
||||||
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 (
|
return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in (
|
||||||
"problem__code",
|
"problem__code",
|
||||||
|
|
|
@ -56,3 +56,11 @@ class ProblemTypeAdmin(admin.ModelAdmin):
|
||||||
[o.pk for o in obj.problem_set.all()] if obj else []
|
[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",)
|
||||||
|
|
|
@ -12,7 +12,7 @@ class JudgeAppConfig(AppConfig):
|
||||||
# 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
|
|
@ -3,7 +3,6 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import os
|
|
||||||
from collections import deque, namedtuple
|
from collections import deque, namedtuple
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
|
||||||
|
@ -25,6 +24,8 @@ from judge.models import (
|
||||||
Submission,
|
Submission,
|
||||||
SubmissionTestCase,
|
SubmissionTestCase,
|
||||||
)
|
)
|
||||||
|
from judge.bridge.utils import VanishedSubmission
|
||||||
|
from judge.caching import cache_wrapper
|
||||||
|
|
||||||
logger = logging.getLogger("judge.bridge")
|
logger = logging.getLogger("judge.bridge")
|
||||||
json_log = logging.getLogger("judge.json.bridge")
|
json_log = logging.getLogger("judge.json.bridge")
|
||||||
|
@ -64,10 +65,10 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
"handshake": self.on_handshake,
|
"handshake": self.on_handshake,
|
||||||
}
|
}
|
||||||
self._working = False
|
self._working = False
|
||||||
|
self._working_data = {}
|
||||||
self._no_response_job = None
|
self._no_response_job = None
|
||||||
self._problems = []
|
|
||||||
self.executors = {}
|
self.executors = {}
|
||||||
self.problems = {}
|
self.problems = set()
|
||||||
self.latency = None
|
self.latency = None
|
||||||
self.time_delta = None
|
self.time_delta = None
|
||||||
self.load = 1e100
|
self.load = 1e100
|
||||||
|
@ -93,12 +94,6 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
|
|
||||||
def on_disconnect(self):
|
def on_disconnect(self):
|
||||||
self._stop_ping.set()
|
self._stop_ping.set()
|
||||||
if self._working:
|
|
||||||
logger.error(
|
|
||||||
"Judge %s disconnected while handling submission %s",
|
|
||||||
self.name,
|
|
||||||
self._working,
|
|
||||||
)
|
|
||||||
self.judges.remove(self)
|
self.judges.remove(self)
|
||||||
if self.name is not None:
|
if self.name is not None:
|
||||||
self._disconnected()
|
self._disconnected()
|
||||||
|
@ -110,21 +105,29 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
self._make_json_log(action="disconnect", info="judge disconnected")
|
self._make_json_log(action="disconnect", info="judge disconnected")
|
||||||
)
|
)
|
||||||
if self._working:
|
if self._working:
|
||||||
Submission.objects.filter(id=self._working).update(
|
self.judges.judge(
|
||||||
status="IE", result="IE", error=""
|
self._working,
|
||||||
)
|
self._working_data["problem"],
|
||||||
json_log.error(
|
self._working_data["language"],
|
||||||
self._make_json_log(
|
self._working_data["source"],
|
||||||
sub=self._working,
|
None,
|
||||||
action="close",
|
0,
|
||||||
info="IE due to shutdown on grading",
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _authenticate(self, id, key):
|
def _authenticate(self, id, key):
|
||||||
try:
|
try:
|
||||||
judge = Judge.objects.get(name=id, is_blocked=False)
|
judge = Judge.objects.get(name=id)
|
||||||
except Judge.DoesNotExist:
|
except Judge.DoesNotExist:
|
||||||
|
if settings.BRIDGED_AUTO_CREATE_JUDGE:
|
||||||
|
judge = Judge()
|
||||||
|
judge.name = id
|
||||||
|
judge.auth_key = key
|
||||||
|
judge.save()
|
||||||
|
result = True
|
||||||
|
else:
|
||||||
|
result = False
|
||||||
|
else:
|
||||||
|
if judge.is_blocked:
|
||||||
result = False
|
result = False
|
||||||
else:
|
else:
|
||||||
result = hmac.compare_digest(judge.auth_key, key)
|
result = hmac.compare_digest(judge.auth_key, key)
|
||||||
|
@ -137,11 +140,52 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def _update_supported_problems(self, problem_packet):
|
||||||
|
# problem_packet is a dict {code: mtimes} from judge-server
|
||||||
|
self.problems = set(p for p, _ in problem_packet)
|
||||||
|
|
||||||
|
def _update_judge_problems(self):
|
||||||
|
chunk_size = 500
|
||||||
|
|
||||||
|
target_problem_codes = self.problems
|
||||||
|
current_problems = _get_judge_problems(self.judge)
|
||||||
|
|
||||||
|
updated = False
|
||||||
|
problems_to_add = list(target_problem_codes - current_problems)
|
||||||
|
problems_to_remove = list(current_problems - target_problem_codes)
|
||||||
|
|
||||||
|
if problems_to_add:
|
||||||
|
for i in range(0, len(problems_to_add), chunk_size):
|
||||||
|
chunk = problems_to_add[i : i + chunk_size]
|
||||||
|
problem_ids = Problem.objects.filter(code__in=chunk).values_list(
|
||||||
|
"id", flat=True
|
||||||
|
)
|
||||||
|
if not problem_ids:
|
||||||
|
continue
|
||||||
|
logger.info("%s: Add %d problems", self.name, len(problem_ids))
|
||||||
|
self.judge.problems.add(*problem_ids)
|
||||||
|
updated = True
|
||||||
|
|
||||||
|
if problems_to_remove:
|
||||||
|
for i in range(0, len(problems_to_remove), chunk_size):
|
||||||
|
chunk = problems_to_remove[i : i + chunk_size]
|
||||||
|
problem_ids = Problem.objects.filter(code__in=chunk).values_list(
|
||||||
|
"id", flat=True
|
||||||
|
)
|
||||||
|
if not problem_ids:
|
||||||
|
continue
|
||||||
|
logger.info("%s: Remove %d problems", self.name, len(problem_ids))
|
||||||
|
self.judge.problems.remove(*problem_ids)
|
||||||
|
updated = True
|
||||||
|
|
||||||
|
if updated:
|
||||||
|
_get_judge_problems.dirty(self.judge)
|
||||||
|
|
||||||
def _connected(self):
|
def _connected(self):
|
||||||
judge = self.judge = Judge.objects.get(name=self.name)
|
judge = self.judge = Judge.objects.get(name=self.name)
|
||||||
judge.start_time = timezone.now()
|
judge.start_time = timezone.now()
|
||||||
judge.online = True
|
judge.online = True
|
||||||
judge.problems.set(Problem.objects.filter(code__in=list(self.problems.keys())))
|
self._update_judge_problems()
|
||||||
judge.runtimes.set(Language.objects.filter(key__in=list(self.executors.keys())))
|
judge.runtimes.set(Language.objects.filter(key__in=list(self.executors.keys())))
|
||||||
|
|
||||||
# Delete now in case we somehow crashed and left some over from the last connection
|
# Delete now in case we somehow crashed and left some over from the last connection
|
||||||
|
@ -176,6 +220,8 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
def _disconnected(self):
|
def _disconnected(self):
|
||||||
Judge.objects.filter(id=self.judge.id).update(online=False)
|
Judge.objects.filter(id=self.judge.id).update(online=False)
|
||||||
RuntimeVersion.objects.filter(judge=self.judge).delete()
|
RuntimeVersion.objects.filter(judge=self.judge).delete()
|
||||||
|
self.judge.problems.clear()
|
||||||
|
_get_judge_problems.dirty(self.judge)
|
||||||
|
|
||||||
def _update_ping(self):
|
def _update_ping(self):
|
||||||
try:
|
try:
|
||||||
|
@ -206,8 +252,7 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
return
|
return
|
||||||
|
|
||||||
self.timeout = 60
|
self.timeout = 60
|
||||||
self._problems = packet["problems"]
|
self._update_supported_problems(packet["problems"])
|
||||||
self.problems = dict(self._problems)
|
|
||||||
self.executors = packet["executors"]
|
self.executors = packet["executors"]
|
||||||
self.name = packet["id"]
|
self.name = packet["id"]
|
||||||
|
|
||||||
|
@ -308,7 +353,15 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
|
|
||||||
def submit(self, id, problem, language, source):
|
def submit(self, id, problem, language, source):
|
||||||
data = self.get_related_submission_data(id)
|
data = self.get_related_submission_data(id)
|
||||||
|
if not data:
|
||||||
|
self._update_internal_error_submission(id, "Submission vanished")
|
||||||
|
raise VanishedSubmission()
|
||||||
self._working = id
|
self._working = id
|
||||||
|
self._working_data = {
|
||||||
|
"problem": problem,
|
||||||
|
"language": language,
|
||||||
|
"source": source,
|
||||||
|
}
|
||||||
self._no_response_job = threading.Timer(20, self._kill_if_no_response)
|
self._no_response_job = threading.Timer(20, self._kill_if_no_response)
|
||||||
self.send(
|
self.send(
|
||||||
{
|
{
|
||||||
|
@ -427,14 +480,12 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
|
|
||||||
def on_supported_problems(self, packet):
|
def on_supported_problems(self, packet):
|
||||||
logger.info("%s: Updated problem list", self.name)
|
logger.info("%s: Updated problem list", self.name)
|
||||||
self._problems = packet["problems"]
|
self._update_supported_problems(packet["problems"])
|
||||||
self.problems = dict(self._problems)
|
|
||||||
if not self.working:
|
if not self.working:
|
||||||
self.judges.update_problems(self)
|
self.judges.update_problems(self)
|
||||||
|
|
||||||
self.judge.problems.set(
|
self._update_judge_problems()
|
||||||
Problem.objects.filter(code__in=list(self.problems.keys()))
|
|
||||||
)
|
|
||||||
json_log.info(
|
json_log.info(
|
||||||
self._make_json_log(action="update-problems", count=len(self.problems))
|
self._make_json_log(action="update-problems", count=len(self.problems))
|
||||||
)
|
)
|
||||||
|
@ -651,8 +702,11 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
self._free_self(packet)
|
self._free_self(packet)
|
||||||
|
|
||||||
id = packet["submission-id"]
|
id = packet["submission-id"]
|
||||||
|
self._update_internal_error_submission(id, packet["message"])
|
||||||
|
|
||||||
|
def _update_internal_error_submission(self, id, message):
|
||||||
if Submission.objects.filter(id=id).update(
|
if Submission.objects.filter(id=id).update(
|
||||||
status="IE", result="IE", error=packet["message"]
|
status="IE", result="IE", error=message
|
||||||
):
|
):
|
||||||
event.post(
|
event.post(
|
||||||
"sub_%s" % Submission.get_id_secret(id), {"type": "internal-error"}
|
"sub_%s" % Submission.get_id_secret(id), {"type": "internal-error"}
|
||||||
|
@ -660,9 +714,9 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
self._post_update_submission(id, "internal-error", done=True)
|
self._post_update_submission(id, "internal-error", done=True)
|
||||||
json_log.info(
|
json_log.info(
|
||||||
self._make_json_log(
|
self._make_json_log(
|
||||||
packet,
|
sub=id,
|
||||||
action="internal-error",
|
action="internal-error",
|
||||||
message=packet["message"],
|
message=message,
|
||||||
finish=True,
|
finish=True,
|
||||||
result="IE",
|
result="IE",
|
||||||
)
|
)
|
||||||
|
@ -671,10 +725,10 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
logger.warning("Unknown submission: %s", id)
|
logger.warning("Unknown submission: %s", id)
|
||||||
json_log.error(
|
json_log.error(
|
||||||
self._make_json_log(
|
self._make_json_log(
|
||||||
packet,
|
sub=id,
|
||||||
action="internal-error",
|
action="internal-error",
|
||||||
info="unknown submission",
|
info="unknown submission",
|
||||||
message=packet["message"],
|
message=message,
|
||||||
finish=True,
|
finish=True,
|
||||||
result="IE",
|
result="IE",
|
||||||
)
|
)
|
||||||
|
@ -905,3 +959,8 @@ class JudgeHandler(ZlibPacketHandler):
|
||||||
|
|
||||||
def on_cleanup(self):
|
def on_cleanup(self):
|
||||||
db.connection.close()
|
db.connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="gjp", timeout=3600)
|
||||||
|
def _get_judge_problems(judge):
|
||||||
|
return set(judge.problems.values_list("code", flat=True))
|
||||||
|
|
|
@ -3,6 +3,8 @@ 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:
|
||||||
|
@ -39,6 +41,8 @@ class JudgeList(object):
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
judge.submit(id, problem, language, source)
|
judge.submit(id, problem, language, source)
|
||||||
|
except VanishedSubmission:
|
||||||
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
"Failed to dispatch %d (%s, %s) to %s",
|
"Failed to dispatch %d (%s, %s) to %s",
|
||||||
|
@ -89,6 +93,7 @@ class JudgeList(object):
|
||||||
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):
|
||||||
|
|
2
judge/bridge/utils.py
Normal file
2
judge/bridge/utils.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
class VanishedSubmission(Exception):
|
||||||
|
pass
|
|
@ -1,13 +1,16 @@
|
||||||
from inspect import signature
|
from inspect import signature
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache, caches
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
MAX_NUM_CHAR = 15
|
from judge.logging import log_debug
|
||||||
|
|
||||||
|
MAX_NUM_CHAR = 50
|
||||||
|
NONE_RESULT = "__None__"
|
||||||
|
|
||||||
|
|
||||||
def cache_wrapper(prefix, timeout=86400):
|
|
||||||
def arg_to_str(arg):
|
def arg_to_str(arg):
|
||||||
if hasattr(arg, "id"):
|
if hasattr(arg, "id"):
|
||||||
return str(arg.id)
|
return str(arg.id)
|
||||||
|
@ -17,31 +20,97 @@ def cache_wrapper(prefix, timeout=86400):
|
||||||
return str(arg)[:MAX_NUM_CHAR]
|
return str(arg)[:MAX_NUM_CHAR]
|
||||||
return str(arg)
|
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):
|
def get_key(func, *args, **kwargs):
|
||||||
args_list = list(args)
|
args_list = list(args)
|
||||||
signature_args = list(signature(func).parameters.keys())
|
signature_args = list(signature(func).parameters.keys())
|
||||||
args_list += [kwargs.get(k) for k in signature_args[len(args) :]]
|
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]
|
args_list = [arg_to_str(i) for i in args_list]
|
||||||
key = prefix + ":" + ":".join(args_list)
|
key = prefix + ":" + ":".join(args_list)
|
||||||
key = key.replace(" ", "_")
|
key = key.replace(" ", "_")
|
||||||
return key
|
return key
|
||||||
|
|
||||||
def decorator(func):
|
def _get(key):
|
||||||
def wrapper(*args, **kwargs):
|
if not l0_cache:
|
||||||
cache_key = get_key(func, *args, **kwargs)
|
return cache.get(key)
|
||||||
result = cache.get(cache_key)
|
result = l0_cache.get(key)
|
||||||
if result is not None:
|
if result is None:
|
||||||
|
result = cache.get(key)
|
||||||
return result
|
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)
|
result = func(*args, **kwargs)
|
||||||
cache.set(cache_key, result, timeout)
|
if result is None:
|
||||||
|
cache_result = NONE_RESULT
|
||||||
|
else:
|
||||||
|
cache_result = result
|
||||||
|
_set(cache_key, cache_result, timeout)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def dirty(*args, **kwargs):
|
def dirty(*args, **kwargs):
|
||||||
cache_key = get_key(func, *args, **kwargs)
|
cache_key = get_key(func, *args, **kwargs)
|
||||||
cache.delete(cache_key)
|
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)
|
||||||
|
if l0_cache:
|
||||||
|
l0_cache.delete_many(keys)
|
||||||
|
|
||||||
wrapper.dirty = dirty
|
wrapper.dirty = dirty
|
||||||
|
wrapper.prefetch_multi = prefetch_multi
|
||||||
|
wrapper.dirty_multi = dirty_multi
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
|
@ -1,190 +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, FilteredRelation, Q
|
|
||||||
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, Notification
|
|
||||||
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 (
|
|
||||||
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()
|
|
||||||
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 self.object.comments.filter(hidden=False, id=parent).exists():
|
|
||||||
return HttpResponseNotFound()
|
|
||||||
|
|
||||||
form = CommentForm(request, request.POST)
|
|
||||||
if form.is_valid():
|
|
||||||
comment = form.save(commit=False)
|
|
||||||
comment.author = request.profile
|
|
||||||
comment.linked_object = self.object
|
|
||||||
|
|
||||||
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_reply = Notification(
|
|
||||||
owner=comment.parent.author, comment=comment, category="Reply"
|
|
||||||
)
|
|
||||||
notification_reply.save()
|
|
||||||
|
|
||||||
# add notification for page authors
|
|
||||||
page_authors = comment.linked_object.authors.all()
|
|
||||||
for user in page_authors:
|
|
||||||
if user == comment.author:
|
|
||||||
continue
|
|
||||||
notification = Notification(
|
|
||||||
owner=user, comment=comment, category="Comment"
|
|
||||||
)
|
|
||||||
notification.save()
|
|
||||||
# except Exception:
|
|
||||||
# pass
|
|
||||||
|
|
||||||
add_mention_notifications(comment)
|
|
||||||
|
|
||||||
return HttpResponseRedirect(comment.get_absolute_url())
|
|
||||||
|
|
||||||
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={"parent": None}),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(CommentedDetailView, self).get_context_data(**kwargs)
|
|
||||||
queryset = self.object.comments
|
|
||||||
context["has_comments"] = queryset.exists()
|
|
||||||
context["comment_lock"] = self.is_comment_locked()
|
|
||||||
queryset = (
|
|
||||||
queryset.select_related("author__user")
|
|
||||||
.filter(hidden=False)
|
|
||||||
.defer("author__about")
|
|
||||||
.annotate(revisions=Count("versions"))
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.request.user.is_authenticated:
|
|
||||||
profile = self.request.profile
|
|
||||||
queryset = queryset.annotate(
|
|
||||||
my_vote=FilteredRelation(
|
|
||||||
"votes", condition=Q(votes__voter_id=profile.id)
|
|
||||||
),
|
|
||||||
).annotate(vote_score=Coalesce(F("my_vote__score"), Value(0)))
|
|
||||||
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["comment_count"] = len(queryset)
|
|
||||||
context["vote_hide_threshold"] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD
|
|
||||||
return context
|
|
|
@ -4,4 +4,5 @@ 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.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
|
||||||
|
|
|
@ -109,6 +109,8 @@ class BaseContestFormat(metaclass=ABCMeta):
|
||||||
)
|
)
|
||||||
for result in queryset:
|
for result in queryset:
|
||||||
problem = str(result["problem_id"])
|
problem = str(result["problem_id"])
|
||||||
|
if not (self.contest.freeze_after or hidden_subtasks.get(problem)):
|
||||||
|
continue
|
||||||
if format_data.get(problem):
|
if format_data.get(problem):
|
||||||
is_after_freeze = (
|
is_after_freeze = (
|
||||||
self.contest.freeze_after
|
self.contest.freeze_after
|
||||||
|
|
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"),
|
||||||
|
]
|
|
@ -16,7 +16,7 @@ class EventPoster(object):
|
||||||
|
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
self._conn = pika.BlockingConnection(
|
self._conn = pika.BlockingConnection(
|
||||||
pika.URLParameters(settings.EVENT_DAEMON_AMQP)
|
pika.URLParameters(settings.EVENT_DAEMON_AMQP),
|
||||||
)
|
)
|
||||||
self._chan = self._conn.channel()
|
self._chan = self._conn.channel()
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ class EventPoster(object):
|
||||||
id = int(time() * 1000000)
|
id = int(time() * 1000000)
|
||||||
self._chan.basic_publish(
|
self._chan.basic_publish(
|
||||||
self._exchange,
|
self._exchange,
|
||||||
"",
|
"#",
|
||||||
json.dumps({"id": id, "channel": channel, "message": message}),
|
json.dumps({"id": id, "channel": channel, "message": message}),
|
||||||
)
|
)
|
||||||
return id
|
return id
|
||||||
|
|
120
judge/feed.py
120
judge/feed.py
|
@ -1,120 +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(
|
|
||||||
"[\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))[: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))
|
|
||||||
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))
|
|
||||||
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
|
||||||
|
|
163
judge/forms.py
163
judge/forms.py
|
@ -11,7 +11,6 @@ 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, ObjectDoesNotExist
|
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.db import transaction
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.forms import (
|
from django.forms import (
|
||||||
CharField,
|
CharField,
|
||||||
|
@ -30,6 +29,7 @@ from django_ace import AceWidget
|
||||||
from judge.models import (
|
from judge.models import (
|
||||||
Contest,
|
Contest,
|
||||||
Language,
|
Language,
|
||||||
|
TestFormatterModel,
|
||||||
Organization,
|
Organization,
|
||||||
PrivateMessage,
|
PrivateMessage,
|
||||||
Problem,
|
Problem,
|
||||||
|
@ -38,11 +38,12 @@ from judge.models import (
|
||||||
Submission,
|
Submission,
|
||||||
BlogPost,
|
BlogPost,
|
||||||
ContestProblem,
|
ContestProblem,
|
||||||
|
TestFormatterModel,
|
||||||
|
ProfileInfo,
|
||||||
)
|
)
|
||||||
|
|
||||||
from judge.widgets import (
|
from judge.widgets import (
|
||||||
HeavyPreviewPageDownWidget,
|
HeavyPreviewPageDownWidget,
|
||||||
MathJaxPagedownWidget,
|
|
||||||
PagedownWidget,
|
PagedownWidget,
|
||||||
Select2MultipleWidget,
|
Select2MultipleWidget,
|
||||||
Select2Widget,
|
Select2Widget,
|
||||||
|
@ -50,8 +51,9 @@ from judge.widgets import (
|
||||||
HeavySelect2Widget,
|
HeavySelect2Widget,
|
||||||
Select2MultipleWidget,
|
Select2MultipleWidget,
|
||||||
DateTimePickerWidget,
|
DateTimePickerWidget,
|
||||||
|
ImageWidget,
|
||||||
|
DatePickerWidget,
|
||||||
)
|
)
|
||||||
from judge.tasks import rescore_contest
|
|
||||||
|
|
||||||
|
|
||||||
def fix_unicode(string, unsafe=tuple("\u202a\u202b\u202d\u202e")):
|
def fix_unicode(string, unsafe=tuple("\u202a\u202b\u202d\u202e")):
|
||||||
|
@ -69,61 +71,61 @@ class UserForm(ModelForm):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Profile
|
model = Profile
|
||||||
fields = [
|
fields = [
|
||||||
"about",
|
"about",
|
||||||
"organizations",
|
|
||||||
"timezone",
|
"timezone",
|
||||||
"language",
|
"language",
|
||||||
"ace_theme",
|
"ace_theme",
|
||||||
"user_script",
|
"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 groups.").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):
|
def file_size_validator(file):
|
||||||
limit = 1 * 1024 * 1024
|
limit = 10 * 1024 * 1024
|
||||||
if file.size > limit:
|
if file.size > limit:
|
||||||
raise ValidationError("File too large. Size should not exceed 1MB.")
|
raise ValidationError("File too large. Size should not exceed 10MB.")
|
||||||
|
|
||||||
|
|
||||||
class ProblemSubmitForm(ModelForm):
|
class ProblemSubmitForm(ModelForm):
|
||||||
|
@ -193,16 +195,32 @@ class EditOrganizationForm(ModelForm):
|
||||||
"slug",
|
"slug",
|
||||||
"short_name",
|
"short_name",
|
||||||
"about",
|
"about",
|
||||||
"logo_override_image",
|
"organization_image",
|
||||||
"admins",
|
"admins",
|
||||||
"is_open",
|
"is_open",
|
||||||
]
|
]
|
||||||
widgets = {"admins": Select2MultipleWidget()}
|
widgets = {
|
||||||
|
"admins": Select2MultipleWidget(),
|
||||||
|
"organization_image": ImageWidget,
|
||||||
|
}
|
||||||
if HeavyPreviewPageDownWidget is not None:
|
if HeavyPreviewPageDownWidget is not None:
|
||||||
widgets["about"] = HeavyPreviewPageDownWidget(
|
widgets["about"] = HeavyPreviewPageDownWidget(
|
||||||
preview=reverse_lazy("organization_preview")
|
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 AddOrganizationForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -212,7 +230,7 @@ class AddOrganizationForm(ModelForm):
|
||||||
"slug",
|
"slug",
|
||||||
"short_name",
|
"short_name",
|
||||||
"about",
|
"about",
|
||||||
"logo_override_image",
|
"organization_image",
|
||||||
"is_open",
|
"is_open",
|
||||||
]
|
]
|
||||||
widgets = {}
|
widgets = {}
|
||||||
|
@ -224,6 +242,7 @@ class AddOrganizationForm(ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.request = kwargs.pop("request", None)
|
self.request = kwargs.pop("request", None)
|
||||||
super(AddOrganizationForm, self).__init__(*args, **kwargs)
|
super(AddOrganizationForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields["organization_image"].required = False
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
res = super(AddOrganizationForm, self).save(commit=False)
|
res = super(AddOrganizationForm, self).save(commit=False)
|
||||||
|
@ -285,16 +304,9 @@ class EditOrganizationContestForm(ModelForm):
|
||||||
"view_contest_scoreboard",
|
"view_contest_scoreboard",
|
||||||
]:
|
]:
|
||||||
self.fields[field].widget.data_url = (
|
self.fields[field].widget.data_url = (
|
||||||
self.fields[field].widget.get_url() + "?org_id=1"
|
self.fields[field].widget.get_url() + f"?org_id={self.org_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def save(self, commit=True):
|
|
||||||
res = super(EditOrganizationContestForm, self).save(commit=False)
|
|
||||||
if commit:
|
|
||||||
res.save()
|
|
||||||
transaction.on_commit(rescore_contest.s(res.key).delay)
|
|
||||||
return res
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Contest
|
model = Contest
|
||||||
fields = (
|
fields = (
|
||||||
|
@ -311,9 +323,10 @@ class EditOrganizationContestForm(ModelForm):
|
||||||
"freeze_after",
|
"freeze_after",
|
||||||
"use_clarifications",
|
"use_clarifications",
|
||||||
"hide_problem_tags",
|
"hide_problem_tags",
|
||||||
|
"public_scoreboard",
|
||||||
"scoreboard_visibility",
|
"scoreboard_visibility",
|
||||||
"run_pretests_only",
|
|
||||||
"points_precision",
|
"points_precision",
|
||||||
|
"rate_limit",
|
||||||
"description",
|
"description",
|
||||||
"og_image",
|
"og_image",
|
||||||
"logo_override_image",
|
"logo_override_image",
|
||||||
|
@ -356,7 +369,7 @@ class AddOrganizationMemberForm(ModelForm):
|
||||||
label=_("New users"),
|
label=_("New users"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean(self):
|
def clean_new_users(self):
|
||||||
new_users = self.cleaned_data.get("new_users") or ""
|
new_users = self.cleaned_data.get("new_users") or ""
|
||||||
usernames = new_users.split()
|
usernames = new_users.split()
|
||||||
invalid_usernames = []
|
invalid_usernames = []
|
||||||
|
@ -374,8 +387,7 @@ class AddOrganizationMemberForm(ModelForm):
|
||||||
usernames=str(invalid_usernames)
|
usernames=str(invalid_usernames)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.cleaned_data["new_users"] = valid_usernames
|
return valid_usernames
|
||||||
return self.cleaned_data
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Organization
|
model = Organization
|
||||||
|
@ -423,13 +435,15 @@ class NewMessageForm(ModelForm):
|
||||||
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(
|
||||||
|
{"placeholder": _("Username/Email")}
|
||||||
|
)
|
||||||
self.fields["password"].widget.attrs.update({"placeholder": _("Password")})
|
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")
|
||||||
|
@ -492,6 +506,15 @@ class ContestCloneForm(Form):
|
||||||
max_length=20,
|
max_length=20,
|
||||||
validators=[RegexValidator("^[a-z0-9]+$", _("Contest id must be ^[a-z0-9]+$"))],
|
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"]
|
||||||
|
@ -499,6 +522,16 @@ class ContestCloneForm(Form):
|
||||||
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 ProblemPointsVoteForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -514,7 +547,7 @@ class ContestProblemForm(ModelForm):
|
||||||
"problem",
|
"problem",
|
||||||
"points",
|
"points",
|
||||||
"partial",
|
"partial",
|
||||||
"output_prefix_override",
|
"show_testcases",
|
||||||
"max_submissions",
|
"max_submissions",
|
||||||
)
|
)
|
||||||
widgets = {
|
widgets = {
|
||||||
|
@ -524,9 +557,43 @@ class ContestProblemForm(ModelForm):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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(
|
class ContestProblemFormSet(
|
||||||
formset_factory(
|
formset_factory(
|
||||||
ContestProblemForm, formset=BaseModelFormSet, extra=6, can_delete=True
|
ContestProblemForm, formset=ContestProblemModelFormSet, extra=6, can_delete=True
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
model = ContestProblem
|
model = ContestProblem
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormatterForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = TestFormatterModel
|
||||||
|
fields = ["file"]
|
||||||
|
|
|
@ -1,44 +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"
|
||||||
try:
|
return mark_safe(markdown(value))
|
||||||
import pygments
|
|
||||||
import pygments.lexers
|
|
||||||
import pygments.formatters
|
|
||||||
import pygments.util
|
|
||||||
except ImportError:
|
|
||||||
|
|
||||||
def highlight_code(code, language, cssclass=None):
|
|
||||||
return _make_pre_code(code)
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
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,
|
|
||||||
pygments.formatters.HtmlFormatter(
|
|
||||||
cssclass=cssclass, linenos="table", wrapcode=True
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return mark_safe(
|
|
||||||
pygments.highlight(
|
|
||||||
code,
|
|
||||||
lexer,
|
|
||||||
pygments.formatters.HtmlFormatter(cssclass=cssclass, wrapcode=True),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
|
@ -21,8 +21,8 @@ from . import (
|
||||||
render,
|
render,
|
||||||
social,
|
social,
|
||||||
spaceless,
|
spaceless,
|
||||||
submission,
|
|
||||||
timedelta,
|
timedelta,
|
||||||
|
comment,
|
||||||
)
|
)
|
||||||
from . import registry
|
from . import registry
|
||||||
|
|
||||||
|
|
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)
|
|
@ -23,5 +23,5 @@ 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}
|
||||||
|
|
|
@ -9,14 +9,16 @@ 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
|
|
||||||
elif isinstance(email, AbstractUser):
|
|
||||||
email = email.email
|
|
||||||
|
|
||||||
gravatar_url = (
|
gravatar_url = (
|
||||||
"//www.gravatar.com/avatar/"
|
"//www.gravatar.com/avatar/"
|
||||||
+ hashlib.md5(utf8bytes(email.strip().lower())).hexdigest()
|
+ hashlib.md5(utf8bytes(email.strip().lower())).hexdigest()
|
||||||
|
|
|
@ -1,112 +1,7 @@
|
||||||
from .. import registry
|
from .. import registry
|
||||||
import markdown as _markdown
|
from judge.markdown import markdown as _markdown
|
||||||
import bleach
|
|
||||||
from django.utils.html import escape
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from pymdownx import superfences
|
|
||||||
|
|
||||||
|
|
||||||
EXTENSIONS = [
|
|
||||||
"pymdownx.arithmatex",
|
|
||||||
"pymdownx.magiclink",
|
|
||||||
"pymdownx.betterem",
|
|
||||||
"pymdownx.details",
|
|
||||||
"pymdownx.emoji",
|
|
||||||
"pymdownx.inlinehilite",
|
|
||||||
"pymdownx.superfences",
|
|
||||||
"pymdownx.tasklist",
|
|
||||||
"markdown.extensions.footnotes",
|
|
||||||
"markdown.extensions.attr_list",
|
|
||||||
"markdown.extensions.def_list",
|
|
||||||
"markdown.extensions.tables",
|
|
||||||
"markdown.extensions.admonition",
|
|
||||||
"nl2br",
|
|
||||||
"mdx_breakless_lists",
|
|
||||||
]
|
|
||||||
|
|
||||||
EXTENSION_CONFIGS = {
|
|
||||||
"pymdownx.superfences": {
|
|
||||||
"custom_fences": [
|
|
||||||
{
|
|
||||||
"name": "sample",
|
|
||||||
"class": "no-border",
|
|
||||||
"format": superfences.fence_code_format,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + [
|
|
||||||
"img",
|
|
||||||
"center",
|
|
||||||
"iframe",
|
|
||||||
"div",
|
|
||||||
"span",
|
|
||||||
"table",
|
|
||||||
"tr",
|
|
||||||
"td",
|
|
||||||
"th",
|
|
||||||
"tr",
|
|
||||||
"pre",
|
|
||||||
"code",
|
|
||||||
"p",
|
|
||||||
"hr",
|
|
||||||
"h1",
|
|
||||||
"h2",
|
|
||||||
"h3",
|
|
||||||
"h4",
|
|
||||||
"h5",
|
|
||||||
"h6",
|
|
||||||
"thead",
|
|
||||||
"tbody",
|
|
||||||
"sup",
|
|
||||||
"dl",
|
|
||||||
"dt",
|
|
||||||
"dd",
|
|
||||||
"br",
|
|
||||||
"details",
|
|
||||||
"summary",
|
|
||||||
]
|
|
||||||
|
|
||||||
ALLOWED_ATTRS = ["src", "width", "height", "href", "class", "open"]
|
|
||||||
|
|
||||||
|
|
||||||
@registry.filter
|
@registry.filter
|
||||||
def markdown(value, lazy_load=False):
|
def markdown(value, lazy_load=False):
|
||||||
extensions = EXTENSIONS
|
return _markdown(value, lazy_load)
|
||||||
html = _markdown.markdown(
|
|
||||||
value, extensions=extensions, extension_configs=EXTENSION_CONFIGS
|
|
||||||
)
|
|
||||||
|
|
||||||
# Don't clean mathjax
|
|
||||||
hash_script_tag = {}
|
|
||||||
soup = BeautifulSoup(html, "html.parser")
|
|
||||||
for script_tag in soup.find_all("script"):
|
|
||||||
allow_math_types = ["math/tex", "math/tex; mode=display"]
|
|
||||||
if script_tag.attrs.get("type", False) in allow_math_types:
|
|
||||||
hash_script_tag[str(hash(str(script_tag)))] = str(script_tag)
|
|
||||||
|
|
||||||
for hashed_tag in hash_script_tag:
|
|
||||||
tag = hash_script_tag[hashed_tag]
|
|
||||||
html = html.replace(tag, hashed_tag)
|
|
||||||
|
|
||||||
html = bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS)
|
|
||||||
|
|
||||||
for hashed_tag in hash_script_tag:
|
|
||||||
tag = hash_script_tag[hashed_tag]
|
|
||||||
html = html.replace(hashed_tag, tag)
|
|
||||||
|
|
||||||
if not html:
|
|
||||||
html = escape(value)
|
|
||||||
if lazy_load:
|
|
||||||
soup = BeautifulSoup(html, features="html.parser")
|
|
||||||
for img in soup.findAll("img"):
|
|
||||||
if img.get("src"):
|
|
||||||
img["data-src"] = img["src"]
|
|
||||||
img["src"] = ""
|
|
||||||
for img in soup.findAll("iframe"):
|
|
||||||
if img.get("src"):
|
|
||||||
img["data-src"] = img["src"]
|
|
||||||
img["src"] = ""
|
|
||||||
html = str(soup)
|
|
||||||
return '<div class="md-typeset">%s</div>' % html
|
|
||||||
|
|
|
@ -155,16 +155,16 @@ def item_title(item):
|
||||||
|
|
||||||
@registry.function
|
@registry.function
|
||||||
@registry.render_with("user/link.html")
|
@registry.render_with("user/link.html")
|
||||||
def link_user(user):
|
def link_user(user, show_image=False):
|
||||||
if isinstance(user, Profile):
|
if isinstance(user, Profile):
|
||||||
user, profile = user.user, user
|
profile = user
|
||||||
elif isinstance(user, AbstractUser):
|
elif isinstance(user, AbstractUser):
|
||||||
profile = user.profile
|
profile = user.profile
|
||||||
elif type(user).__name__ == "ContestRankingProfile":
|
elif isinstance(user, int):
|
||||||
user, profile = user.user, user
|
profile = Profile(id=user)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Expected profile or user, got %s" % (type(user),))
|
raise ValueError("Expected profile or user, got %s" % (type(user),))
|
||||||
return {"user": user, "profile": profile}
|
return {"profile": profile, "show_image": show_image}
|
||||||
|
|
||||||
|
|
||||||
@registry.function
|
@registry.function
|
||||||
|
|
|
@ -48,5 +48,9 @@ for name, template, url_func in SHARES:
|
||||||
@registry.function
|
@registry.function
|
||||||
def recaptcha_init(language=None):
|
def recaptcha_init(language=None):
|
||||||
return get_template("snowpenguin/recaptcha/recaptcha_init.html").render(
|
return get_template("snowpenguin/recaptcha/recaptcha_init.html").render(
|
||||||
{"explicit": False, "language": language}
|
{
|
||||||
|
"explicit": False,
|
||||||
|
"language": language,
|
||||||
|
"recaptcha_host": "https://google.com",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
from . import registry
|
|
||||||
|
|
||||||
|
|
||||||
@registry.function
|
|
||||||
def submission_layout(
|
|
||||||
submission,
|
|
||||||
profile_id,
|
|
||||||
user,
|
|
||||||
editable_problem_ids,
|
|
||||||
completed_problem_ids,
|
|
||||||
tester_problem_ids,
|
|
||||||
):
|
|
||||||
problem_id = submission.problem_id
|
|
||||||
|
|
||||||
if problem_id in editable_problem_ids:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if problem_id in tester_problem_ids:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if profile_id == submission.user_id:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if user.has_perm("judge.change_submission"):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if user.has_perm("judge.view_all_submission"):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if submission.problem.is_public and user.has_perm("judge.view_public_submission"):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if hasattr(submission, "contest"):
|
|
||||||
contest = submission.contest.participation.contest
|
|
||||||
if contest.is_editable_by(user):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if submission.problem_id in completed_problem_ids and submission.problem.is_public:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
12
judge/logging.py
Normal file
12
judge/logging.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
error_log = logging.getLogger("judge.errors")
|
||||||
|
debug_log = logging.getLogger("judge.debug")
|
||||||
|
|
||||||
|
|
||||||
|
def log_exception(msg):
|
||||||
|
error_log.exception(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def log_debug(category, data):
|
||||||
|
debug_log.info(f"{category}: {data}")
|
|
@ -1,6 +1,5 @@
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from judge.models import *
|
from judge.models import *
|
||||||
from collections import defaultdict
|
|
||||||
import csv
|
import csv
|
||||||
import os
|
import os
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
|
@ -89,14 +89,13 @@ class Command(BaseCommand):
|
||||||
if trans is None
|
if trans is None
|
||||||
else trans.description,
|
else trans.description,
|
||||||
"url": "",
|
"url": "",
|
||||||
"math_engine": maker.math_engine,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.replace('"//', '"https://')
|
.replace('"//', '"https://')
|
||||||
.replace("'//", "'https://")
|
.replace("'//", "'https://")
|
||||||
)
|
)
|
||||||
maker.title = problem_name
|
maker.title = problem_name
|
||||||
for file in ("style.css", "pygment-github.css", "mathjax3_config.js"):
|
for file in "style.css":
|
||||||
maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file))
|
maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file))
|
||||||
maker.make(debug=True)
|
maker.make(debug=True)
|
||||||
if not maker.success:
|
if not maker.success:
|
||||||
|
|
149
judge/markdown.py
Normal file
149
judge/markdown.py
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
import markdown as _markdown
|
||||||
|
import bleach
|
||||||
|
from django.utils.html import escape
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from pymdownx import superfences
|
||||||
|
from django.conf import settings
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from judge.markdown_extensions import YouTubeExtension, EmoticonExtension
|
||||||
|
|
||||||
|
|
||||||
|
EXTENSIONS = [
|
||||||
|
"pymdownx.arithmatex",
|
||||||
|
"pymdownx.magiclink",
|
||||||
|
"pymdownx.betterem",
|
||||||
|
"pymdownx.details",
|
||||||
|
"pymdownx.emoji",
|
||||||
|
"pymdownx.inlinehilite",
|
||||||
|
"pymdownx.superfences",
|
||||||
|
"pymdownx.highlight",
|
||||||
|
"pymdownx.tasklist",
|
||||||
|
"markdown.extensions.footnotes",
|
||||||
|
"markdown.extensions.attr_list",
|
||||||
|
"markdown.extensions.def_list",
|
||||||
|
"markdown.extensions.tables",
|
||||||
|
"markdown.extensions.admonition",
|
||||||
|
"nl2br",
|
||||||
|
"mdx_breakless_lists",
|
||||||
|
YouTubeExtension(),
|
||||||
|
EmoticonExtension(),
|
||||||
|
]
|
||||||
|
|
||||||
|
EXTENSION_CONFIGS = {
|
||||||
|
"pymdownx.arithmatex": {
|
||||||
|
"generic": True,
|
||||||
|
},
|
||||||
|
"pymdownx.superfences": {
|
||||||
|
"custom_fences": [
|
||||||
|
{
|
||||||
|
"name": "sample",
|
||||||
|
"class": "no-border",
|
||||||
|
"format": superfences.fence_code_format,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"pymdownx.highlight": {
|
||||||
|
"auto_title": True,
|
||||||
|
"auto_title_map": {
|
||||||
|
"Text Only": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + [
|
||||||
|
"img",
|
||||||
|
"center",
|
||||||
|
"iframe",
|
||||||
|
"div",
|
||||||
|
"span",
|
||||||
|
"table",
|
||||||
|
"tr",
|
||||||
|
"td",
|
||||||
|
"th",
|
||||||
|
"tr",
|
||||||
|
"pre",
|
||||||
|
"code",
|
||||||
|
"p",
|
||||||
|
"hr",
|
||||||
|
"h1",
|
||||||
|
"h2",
|
||||||
|
"h3",
|
||||||
|
"h4",
|
||||||
|
"h5",
|
||||||
|
"h6",
|
||||||
|
"thead",
|
||||||
|
"tbody",
|
||||||
|
"sup",
|
||||||
|
"dl",
|
||||||
|
"dt",
|
||||||
|
"dd",
|
||||||
|
"br",
|
||||||
|
"details",
|
||||||
|
"summary",
|
||||||
|
]
|
||||||
|
|
||||||
|
ALLOWED_ATTRS = [
|
||||||
|
"src",
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"href",
|
||||||
|
"class",
|
||||||
|
"open",
|
||||||
|
"title",
|
||||||
|
"frameborder",
|
||||||
|
"allow",
|
||||||
|
"allowfullscreen",
|
||||||
|
"loading",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_img_iframe_with_lazy_load(soup):
|
||||||
|
for img in soup.findAll("img"):
|
||||||
|
if img.get("src"):
|
||||||
|
img["loading"] = "lazy"
|
||||||
|
for img in soup.findAll("iframe"):
|
||||||
|
if img.get("src"):
|
||||||
|
img["loading"] = "lazy"
|
||||||
|
return soup
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_images_with_featherlight(soup):
|
||||||
|
for img in soup.findAll("img"):
|
||||||
|
if img.get("src"):
|
||||||
|
link = soup.new_tag("a", href=img["src"], **{"data-featherlight": "image"})
|
||||||
|
img.wrap(link)
|
||||||
|
return soup
|
||||||
|
|
||||||
|
|
||||||
|
def _open_external_links_in_new_tab(soup):
|
||||||
|
domain = settings.SITE_DOMAIN.lower()
|
||||||
|
for a in soup.findAll("a", href=True):
|
||||||
|
href = a["href"]
|
||||||
|
if href.startswith("http://") or href.startswith("https://"):
|
||||||
|
link_domain = urlparse(href).netloc.lower()
|
||||||
|
if link_domain != domain:
|
||||||
|
a["target"] = "_blank"
|
||||||
|
return soup
|
||||||
|
|
||||||
|
|
||||||
|
def markdown(value, lazy_load=False):
|
||||||
|
extensions = EXTENSIONS
|
||||||
|
html = _markdown.markdown(
|
||||||
|
value, extensions=extensions, extension_configs=EXTENSION_CONFIGS
|
||||||
|
)
|
||||||
|
|
||||||
|
html = bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS)
|
||||||
|
|
||||||
|
if not html:
|
||||||
|
html = escape(value)
|
||||||
|
|
||||||
|
soup = BeautifulSoup(html, features="html.parser")
|
||||||
|
if lazy_load:
|
||||||
|
soup = _wrap_img_iframe_with_lazy_load(soup)
|
||||||
|
|
||||||
|
soup = _wrap_images_with_featherlight(soup)
|
||||||
|
soup = _open_external_links_in_new_tab(soup)
|
||||||
|
html = str(soup)
|
||||||
|
|
||||||
|
return '<div class="md-typeset content-description">%s</div>' % html
|
2
judge/markdown_extensions/__init__.py
Normal file
2
judge/markdown_extensions/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
from .youtube import YouTubeExtension
|
||||||
|
from .emoticon import EmoticonExtension
|
112
judge/markdown_extensions/emoticon.py
Normal file
112
judge/markdown_extensions/emoticon.py
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import markdown
|
||||||
|
from markdown.extensions import Extension
|
||||||
|
from markdown.inlinepatterns import InlineProcessor
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
import re
|
||||||
|
|
||||||
|
EMOTICON_EMOJI_MAP = {
|
||||||
|
":D": "\U0001F603", # Smiling Face with Open Mouth
|
||||||
|
":)": "\U0001F642", # Slightly Smiling Face
|
||||||
|
":-)": "\U0001F642", # Slightly Smiling Face with Nose
|
||||||
|
":(": "\U0001F641", # Slightly Frowning Face
|
||||||
|
":-(": "\U0001F641", # Slightly Frowning Face with Nose
|
||||||
|
";)": "\U0001F609", # Winking Face
|
||||||
|
";-)": "\U0001F609", # Winking Face with Nose
|
||||||
|
":P": "\U0001F61B", # Face with Tongue
|
||||||
|
":-P": "\U0001F61B", # Face with Tongue and Nose
|
||||||
|
":p": "\U0001F61B", # Face with Tongue
|
||||||
|
":-p": "\U0001F61B", # Face with Tongue and Nose
|
||||||
|
";P": "\U0001F61C", # Winking Face with Tongue
|
||||||
|
";-P": "\U0001F61C", # Winking Face with Tongue and Nose
|
||||||
|
";p": "\U0001F61C", # Winking Face with Tongue
|
||||||
|
";-p": "\U0001F61C", # Winking Face with Tongue and Nose
|
||||||
|
":'(": "\U0001F622", # Crying Face
|
||||||
|
":o": "\U0001F62E", # Face with Open Mouth
|
||||||
|
":-o": "\U0001F62E", # Face with Open Mouth and Nose
|
||||||
|
":O": "\U0001F62E", # Face with Open Mouth
|
||||||
|
":-O": "\U0001F62E", # Face with Open Mouth and Nose
|
||||||
|
":-0": "\U0001F62E", # Face with Open Mouth and Nose
|
||||||
|
">:(": "\U0001F620", # Angry Face
|
||||||
|
">:-(": "\U0001F620", # Angry Face with Nose
|
||||||
|
">:)": "\U0001F608", # Smiling Face with Horns
|
||||||
|
">:-)": "\U0001F608", # Smiling Face with Horns and Nose
|
||||||
|
"XD": "\U0001F606", # Grinning Squinting Face
|
||||||
|
"xD": "\U0001F606", # Grinning Squinting Face
|
||||||
|
"B)": "\U0001F60E", # Smiling Face with Sunglasses
|
||||||
|
"B-)": "\U0001F60E", # Smiling Face with Sunglasses and Nose
|
||||||
|
"O:)": "\U0001F607", # Smiling Face with Halo
|
||||||
|
"O:-)": "\U0001F607", # Smiling Face with Halo and Nose
|
||||||
|
"0:)": "\U0001F607", # Smiling Face with Halo
|
||||||
|
"0:-)": "\U0001F607", # Smiling Face with Halo and Nose
|
||||||
|
">:P": "\U0001F92A", # Zany Face (sticking out tongue and winking)
|
||||||
|
">:-P": "\U0001F92A", # Zany Face with Nose
|
||||||
|
">:p": "\U0001F92A", # Zany Face (sticking out tongue and winking)
|
||||||
|
">:-p": "\U0001F92A", # Zany Face with Nose
|
||||||
|
":/": "\U0001F615", # Confused Face
|
||||||
|
":-/": "\U0001F615", # Confused Face with Nose
|
||||||
|
":\\": "\U0001F615", # Confused Face
|
||||||
|
":-\\": "\U0001F615", # Confused Face with Nose
|
||||||
|
"3:)": "\U0001F608", # Smiling Face with Horns
|
||||||
|
"3:-)": "\U0001F608", # Smiling Face with Horns and Nose
|
||||||
|
"<3": "\u2764\uFE0F", # Red Heart
|
||||||
|
"</3": "\U0001F494", # Broken Heart
|
||||||
|
":*": "\U0001F618", # Face Blowing a Kiss
|
||||||
|
":-*": "\U0001F618", # Face Blowing a Kiss with Nose
|
||||||
|
";P": "\U0001F61C", # Winking Face with Tongue
|
||||||
|
";-P": "\U0001F61C",
|
||||||
|
">:P": "\U0001F61D", # Face with Stuck-Out Tongue and Tightly-Closed Eyes
|
||||||
|
":-/": "\U0001F615", # Confused Face
|
||||||
|
":/": "\U0001F615",
|
||||||
|
":\\": "\U0001F615",
|
||||||
|
":-\\": "\U0001F615",
|
||||||
|
":|": "\U0001F610", # Neutral Face
|
||||||
|
":-|": "\U0001F610",
|
||||||
|
"8)": "\U0001F60E", # Smiling Face with Sunglasses
|
||||||
|
"8-)": "\U0001F60E",
|
||||||
|
"O:)": "\U0001F607", # Smiling Face with Halo
|
||||||
|
"O:-)": "\U0001F607",
|
||||||
|
":3": "\U0001F60A", # Smiling Face with Smiling Eyes
|
||||||
|
"^.^": "\U0001F60A",
|
||||||
|
"-_-": "\U0001F611", # Expressionless Face
|
||||||
|
"T_T": "\U0001F62D", # Loudly Crying Face
|
||||||
|
"T.T": "\U0001F62D",
|
||||||
|
">.<": "\U0001F623", # Persevering Face
|
||||||
|
"x_x": "\U0001F635", # Dizzy Face
|
||||||
|
"X_X": "\U0001F635",
|
||||||
|
":]": "\U0001F600", # Grinning Face
|
||||||
|
":[": "\U0001F641", # Slightly Frowning Face
|
||||||
|
"=]": "\U0001F600",
|
||||||
|
"=[": "\U0001F641",
|
||||||
|
"D:<": "\U0001F621", # Pouting Face
|
||||||
|
"D:": "\U0001F629", # Weary Face
|
||||||
|
"D=": "\U0001F6AB", # No Entry Sign (sometimes used to denote dismay or frustration)
|
||||||
|
":'D": "\U0001F602", # Face with Tears of Joy
|
||||||
|
"D':": "\U0001F625", # Disappointed but Relieved Face
|
||||||
|
"D8": "\U0001F631", # Face Screaming in Fear
|
||||||
|
"-.-": "\U0001F644", # Face with Rolling Eyes
|
||||||
|
"-_-;": "\U0001F612", # Unamused
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class EmoticonEmojiInlineProcessor(InlineProcessor):
|
||||||
|
def handleMatch(self, m, data):
|
||||||
|
emoticon = m.group(1)
|
||||||
|
emoji = EMOTICON_EMOJI_MAP.get(emoticon, "")
|
||||||
|
if emoji:
|
||||||
|
el = etree.Element("span")
|
||||||
|
el.text = markdown.util.AtomicString(emoji)
|
||||||
|
el.set("class", "big-emoji")
|
||||||
|
return el, m.start(0), m.end(0)
|
||||||
|
else:
|
||||||
|
return None, m.start(0), m.end(0)
|
||||||
|
|
||||||
|
|
||||||
|
class EmoticonExtension(Extension):
|
||||||
|
def extendMarkdown(self, md):
|
||||||
|
emoticon_pattern = (
|
||||||
|
r"(?:(?<=\s)|^)" # Lookbehind for a whitespace character or the start of the string
|
||||||
|
r"(" + "|".join(map(re.escape, EMOTICON_EMOJI_MAP.keys())) + r")"
|
||||||
|
r"(?=\s|$)" # Lookahead for a whitespace character or the end of the string
|
||||||
|
)
|
||||||
|
emoticon_processor = EmoticonEmojiInlineProcessor(emoticon_pattern, md)
|
||||||
|
md.inlinePatterns.register(emoticon_processor, "emoticon_to_emoji", 1)
|
36
judge/markdown_extensions/youtube.py
Normal file
36
judge/markdown_extensions/youtube.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import markdown
|
||||||
|
from markdown.inlinepatterns import InlineProcessor
|
||||||
|
from markdown.extensions import Extension
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
|
YOUTUBE_REGEX = (
|
||||||
|
r"(https?://)?(www\.)?" "(youtube\.com/watch\?v=|youtu\.be/)" "([\w-]+)(&[\w=]*)?"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class YouTubeEmbedProcessor(InlineProcessor):
|
||||||
|
def handleMatch(self, m, data):
|
||||||
|
youtube_id = m.group(4)
|
||||||
|
if not youtube_id:
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
# Create an iframe element with the YouTube embed URL
|
||||||
|
iframe = etree.Element("iframe")
|
||||||
|
iframe.set("width", "100%")
|
||||||
|
iframe.set("height", "360")
|
||||||
|
iframe.set("src", f"https://www.youtube.com/embed/{youtube_id}")
|
||||||
|
iframe.set("frameborder", "0")
|
||||||
|
iframe.set("allowfullscreen", "true")
|
||||||
|
center = etree.Element("center")
|
||||||
|
center.append(iframe)
|
||||||
|
|
||||||
|
# Return the iframe as the element to replace the match, along with the start and end indices
|
||||||
|
return center, m.start(0), m.end(0)
|
||||||
|
|
||||||
|
|
||||||
|
class YouTubeExtension(Extension):
|
||||||
|
def extendMarkdown(self, md):
|
||||||
|
# Create the YouTube link pattern
|
||||||
|
YOUTUBE_PATTERN = YouTubeEmbedProcessor(YOUTUBE_REGEX, md)
|
||||||
|
# Register the pattern to apply the YouTubeEmbedProcessor
|
||||||
|
md.inlinePatterns.register(YOUTUBE_PATTERN, "youtube", 175)
|
|
@ -1,7 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import lxml.html as lh
|
import lxml.html as lh
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
from lxml.html.clean import clean_html
|
from lxml_html_clean import clean_html
|
||||||
|
|
||||||
|
|
||||||
def strip_error_html(apps, schema_editor):
|
def strip_error_html(apps, schema_editor):
|
||||||
|
|
File diff suppressed because one or more lines are too long
48
judge/migrations/0156_auto_20230801_0107.py
Normal file
48
judge/migrations/0156_auto_20230801_0107.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-07-31 18:07
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import judge.models.problem_data
|
||||||
|
import judge.utils.problem_data
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0155_output_only"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="problemdata",
|
||||||
|
name="signature_handler",
|
||||||
|
field=models.FileField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
storage=judge.utils.problem_data.ProblemDataStorage(),
|
||||||
|
upload_to=judge.models.problem_data.problem_directory_file,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.FileExtensionValidator(
|
||||||
|
allowed_extensions=["cpp"]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
verbose_name="signature handler",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="problemdata",
|
||||||
|
name="signature_header",
|
||||||
|
field=models.FileField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
storage=judge.utils.problem_data.ProblemDataStorage(),
|
||||||
|
upload_to=judge.models.problem_data.problem_directory_file,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.FileExtensionValidator(
|
||||||
|
allowed_extensions=["h"]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
verbose_name="signature header",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
22
judge/migrations/0157_auto_20230801_1145.py
Normal file
22
judge/migrations/0157_auto_20230801_1145.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-08-01 04:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0156_auto_20230801_0107"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="problemdata",
|
||||||
|
name="use_ioi_signature",
|
||||||
|
field=models.BooleanField(
|
||||||
|
help_text="Use IOI Signature",
|
||||||
|
null=True,
|
||||||
|
verbose_name="is IOI signature",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
70
judge/migrations/0158_migrate_pagevote.py
Normal file
70
judge/migrations/0158_migrate_pagevote.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-08-03 07:40
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_pagevote(apps, schema_editor):
|
||||||
|
PageVote = apps.get_model("judge", "PageVote")
|
||||||
|
Problem = apps.get_model("judge", "Problem")
|
||||||
|
Solution = apps.get_model("judge", "Solution")
|
||||||
|
BlogPost = apps.get_model("judge", "BlogPost")
|
||||||
|
Contest = apps.get_model("judge", "Contest")
|
||||||
|
|
||||||
|
for vote in PageVote.objects.all():
|
||||||
|
page = vote.page
|
||||||
|
try:
|
||||||
|
if page.startswith("p:"):
|
||||||
|
code = page[2:]
|
||||||
|
vote.linked_object = Problem.objects.get(code=code)
|
||||||
|
elif page.startswith("s:"):
|
||||||
|
code = page[2:]
|
||||||
|
vote.linked_object = Solution.objects.get(problem__code=code)
|
||||||
|
elif page.startswith("c:"):
|
||||||
|
key = page[2:]
|
||||||
|
vote.linked_object = Contest.objects.get(key=key)
|
||||||
|
elif page.startswith("b:"):
|
||||||
|
blog_id = page[2:]
|
||||||
|
vote.linked_object = BlogPost.objects.get(id=blog_id)
|
||||||
|
vote.save()
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
vote.delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("contenttypes", "0002_remove_content_type_name"),
|
||||||
|
("judge", "0157_auto_20230801_1145"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="pagevote",
|
||||||
|
name="content_type",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="contenttypes.contenttype",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="pagevote",
|
||||||
|
name="object_id",
|
||||||
|
field=models.PositiveIntegerField(default=None),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="pagevote",
|
||||||
|
unique_together={("content_type", "object_id")},
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="pagevote",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["content_type", "object_id"],
|
||||||
|
name="judge_pagev_content_ed8899_idx",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate_pagevote, migrations.RunPython.noop, atomic=True),
|
||||||
|
]
|
23
judge/migrations/0159_auto_20230803_1518.py
Normal file
23
judge/migrations/0159_auto_20230803_1518.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-08-03 08:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("contenttypes", "0002_remove_content_type_name"),
|
||||||
|
("judge", "0158_migrate_pagevote"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="pagevote",
|
||||||
|
name="content_type",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="contenttypes.contenttype",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
80
judge/migrations/0160_migrate_bookmark.py
Normal file
80
judge/migrations/0160_migrate_bookmark.py
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-08-03 08:32
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_bookmark(apps, schema_editor):
|
||||||
|
BookMark = apps.get_model("judge", "BookMark")
|
||||||
|
Problem = apps.get_model("judge", "Problem")
|
||||||
|
Solution = apps.get_model("judge", "Solution")
|
||||||
|
BlogPost = apps.get_model("judge", "BlogPost")
|
||||||
|
Contest = apps.get_model("judge", "Contest")
|
||||||
|
|
||||||
|
for bookmark in BookMark.objects.all():
|
||||||
|
page = bookmark.page
|
||||||
|
try:
|
||||||
|
if page.startswith("p:"):
|
||||||
|
code = page[2:]
|
||||||
|
bookmark.linked_object = Problem.objects.get(code=code)
|
||||||
|
elif page.startswith("s:"):
|
||||||
|
code = page[2:]
|
||||||
|
bookmark.linked_object = Solution.objects.get(problem__code=code)
|
||||||
|
elif page.startswith("c:"):
|
||||||
|
key = page[2:]
|
||||||
|
bookmark.linked_object = Contest.objects.get(key=key)
|
||||||
|
elif page.startswith("b:"):
|
||||||
|
blog_id = page[2:]
|
||||||
|
bookmark.linked_object = BlogPost.objects.get(id=blog_id)
|
||||||
|
bookmark.save()
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
bookmark.delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("contenttypes", "0002_remove_content_type_name"),
|
||||||
|
("judge", "0159_auto_20230803_1518"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="bookmark",
|
||||||
|
name="content_type",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="contenttypes.contenttype",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="bookmark",
|
||||||
|
name="object_id",
|
||||||
|
field=models.PositiveIntegerField(default=1),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="bookmark",
|
||||||
|
name="score",
|
||||||
|
field=models.IntegerField(default=0, verbose_name="votes"),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="bookmark",
|
||||||
|
unique_together={("content_type", "object_id")},
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="bookmark",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["content_type", "object_id"],
|
||||||
|
name="judge_bookm_content_964329_idx",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="makebookmark",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["user", "bookmark"], name="judge_makeb_user_id_f0e226_idx"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate_bookmark, migrations.RunPython.noop, atomic=True),
|
||||||
|
]
|
28
judge/migrations/0161_auto_20230803_1536.py
Normal file
28
judge/migrations/0161_auto_20230803_1536.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-08-03 08:36
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("contenttypes", "0002_remove_content_type_name"),
|
||||||
|
("judge", "0160_migrate_bookmark"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="bookmark",
|
||||||
|
name="content_type",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="contenttypes.contenttype",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="bookmark",
|
||||||
|
name="object_id",
|
||||||
|
field=models.PositiveIntegerField(),
|
||||||
|
),
|
||||||
|
]
|
21
judge/migrations/0162_profile_image.py
Normal file
21
judge/migrations/0162_profile_image.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-08-24 00:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import judge.models.profile
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0161_auto_20230803_1536"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="profile",
|
||||||
|
name="profile_image",
|
||||||
|
field=models.ImageField(
|
||||||
|
null=True, upload_to=judge.models.profile.profile_image_path
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
18
judge/migrations/0163_email_change.py
Normal file
18
judge/migrations/0163_email_change.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-08-25 00:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0162_profile_image"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="profile",
|
||||||
|
name="email_change_pending",
|
||||||
|
field=models.EmailField(blank=True, max_length=254, null=True),
|
||||||
|
),
|
||||||
|
]
|
30
judge/migrations/0164_show_testcase.py
Normal file
30
judge/migrations/0164_show_testcase.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-08-25 23:03
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_show_testcases(apps, schema_editor):
|
||||||
|
ContestProblem = apps.get_model("judge", "ContestProblem")
|
||||||
|
|
||||||
|
for c in ContestProblem.objects.all():
|
||||||
|
if c.output_prefix_override == 1:
|
||||||
|
c.show_testcases = True
|
||||||
|
c.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0163_email_change"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="contestproblem",
|
||||||
|
name="show_testcases",
|
||||||
|
field=models.BooleanField(default=False, verbose_name="visible testcases"),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
migrate_show_testcases, migrations.RunPython.noop, atomic=True
|
||||||
|
),
|
||||||
|
]
|
17
judge/migrations/0165_drop_output_prefix_override.py
Normal file
17
judge/migrations/0165_drop_output_prefix_override.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-08-25 23:11
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0164_show_testcase"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="contestproblem",
|
||||||
|
name="output_prefix_override",
|
||||||
|
),
|
||||||
|
]
|
28
judge/migrations/0166_display_rank_index.py
Normal file
28
judge/migrations/0166_display_rank_index.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-08-28 01:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0165_drop_output_prefix_override"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="profile",
|
||||||
|
name="display_rank",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("user", "Normal User"),
|
||||||
|
("setter", "Problem Setter"),
|
||||||
|
("admin", "Admin"),
|
||||||
|
],
|
||||||
|
db_index=True,
|
||||||
|
default="user",
|
||||||
|
max_length=10,
|
||||||
|
verbose_name="display rank",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
32
judge/migrations/0167_ultimate_contest_format.py
Normal file
32
judge/migrations/0167_ultimate_contest_format.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-09-01 00:09
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0166_display_rank_index"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="contest",
|
||||||
|
name="format_name",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("atcoder", "AtCoder"),
|
||||||
|
("default", "Default"),
|
||||||
|
("ecoo", "ECOO"),
|
||||||
|
("icpc", "ICPC"),
|
||||||
|
("ioi", "IOI"),
|
||||||
|
("ioi16", "New IOI"),
|
||||||
|
("ultimate", "Ultimate"),
|
||||||
|
],
|
||||||
|
default="default",
|
||||||
|
help_text="The contest format module to use.",
|
||||||
|
max_length=32,
|
||||||
|
verbose_name="contest format",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
24
judge/migrations/0168_css_background.py
Normal file
24
judge/migrations/0168_css_background.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-09-02 00:30
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0167_ultimate_contest_format"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="profile",
|
||||||
|
name="css_background",
|
||||||
|
field=models.TextField(
|
||||||
|
blank=True,
|
||||||
|
help_text='CSS custom background properties: url("image_url"), color, etc',
|
||||||
|
max_length=300,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Custom background",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
22
judge/migrations/0169_public_scoreboard.py
Normal file
22
judge/migrations/0169_public_scoreboard.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-09-17 01:55
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0168_css_background"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="contest",
|
||||||
|
name="public_scoreboard",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Ranking page is public even for private contests.",
|
||||||
|
verbose_name="public scoreboard",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
30
judge/migrations/0170_contests_summary.py
Normal file
30
judge/migrations/0170_contests_summary.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 3.2.21 on 2023-10-02 03:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0169_public_scoreboard"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ContestsSummary",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("scores", models.JSONField(blank=True, null=True)),
|
||||||
|
("key", models.CharField(max_length=20, unique=True)),
|
||||||
|
("contests", models.ManyToManyField(to="judge.Contest")),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
68
judge/migrations/0171_update_notification.py
Normal file
68
judge/migrations/0171_update_notification.py
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-10-10 21:17
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
|
||||||
|
# Run this in shell
|
||||||
|
def migrate_notif(apps, schema_editor):
|
||||||
|
Notification = apps.get_model("judge", "Notification")
|
||||||
|
Profile = apps.get_model("judge", "Profile")
|
||||||
|
NotificationProfile = apps.get_model("judge", "NotificationProfile")
|
||||||
|
|
||||||
|
unread_count = defaultdict(int)
|
||||||
|
for c in Notification.objects.all():
|
||||||
|
if c.comment:
|
||||||
|
c.html_link = (
|
||||||
|
f'<a href="{c.comment.get_absolute_url()}">{c.comment.page_title}</a>'
|
||||||
|
)
|
||||||
|
c.author = c.comment.author
|
||||||
|
c.save()
|
||||||
|
if c.read is False:
|
||||||
|
unread_count[c.author] += 1
|
||||||
|
|
||||||
|
for user in unread_count:
|
||||||
|
np = NotificationProfile(user=user)
|
||||||
|
np.unread_count = unread_count[user]
|
||||||
|
np.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0170_contests_summary"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="contestssummary",
|
||||||
|
options={
|
||||||
|
"verbose_name": "contests summary",
|
||||||
|
"verbose_name_plural": "contests summaries",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="NotificationProfile",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("unread_count", models.IntegerField(default=0)),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="judge.profile"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
18
judge/migrations/0172_index_rating.py
Normal file
18
judge/migrations/0172_index_rating.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-10-10 23:15
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0171_update_notification"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="profile",
|
||||||
|
name="rating",
|
||||||
|
field=models.IntegerField(db_index=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
]
|
25
judge/migrations/0173_fulltext.py
Normal file
25
judge/migrations/0173_fulltext.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-10-14 00:53
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0172_index_rating"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunSQL(
|
||||||
|
(
|
||||||
|
"CREATE FULLTEXT INDEX IF NOT EXISTS code_name_index ON judge_problem (code, name)",
|
||||||
|
),
|
||||||
|
reverse_sql=migrations.RunSQL.noop,
|
||||||
|
),
|
||||||
|
migrations.RunSQL(
|
||||||
|
(
|
||||||
|
"CREATE FULLTEXT INDEX IF NOT EXISTS key_name_index ON judge_contest (`key`, name)",
|
||||||
|
),
|
||||||
|
reverse_sql=migrations.RunSQL.noop,
|
||||||
|
),
|
||||||
|
]
|
50
judge/migrations/0174_contest_summary_result.py
Normal file
50
judge/migrations/0174_contest_summary_result.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-11-24 05:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0173_fulltext"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="contestssummary",
|
||||||
|
name="results",
|
||||||
|
field=models.JSONField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="contest",
|
||||||
|
name="authors",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
help_text="These users will be able to edit the contest.",
|
||||||
|
related_name="_judge_contest_authors_+",
|
||||||
|
to="judge.Profile",
|
||||||
|
verbose_name="authors",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="contest",
|
||||||
|
name="curators",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
help_text="These users will be able to edit the contest, but will not be listed as authors.",
|
||||||
|
related_name="_judge_contest_curators_+",
|
||||||
|
to="judge.Profile",
|
||||||
|
verbose_name="curators",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="contest",
|
||||||
|
name="testers",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
help_text="These users will be able to view the contest, but not edit it.",
|
||||||
|
related_name="_judge_contest_testers_+",
|
||||||
|
to="judge.Profile",
|
||||||
|
verbose_name="testers",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
20
judge/migrations/0175_add_profile_index.py
Normal file
20
judge/migrations/0175_add_profile_index.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-11-29 02:26
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0174_contest_summary_result"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="profile",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["is_unlisted", "performance_points"],
|
||||||
|
name="judge_profi_is_unli_d4034c_idx",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
30
judge/migrations/0176_comment_revision_count.py
Normal file
30
judge/migrations/0176_comment_revision_count.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-12-06 01:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
# Run this in shell
|
||||||
|
def migrate_revision(apps, schema_editor):
|
||||||
|
Comment = apps.get_model("judge", "Comment")
|
||||||
|
|
||||||
|
for c in Comment.objects.all():
|
||||||
|
c.revision_count = c.versions.count()
|
||||||
|
c.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0175_add_profile_index"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="comment",
|
||||||
|
name="revision_count",
|
||||||
|
field=models.PositiveIntegerField(default=1),
|
||||||
|
),
|
||||||
|
# migrations.RunPython(
|
||||||
|
# migrate_revision, migrations.RunPython.noop, atomic=True
|
||||||
|
# ),
|
||||||
|
]
|
35
judge/migrations/0177_test_formatter.py
Normal file
35
judge/migrations/0177_test_formatter.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
from django.db import migrations, models
|
||||||
|
import judge.models.test_formatter
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0176_comment_revision_count"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="TestFormatterModel",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"file",
|
||||||
|
models.FileField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
upload_to=judge.models.test_formatter.test_formatter_path,
|
||||||
|
verbose_name="testcase file",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
]
|
17
judge/migrations/0178_remove_user_script.py
Normal file
17
judge/migrations/0178_remove_user_script.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 3.2.18 on 2024-01-14 01:04
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0177_test_formatter"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="profile",
|
||||||
|
name="user_script",
|
||||||
|
),
|
||||||
|
]
|
19
judge/migrations/0179_submission_result_lang_index.py
Normal file
19
judge/migrations/0179_submission_result_lang_index.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 3.2.18 on 2024-01-23 00:32
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0178_remove_user_script"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="submission",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["language", "result"], name="judge_submi_languag_874af4_idx"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
78
judge/migrations/0180_course.py
Normal file
78
judge/migrations/0180_course.py
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
# Generated by Django 3.2.18 on 2024-02-15 02:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0179_submission_result_lang_index"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="CourseLesson",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("title", models.TextField(verbose_name="course title")),
|
||||||
|
("content", models.TextField(verbose_name="course content")),
|
||||||
|
("order", models.IntegerField(default=0, verbose_name="order")),
|
||||||
|
("points", models.IntegerField(verbose_name="points")),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="courseresource",
|
||||||
|
name="course",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="course",
|
||||||
|
name="ending_time",
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="courserole",
|
||||||
|
name="course",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="judge.course",
|
||||||
|
verbose_name="course",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="CourseAssignment",
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="CourseResource",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="courselesson",
|
||||||
|
name="course",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="judge.course",
|
||||||
|
verbose_name="course",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="courselesson",
|
||||||
|
name="problems",
|
||||||
|
field=models.ManyToManyField(to="judge.Problem"),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="courserole",
|
||||||
|
unique_together={("course", "user")},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="courselesson",
|
||||||
|
name="problems",
|
||||||
|
field=models.ManyToManyField(blank=True, to="judge.Problem"),
|
||||||
|
),
|
||||||
|
]
|
50
judge/migrations/0181_remove_math_engine.py
Normal file
50
judge/migrations/0181_remove_math_engine.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# Generated by Django 3.2.18 on 2024-02-26 20:43
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0180_course"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="profile",
|
||||||
|
name="math_engine",
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="course",
|
||||||
|
name="about",
|
||||||
|
field=models.TextField(verbose_name="course description"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="courselesson",
|
||||||
|
name="course",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="lessons",
|
||||||
|
to="judge.course",
|
||||||
|
verbose_name="course",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="courselesson",
|
||||||
|
name="problems",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, to="judge.Problem", verbose_name="problem"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="courserole",
|
||||||
|
name="user",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="course_roles",
|
||||||
|
to="judge.profile",
|
||||||
|
verbose_name="user",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
75
judge/migrations/0182_rename_customcpp.py
Normal file
75
judge/migrations/0182_rename_customcpp.py
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
# Generated by Django 3.2.18 on 2024-03-19 04:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_checker(apps, schema_editor):
|
||||||
|
ProblemData = apps.get_model("judge", "ProblemData")
|
||||||
|
ProblemTestCase = apps.get_model("judge", "ProblemTestCase")
|
||||||
|
|
||||||
|
for p in ProblemData.objects.all():
|
||||||
|
if p.checker == "customval":
|
||||||
|
p.checker = "customcpp"
|
||||||
|
p.save()
|
||||||
|
|
||||||
|
for p in ProblemTestCase.objects.all():
|
||||||
|
if p.checker == "customval":
|
||||||
|
p.checker = "customcpp"
|
||||||
|
p.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0181_remove_math_engine"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(migrate_checker, migrations.RunPython.noop, atomic=True),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="problemdata",
|
||||||
|
name="checker",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("standard", "Standard"),
|
||||||
|
("floats", "Floats"),
|
||||||
|
("floatsabs", "Floats (absolute)"),
|
||||||
|
("floatsrel", "Floats (relative)"),
|
||||||
|
("rstripped", "Non-trailing spaces"),
|
||||||
|
("sorted", "Unordered"),
|
||||||
|
("identical", "Byte identical"),
|
||||||
|
("linecount", "Line-by-line"),
|
||||||
|
("custom", "Custom checker (PY)"),
|
||||||
|
("customcpp", "Custom checker (CPP)"),
|
||||||
|
("interact", "Interactive"),
|
||||||
|
("testlib", "Testlib"),
|
||||||
|
],
|
||||||
|
max_length=10,
|
||||||
|
verbose_name="checker",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="problemtestcase",
|
||||||
|
name="checker",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("standard", "Standard"),
|
||||||
|
("floats", "Floats"),
|
||||||
|
("floatsabs", "Floats (absolute)"),
|
||||||
|
("floatsrel", "Floats (relative)"),
|
||||||
|
("rstripped", "Non-trailing spaces"),
|
||||||
|
("sorted", "Unordered"),
|
||||||
|
("identical", "Byte identical"),
|
||||||
|
("linecount", "Line-by-line"),
|
||||||
|
("custom", "Custom checker (PY)"),
|
||||||
|
("customcpp", "Custom checker (CPP)"),
|
||||||
|
("interact", "Interactive"),
|
||||||
|
("testlib", "Testlib"),
|
||||||
|
],
|
||||||
|
max_length=10,
|
||||||
|
verbose_name="checker",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
45
judge/migrations/0183_rename_custom_checker_cpp.py
Normal file
45
judge/migrations/0183_rename_custom_checker_cpp.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# Generated by Django 3.2.18 on 2024-03-19 04:45
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import judge.models.problem_data
|
||||||
|
import judge.utils.problem_data
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_checker(apps, schema_editor):
|
||||||
|
ProblemData = apps.get_model("judge", "ProblemData")
|
||||||
|
|
||||||
|
for p in ProblemData.objects.all():
|
||||||
|
p.custom_checker_cpp = p.custom_validator
|
||||||
|
p.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0182_rename_customcpp"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="problemdata",
|
||||||
|
name="custom_checker_cpp",
|
||||||
|
field=models.FileField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
storage=judge.utils.problem_data.ProblemDataStorage(),
|
||||||
|
upload_to=judge.models.problem_data.problem_directory_file,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.FileExtensionValidator(
|
||||||
|
allowed_extensions=["cpp"]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
verbose_name="custom cpp checker file",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate_checker, migrations.RunPython.noop, atomic=True),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="problemdata",
|
||||||
|
name="custom_validator",
|
||||||
|
),
|
||||||
|
]
|
28
judge/migrations/0184_contest_rate_limit.py
Normal file
28
judge/migrations/0184_contest_rate_limit.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 3.2.18 on 2024-03-23 04:07
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0183_rename_custom_checker_cpp"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="contest",
|
||||||
|
name="rate_limit",
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Maximum number of submissions per minute. Leave empty if you don't want rate limit.",
|
||||||
|
null=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(5),
|
||||||
|
],
|
||||||
|
verbose_name="rate limit",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
18
judge/migrations/0185_rename_org_profile_colum.py
Normal file
18
judge/migrations/0185_rename_org_profile_colum.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.18 on 2024-04-12 05:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0184_contest_rate_limit"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="organizationprofile",
|
||||||
|
old_name="users",
|
||||||
|
new_name="profile",
|
||||||
|
),
|
||||||
|
]
|
43
judge/migrations/0186_change_about_fields_max_len.py
Normal file
43
judge/migrations/0186_change_about_fields_max_len.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
# Generated by Django 3.2.18 on 2024-04-12 17:04
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def truncate_about_text(apps, schema_editor):
|
||||||
|
Organization = apps.get_model("judge", "Organization")
|
||||||
|
Profile = apps.get_model("judge", "Profile")
|
||||||
|
|
||||||
|
for org in Organization.objects.all():
|
||||||
|
if len(org.about) > 10000:
|
||||||
|
org.about = org.about[:10000]
|
||||||
|
org.save()
|
||||||
|
|
||||||
|
for profile in Profile.objects.all():
|
||||||
|
if profile.about and len(profile.about) > 10000:
|
||||||
|
profile.about = profile.about[:10000]
|
||||||
|
profile.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0185_rename_org_profile_colum"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(truncate_about_text),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="organization",
|
||||||
|
name="about",
|
||||||
|
field=models.CharField(
|
||||||
|
max_length=10000, verbose_name="organization description"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="profile",
|
||||||
|
name="about",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, max_length=10000, null=True, verbose_name="self-description"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
69
judge/migrations/0187_profile_info.py
Normal file
69
judge/migrations/0187_profile_info.py
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
# Generated by Django 3.2.18 on 2024-04-27 03:35
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0186_change_about_fields_max_len"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="profile",
|
||||||
|
name="is_banned_problem_voting",
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ProfileInfo",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tshirt_size",
|
||||||
|
models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("S", "Small (S)"),
|
||||||
|
("M", "Medium (M)"),
|
||||||
|
("L", "Large (L)"),
|
||||||
|
("XL", "Extra Large (XL)"),
|
||||||
|
("XXL", "2 Extra Large (XXL)"),
|
||||||
|
],
|
||||||
|
max_length=5,
|
||||||
|
null=True,
|
||||||
|
verbose_name="t-shirt size",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"date_of_birth",
|
||||||
|
models.DateField(
|
||||||
|
blank=True, null=True, verbose_name="date of birth"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"address",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=255, null=True, verbose_name="address"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"profile",
|
||||||
|
models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="info",
|
||||||
|
to="judge.profile",
|
||||||
|
verbose_name="profile associated",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
110
judge/migrations/0188_official_contest.py
Normal file
110
judge/migrations/0188_official_contest.py
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
# Generated by Django 3.2.18 on 2024-05-30 04:32
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0187_profile_info"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="OfficialContestCategory",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
unique=True,
|
||||||
|
verbose_name="official contest category",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "official contest category",
|
||||||
|
"verbose_name_plural": "official contest categories",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="OfficialContestLocation",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
unique=True,
|
||||||
|
verbose_name="official contest location",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "official contest location",
|
||||||
|
"verbose_name_plural": "official contest locations",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="OfficialContest",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("year", models.PositiveIntegerField(verbose_name="year")),
|
||||||
|
(
|
||||||
|
"category",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="judge.officialcontestcategory",
|
||||||
|
verbose_name="contest category",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"contest",
|
||||||
|
models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="official",
|
||||||
|
to="judge.contest",
|
||||||
|
verbose_name="contest",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"location",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="judge.officialcontestlocation",
|
||||||
|
verbose_name="contest location",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "official contest",
|
||||||
|
"verbose_name_plural": "official contests",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
21
judge/migrations/0189_organization_image.py
Normal file
21
judge/migrations/0189_organization_image.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Django 3.2.21 on 2024-07-08 00:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import judge.models.profile
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0188_official_contest"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="organization",
|
||||||
|
name="organization_image",
|
||||||
|
field=models.ImageField(
|
||||||
|
null=True, upload_to=judge.models.profile.organization_image_path
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
21
judge/migrations/0190_deprecate_old_notif_fields.py
Normal file
21
judge/migrations/0190_deprecate_old_notif_fields.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Django 3.2.18 on 2024-08-13 10:36
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0189_organization_image"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="notification",
|
||||||
|
name="comment",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="notification",
|
||||||
|
name="read",
|
||||||
|
),
|
||||||
|
]
|
17
judge/migrations/0191_deprecate_old_org_image.py
Normal file
17
judge/migrations/0191_deprecate_old_org_image.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 3.2.18 on 2024-08-22 03:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0190_deprecate_old_notif_fields"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="organization",
|
||||||
|
name="logo_override_image",
|
||||||
|
),
|
||||||
|
]
|
44
judge/migrations/0192_course_lesson_problem.py
Normal file
44
judge/migrations/0192_course_lesson_problem.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
# Generated by Django 3.2.21 on 2024-09-02 05:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0191_deprecate_old_org_image"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="CourseLessonProblem",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("order", models.IntegerField(default=0, verbose_name="order")),
|
||||||
|
("score", models.IntegerField(default=0, verbose_name="score")),
|
||||||
|
(
|
||||||
|
"lesson",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="lesson_problems",
|
||||||
|
to="judge.courselesson",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"problem",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="judge.problem"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
29
judge/migrations/0193_remove_old_course_problems.py
Normal file
29
judge/migrations/0193_remove_old_course_problems.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# Generated by Django 3.2.18 on 2024-09-03 14:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_problems_to_courselessonproblem(apps, schema_editor):
|
||||||
|
CourseLesson = apps.get_model("judge", "CourseLesson")
|
||||||
|
CourseLessonProblem = apps.get_model("judge", "CourseLessonProblem")
|
||||||
|
|
||||||
|
for lesson in CourseLesson.objects.all():
|
||||||
|
for problem in lesson.problems.all():
|
||||||
|
CourseLessonProblem.objects.create(
|
||||||
|
lesson=lesson, problem=problem, order=1, score=1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0192_course_lesson_problem"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(migrate_problems_to_courselessonproblem),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="courselesson",
|
||||||
|
name="problems",
|
||||||
|
),
|
||||||
|
]
|
67
judge/migrations/0194_course_contest.py
Normal file
67
judge/migrations/0194_course_contest.py
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
# Generated by Django 3.2.21 on 2024-09-30 22:31
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0193_remove_old_course_problems"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="contest",
|
||||||
|
name="is_in_course",
|
||||||
|
field=models.BooleanField(default=False, verbose_name="contest in course"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="courselesson",
|
||||||
|
name="is_visible",
|
||||||
|
field=models.BooleanField(default=True, verbose_name="publicly visible"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="courselesson",
|
||||||
|
name="content",
|
||||||
|
field=models.TextField(verbose_name="lesson content"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="courselesson",
|
||||||
|
name="title",
|
||||||
|
field=models.TextField(verbose_name="lesson title"),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="CourseContest",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("order", models.IntegerField(default=0, verbose_name="order")),
|
||||||
|
("points", models.IntegerField(verbose_name="points")),
|
||||||
|
(
|
||||||
|
"contest",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="course",
|
||||||
|
to="judge.contest",
|
||||||
|
unique=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"course",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="contests",
|
||||||
|
to="judge.course",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,9 +1,10 @@
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from django.conf import settings
|
|
||||||
import os
|
import os
|
||||||
from django.core.cache import cache
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from judge.caching import cache_wrapper
|
from judge.caching import cache_wrapper
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,67 +14,69 @@ class CollabFilter:
|
||||||
|
|
||||||
# name = 'collab_filter' or 'collab_filter_time'
|
# name = 'collab_filter' or 'collab_filter_time'
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
embeddings = np.load(
|
self.embeddings = np.load(
|
||||||
os.path.join(settings.ML_OUTPUT_PATH, name + "/embeddings.npz"),
|
os.path.join(settings.ML_OUTPUT_PATH, name + "/embeddings.npz"),
|
||||||
allow_pickle=True,
|
allow_pickle=True,
|
||||||
)
|
)
|
||||||
arr0, arr1 = embeddings.files
|
_, problem_arr = self.embeddings.files
|
||||||
self.name = name
|
self.name = name
|
||||||
self.user_embeddings = embeddings[arr0]
|
self.problem_embeddings = self.embeddings[problem_arr].item()
|
||||||
self.problem_embeddings = embeddings[arr1]
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def compute_scores(self, query_embedding, item_embeddings, measure=DOT):
|
def compute_scores(self, query_embedding, item_embeddings, measure=DOT):
|
||||||
"""Computes the scores of the candidates given a query.
|
"""Return {id: score}"""
|
||||||
Args:
|
|
||||||
query_embedding: a vector of shape [k], representing the query embedding.
|
|
||||||
item_embeddings: a matrix of shape [N, k], such that row i is the embedding
|
|
||||||
of item i.
|
|
||||||
measure: a string specifying the similarity measure to be used. Can be
|
|
||||||
either DOT or COSINE.
|
|
||||||
Returns:
|
|
||||||
scores: a vector of shape [N], such that scores[i] is the score of item i.
|
|
||||||
"""
|
|
||||||
u = query_embedding
|
u = query_embedding
|
||||||
V = item_embeddings
|
V = np.stack(list(item_embeddings.values()))
|
||||||
if measure == self.COSINE:
|
if measure == self.COSINE:
|
||||||
V = V / np.linalg.norm(V, axis=1, keepdims=True)
|
V = V / np.linalg.norm(V, axis=1, keepdims=True)
|
||||||
u = u / np.linalg.norm(u)
|
u = u / np.linalg.norm(u)
|
||||||
scores = u.dot(V.T)
|
scores = u.dot(V.T)
|
||||||
return scores
|
scores_by_id = {id_: s for id_, s in zip(item_embeddings.keys(), scores)}
|
||||||
|
return scores_by_id
|
||||||
|
|
||||||
|
def _get_embedding_version(self):
|
||||||
|
first_problem = self.problem_embeddings[0]
|
||||||
|
array_bytes = first_problem.tobytes()
|
||||||
|
hash_object = hashlib.sha256(array_bytes)
|
||||||
|
hash_bytes = hash_object.digest()
|
||||||
|
return hash_bytes.hex()[:5]
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="CFgue", timeout=86400)
|
||||||
|
def _get_user_embedding(self, user_id, embedding_version):
|
||||||
|
user_arr, _ = self.embeddings.files
|
||||||
|
user_embeddings = self.embeddings[user_arr].item()
|
||||||
|
if user_id not in user_embeddings:
|
||||||
|
return user_embeddings[0]
|
||||||
|
return user_embeddings[user_id]
|
||||||
|
|
||||||
|
def get_user_embedding(self, user_id):
|
||||||
|
version = self._get_embedding_version()
|
||||||
|
return self._get_user_embedding(user_id, version)
|
||||||
|
|
||||||
@cache_wrapper(prefix="user_recommendations", timeout=3600)
|
@cache_wrapper(prefix="user_recommendations", timeout=3600)
|
||||||
def user_recommendations(self, user, problems, measure=DOT, limit=None):
|
def user_recommendations(self, user_id, problems, measure=DOT, limit=None):
|
||||||
uid = user.id
|
user_embedding = self.get_user_embedding(user_id)
|
||||||
if uid >= len(self.user_embeddings):
|
scores = self.compute_scores(user_embedding, self.problem_embeddings, measure)
|
||||||
uid = 0
|
|
||||||
scores = self.compute_scores(
|
|
||||||
self.user_embeddings[uid], self.problem_embeddings, measure
|
|
||||||
)
|
|
||||||
|
|
||||||
res = [] # [(score, problem)]
|
res = [] # [(score, problem)]
|
||||||
for pid in problems:
|
for pid in problems:
|
||||||
# pid = problem.id
|
if pid in scores:
|
||||||
if pid < len(scores):
|
|
||||||
res.append((scores[pid], pid))
|
res.append((scores[pid], pid))
|
||||||
|
|
||||||
res.sort(reverse=True, key=lambda x: x[0])
|
res.sort(reverse=True, key=lambda x: x[0])
|
||||||
res = res[:limit]
|
return res[:limit]
|
||||||
return res
|
|
||||||
|
|
||||||
# return a list of pid
|
# return a list of pid
|
||||||
def problem_neighbors(self, problem, problemset, measure=DOT, limit=None):
|
def problem_neighbors(self, problem, problemset, measure=DOT, limit=None):
|
||||||
pid = problem.id
|
pid = problem.id
|
||||||
if pid >= len(self.problem_embeddings):
|
if pid not in self.problem_embeddings:
|
||||||
return []
|
return []
|
||||||
scores = self.compute_scores(
|
embedding = self.problem_embeddings[pid]
|
||||||
self.problem_embeddings[pid], self.problem_embeddings, measure
|
scores = self.compute_scores(embedding, self.problem_embeddings, measure)
|
||||||
)
|
|
||||||
res = []
|
res = []
|
||||||
for p in problemset:
|
for p in problemset:
|
||||||
if p < len(scores):
|
if p in scores:
|
||||||
res.append((scores[p], p))
|
res.append((scores[p], p))
|
||||||
res.sort(reverse=True, key=lambda x: x[0])
|
res.sort(reverse=True, key=lambda x: x[0])
|
||||||
return res[:limit]
|
return res[:limit]
|
||||||
|
|
|
@ -2,11 +2,9 @@ from reversion import revisions
|
||||||
|
|
||||||
from judge.models.choices import (
|
from judge.models.choices import (
|
||||||
ACE_THEMES,
|
ACE_THEMES,
|
||||||
EFFECTIVE_MATH_ENGINES,
|
|
||||||
MATH_ENGINES_CHOICES,
|
|
||||||
TIMEZONE,
|
TIMEZONE,
|
||||||
)
|
)
|
||||||
from judge.models.comment import Comment, CommentLock, CommentVote, Notification
|
from judge.models.comment import Comment, CommentLock, CommentVote
|
||||||
from judge.models.contest import (
|
from judge.models.contest import (
|
||||||
Contest,
|
Contest,
|
||||||
ContestMoss,
|
ContestMoss,
|
||||||
|
@ -16,6 +14,10 @@ from judge.models.contest import (
|
||||||
ContestTag,
|
ContestTag,
|
||||||
Rating,
|
Rating,
|
||||||
ContestProblemClarification,
|
ContestProblemClarification,
|
||||||
|
ContestsSummary,
|
||||||
|
OfficialContestCategory,
|
||||||
|
OfficialContestLocation,
|
||||||
|
OfficialContest,
|
||||||
)
|
)
|
||||||
from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex
|
from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex
|
||||||
from judge.models.message import PrivateMessage, PrivateMessageThread
|
from judge.models.message import PrivateMessage, PrivateMessageThread
|
||||||
|
@ -44,6 +46,7 @@ from judge.models.profile import (
|
||||||
Profile,
|
Profile,
|
||||||
Friend,
|
Friend,
|
||||||
OrganizationProfile,
|
OrganizationProfile,
|
||||||
|
ProfileInfo,
|
||||||
)
|
)
|
||||||
from judge.models.runtime import Judge, Language, RuntimeVersion
|
from judge.models.runtime import Judge, Language, RuntimeVersion
|
||||||
from judge.models.submission import (
|
from judge.models.submission import (
|
||||||
|
@ -52,11 +55,21 @@ from judge.models.submission import (
|
||||||
SubmissionSource,
|
SubmissionSource,
|
||||||
SubmissionTestCase,
|
SubmissionTestCase,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from judge.models.test_formatter import TestFormatterModel
|
||||||
from judge.models.ticket import Ticket, TicketMessage
|
from judge.models.ticket import Ticket, TicketMessage
|
||||||
from judge.models.volunteer import VolunteerProblemVote
|
from judge.models.volunteer import VolunteerProblemVote
|
||||||
from judge.models.pagevote import PageVote, PageVoteVoter
|
from judge.models.pagevote import PageVote, PageVoteVoter
|
||||||
from judge.models.bookmark import BookMark, MakeBookMark
|
from judge.models.bookmark import BookMark, MakeBookMark
|
||||||
from judge.models.course import Course
|
from judge.models.course import (
|
||||||
|
Course,
|
||||||
|
CourseRole,
|
||||||
|
CourseLesson,
|
||||||
|
CourseLessonProblem,
|
||||||
|
CourseContest,
|
||||||
|
)
|
||||||
|
from judge.models.notification import Notification, NotificationProfile
|
||||||
|
from judge.models.test_formatter import TestFormatterModel
|
||||||
|
|
||||||
revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"])
|
revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"])
|
||||||
revisions.register(Problem, follow=["language_limits"])
|
revisions.register(Problem, follow=["language_limits"])
|
||||||
|
|
|
@ -2,8 +2,11 @@ from django.db import models
|
||||||
from django.db.models import CASCADE
|
from django.db.models import CASCADE
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from judge.models.profile import Profile
|
from judge.models.profile import Profile
|
||||||
|
from judge.caching import cache_wrapper
|
||||||
|
|
||||||
__all__ = ["BookMark"]
|
__all__ = ["BookMark"]
|
||||||
|
|
||||||
|
@ -13,40 +16,26 @@ class BookMark(models.Model):
|
||||||
max_length=30,
|
max_length=30,
|
||||||
verbose_name=_("associated page"),
|
verbose_name=_("associated page"),
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
) # deprecated
|
||||||
|
score = models.IntegerField(verbose_name=_("votes"), default=0)
|
||||||
|
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||||
|
object_id = models.PositiveIntegerField()
|
||||||
|
linked_object = GenericForeignKey("content_type", "object_id")
|
||||||
|
|
||||||
def get_bookmark(self, user):
|
@cache_wrapper(prefix="BMgb")
|
||||||
userqueryset = MakeBookMark.objects.filter(bookmark=self, user=user)
|
def is_bookmarked_by(self, user):
|
||||||
if userqueryset.exists():
|
return MakeBookMark.objects.filter(bookmark=self, user=user).exists()
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def page_object(self):
|
|
||||||
from judge.models.contest import Contest
|
|
||||||
from judge.models.interface import BlogPost
|
|
||||||
from judge.models.problem import Problem, Solution
|
|
||||||
|
|
||||||
try:
|
|
||||||
page = self.page
|
|
||||||
if page.startswith("p:"):
|
|
||||||
return Problem.objects.get(code=page[2:])
|
|
||||||
elif page.startswith("c:"):
|
|
||||||
return Contest.objects.get(key=page[2:])
|
|
||||||
elif page.startswith("b:"):
|
|
||||||
return BlogPost.objects.get(id=page[2:])
|
|
||||||
elif page.startswith("s:"):
|
|
||||||
return Solution.objects.get(problem__code=page[2:])
|
|
||||||
return None
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
return None
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("bookmark")
|
verbose_name = _("bookmark")
|
||||||
verbose_name_plural = _("bookmarks")
|
verbose_name_plural = _("bookmarks")
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["content_type", "object_id"]),
|
||||||
|
]
|
||||||
|
unique_together = ("content_type", "object_id")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.page
|
return f"bookmark for {self.linked_object}"
|
||||||
|
|
||||||
|
|
||||||
class MakeBookMark(models.Model):
|
class MakeBookMark(models.Model):
|
||||||
|
@ -56,6 +45,30 @@ class MakeBookMark(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["user", "bookmark"]),
|
||||||
|
]
|
||||||
unique_together = ["user", "bookmark"]
|
unique_together = ["user", "bookmark"]
|
||||||
verbose_name = _("make bookmark")
|
verbose_name = _("make bookmark")
|
||||||
verbose_name_plural = _("make bookmarks")
|
verbose_name_plural = _("make bookmarks")
|
||||||
|
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="gocb", expected_type=BookMark)
|
||||||
|
def _get_or_create_bookmark(content_type, object_id):
|
||||||
|
bookmark, created = BookMark.objects.get_or_create(
|
||||||
|
content_type=content_type,
|
||||||
|
object_id=object_id,
|
||||||
|
)
|
||||||
|
return bookmark
|
||||||
|
|
||||||
|
|
||||||
|
class Bookmarkable:
|
||||||
|
def get_or_create_bookmark(self):
|
||||||
|
content_type = ContentType.objects.get_for_model(self)
|
||||||
|
object_id = self.pk
|
||||||
|
return _get_or_create_bookmark(content_type, object_id)
|
||||||
|
|
||||||
|
|
||||||
|
def dirty_bookmark(bookmark, profile):
|
||||||
|
bookmark.is_bookmarked_by.dirty(bookmark, profile)
|
||||||
|
_get_or_create_bookmark.dirty(bookmark.content_type, bookmark.object_id)
|
||||||
|
|
|
@ -54,13 +54,3 @@ ACE_THEMES = (
|
||||||
("vibrant_ink", "Vibrant Ink"),
|
("vibrant_ink", "Vibrant Ink"),
|
||||||
("xcode", "XCode"),
|
("xcode", "XCode"),
|
||||||
)
|
)
|
||||||
|
|
||||||
MATH_ENGINES_CHOICES = (
|
|
||||||
("tex", _("Leave as LaTeX")),
|
|
||||||
("svg", _("SVG with PNG fallback")),
|
|
||||||
("mml", _("MathML only")),
|
|
||||||
("jax", _("MathJax with SVG/PNG fallback")),
|
|
||||||
("auto", _("Detect best quality")),
|
|
||||||
)
|
|
||||||
|
|
||||||
EFFECTIVE_MATH_ENGINES = ("svg", "mml", "tex", "jax")
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue