Compare commits
No commits in common. "master" and "cuom1999" have entirely different histories.
2978 changed files with 41254 additions and 206648 deletions
17
.github/workflows/build.yml
vendored
Normal file
17
.github/workflows/build.yml
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
name: build
|
||||||
|
on: [push, pull_request]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Set up Python 3.7
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: 3.7
|
||||||
|
- name: Install flake8
|
||||||
|
run: pip install flake8 flake8-import-order flake8-future-import flake8-commas flake8-logging-format
|
||||||
|
- name: Lint with flake8
|
||||||
|
run: |
|
||||||
|
flake8 --version
|
||||||
|
flake8
|
40
.github/workflows/compilemessages.yml
vendored
Normal file
40
.github/workflows/compilemessages.yml
vendored
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
name: compilemessages
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'locale/**'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'locale/**'
|
||||||
|
jobs:
|
||||||
|
compilemessages:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Set up Python 3.7
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: 3.7
|
||||||
|
- name: Checkout submodules
|
||||||
|
run: |
|
||||||
|
git submodule init
|
||||||
|
git submodule update
|
||||||
|
- name: Install requirements
|
||||||
|
run: |
|
||||||
|
sudo apt-get install gettext
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install pymysql
|
||||||
|
- name: Check .po file validity
|
||||||
|
run: |
|
||||||
|
fail=0
|
||||||
|
while read -r file; do
|
||||||
|
if ! msgfmt --check-format "$file"; then
|
||||||
|
fail=$((fail + 1))
|
||||||
|
fi
|
||||||
|
done < <(find locale -name '*.po')
|
||||||
|
exit "$fail"
|
||||||
|
shell: bash
|
||||||
|
- name: Compile messages
|
||||||
|
run: |
|
||||||
|
echo "STATIC_ROOT = '/tmp'" > dmoj/local_settings.py
|
||||||
|
python manage.py compilemessages
|
33
.github/workflows/init.yml
vendored
33
.github/workflows/init.yml
vendored
|
@ -1,33 +0,0 @@
|
||||||
name: Lint
|
|
||||||
|
|
||||||
on:
|
|
||||||
# Trigger the workflow on push or pull request,
|
|
||||||
# but only for the main branch
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
run-linters:
|
|
||||||
name: Run linters
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out Git repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v1
|
|
||||||
with:
|
|
||||||
python-version: 3.8
|
|
||||||
|
|
||||||
- name: Install Python dependencies
|
|
||||||
run: pip install black
|
|
||||||
|
|
||||||
- name: Run linters
|
|
||||||
uses: wearerequired/lint-action@v2
|
|
||||||
with:
|
|
||||||
black: true
|
|
53
.github/workflows/makemessages.yml
vendored
Normal file
53
.github/workflows/makemessages.yml
vendored
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
name: makemessages
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
jobs:
|
||||||
|
makemessages:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Set up Python 3.7
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: 3.7
|
||||||
|
- name: Checkout submodules
|
||||||
|
run: |
|
||||||
|
git submodule init
|
||||||
|
git submodule update
|
||||||
|
- name: Install requirements
|
||||||
|
run: |
|
||||||
|
sudo apt-get install gettext
|
||||||
|
curl -O https://artifacts.crowdin.com/repo/deb/crowdin.deb
|
||||||
|
sudo dpkg -i crowdin.deb
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install pymysql
|
||||||
|
- name: Collect localizable strings
|
||||||
|
run: |
|
||||||
|
echo "STATIC_ROOT = '/tmp'" > dmoj/local_settings.py
|
||||||
|
python manage.py makemessages -l en -e py,html,txt
|
||||||
|
python manage.py makemessages -l en -d djangojs
|
||||||
|
- name: Upload strings to Crowdin
|
||||||
|
env:
|
||||||
|
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
|
||||||
|
run: |
|
||||||
|
cat > crowdin.yaml <<EOF
|
||||||
|
project_identifier: dmoj
|
||||||
|
|
||||||
|
files:
|
||||||
|
- source: /locale/en/LC_MESSAGES/django.po
|
||||||
|
translation: /locale/%two_letters_code%/LC_MESSAGES/django.po
|
||||||
|
languages_mapping:
|
||||||
|
two_letters_code:
|
||||||
|
zh-CN: zh_Hans
|
||||||
|
sr-CS: sr_Latn
|
||||||
|
- source: /locale/en/LC_MESSAGES/djangojs.po
|
||||||
|
translation: /locale/%two_letters_code%/LC_MESSAGES/djangojs.po
|
||||||
|
languages_mapping:
|
||||||
|
two_letters_code:
|
||||||
|
zh-CN: zh_Hans
|
||||||
|
sr-CS: sr_Latn
|
||||||
|
EOF
|
||||||
|
echo "api_key: ${CROWDIN_API_TOKEN}" >> crowdin.yaml
|
||||||
|
crowdin upload sources
|
67
.github/workflows/updatemessages.yml
vendored
Normal file
67
.github/workflows/updatemessages.yml
vendored
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
name: updatemessages
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 * * * *'
|
||||||
|
jobs:
|
||||||
|
updatemessages:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Set up Python 3.7
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: 3.7
|
||||||
|
- name: Checkout submodules
|
||||||
|
run: |
|
||||||
|
git submodule init
|
||||||
|
git submodule update
|
||||||
|
- name: Install requirements
|
||||||
|
run: |
|
||||||
|
sudo apt-get install gettext
|
||||||
|
curl -O https://artifacts.crowdin.com/repo/deb/crowdin.deb
|
||||||
|
sudo dpkg -i crowdin.deb
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install pymysql
|
||||||
|
- name: Download strings from Crowdin
|
||||||
|
env:
|
||||||
|
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
|
||||||
|
run: |
|
||||||
|
cat > crowdin.yaml <<EOF
|
||||||
|
project_identifier: dmoj
|
||||||
|
|
||||||
|
files:
|
||||||
|
- source: /locale/en/LC_MESSAGES/django.po
|
||||||
|
translation: /locale/%two_letters_code%/LC_MESSAGES/django.po
|
||||||
|
languages_mapping:
|
||||||
|
two_letters_code:
|
||||||
|
zh-CN: zh_Hans
|
||||||
|
zh-TW: zh_Hant
|
||||||
|
sr-CS: sr_Latn
|
||||||
|
- source: /locale/en/LC_MESSAGES/djangojs.po
|
||||||
|
translation: /locale/%two_letters_code%/LC_MESSAGES/djangojs.po
|
||||||
|
languages_mapping:
|
||||||
|
two_letters_code:
|
||||||
|
zh-CN: zh_Hans
|
||||||
|
zh-TW: zh_Hant
|
||||||
|
sr-CS: sr_Latn
|
||||||
|
EOF
|
||||||
|
echo "api_key: ${CROWDIN_API_TOKEN}" >> crowdin.yaml
|
||||||
|
crowdin download
|
||||||
|
rm crowdin.yaml
|
||||||
|
- name: Cleanup
|
||||||
|
run: |
|
||||||
|
rm -rf src/
|
||||||
|
git add locale
|
||||||
|
git checkout .
|
||||||
|
git clean -fd
|
||||||
|
- name: Create pull request
|
||||||
|
uses: peter-evans/create-pull-request@v1.4.1-multi
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.REPO_SCOPED_TOKEN }}
|
||||||
|
COMMIT_MESSAGE: 'i18n: update translations from Crowdin'
|
||||||
|
PULL_REQUEST_TITLE: 'Update translations from Crowdin'
|
||||||
|
PULL_REQUEST_BODY: This PR has been auto-generated to pull in latest translations from [Crowdin](https://translate.dmoj.ca).
|
||||||
|
PULL_REQUEST_LABELS: i18n, enhancement
|
||||||
|
PULL_REQUEST_REVIEWERS: Xyene, quantum5
|
||||||
|
PULL_REQUEST_BRANCH: update-i18n
|
||||||
|
BRANCH_SUFFIX: none
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -5,7 +5,6 @@
|
||||||
*.py[co]
|
*.py[co]
|
||||||
*.mo
|
*.mo
|
||||||
*~
|
*~
|
||||||
[a-z0-9]{40,}
|
|
||||||
dmoj/local_settings.py
|
dmoj/local_settings.py
|
||||||
resources/style.css
|
resources/style.css
|
||||||
resources/content-description.css
|
resources/content-description.css
|
||||||
|
@ -13,7 +12,4 @@ resources/ranks.css
|
||||||
resources/table.css
|
resources/table.css
|
||||||
sass_processed
|
sass_processed
|
||||||
<desired bridge log path>
|
<desired bridge log path>
|
||||||
node_modules/
|
|
||||||
package-lock.json
|
|
||||||
/src
|
|
||||||
|
|
||||||
|
|
8
.gitmodules
vendored
Normal file
8
.gitmodules
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
[submodule "resources/pagedown"]
|
||||||
|
path = resources/pagedown
|
||||||
|
url = https://github.com/DMOJ/dmoj-pagedown.git
|
||||||
|
branch = master
|
||||||
|
[submodule "resources/libs"]
|
||||||
|
path = resources/libs
|
||||||
|
url = https://github.com/DMOJ/site-assets.git
|
||||||
|
branch = master
|
|
@ -1,17 +0,0 @@
|
||||||
repos:
|
|
||||||
- repo: https://github.com/rtts/djhtml
|
|
||||||
rev: 'v1.5.2' # replace with the latest tag on GitHub
|
|
||||||
hooks:
|
|
||||||
- id: djhtml
|
|
||||||
entry: djhtml -i -t 2
|
|
||||||
files: templates/.
|
|
||||||
- id: djcss
|
|
||||||
types: [scss]
|
|
||||||
- repo: https://github.com/psf/black
|
|
||||||
rev: 22.12.0
|
|
||||||
hooks:
|
|
||||||
- id: black
|
|
||||||
- repo: https://github.com/hadialqattan/pycln
|
|
||||||
rev: 'v2.3.0'
|
|
||||||
hooks:
|
|
||||||
- id: pycln
|
|
8
502.html
8
502.html
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>502 bad gateway - LQDOJ</title>
|
<title>502 bad gateway - DMOJ</title>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
hr {
|
hr {
|
||||||
|
@ -49,10 +49,14 @@
|
||||||
<br>
|
<br>
|
||||||
<div class="popup">
|
<div class="popup">
|
||||||
<div>
|
<div>
|
||||||
<img class="logo" src="logo.svg" alt="LQDOJ">
|
<img class="logo" src="logo.png" alt="DMOJ">
|
||||||
</div>
|
</div>
|
||||||
<h1 style="width: 100%;">Oops, LQDOJ is down now.</h1>
|
<h1 style="width: 100%;">Oops, LQDOJ is down now.</h1>
|
||||||
</div>
|
</div>
|
||||||
|
<br>
|
||||||
|
<hr>
|
||||||
|
<br>
|
||||||
|
<h2 class="msg">But don't worry, we'll be back soon. <br> In the free time, you can read my idol's codes <a href="http://codeforces.com/profile/cuom1999" style="color:red; text-decoration:none">cuom1999</a></h2>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
4547
<desired bridge log path>
Normal file
4547
<desired bridge log path>
Normal file
File diff suppressed because it is too large
Load diff
1078
README.html
Normal file
1078
README.html
Normal file
File diff suppressed because one or more lines are too long
338
README.md
338
README.md
|
@ -1,327 +1,25 @@
|
||||||
LQDOJ: Le Quy Don Online Judge
|
# online-judge
|
||||||
===
|
### 1. Activate virtualenv:
|
||||||
|
|
||||||
[![](https://github.com/DMOJ/online-judge/workflows/build/badge.svg)](https://lqdoj.edu.vn/)
|
|
||||||
[![Python](https://img.shields.io/pypi/pyversions/tensorflow.svg?style=plastic)](https://python.org)
|
|
||||||
[![OS](https://img.shields.io/badge/Ubuntu-16.04%20%7C%2018.04%20%7C%2020.04-brightgreen)](https://ubuntu.com/download)
|
|
||||||
[![License](https://img.shields.io/badge/license-AGPL--3.0-blue)](https://www.gnu.org/licenses/agpl-3.0.en.html)
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Homepage: [https://lqdoj.edu.vn](https://lqdoj.edu.vn)
|
|
||||||
|
|
||||||
Based on [DMOJ](https://dmoj.ca/).
|
|
||||||
|
|
||||||
Supported languages:
|
|
||||||
|
|
||||||
- Assembly (x64)
|
|
||||||
- AWK
|
|
||||||
- C
|
|
||||||
- C++03 / C++11 / C++14 / C++17 / C++20
|
|
||||||
- Java 11
|
|
||||||
- Pascal
|
|
||||||
- Perl
|
|
||||||
- Python 2 / Python 3
|
|
||||||
- PyPy 2 / PyPy 3
|
|
||||||
|
|
||||||
Support plagiarism detection via [Stanford MOSS](https://theory.stanford.edu/~aiken/moss/).
|
|
||||||
|
|
||||||
## 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`.
|
|
||||||
|
|
||||||
|
|
||||||
- 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:
|
|
||||||
|
|
||||||
1. Missing the `local_settings.py`. You need to copy the `local_settings.py` in order to pass the check.
|
|
||||||
2. Missing the problem folder in `local_settings.py`. You need to create a folder to contain all problem packages and configure in `local_settings.py`.
|
|
||||||
3. Missing static folder in `local_settings.py`. Similar to problem folder, make sure to configure `STATIC_FILES` inside `local_settings.py`.
|
|
||||||
4. Missing configure file for judges. Each judge must have a seperate configure file. To create this file, you can run `python dmojauto-conf`. Checkout all sample files here https://github.com/DMOJ/docs/blob/master/sample_files.
|
|
||||||
5. Missing timezone data for SQL. If you're using Ubuntu and you're following DMOJ's installation guide for the server, and you are getting the error mentioned in https://github.com/LQDJudge/online-judge/issues/45, then you can follow this method to fix:
|
|
||||||
```
|
|
||||||
mysql
|
|
||||||
-- You may have to do this if you haven't set root password for MySQL, replace mypass with your password
|
|
||||||
-- SET PASSWORD FOR 'root'@'localhost' = PASSWORD('mypass');
|
|
||||||
-- FLUSH PRIVILEGES;
|
|
||||||
exit
|
|
||||||
mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -D mysql -u root -p
|
|
||||||
mysql -u root -p -e "flush tables;" mysql
|
|
||||||
```
|
|
||||||
6. Missing the chat secret key, you must generate a Fernet key, and assign a variable in `local_settings.py` like this
|
|
||||||
```python
|
|
||||||
CHAT_SECRET_KEY = "81HqDtbqAywKSOumSxxxxxxxxxxxxxxxxx="
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Suppose you finished all the installation. Everytime you want to run a local server, follow these steps:
|
|
||||||
|
|
||||||
1. Activate virtualenv:
|
|
||||||
```bash
|
|
||||||
source dmojsite/bin/activate
|
source dmojsite/bin/activate
|
||||||
```
|
### 2. Remember to change the local_settings
|
||||||
|
|
||||||
2. Run server:
|
### 3. Run server:
|
||||||
```bash
|
python manage.py runserver 0.0.0.0:8000
|
||||||
python3 manage.py runserver 0.0.0.0:8000
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Create a bridge (this is opened in a different terminal with the second step if you are using the same machine)
|
### 4. Create configure file for judge:
|
||||||
```bash
|
python dmojauto-conf
|
||||||
python3 manage.py runbridged
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Create a judge (another terminal)
|
### 5. Create folder for problems, change the dir in judge conf file and local_settings.py
|
||||||
```bash
|
|
||||||
dmoj 0.0.0.0 -p 9999 -c <path to yml configure file>
|
|
||||||
```
|
|
||||||
Here we suppose you use the default port 9999 for bridge in `settings.py`. You can create multiple judges, each should be in a seperate terminal.
|
|
||||||
|
|
||||||
**Optional**
|
### 6. Connect judge:
|
||||||
|
+ python manage.py runbridged
|
||||||
|
+ dmoj 0.0.0.0 -p 9999 -c judge/conf1.yml (depend on port in the local_settings.py and directory of conf file)
|
||||||
|
|
||||||
5. Run celery worker (This is server's queue. It may be necessary in some functions)
|
### 7. Update vietnamese translation:
|
||||||
```bash
|
- go to locale/vi
|
||||||
celery -A dmoj_celery worker
|
- modify .po file
|
||||||
```
|
- python manage.py compilemessages
|
||||||
|
- python manage.py compilejsi18n
|
||||||
|
|
||||||
6. Run a live event server (So everything is updated lively like in the production)
|
###8. Run chat server:
|
||||||
```bash
|
docker run -p 6379:6379 -d redis:2.8
|
||||||
node websocket/daemon.js
|
|
||||||
```
|
|
||||||
|
|
||||||
7. To use subdomain for each organization, go to admin page -> navigation bar -> sites, add domain name (e.g, "localhost:8000"). Then go to add `USE_SUBDOMAIN = True` to local_settings.py.
|
|
||||||
|
|
||||||
## Deploy
|
|
||||||
Most of the steps are similar to Django tutorials. Here are two usual steps:
|
|
||||||
|
|
||||||
1. Update vietnamese translation:
|
|
||||||
- If you add any new phrases in the code, ```python3 manage.py makemessages```
|
|
||||||
- go to `locale/vi`
|
|
||||||
- modify `.po` file
|
|
||||||
- ```python3 manage.py compilemessages```
|
|
||||||
- ```python3 manage.py compilejsi18n```
|
|
||||||
|
|
||||||
2. Update styles (using SASS)
|
|
||||||
- Change .css/.scss files in `resources` folder
|
|
||||||
- ```./make_style.sh && python3 manage.py collectstatic```
|
|
||||||
- Sometimes you need to press `Ctrl + F5` to see the new user interface in browser.
|
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
|
|
||||||
### Leaderboard
|
|
||||||
|
|
||||||
Leaderboard with information about contest rating, performance points and real name of all users.
|
|
||||||
|
|
||||||
![](https://raw.githubusercontent.com/emladevops/LQDOJ-image/main/brave_SK67WA26FA.png#gh-light-mode-only)
|
|
||||||
![](https://raw.githubusercontent.com/emladevops/LQDOJ-image/main/brave_cmqqCnwaFc.png#gh-dark-mode-only)
|
|
||||||
|
|
||||||
### Admin dashboard
|
|
||||||
|
|
||||||
Admin dashboard helps you easily managing problems, users, contests and blog posts.
|
|
||||||
|
|
||||||
![](https://i.imgur.com/iccr3mh.png)
|
|
||||||
|
|
||||||
### Statement editor
|
|
||||||
|
|
||||||
You can write the problems' statement in Markdown with LaTeX figures and formulas supported.
|
|
||||||
|
|
||||||
![](https://i.imgur.com/CQVC754.png)
|
|
||||||
|
|
||||||
### Chat
|
|
||||||
|
|
||||||
Users can communicate with each other and can see who's online.
|
|
||||||
|
|
||||||
![](https://raw.githubusercontent.com/emladevops/LQDOJ-image/main/brave_kPsC5bJluc.png#gh-light-mode-only)
|
|
||||||
![](https://raw.githubusercontent.com/emladevops/LQDOJ-image/main/brave_AtrEzXzEAx.png#gh-dark-mode-only)
|
|
||||||
|
|
|
@ -2,7 +2,4 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class ChatBoxConfig(AppConfig):
|
class ChatBoxConfig(AppConfig):
|
||||||
name = "chat_box"
|
name = 'chat_box'
|
||||||
|
|
||||||
def ready(self):
|
|
||||||
from . import models
|
|
||||||
|
|
64
chat_box/consumers.py
Normal file
64
chat_box/consumers.py
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import json
|
||||||
|
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||||
|
from .models import Message
|
||||||
|
from .views import format_time
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
|
|
||||||
|
|
||||||
|
from judge.models.profile import Profile
|
||||||
|
|
||||||
|
|
||||||
|
class ChatConsumer(AsyncWebsocketConsumer):
|
||||||
|
async def connect(self):
|
||||||
|
self.room_name = 'room'
|
||||||
|
self.room_group_name = 'chat_%s' % self.room_name
|
||||||
|
|
||||||
|
# Join room group
|
||||||
|
await self.channel_layer.group_add(
|
||||||
|
self.room_group_name,
|
||||||
|
self.channel_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.accept()
|
||||||
|
|
||||||
|
async def disconnect(self, close_code):
|
||||||
|
# Leave room group
|
||||||
|
await self.channel_layer.group_discard(
|
||||||
|
self.room_group_name,
|
||||||
|
self.channel_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Receive message from WebSocket
|
||||||
|
async def receive(self, text_data):
|
||||||
|
text_data_json = json.loads(text_data)
|
||||||
|
message = text_data_json['message']
|
||||||
|
time = save_data_and_get_time(message)
|
||||||
|
message['time'] = format_time(time)
|
||||||
|
|
||||||
|
# Send message to room group
|
||||||
|
await self.channel_layer.group_send(
|
||||||
|
self.room_group_name,
|
||||||
|
{
|
||||||
|
'type': 'chat_message',
|
||||||
|
'message': message,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Receive message from room group
|
||||||
|
async def chat_message(self, event):
|
||||||
|
message = event['message']
|
||||||
|
# Send message to WebSocket
|
||||||
|
await self.send(text_data=json.dumps({
|
||||||
|
'message': message,
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
# return time
|
||||||
|
def save_data_and_get_time(message):
|
||||||
|
new_message = Message(body=message['body'],
|
||||||
|
author=Profile.objects
|
||||||
|
.get(pk=message['author_id']),
|
||||||
|
)
|
||||||
|
new_message.save()
|
||||||
|
return new_message.time
|
|
@ -9,43 +9,22 @@ class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("judge", "0100_auto_20200127_0059"),
|
('judge', '0100_auto_20200127_0059'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="Message",
|
name='Message',
|
||||||
fields=[
|
fields=[
|
||||||
(
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
"id",
|
('time', models.DateTimeField(auto_now_add=True, verbose_name='posted time')),
|
||||||
models.AutoField(
|
('body', models.TextField(max_length=8192, verbose_name='body of comment')),
|
||||||
auto_created=True,
|
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Profile', verbose_name='user')),
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"time",
|
|
||||||
models.DateTimeField(auto_now_add=True, verbose_name="posted time"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"body",
|
|
||||||
models.TextField(max_length=8192, verbose_name="body of comment"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"author",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="judge.Profile",
|
|
||||||
verbose_name="user",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
"verbose_name": "message",
|
'verbose_name': 'message',
|
||||||
"verbose_name_plural": "messages",
|
'verbose_name_plural': 'messages',
|
||||||
"ordering": ("-time",),
|
'ordering': ('-time',),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
# Generated by Django 2.2.12 on 2020-05-05 15:54
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("chat_box", "0001_initial"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="message",
|
|
||||||
name="hidden",
|
|
||||||
field=models.BooleanField(default=False, verbose_name="is hidden"),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,18 +0,0 @@
|
||||||
# Generated by Django 2.2.12 on 2020-05-05 16:06
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("chat_box", "0002_message_hidden"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="message",
|
|
||||||
name="hidden",
|
|
||||||
field=models.BooleanField(default=True, verbose_name="is hidden"),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,18 +0,0 @@
|
||||||
# Generated by Django 2.2.12 on 2020-05-05 16:36
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("chat_box", "0003_auto_20200505_2306"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="message",
|
|
||||||
name="hidden",
|
|
||||||
field=models.BooleanField(default=False, verbose_name="is hidden"),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,58 +0,0 @@
|
||||||
# Generated by Django 2.2.17 on 2021-10-11 00:14
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("judge", "0116_auto_20211011_0645"),
|
|
||||||
("chat_box", "0004_auto_20200505_2336"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Room",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"user_one",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="user_one",
|
|
||||||
to="judge.Profile",
|
|
||||||
verbose_name="user 1",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"user_two",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="user_two",
|
|
||||||
to="judge.Profile",
|
|
||||||
verbose_name="user 2",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="message",
|
|
||||||
name="room",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
default=None,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="chat_box.Room",
|
|
||||||
verbose_name="room id",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,48 +0,0 @@
|
||||||
# Generated by Django 2.2.17 on 2021-11-12 05:27
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("judge", "0116_auto_20211011_0645"),
|
|
||||||
("chat_box", "0005_auto_20211011_0714"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="UserRoom",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("last_seen", models.DateTimeField(verbose_name="last seen")),
|
|
||||||
(
|
|
||||||
"room",
|
|
||||||
models.ForeignKey(
|
|
||||||
default=None,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="chat_box.Room",
|
|
||||||
verbose_name="room id",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"user",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="judge.Profile",
|
|
||||||
verbose_name="user",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,18 +0,0 @@
|
||||||
# Generated by Django 2.2.17 on 2021-11-12 05:55
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("chat_box", "0006_userroom"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="userroom",
|
|
||||||
name="last_seen",
|
|
||||||
field=models.DateTimeField(auto_now_add=True, verbose_name="last seen"),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,39 +0,0 @@
|
||||||
# Generated by Django 2.2.17 on 2021-11-18 10:26
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("judge", "0116_auto_20211011_0645"),
|
|
||||||
("chat_box", "0007_auto_20211112_1255"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Ignore",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("ignored_users", models.ManyToManyField(to="judge.Profile")),
|
|
||||||
(
|
|
||||||
"user",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="ignored_chat_users",
|
|
||||||
to="judge.Profile",
|
|
||||||
verbose_name="user",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,24 +0,0 @@
|
||||||
# Generated by Django 2.2.25 on 2022-06-18 07:52
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("chat_box", "0008_ignore"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="ignore",
|
|
||||||
name="user",
|
|
||||||
field=models.OneToOneField(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="ignored_chat_users",
|
|
||||||
to="judge.Profile",
|
|
||||||
verbose_name="user",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,18 +0,0 @@
|
||||||
# Generated by Django 2.2.25 on 2022-10-27 20:00
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("judge", "0135_auto_20221028_0300"),
|
|
||||||
("chat_box", "0009_auto_20220618_1452"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterUniqueTogether(
|
|
||||||
name="userroom",
|
|
||||||
unique_together={("user", "room")},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,20 +0,0 @@
|
||||||
# Generated by Django 3.2.18 on 2023-02-18 21:10
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("chat_box", "0010_auto_20221028_0300"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="message",
|
|
||||||
name="hidden",
|
|
||||||
field=models.BooleanField(
|
|
||||||
db_index=True, default=False, verbose_name="is hidden"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,34 +0,0 @@
|
||||||
# Generated by Django 3.2.18 on 2023-03-08 07:17
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("judge", "0154_add_submission_indexes"),
|
|
||||||
("chat_box", "0011_alter_message_hidden"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name="message",
|
|
||||||
options={
|
|
||||||
"ordering": ("-id",),
|
|
||||||
"verbose_name": "message",
|
|
||||||
"verbose_name_plural": "messages",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="message",
|
|
||||||
name="hidden",
|
|
||||||
field=models.BooleanField(default=False, verbose_name="is hidden"),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="message",
|
|
||||||
index=models.Index(
|
|
||||||
fields=["hidden", "room", "-id"], name="chat_box_me_hidden_b2307a_idx"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,20 +0,0 @@
|
||||||
# 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"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,38 +0,0 @@
|
||||||
# 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),
|
|
||||||
]
|
|
|
@ -1,33 +0,0 @@
|
||||||
# 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),
|
|
||||||
]
|
|
|
@ -1,32 +0,0 @@
|
||||||
# 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,154 +1,28 @@
|
||||||
|
from asgiref.sync import async_to_sync
|
||||||
|
from channels.layers import get_channel_layer
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import CASCADE, Q
|
from django.db.models import CASCADE
|
||||||
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']
|
||||||
|
|
||||||
|
|
||||||
class Room(models.Model):
|
|
||||||
user_one = models.ForeignKey(
|
|
||||||
Profile, related_name="user_one", verbose_name="user 1", on_delete=CASCADE
|
|
||||||
)
|
|
||||||
user_two = models.ForeignKey(
|
|
||||||
Profile, related_name="user_two", verbose_name="user 2", on_delete=CASCADE
|
|
||||||
)
|
|
||||||
last_msg_time = models.DateTimeField(
|
|
||||||
verbose_name=_("last seen"), null=True, db_index=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
app_label = "chat_box"
|
|
||||||
unique_together = ("user_one", "user_two")
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def _cached_info(self):
|
|
||||||
return get_room_info(self.id)
|
|
||||||
|
|
||||||
def contain(self, profile):
|
|
||||||
return profile.id in [self.user_one_id, self.user_two_id]
|
|
||||||
|
|
||||||
def other_user(self, profile):
|
|
||||||
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):
|
|
||||||
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(
|
time = models.DateTimeField(verbose_name=_('posted time'), auto_now_add=True)
|
||||||
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)
|
|
||||||
room = models.ForeignKey(
|
|
||||||
Room, verbose_name="room id", on_delete=CASCADE, default=None, null=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
new_message = self.id
|
||||||
self.body = self.body.strip()
|
self.body = self.body.strip()
|
||||||
super(Message, self).save(*args, **kwargs)
|
super(Message, self).save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "message"
|
app_label = 'chat_box'
|
||||||
verbose_name_plural = "messages"
|
verbose_name = 'message'
|
||||||
ordering = ("-id",)
|
verbose_name_plural = 'messages'
|
||||||
indexes = [
|
ordering = ('-time',)
|
||||||
models.Index(fields=["hidden", "room", "-id"]),
|
|
||||||
]
|
|
||||||
app_label = "chat_box"
|
|
||||||
|
|
||||||
|
|
||||||
class UserRoom(models.Model):
|
|
||||||
user = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE)
|
|
||||||
room = models.ForeignKey(
|
|
||||||
Room, verbose_name="room id", on_delete=CASCADE, default=None, null=True
|
|
||||||
)
|
|
||||||
last_seen = models.DateTimeField(verbose_name=_("last seen"), auto_now_add=True)
|
|
||||||
unread_count = models.IntegerField(default=0, db_index=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
unique_together = ("user", "room")
|
|
||||||
app_label = "chat_box"
|
|
||||||
|
|
||||||
|
|
||||||
class Ignore(models.Model):
|
|
||||||
user = models.OneToOneField(
|
|
||||||
Profile,
|
|
||||||
related_name="ignored_chat_users",
|
|
||||||
verbose_name=_("user"),
|
|
||||||
on_delete=CASCADE,
|
|
||||||
db_index=True,
|
|
||||||
)
|
|
||||||
ignored_users = models.ManyToManyField(Profile)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
app_label = "chat_box"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def is_ignored(self, current_user, new_friend):
|
|
||||||
try:
|
|
||||||
return current_user.ignored_chat_users.ignored_users.filter(
|
|
||||||
id=new_friend.id
|
|
||||||
).exists()
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_ignored_users(self, user):
|
|
||||||
try:
|
|
||||||
return self.objects.get(user=user).ignored_users.all()
|
|
||||||
except:
|
|
||||||
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
|
|
||||||
def add_ignore(self, current_user, friend):
|
|
||||||
ignore, created = self.objects.get_or_create(user=current_user)
|
|
||||||
ignore.ignored_users.add(friend)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def remove_ignore(self, current_user, friend):
|
|
||||||
ignore, created = self.objects.get_or_create(user=current_user)
|
|
||||||
ignore.ignored_users.remove(friend)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def toggle_ignore(self, current_user, friend):
|
|
||||||
if self.is_ignored(current_user, friend):
|
|
||||||
self.remove_ignore(current_user, friend)
|
|
||||||
else:
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
7
chat_box/routing.py
Normal file
7
chat_box/routing.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from django.urls import re_path
|
||||||
|
|
||||||
|
from . import consumers
|
||||||
|
|
||||||
|
websocket_urlpatterns = [
|
||||||
|
re_path(r'ws/chat/', consumers.ChatConsumer),
|
||||||
|
]
|
|
@ -1,51 +0,0 @@
|
||||||
from cryptography.fernet import Fernet
|
|
||||||
import hmac
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db.models import OuterRef, Count, Subquery, IntegerField, Q
|
|
||||||
from django.db.models.functions import Coalesce
|
|
||||||
|
|
||||||
from chat_box.models import Ignore, Message, UserRoom, Room
|
|
||||||
|
|
||||||
from judge.caching import cache_wrapper
|
|
||||||
|
|
||||||
secret_key = settings.CHAT_SECRET_KEY
|
|
||||||
fernet = Fernet(secret_key)
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt_url(creator_id, other_id):
|
|
||||||
message = str(creator_id) + "_" + str(other_id)
|
|
||||||
return fernet.encrypt(message.encode()).decode()
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt_url(message_encrypted):
|
|
||||||
try:
|
|
||||||
dec_message = fernet.decrypt(message_encrypted.encode()).decode()
|
|
||||||
creator_id, other_id = dec_message.split("_")
|
|
||||||
return int(creator_id), int(other_id)
|
|
||||||
except Exception as e:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt_channel(channel):
|
|
||||||
return (
|
|
||||||
hmac.new(
|
|
||||||
settings.CHAT_SECRET_KEY.encode(),
|
|
||||||
channel.encode(),
|
|
||||||
hashlib.sha512,
|
|
||||||
).hexdigest()[:16]
|
|
||||||
+ "%s" % channel
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@cache_wrapper(prefix="gub")
|
|
||||||
def get_unread_boxes(profile):
|
|
||||||
ignored_rooms = Ignore.get_ignored_rooms(profile)
|
|
||||||
unread_boxes = (
|
|
||||||
UserRoom.objects.filter(user=profile, unread_count__gt=0)
|
|
||||||
.exclude(room__in=ignored_rooms)
|
|
||||||
.count()
|
|
||||||
)
|
|
||||||
|
|
||||||
return unread_boxes
|
|
|
@ -1,554 +1,48 @@
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
from django.http import (
|
from django.http import HttpResponse
|
||||||
HttpResponse,
|
|
||||||
JsonResponse,
|
|
||||||
HttpResponseBadRequest,
|
|
||||||
HttpResponsePermanentRedirect,
|
|
||||||
HttpResponseRedirect,
|
|
||||||
)
|
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.core.exceptions import PermissionDenied
|
|
||||||
from django.shortcuts import render
|
|
||||||
from django.forms.models import model_to_dict
|
|
||||||
from django.db.models import (
|
|
||||||
Case,
|
|
||||||
BooleanField,
|
|
||||||
When,
|
|
||||||
Q,
|
|
||||||
Subquery,
|
|
||||||
OuterRef,
|
|
||||||
Exists,
|
|
||||||
Count,
|
|
||||||
IntegerField,
|
|
||||||
F,
|
|
||||||
Max,
|
|
||||||
)
|
|
||||||
from django.db.models.functions import Coalesce
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
|
|
||||||
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 .models import Message
|
||||||
|
import json
|
||||||
|
|
||||||
from chat_box.models import Message, Profile, Room, UserRoom, Ignore, get_room_info
|
|
||||||
from chat_box.utils import encrypt_url, decrypt_url, encrypt_channel, get_unread_boxes
|
|
||||||
|
|
||||||
from reversion import revisions
|
def format_time(time):
|
||||||
|
return time.strftime('%H:%M %p %d-%m-%Y')
|
||||||
|
|
||||||
|
|
||||||
|
def format_messages(messages):
|
||||||
|
msg_list = [{
|
||||||
|
'time': format_time(msg.time),
|
||||||
|
'author': str(msg.author),
|
||||||
|
'body': msg.body,
|
||||||
|
'image': gravatar(msg.author, 32),
|
||||||
|
} for msg in messages]
|
||||||
|
return json.dumps(msg_list)
|
||||||
|
|
||||||
|
|
||||||
class ChatView(ListView):
|
class ChatView(ListView):
|
||||||
context_object_name = "message"
|
model = Message
|
||||||
template_name = "chat/chat.html"
|
context_object_name = 'message'
|
||||||
title = _("LQDOJ Chat")
|
template_name = 'chat/chat.html'
|
||||||
|
title = _('Chat Box')
|
||||||
def __init__(self):
|
paginate_by = 50
|
||||||
super().__init__()
|
paginator = Paginator(Message.objects.all(), paginate_by)
|
||||||
self.room_id = None
|
|
||||||
self.room = None
|
|
||||||
self.messages = None
|
|
||||||
self.first_page_size = 20 # only for first request
|
|
||||||
self.follow_up_page_size = 50
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.messages
|
|
||||||
|
|
||||||
def has_next(self):
|
|
||||||
try:
|
|
||||||
msg = Message.objects.filter(room=self.room_id).earliest("id")
|
|
||||||
except Exception as e:
|
|
||||||
return False
|
|
||||||
return msg not in self.messages
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
request_room = kwargs["room_id"]
|
page = request.GET.get('page')
|
||||||
page_size = self.follow_up_page_size
|
if (page == None):
|
||||||
try:
|
|
||||||
last_id = int(request.GET.get("last_id"))
|
|
||||||
except Exception:
|
|
||||||
last_id = 1e15
|
|
||||||
page_size = self.first_page_size
|
|
||||||
only_messages = request.GET.get("only_messages")
|
|
||||||
|
|
||||||
if request_room:
|
|
||||||
try:
|
|
||||||
self.room = Room.objects.get(id=request_room)
|
|
||||||
if not can_access_room(request, self.room):
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
except Room.DoesNotExist:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
else:
|
|
||||||
request_room = None
|
|
||||||
|
|
||||||
self.room_id = request_room
|
|
||||||
self.messages = (
|
|
||||||
Message.objects.filter(hidden=False, room=self.room_id, id__lt=last_id)
|
|
||||||
.select_related("author")
|
|
||||||
.only("body", "time", "author__rating", "author__display_rank")[:page_size]
|
|
||||||
)
|
|
||||||
if not only_messages:
|
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
return render(
|
cur_page = self.paginator.get_page(page)
|
||||||
request,
|
return HttpResponse(format_messages(cur_page.object_list))
|
||||||
"chat/message_list.html",
|
|
||||||
{
|
|
||||||
"object_list": self.messages,
|
|
||||||
"has_next": self.has_next(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['title'] = self.title
|
||||||
|
|
||||||
|
for msg in context['message']:
|
||||||
|
msg.time = format_time(msg.time)
|
||||||
|
|
||||||
context["title"] = self.title
|
|
||||||
context["last_msg"] = event.last()
|
|
||||||
context["status_sections"] = get_status_context(self.request.profile)
|
|
||||||
context["room"] = self.room_id
|
|
||||||
context["has_next"] = self.has_next()
|
|
||||||
context["unread_count_lobby"] = get_unread_count(None, self.request.profile)
|
|
||||||
context["chat_channel"] = encrypt_channel(
|
|
||||||
"chat_" + str(self.request.profile.id)
|
|
||||||
)
|
|
||||||
context["chat_lobby_channel"] = encrypt_channel("chat_lobby")
|
|
||||||
if self.room:
|
|
||||||
users_room = [self.room.user_one, self.room.user_two]
|
|
||||||
users_room.remove(self.request.profile)
|
|
||||||
context["other_user"] = users_room[0]
|
|
||||||
context["other_online"] = get_user_online_status(context["other_user"])
|
|
||||||
context["is_ignored"] = Ignore.is_ignored(
|
|
||||||
self.request.profile, context["other_user"]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
context["online_count"] = get_online_count()
|
|
||||||
context["message_template"] = {
|
|
||||||
"author": self.request.profile,
|
|
||||||
"id": "$id",
|
|
||||||
"time": timezone.now(),
|
|
||||||
"body": "$body",
|
|
||||||
}
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
def delete_message(request):
|
|
||||||
ret = {"delete": "done"}
|
|
||||||
|
|
||||||
if request.method == "GET":
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
try:
|
|
||||||
messid = int(request.POST.get("message"))
|
|
||||||
mess = Message.objects.get(id=messid)
|
|
||||||
except:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
if not request.user.is_staff and request.profile != mess.author:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
mess.hidden = True
|
|
||||||
mess.save()
|
|
||||||
|
|
||||||
return JsonResponse(ret)
|
|
||||||
|
|
||||||
|
|
||||||
def mute_message(request):
|
|
||||||
ret = {"mute": "done"}
|
|
||||||
|
|
||||||
if request.method == "GET":
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
if not request.user.is_staff:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
try:
|
|
||||||
messid = int(request.POST.get("message"))
|
|
||||||
mess = Message.objects.get(id=messid)
|
|
||||||
except:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
with revisions.create_revision():
|
|
||||||
revisions.set_comment(_("Mute chat") + ": " + mess.body)
|
|
||||||
revisions.set_user(request.user)
|
|
||||||
mess.author.mute = True
|
|
||||||
mess.author.save()
|
|
||||||
|
|
||||||
Message.objects.filter(room=None, author=mess.author).update(hidden=True)
|
|
||||||
|
|
||||||
return JsonResponse(ret)
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
def post_message(request):
|
|
||||||
ret = {"msg": "posted"}
|
|
||||||
|
|
||||||
if request.method != "POST":
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
if len(request.POST["body"]) > 5000 or len(request.POST["body"].strip()) == 0:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
room = None
|
|
||||||
if request.POST["room"]:
|
|
||||||
room = Room.objects.get(id=request.POST["room"])
|
|
||||||
|
|
||||||
if not check_valid_message(request, room):
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
new_message = Message(author=request.profile, body=request.POST["body"], room=room)
|
|
||||||
new_message.save()
|
|
||||||
|
|
||||||
if not room:
|
|
||||||
event.post(
|
|
||||||
encrypt_channel("chat_lobby"),
|
|
||||||
{
|
|
||||||
"type": "lobby",
|
|
||||||
"author_id": request.profile.id,
|
|
||||||
"message": new_message.id,
|
|
||||||
"room": "None",
|
|
||||||
"tmp_id": request.POST.get("tmp_id"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
get_room_info.dirty(room.id)
|
|
||||||
room.last_msg_time = new_message.time
|
|
||||||
room.save()
|
|
||||||
|
|
||||||
for user in room.users():
|
|
||||||
event.post(
|
|
||||||
encrypt_channel("chat_" + str(user.id)),
|
|
||||||
{
|
|
||||||
"type": "private",
|
|
||||||
"author_id": request.profile.id,
|
|
||||||
"message": new_message.id,
|
|
||||||
"room": room.id,
|
|
||||||
"tmp_id": request.POST.get("tmp_id"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if user != request.profile:
|
|
||||||
UserRoom.objects.filter(user=user, room=room).update(
|
|
||||||
unread_count=F("unread_count") + 1
|
|
||||||
)
|
|
||||||
get_unread_boxes.dirty(user)
|
|
||||||
|
|
||||||
return JsonResponse(ret)
|
|
||||||
|
|
||||||
|
|
||||||
def can_access_room(request, room):
|
|
||||||
return not room or room.contain(request.profile)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def chat_message_ajax(request):
|
|
||||||
if request.method != "GET":
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
try:
|
|
||||||
message_id = request.GET["message"]
|
|
||||||
except KeyError:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
try:
|
|
||||||
message = Message.objects.filter(hidden=False).get(id=message_id)
|
|
||||||
room = message.room
|
|
||||||
if not can_access_room(request, room):
|
|
||||||
return HttpResponse("Unauthorized", status=401)
|
|
||||||
except Message.DoesNotExist:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
return render(
|
|
||||||
request,
|
|
||||||
"chat/message.html",
|
|
||||||
{
|
|
||||||
"message": message,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def update_last_seen(request, **kwargs):
|
|
||||||
if "room_id" in kwargs:
|
|
||||||
room_id = kwargs["room_id"]
|
|
||||||
elif request.method == "GET":
|
|
||||||
room_id = request.GET.get("room")
|
|
||||||
elif request.method == "POST":
|
|
||||||
room_id = request.POST.get("room")
|
|
||||||
else:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
try:
|
|
||||||
profile = request.profile
|
|
||||||
room = None
|
|
||||||
if room_id:
|
|
||||||
room = Room.objects.filter(id=int(room_id)).first()
|
|
||||||
except Room.DoesNotExist:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
if not can_access_room(request, room):
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
user_room, _ = UserRoom.objects.get_or_create(user=profile, room=room)
|
|
||||||
user_room.last_seen = timezone.now()
|
|
||||||
user_room.unread_count = 0
|
|
||||||
user_room.save()
|
|
||||||
|
|
||||||
get_unread_boxes.dirty(profile)
|
|
||||||
|
|
||||||
return JsonResponse({"msg": "updated"})
|
|
||||||
|
|
||||||
|
|
||||||
def get_online_count():
|
|
||||||
last_5_minutes = timezone.now() - timezone.timedelta(minutes=5)
|
|
||||||
return Profile.objects.filter(last_access__gte=last_5_minutes).count()
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_online_status(user):
|
|
||||||
time_diff = timezone.now() - user.last_access
|
|
||||||
is_online = time_diff <= timezone.timedelta(minutes=5)
|
|
||||||
return is_online
|
|
||||||
|
|
||||||
|
|
||||||
def user_online_status_ajax(request):
|
|
||||||
if request.method != "GET":
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
user_id = request.GET.get("user")
|
|
||||||
|
|
||||||
if user_id:
|
|
||||||
try:
|
|
||||||
user_id = int(user_id)
|
|
||||||
user = Profile.objects.get(id=user_id)
|
|
||||||
except Exception as e:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
is_online = get_user_online_status(user)
|
|
||||||
return render(
|
|
||||||
request,
|
|
||||||
"chat/user_online_status.html",
|
|
||||||
{
|
|
||||||
"other_user": user,
|
|
||||||
"other_online": is_online,
|
|
||||||
"is_ignored": Ignore.is_ignored(request.profile, user),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return render(
|
|
||||||
request,
|
|
||||||
"chat/user_online_status.html",
|
|
||||||
{
|
|
||||||
"online_count": get_online_count(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_online_status(profile, other_profile_ids, rooms=None):
|
|
||||||
if not other_profile_ids:
|
|
||||||
return None
|
|
||||||
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 = []
|
|
||||||
if rooms:
|
|
||||||
unread_count = get_unread_count(rooms, profile)
|
|
||||||
count = {}
|
|
||||||
last_msg = {}
|
|
||||||
room_of_user = {}
|
|
||||||
for i in 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 other_profile in other_profiles:
|
|
||||||
is_online = False
|
|
||||||
if other_profile.last_access >= last_5_minutes:
|
|
||||||
is_online = True
|
|
||||||
user_dict = {"user": other_profile, "is_online": is_online}
|
|
||||||
if rooms:
|
|
||||||
user_dict.update(
|
|
||||||
{
|
|
||||||
"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)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def get_status_context(profile, include_ignored=False):
|
|
||||||
if include_ignored:
|
|
||||||
ignored_users = []
|
|
||||||
queryset = Profile.objects
|
|
||||||
else:
|
|
||||||
ignored_users = list(
|
|
||||||
Ignore.get_ignored_users(profile).values_list("id", flat=True)
|
|
||||||
)
|
|
||||||
queryset = Profile.objects.exclude(id__in=ignored_users)
|
|
||||||
|
|
||||||
last_5_minutes = timezone.now() - timezone.timedelta(minutes=5)
|
|
||||||
recent_profile = (
|
|
||||||
Room.objects.filter(Q(user_one=profile) | Q(user_two=profile))
|
|
||||||
.annotate(
|
|
||||||
other_user=Case(
|
|
||||||
When(user_one=profile, then="user_two"),
|
|
||||||
default="user_one",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.filter(last_msg_time__isnull=False)
|
|
||||||
.exclude(other_user__in=ignored_users)
|
|
||||||
.order_by("-last_msg_time")
|
|
||||||
.values("other_user", "id")[:20]
|
|
||||||
)
|
|
||||||
|
|
||||||
recent_profile_ids = [str(i["other_user"]) for i in recent_profile]
|
|
||||||
recent_rooms = [int(i["id"]) for i in recent_profile]
|
|
||||||
Room.prefetch_room_cache(recent_rooms)
|
|
||||||
|
|
||||||
admin_list = (
|
|
||||||
queryset.filter(display_rank="admin")
|
|
||||||
.exclude(id__in=recent_profile_ids)
|
|
||||||
.values_list("id", flat=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"title": _("Recent"),
|
|
||||||
"user_list": get_online_status(profile, recent_profile_ids, recent_rooms),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": _("Admin"),
|
|
||||||
"user_list": get_online_status(profile, admin_list),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def online_status_ajax(request):
|
|
||||||
return render(
|
|
||||||
request,
|
|
||||||
"chat/online_status.html",
|
|
||||||
{
|
|
||||||
"status_sections": get_status_context(request.profile),
|
|
||||||
"unread_count_lobby": get_unread_count(None, request.profile),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def get_room(user_one, user_two):
|
|
||||||
if user_one.id > user_two.id:
|
|
||||||
user_one, user_two = user_two, user_one
|
|
||||||
room, created = Room.objects.get_or_create(user_one=user_one, user_two=user_two)
|
|
||||||
return room
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def get_or_create_room(request):
|
|
||||||
if request.method == "GET":
|
|
||||||
decrypted_other_id = request.GET.get("other")
|
|
||||||
elif request.method == "POST":
|
|
||||||
decrypted_other_id = request.POST.get("other")
|
|
||||||
else:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
request_id, other_id = decrypt_url(decrypted_other_id)
|
|
||||||
if not other_id or not request_id or request_id != request.profile.id:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
try:
|
|
||||||
other_user = Profile.objects.get(id=int(other_id))
|
|
||||||
except Exception:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
user = request.profile
|
|
||||||
|
|
||||||
if not other_user or not user:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
# TODO: each user can only create <= 300 rooms
|
|
||||||
room = get_room(other_user, user)
|
|
||||||
for u in [other_user, user]:
|
|
||||||
user_room, created = UserRoom.objects.get_or_create(user=u, room=room)
|
|
||||||
if created:
|
|
||||||
user_room.last_seen = timezone.now()
|
|
||||||
user_room.save()
|
|
||||||
|
|
||||||
room_url = reverse("chat", kwargs={"room_id": room.id})
|
|
||||||
if request.method == "GET":
|
|
||||||
return JsonResponse(
|
|
||||||
{
|
|
||||||
"room": room.id,
|
|
||||||
"other_user_id": other_user.id,
|
|
||||||
"url": room_url,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return HttpResponseRedirect(room_url)
|
|
||||||
|
|
||||||
|
|
||||||
def get_unread_count(rooms, user):
|
|
||||||
if rooms:
|
|
||||||
return UserRoom.objects.filter(
|
|
||||||
user=user, room__in=rooms, unread_count__gt=0
|
|
||||||
).values("unread_count", "room")
|
|
||||||
else: # lobby
|
|
||||||
user_room = UserRoom.objects.filter(user=user, room__isnull=True).first()
|
|
||||||
if not user_room:
|
|
||||||
return 0
|
|
||||||
last_seen = user_room.last_seen
|
|
||||||
res = (
|
|
||||||
Message.objects.filter(room__isnull=True, time__gte=last_seen)
|
|
||||||
.exclude(author=user)
|
|
||||||
.exclude(hidden=True)
|
|
||||||
.count()
|
|
||||||
)
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def toggle_ignore(request, **kwargs):
|
|
||||||
user_id = kwargs["user_id"]
|
|
||||||
if not user_id:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
try:
|
|
||||||
other_user = Profile.objects.get(id=user_id)
|
|
||||||
except:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
Ignore.toggle_ignore(request.profile, other_user)
|
|
||||||
next_url = request.GET.get("next", "/")
|
|
||||||
return HttpResponseRedirect(next_url)
|
|
||||||
|
|
|
@ -12,6 +12,6 @@ if (2, 2) <= django.VERSION < (3,):
|
||||||
# attribute where the exact query sent to the database is saved.
|
# attribute where the exact query sent to the database is saved.
|
||||||
# See MySQLdb/cursors.py in the source distribution.
|
# See MySQLdb/cursors.py in the source distribution.
|
||||||
# MySQLdb returns string, PyMySQL bytes.
|
# MySQLdb returns string, PyMySQL bytes.
|
||||||
return force_text(getattr(cursor, "_executed", None), errors="replace")
|
return force_text(getattr(cursor, '_executed', None), errors='replace')
|
||||||
|
|
||||||
DatabaseOperations.last_executed_query = last_executed_query
|
DatabaseOperations.last_executed_query = last_executed_query
|
||||||
|
|
|
@ -11,12 +11,6 @@
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace_editor {
|
|
||||||
overflow: hidden;
|
|
||||||
font: 12px/normal 'Fira Code', 'Monaco', 'Menlo', monospace;
|
|
||||||
direction: 1tr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.django-ace-widget.loading {
|
.django-ace-widget.loading {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,17 +77,15 @@
|
||||||
mode = widget.getAttribute('data-mode'),
|
mode = widget.getAttribute('data-mode'),
|
||||||
theme = widget.getAttribute('data-theme'),
|
theme = widget.getAttribute('data-theme'),
|
||||||
wordwrap = widget.getAttribute('data-wordwrap'),
|
wordwrap = widget.getAttribute('data-wordwrap'),
|
||||||
toolbar = prev(widget);
|
toolbar = prev(widget),
|
||||||
var main_block = div.parentNode.parentNode;
|
main_block = toolbar.parentNode;
|
||||||
|
|
||||||
if (toolbar != null) {
|
|
||||||
// Toolbar maximize/minimize button
|
// Toolbar maximize/minimize button
|
||||||
var min_max = toolbar.getElementsByClassName('django-ace-max_min');
|
var min_max = toolbar.getElementsByClassName('django-ace-max_min');
|
||||||
min_max[0].onclick = function () {
|
min_max[0].onclick = function () {
|
||||||
minimizeMaximize(widget, main_block, editor);
|
minimizeMaximize(widget, main_block, editor);
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
editor.getSession().setValue(textarea.value);
|
editor.getSession().setValue(textarea.value);
|
||||||
|
|
||||||
|
@ -162,7 +160,7 @@
|
||||||
]);
|
]);
|
||||||
|
|
||||||
window[widget.id] = editor;
|
window[widget.id] = editor;
|
||||||
setTimeout(() => $(widget).trigger('ace_load', [editor]), 100);
|
$(widget).trigger('ace_load', [editor]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
|
|
@ -11,33 +11,22 @@ from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
|
||||||
class AceWidget(forms.Textarea):
|
class AceWidget(forms.Textarea):
|
||||||
def __init__(
|
def __init__(self, mode=None, theme=None, wordwrap=False, width='100%', height='300px',
|
||||||
self,
|
no_ace_media=False, *args, **kwargs):
|
||||||
mode=None,
|
|
||||||
theme=None,
|
|
||||||
wordwrap=False,
|
|
||||||
width="100%",
|
|
||||||
height="300px",
|
|
||||||
no_ace_media=False,
|
|
||||||
toolbar=True,
|
|
||||||
*args,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.wordwrap = wordwrap
|
self.wordwrap = wordwrap
|
||||||
self.width = width
|
self.width = width
|
||||||
self.height = height
|
self.height = height
|
||||||
self.ace_media = not no_ace_media
|
self.ace_media = not no_ace_media
|
||||||
self.toolbar = toolbar
|
|
||||||
super(AceWidget, self).__init__(*args, **kwargs)
|
super(AceWidget, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media(self):
|
def media(self):
|
||||||
js = [urljoin(settings.ACE_URL, "ace.js")] if self.ace_media else []
|
js = [urljoin(settings.ACE_URL, 'ace.js')] if self.ace_media else []
|
||||||
js.append("django_ace/widget.js")
|
js.append('django_ace/widget.js')
|
||||||
css = {
|
css = {
|
||||||
"screen": ["django_ace/widget.css"],
|
'screen': ['django_ace/widget.css'],
|
||||||
}
|
}
|
||||||
return forms.Media(js=js, css=css)
|
return forms.Media(js=js, css=css)
|
||||||
|
|
||||||
|
@ -45,32 +34,24 @@ class AceWidget(forms.Textarea):
|
||||||
attrs = attrs or {}
|
attrs = attrs or {}
|
||||||
|
|
||||||
ace_attrs = {
|
ace_attrs = {
|
||||||
"class": "django-ace-widget loading",
|
'class': 'django-ace-widget loading',
|
||||||
"style": "width:%s; height:%s" % (self.width, self.height),
|
'style': 'width:%s; height:%s' % (self.width, self.height),
|
||||||
"id": "ace_%s" % name,
|
'id': 'ace_%s' % name,
|
||||||
}
|
}
|
||||||
if self.mode:
|
if self.mode:
|
||||||
ace_attrs["data-mode"] = self.mode
|
ace_attrs['data-mode'] = self.mode
|
||||||
if self.theme:
|
if self.theme:
|
||||||
ace_attrs["data-theme"] = self.theme
|
ace_attrs['data-theme'] = self.theme
|
||||||
if self.wordwrap:
|
if self.wordwrap:
|
||||||
ace_attrs["data-wordwrap"] = "true"
|
ace_attrs['data-wordwrap'] = 'true'
|
||||||
|
|
||||||
attrs.update(
|
attrs.update(style='width: 100%; min-width: 100%; max-width: 100%; resize: none')
|
||||||
style="width: 100%; min-width: 100%; max-width: 100%; resize: none"
|
|
||||||
)
|
|
||||||
textarea = super(AceWidget, self).render(name, value, attrs)
|
textarea = super(AceWidget, self).render(name, value, attrs)
|
||||||
|
|
||||||
html = "<div%s><div></div></div>%s" % (flatatt(ace_attrs), textarea)
|
html = '<div%s><div></div></div>%s' % (flatatt(ace_attrs), textarea)
|
||||||
|
|
||||||
if self.toolbar:
|
# add toolbar
|
||||||
toolbar = (
|
html = ('<div class="django-ace-editor"><div style="width: 100%%" class="django-ace-toolbar">'
|
||||||
'<div style="width: {}" class="django-ace-toolbar">'
|
'<a href="./" class="django-ace-max_min"></a></div>%s</div>') % html
|
||||||
'<a href="#" class="django-ace-max_min"></a>'
|
|
||||||
"</div>"
|
|
||||||
).format(self.width)
|
|
||||||
html = toolbar + html
|
|
||||||
|
|
||||||
html = '<div class="django-ace-editor">{}</div>'.format(html)
|
|
||||||
|
|
||||||
return mark_safe(html)
|
return mark_safe(html)
|
||||||
|
|
|
@ -4,30 +4,24 @@ import socket
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
from celery.signals import task_failure
|
from celery.signals import task_failure
|
||||||
|
|
||||||
app = Celery("dmoj")
|
app = Celery('dmoj')
|
||||||
|
|
||||||
from django.conf import settings # noqa: E402, I202, django must be imported here
|
from django.conf import settings # noqa: E402, I202, django must be imported here
|
||||||
|
app.config_from_object(settings, namespace='CELERY')
|
||||||
|
|
||||||
app.config_from_object(settings, namespace="CELERY")
|
if hasattr(settings, 'CELERY_BROKER_URL_SECRET'):
|
||||||
|
|
||||||
if hasattr(settings, "CELERY_BROKER_URL_SECRET"):
|
|
||||||
app.conf.broker_url = settings.CELERY_BROKER_URL_SECRET
|
app.conf.broker_url = settings.CELERY_BROKER_URL_SECRET
|
||||||
if hasattr(settings, "CELERY_RESULT_BACKEND_SECRET"):
|
if hasattr(settings, 'CELERY_RESULT_BACKEND_SECRET'):
|
||||||
app.conf.result_backend = settings.CELERY_RESULT_BACKEND_SECRET
|
app.conf.result_backend = settings.CELERY_RESULT_BACKEND_SECRET
|
||||||
|
|
||||||
# Load task modules from all registered Django app configs.
|
# Load task modules from all registered Django app configs.
|
||||||
app.autodiscover_tasks()
|
app.autodiscover_tasks()
|
||||||
|
|
||||||
# Logger to enable errors be reported.
|
# Logger to enable errors be reported.
|
||||||
logger = logging.getLogger("judge.celery")
|
logger = logging.getLogger('judge.celery')
|
||||||
|
|
||||||
|
|
||||||
@task_failure.connect()
|
@task_failure.connect()
|
||||||
def celery_failure_log(sender, task_id, exception, traceback, *args, **kwargs):
|
def celery_failure_log(sender, task_id, exception, traceback, *args, **kwargs):
|
||||||
logger.error(
|
logger.error('Celery Task %s: %s on %s', sender.name, task_id, socket.gethostname(), # noqa: G201
|
||||||
"Celery Task %s: %s on %s",
|
exc_info=(type(exception), exception, traceback))
|
||||||
sender.name,
|
|
||||||
task_id,
|
|
||||||
socket.gethostname(), # noqa: G201
|
|
||||||
exc_info=(type(exception), exception, traceback),
|
|
||||||
)
|
|
||||||
|
|
12
dmoj/routing.py
Normal file
12
dmoj/routing.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from channels.auth import AuthMiddlewareStack
|
||||||
|
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||||
|
import chat_box.routing
|
||||||
|
|
||||||
|
application = ProtocolTypeRouter({
|
||||||
|
# (http->django views is added by default)
|
||||||
|
'websocket': AuthMiddlewareStack(
|
||||||
|
URLRouter(
|
||||||
|
chat_box.routing.websocket_urlpatterns
|
||||||
|
)
|
||||||
|
),
|
||||||
|
})
|
553
dmoj/settings.py
553
dmoj/settings.py
|
@ -22,7 +22,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||||
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = "5*9f5q57mqmlz2#f$x1h76&jxy#yortjl1v+l*6hd18$d*yx#0"
|
SECRET_KEY = '5*9f5q57mqmlz2#f$x1h76&jxy#yortjl1v+l*6hd18$d*yx#0'
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
@ -30,10 +30,9 @@ DEBUG = True
|
||||||
ALLOWED_HOSTS = []
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
SITE_ID = 1
|
SITE_ID = 1
|
||||||
SITE_NAME = "LQDOJ"
|
SITE_NAME = 'LQDOJ'
|
||||||
SITE_LONG_NAME = "LQDOJ: Le Quy Don Online Judge"
|
SITE_LONG_NAME = 'LQDOJ: Le Quy Don Online Judge'
|
||||||
SITE_ADMIN_EMAIL = False
|
SITE_ADMIN_EMAIL = False
|
||||||
SITE_DOMAIN = "lqdoj.edu.vn"
|
|
||||||
|
|
||||||
DMOJ_REQUIRE_STAFF_2FA = True
|
DMOJ_REQUIRE_STAFF_2FA = True
|
||||||
|
|
||||||
|
@ -47,9 +46,11 @@ DMOJ_PP_STEP = 0.95
|
||||||
DMOJ_PP_ENTRIES = 100
|
DMOJ_PP_ENTRIES = 100
|
||||||
DMOJ_PP_BONUS_FUNCTION = lambda n: 300 * (1 - 0.997 ** n) # noqa: E731
|
DMOJ_PP_BONUS_FUNCTION = lambda n: 300 * (1 - 0.997 ** n) # noqa: E731
|
||||||
|
|
||||||
NODEJS = "/usr/bin/node"
|
NODEJS = '/usr/bin/node'
|
||||||
EXIFTOOL = "/usr/bin/exiftool"
|
EXIFTOOL = '/usr/bin/exiftool'
|
||||||
ACE_URL = "//cdnjs.cloudflare.com/ajax/libs/ace/1.1.3"
|
ACE_URL = '//cdnjs.cloudflare.com/ajax/libs/ace/1.1.3'
|
||||||
|
SELECT2_JS_URL = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js'
|
||||||
|
DEFAULT_SELECT2_CSS = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css'
|
||||||
|
|
||||||
DMOJ_CAMO_URL = None
|
DMOJ_CAMO_URL = None
|
||||||
DMOJ_CAMO_KEY = None
|
DMOJ_CAMO_KEY = None
|
||||||
|
@ -61,32 +62,26 @@ DMOJ_PROBLEM_MAX_TIME_LIMIT = 60 # seconds
|
||||||
DMOJ_PROBLEM_MIN_MEMORY_LIMIT = 0 # kilobytes
|
DMOJ_PROBLEM_MIN_MEMORY_LIMIT = 0 # kilobytes
|
||||||
DMOJ_PROBLEM_MAX_MEMORY_LIMIT = 1048576 # kilobytes
|
DMOJ_PROBLEM_MAX_MEMORY_LIMIT = 1048576 # kilobytes
|
||||||
DMOJ_PROBLEM_MIN_PROBLEM_POINTS = 0
|
DMOJ_PROBLEM_MIN_PROBLEM_POINTS = 0
|
||||||
DMOJ_SUBMISSION_ROOT = "/tmp"
|
DMOJ_RATING_COLORS = False
|
||||||
DMOJ_RATING_COLORS = True
|
|
||||||
DMOJ_EMAIL_THROTTLING = (10, 60)
|
DMOJ_EMAIL_THROTTLING = (10, 60)
|
||||||
DMOJ_STATS_LANGUAGE_THRESHOLD = 10
|
DMOJ_STATS_LANGUAGE_THRESHOLD = 10
|
||||||
DMOJ_SUBMISSIONS_REJUDGE_LIMIT = 10
|
DMOJ_SUBMISSIONS_REJUDGE_LIMIT = 10
|
||||||
# Maximum number of submissions a single user can queue without the `spam_submission` permission
|
# Maximum number of submissions a single user can queue without the `spam_submission` permission
|
||||||
DMOJ_SUBMISSION_LIMIT = 3
|
DMOJ_SUBMISSION_LIMIT = 2
|
||||||
DMOJ_BLOG_NEW_PROBLEM_COUNT = 7
|
DMOJ_BLOG_NEW_PROBLEM_COUNT = 7
|
||||||
DMOJ_BLOG_NEW_CONTEST_COUNT = 7
|
|
||||||
DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT = 7
|
DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT = 7
|
||||||
DMOJ_TOTP_TOLERANCE_HALF_MINUTES = 1
|
DMOJ_TOTP_TOLERANCE_HALF_MINUTES = 1
|
||||||
DMOJ_USER_MAX_ORGANIZATION_COUNT = 10
|
DMOJ_USER_MAX_ORGANIZATION_COUNT = 3
|
||||||
DMOJ_USER_MAX_ORGANIZATION_ADD = 5
|
|
||||||
DMOJ_COMMENT_VOTE_HIDE_THRESHOLD = -5
|
DMOJ_COMMENT_VOTE_HIDE_THRESHOLD = -5
|
||||||
DMOJ_PDF_PROBLEM_CACHE = ""
|
DMOJ_PDF_PROBLEM_CACHE = ''
|
||||||
DMOJ_PDF_PROBLEM_TEMP_DIR = tempfile.gettempdir()
|
DMOJ_PDF_PROBLEM_TEMP_DIR = tempfile.gettempdir()
|
||||||
DMOJ_STATS_SUBMISSION_RESULT_COLORS = {
|
DMOJ_STATS_SUBMISSION_RESULT_COLORS = {
|
||||||
"TLE": "#a3bcbd",
|
'TLE': '#a3bcbd',
|
||||||
"AC": "#00a92a",
|
'AC': '#00a92a',
|
||||||
"WA": "#ed4420",
|
'WA': '#ed4420',
|
||||||
"CE": "#42586d",
|
'CE': '#42586d',
|
||||||
"ERR": "#ffa71c",
|
'ERR': '#ffa71c',
|
||||||
}
|
}
|
||||||
DMOJ_PROFILE_IMAGE_ROOT = "profile_images"
|
|
||||||
DMOJ_ORGANIZATION_IMAGE_ROOT = "organization_images"
|
|
||||||
DMOJ_TEST_FORMATTER_ROOT = "test_formatter"
|
|
||||||
|
|
||||||
MARKDOWN_STYLES = {}
|
MARKDOWN_STYLES = {}
|
||||||
MARKDOWN_DEFAULT_STYLE = {}
|
MARKDOWN_DEFAULT_STYLE = {}
|
||||||
|
@ -94,15 +89,16 @@ MARKDOWN_DEFAULT_STYLE = {}
|
||||||
MATHOID_URL = False
|
MATHOID_URL = False
|
||||||
MATHOID_GZIP = False
|
MATHOID_GZIP = False
|
||||||
MATHOID_MML_CACHE = None
|
MATHOID_MML_CACHE = None
|
||||||
MATHOID_CSS_CACHE = "default"
|
MATHOID_CSS_CACHE = 'default'
|
||||||
MATHOID_DEFAULT_TYPE = "auto"
|
MATHOID_DEFAULT_TYPE = 'auto'
|
||||||
MATHOID_MML_CACHE_TTL = 86400
|
MATHOID_MML_CACHE_TTL = 86400
|
||||||
MATHOID_CACHE_ROOT = tempfile.gettempdir() + "/mathoidCache"
|
MATHOID_CACHE_ROOT = ''
|
||||||
MATHOID_CACHE_URL = False
|
MATHOID_CACHE_URL = False
|
||||||
|
|
||||||
TEXOID_GZIP = False
|
TEXOID_GZIP = False
|
||||||
TEXOID_META_CACHE = "default"
|
TEXOID_META_CACHE = 'default'
|
||||||
TEXOID_META_CACHE_TTL = 86400
|
TEXOID_META_CACHE_TTL = 86400
|
||||||
|
DMOJ_NEWSLETTER_ID_ON_REGISTER = None
|
||||||
|
|
||||||
BAD_MAIL_PROVIDERS = ()
|
BAD_MAIL_PROVIDERS = ()
|
||||||
BAD_MAIL_PROVIDER_REGEX = ()
|
BAD_MAIL_PROVIDER_REGEX = ()
|
||||||
|
@ -113,30 +109,27 @@ TIMEZONE_MAP = None
|
||||||
TIMEZONE_DETECT_BACKEND = None
|
TIMEZONE_DETECT_BACKEND = None
|
||||||
|
|
||||||
TERMS_OF_SERVICE_URL = None
|
TERMS_OF_SERVICE_URL = None
|
||||||
DEFAULT_USER_LANGUAGE = "PY3"
|
DEFAULT_USER_LANGUAGE = 'CPP11'
|
||||||
|
|
||||||
PHANTOMJS = ""
|
PHANTOMJS = ''
|
||||||
PHANTOMJS_PDF_ZOOM = 0.75
|
PHANTOMJS_PDF_ZOOM = 0.75
|
||||||
PHANTOMJS_PDF_TIMEOUT = 5.0
|
PHANTOMJS_PDF_TIMEOUT = 5.0
|
||||||
PHANTOMJS_PAPER_SIZE = "Letter"
|
PHANTOMJS_PAPER_SIZE = 'Letter'
|
||||||
|
|
||||||
SLIMERJS = ""
|
SLIMERJS = ''
|
||||||
SLIMERJS_PDF_ZOOM = 0.75
|
SLIMERJS_PDF_ZOOM = 0.75
|
||||||
SLIMERJS_FIREFOX_PATH = ""
|
SLIMERJS_FIREFOX_PATH = ''
|
||||||
SLIMERJS_PAPER_SIZE = "Letter"
|
SLIMERJS_PAPER_SIZE = 'Letter'
|
||||||
|
|
||||||
PUPPETEER_MODULE = "/usr/lib/node_modules/puppeteer"
|
PUPPETEER_MODULE = '/usr/lib/node_modules/puppeteer'
|
||||||
PUPPETEER_PAPER_SIZE = "Letter"
|
PUPPETEER_PAPER_SIZE = 'Letter'
|
||||||
|
|
||||||
USE_SELENIUM = False
|
|
||||||
SELENIUM_CUSTOM_CHROME_PATH = None
|
|
||||||
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 = "//cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
|
FONTAWESOME_CSS = '//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css'
|
||||||
DMOJ_CANONICAL = ""
|
DMOJ_CANONICAL = ''
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
|
@ -148,315 +141,350 @@ except ImportError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
del wpadmin
|
del wpadmin
|
||||||
INSTALLED_APPS += ("wpadmin",)
|
INSTALLED_APPS += ('wpadmin',)
|
||||||
|
|
||||||
WPADMIN = {
|
WPADMIN = {
|
||||||
"admin": {
|
'admin': {
|
||||||
"title": "LQDOJ Admin",
|
'title': 'LQDOJ Admin',
|
||||||
"menu": {
|
'menu': {
|
||||||
"top": "wpadmin.menu.menus.BasicTopMenu",
|
'top': 'wpadmin.menu.menus.BasicTopMenu',
|
||||||
"left": "wpadmin.menu.custom.CustomModelLeftMenuWithDashboard",
|
'left': 'wpadmin.menu.custom.CustomModelLeftMenuWithDashboard',
|
||||||
},
|
},
|
||||||
"custom_menu": [
|
'custom_menu': [
|
||||||
{
|
{
|
||||||
"model": "judge.Problem",
|
'model': 'judge.Problem',
|
||||||
"icon": "fa-question-circle",
|
'icon': 'fa-question-circle',
|
||||||
"children": [
|
'children': [
|
||||||
"judge.ProblemGroup",
|
'judge.ProblemGroup',
|
||||||
"judge.ProblemType",
|
'judge.ProblemType',
|
||||||
"judge.ProblemPointsVote",
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model": "judge.Submission",
|
'model': 'judge.Submission',
|
||||||
"icon": "fa-check-square",
|
'icon': 'fa-check-square-o',
|
||||||
"children": [
|
'children': [
|
||||||
"judge.Language",
|
'judge.Language',
|
||||||
"judge.Judge",
|
'judge.Judge',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model": "judge.Contest",
|
'model': 'judge.Contest',
|
||||||
"icon": "fa-bar-chart",
|
'icon': 'fa-bar-chart',
|
||||||
"children": [
|
'children': [
|
||||||
"judge.ContestParticipation",
|
'judge.ContestParticipation',
|
||||||
"judge.ContestTag",
|
'judge.ContestTag',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model": "auth.User",
|
'model': 'auth.User',
|
||||||
"icon": "fa-user",
|
'icon': 'fa-user',
|
||||||
"children": [
|
'children': [
|
||||||
"auth.Group",
|
'auth.Group',
|
||||||
"registration.RegistrationProfile",
|
'registration.RegistrationProfile',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model": "judge.Profile",
|
'model': 'judge.Profile',
|
||||||
"icon": "fa-user-plus",
|
'icon': 'fa-user-plus',
|
||||||
"children": [
|
'children': [
|
||||||
"judge.Organization",
|
'judge.Organization',
|
||||||
"judge.OrganizationRequest",
|
'judge.OrganizationRequest',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model": "judge.NavigationBar",
|
'model': 'judge.NavigationBar',
|
||||||
"icon": "fa-bars",
|
'icon': 'fa-bars',
|
||||||
"children": [
|
'children': [
|
||||||
"judge.MiscConfig",
|
'judge.MiscConfig',
|
||||||
"judge.License",
|
'judge.License',
|
||||||
"sites.Site",
|
'sites.Site',
|
||||||
"redirects.Redirect",
|
'redirects.Redirect',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
("judge.BlogPost", "fa-rss-square"),
|
('judge.BlogPost', 'fa-rss-square'),
|
||||||
("judge.Ticket", "fa-exclamation-circle"),
|
('judge.Comment', 'fa-comment-o'),
|
||||||
("admin.LogEntry", "fa-empire"),
|
('flatpages.FlatPage', 'fa-file-text-o'),
|
||||||
|
('judge.Solution', 'fa-pencil'),
|
||||||
],
|
],
|
||||||
"dashboard": {
|
'dashboard': {
|
||||||
"breadcrumbs": True,
|
'breadcrumbs': True,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
INSTALLED_APPS += (
|
INSTALLED_APPS += (
|
||||||
"django.contrib.admin",
|
'django.contrib.admin',
|
||||||
"judge",
|
'judge',
|
||||||
"django.contrib.auth",
|
'django.contrib.auth',
|
||||||
"django.contrib.contenttypes",
|
'django.contrib.contenttypes',
|
||||||
"django.contrib.flatpages",
|
'django.contrib.flatpages',
|
||||||
"django.contrib.sessions",
|
'django.contrib.sessions',
|
||||||
"django.contrib.messages",
|
'django.contrib.messages',
|
||||||
"django.contrib.redirects",
|
'django.contrib.redirects',
|
||||||
"django.contrib.staticfiles",
|
'django.contrib.staticfiles',
|
||||||
"django.contrib.sites",
|
'django.contrib.sites',
|
||||||
"django.contrib.sitemaps",
|
'django.contrib.sitemaps',
|
||||||
"registration",
|
'registration',
|
||||||
"mptt",
|
'mptt',
|
||||||
"reversion",
|
'reversion',
|
||||||
"reversion_compare",
|
'django_social_share',
|
||||||
"django_social_share",
|
'social_django',
|
||||||
"social_django",
|
'compressor',
|
||||||
"compressor",
|
'django_ace',
|
||||||
"django_ace",
|
'pagedown',
|
||||||
"pagedown",
|
'sortedm2m',
|
||||||
"sortedm2m",
|
'statici18n',
|
||||||
"statici18n",
|
'impersonate',
|
||||||
"impersonate",
|
'django_jinja',
|
||||||
"django_jinja",
|
'chat_box',
|
||||||
"chat_box",
|
'channels',
|
||||||
"django.forms",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
MIDDLEWARE = (
|
MIDDLEWARE = (
|
||||||
"judge.middleware.SlowRequestMiddleware",
|
'judge.middleware.ShortCircuitMiddleware',
|
||||||
"judge.middleware.ShortCircuitMiddleware",
|
'django.middleware.common.CommonMiddleware',
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
"django.middleware.locale.LocaleMiddleware",
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
"django.middleware.common.CommonMiddleware",
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
'judge.middleware.DMOJLoginMiddleware',
|
||||||
"judge.middleware.DMOJLoginMiddleware",
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
'judge.user_log.LogUserAccessMiddleware',
|
||||||
"judge.user_log.LogUserAccessMiddleware",
|
'judge.timezone.TimezoneMiddleware',
|
||||||
"judge.timezone.TimezoneMiddleware",
|
'impersonate.middleware.ImpersonateMiddleware',
|
||||||
"impersonate.middleware.ImpersonateMiddleware",
|
'judge.middleware.DMOJImpersonationMiddleware',
|
||||||
"judge.middleware.DMOJImpersonationMiddleware",
|
'judge.middleware.ContestMiddleware',
|
||||||
"judge.middleware.ContestMiddleware",
|
'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
|
||||||
"judge.middleware.DarkModeMiddleware",
|
'judge.social_auth.SocialAuthExceptionMiddleware',
|
||||||
"judge.middleware.SubdomainMiddleware",
|
'django.contrib.redirects.middleware.RedirectFallbackMiddleware',
|
||||||
"django.contrib.flatpages.middleware.FlatpageFallbackMiddleware",
|
|
||||||
"judge.social_auth.SocialAuthExceptionMiddleware",
|
|
||||||
"django.contrib.redirects.middleware.RedirectFallbackMiddleware",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
X_FRAME_OPTIONS = "SAMEORIGIN"
|
IMPERSONATE_REQUIRE_SUPERUSER = True
|
||||||
|
IMPERSONATE_DISABLE_LOGGING = True
|
||||||
LANGUAGE_COOKIE_AGE = 8640000
|
|
||||||
|
|
||||||
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
|
|
||||||
|
|
||||||
IMPERSONATE = {
|
|
||||||
"REQUIRE_SUPERUSER": True,
|
|
||||||
"DISABLE_LOGGING": True,
|
|
||||||
"ADMIN_DELETE_PERMISSION": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
ACCOUNT_ACTIVATION_DAYS = 7
|
ACCOUNT_ACTIVATION_DAYS = 7
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{
|
{
|
||||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NAME": "judge.utils.pwned.PwnedPasswordsValidator",
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
'NAME': 'judge.utils.pwned.PwnedPasswordsValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
SILENCED_SYSTEM_CHECKS = ["urls.W002", "fields.W342"]
|
SILENCED_SYSTEM_CHECKS = ['urls.W002', 'fields.W342']
|
||||||
|
|
||||||
ROOT_URLCONF = "dmoj.urls"
|
ROOT_URLCONF = 'dmoj.urls'
|
||||||
LOGIN_REDIRECT_URL = "/user"
|
LOGIN_REDIRECT_URL = '/user'
|
||||||
WSGI_APPLICATION = "dmoj.wsgi.application"
|
WSGI_APPLICATION = 'dmoj.wsgi.application'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
"BACKEND": "django_jinja.backend.Jinja2",
|
'BACKEND': 'django_jinja.backend.Jinja2',
|
||||||
"DIRS": [
|
'DIRS': [
|
||||||
os.path.join(BASE_DIR, "templates"),
|
os.path.join(BASE_DIR, 'templates'),
|
||||||
],
|
],
|
||||||
"APP_DIRS": False,
|
'APP_DIRS': False,
|
||||||
"OPTIONS": {
|
'OPTIONS': {
|
||||||
"match_extension": (".html", ".txt"),
|
'match_extension': ('.html', '.txt'),
|
||||||
"match_regex": "^(?!admin/)",
|
'match_regex': '^(?!admin/)',
|
||||||
"context_processors": [
|
'context_processors': [
|
||||||
"django.template.context_processors.media",
|
'django.template.context_processors.media',
|
||||||
"django.template.context_processors.tz",
|
'django.template.context_processors.tz',
|
||||||
"django.template.context_processors.i18n",
|
'django.template.context_processors.i18n',
|
||||||
"django.template.context_processors.request",
|
'django.template.context_processors.request',
|
||||||
"django.contrib.messages.context_processors.messages",
|
'django.contrib.messages.context_processors.messages',
|
||||||
"judge.template_context.comet_location",
|
'judge.template_context.comet_location',
|
||||||
"judge.template_context.get_resource",
|
'judge.template_context.get_resource',
|
||||||
"judge.template_context.general_info",
|
'judge.template_context.general_info',
|
||||||
"judge.template_context.site",
|
'judge.template_context.site',
|
||||||
"judge.template_context.site_name",
|
'judge.template_context.site_name',
|
||||||
"judge.template_context.misc_config",
|
'judge.template_context.misc_config',
|
||||||
"social_django.context_processors.backends",
|
'judge.template_context.math_setting',
|
||||||
"social_django.context_processors.login_redirect",
|
'social_django.context_processors.backends',
|
||||||
|
'social_django.context_processors.login_redirect',
|
||||||
],
|
],
|
||||||
"autoescape": select_autoescape(["html", "xml"]),
|
'autoescape': select_autoescape(['html', 'xml']),
|
||||||
"trim_blocks": True,
|
'trim_blocks': True,
|
||||||
"lstrip_blocks": True,
|
'lstrip_blocks': True,
|
||||||
"extensions": DEFAULT_EXTENSIONS
|
'extensions': DEFAULT_EXTENSIONS + [
|
||||||
+ [
|
'compressor.contrib.jinja2ext.CompressorExtension',
|
||||||
"compressor.contrib.jinja2ext.CompressorExtension",
|
'judge.jinja2.DMOJExtension',
|
||||||
"judge.jinja2.DMOJExtension",
|
'judge.jinja2.spaceless.SpacelessExtension',
|
||||||
"judge.jinja2.spaceless.SpacelessExtension",
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
"APP_DIRS": True,
|
'APP_DIRS': True,
|
||||||
"DIRS": [
|
'DIRS': [
|
||||||
os.path.join(BASE_DIR, "templates"),
|
os.path.join(BASE_DIR, 'templates'),
|
||||||
],
|
],
|
||||||
"OPTIONS": {
|
'OPTIONS': {
|
||||||
"context_processors": [
|
'context_processors': [
|
||||||
"django.contrib.auth.context_processors.auth",
|
'django.contrib.auth.context_processors.auth',
|
||||||
"django.template.context_processors.media",
|
'django.template.context_processors.media',
|
||||||
"django.template.context_processors.tz",
|
'django.template.context_processors.tz',
|
||||||
"django.template.context_processors.i18n",
|
'django.template.context_processors.i18n',
|
||||||
"django.template.context_processors.request",
|
'django.template.context_processors.request',
|
||||||
"django.contrib.messages.context_processors.messages",
|
'django.contrib.messages.context_processors.messages',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
LOCALE_PATHS = [
|
LOCALE_PATHS = [
|
||||||
os.path.join(BASE_DIR, "locale"),
|
os.path.join(BASE_DIR, 'locale'),
|
||||||
]
|
]
|
||||||
|
|
||||||
LANGUAGES = [
|
LANGUAGES = [
|
||||||
("vi", _("Vietnamese")),
|
('de', _('German')),
|
||||||
("en", _("English")),
|
('en', _('English')),
|
||||||
|
('es', _('Spanish')),
|
||||||
|
('fr', _('French')),
|
||||||
|
('hr', _('Croatian')),
|
||||||
|
('hu', _('Hungarian')),
|
||||||
|
('ja', _('Japanese')),
|
||||||
|
('ko', _('Korean')),
|
||||||
|
('pt', _('Brazilian Portuguese')),
|
||||||
|
('ro', _('Romanian')),
|
||||||
|
('ru', _('Russian')),
|
||||||
|
('sr-latn', _('Serbian (Latin)')),
|
||||||
|
('tr', _('Turkish')),
|
||||||
|
('vi', _('Vietnamese')),
|
||||||
|
('zh-hans', _('Simplified Chinese')),
|
||||||
|
('zh-hant', _('Traditional Chinese')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
MARKDOWN_ADMIN_EDITABLE_STYLE = {
|
||||||
|
'safe_mode': False,
|
||||||
|
'use_camo': True,
|
||||||
|
'texoid': True,
|
||||||
|
'math': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
MARKDOWN_DEFAULT_STYLE = {
|
||||||
|
'safe_mode': True,
|
||||||
|
'nofollow': True,
|
||||||
|
'use_camo': True,
|
||||||
|
'math': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
MARKDOWN_USER_LARGE_STYLE = {
|
||||||
|
'safe_mode': True,
|
||||||
|
'nofollow': True,
|
||||||
|
'use_camo': True,
|
||||||
|
'math': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
MARKDOWN_STYLES = {
|
||||||
|
'comment': MARKDOWN_DEFAULT_STYLE,
|
||||||
|
'self-description': MARKDOWN_USER_LARGE_STYLE,
|
||||||
|
'problem': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
||||||
|
'contest': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
||||||
|
'language': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
||||||
|
'license': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
||||||
|
'judge': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
||||||
|
'blog': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
||||||
|
'solution': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
||||||
|
'contest_tag': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
||||||
|
'organization-about': MARKDOWN_USER_LARGE_STYLE,
|
||||||
|
'ticket': MARKDOWN_USER_LARGE_STYLE,
|
||||||
|
}
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
|
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
'default': {
|
||||||
"ENGINE": "django.db.backends.sqlite3",
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
|
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ENABLE_FTS = False
|
ENABLE_FTS = False
|
||||||
|
|
||||||
# Bridged configuration
|
# Bridged configuration
|
||||||
BRIDGED_JUDGE_ADDRESS = [("localhost", 9999)]
|
BRIDGED_JUDGE_ADDRESS = [('localhost', 9999)]
|
||||||
BRIDGED_JUDGE_PROXIES = None
|
BRIDGED_JUDGE_PROXIES = None
|
||||||
BRIDGED_DJANGO_ADDRESS = [("localhost", 9998)]
|
BRIDGED_DJANGO_ADDRESS = [('localhost', 9998)]
|
||||||
BRIDGED_DJANGO_CONNECT = None
|
BRIDGED_DJANGO_CONNECT = None
|
||||||
BRIDGED_AUTO_CREATE_JUDGE = False
|
|
||||||
|
|
||||||
# Event Server configuration
|
# Event Server configuration
|
||||||
EVENT_DAEMON_USE = False
|
EVENT_DAEMON_USE = False
|
||||||
EVENT_DAEMON_POST = "ws://localhost:9997/"
|
EVENT_DAEMON_POST = 'ws://localhost:9997/'
|
||||||
EVENT_DAEMON_GET = "ws://localhost:9996/"
|
EVENT_DAEMON_GET = 'ws://localhost:9996/'
|
||||||
EVENT_DAEMON_POLL = "/channels/"
|
EVENT_DAEMON_POLL = '/channels/'
|
||||||
EVENT_DAEMON_KEY = None
|
EVENT_DAEMON_KEY = None
|
||||||
EVENT_DAEMON_AMQP_EXCHANGE = "dmoj-events"
|
EVENT_DAEMON_AMQP_EXCHANGE = 'dmoj-events'
|
||||||
EVENT_DAEMON_SUBMISSION_KEY = (
|
EVENT_DAEMON_SUBMISSION_KEY = '6Sdmkx^%pk@GsifDfXcwX*Y7LRF%RGT8vmFpSxFBT$fwS7trc8raWfN#CSfQuKApx&$B#Gh2L7p%W!Ww'
|
||||||
"6Sdmkx^%pk@GsifDfXcwX*Y7LRF%RGT8vmFpSxFBT$fwS7trc8raWfN#CSfQuKApx&$B#Gh2L7p%W!Ww"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
||||||
|
|
||||||
# Whatever you do, this better be one of the entries in `LANGUAGES`.
|
# Whatever you do, this better be one of the entries in `LANGUAGES`.
|
||||||
LANGUAGE_CODE = "vi"
|
LANGUAGE_CODE = 'vi'
|
||||||
TIME_ZONE = "Asia/Ho_Chi_Minh"
|
TIME_ZONE = 'Asia/Ho_Chi_Minh'
|
||||||
DEFAULT_USER_TIME_ZONE = "Asia/Ho_Chi_Minh"
|
DEFAULT_USER_TIME_ZONE = 'Asia/Ho_Chi_Minh'
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
USE_L10N = True
|
USE_L10N = True
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
# Cookies
|
# Cookies
|
||||||
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
|
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
||||||
|
|
||||||
DMOJ_RESOURCES = os.path.join(BASE_DIR, "resources")
|
DMOJ_RESOURCES = os.path.join(BASE_DIR, 'resources')
|
||||||
STATICFILES_FINDERS = (
|
STATICFILES_FINDERS = (
|
||||||
"django.contrib.staticfiles.finders.FileSystemFinder",
|
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||||
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||||
)
|
)
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
os.path.join(BASE_DIR, "resources"),
|
os.path.join(BASE_DIR, 'resources'),
|
||||||
]
|
]
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
# Define a cache
|
# Define a cache
|
||||||
CACHES = {}
|
CACHES = {}
|
||||||
|
|
||||||
# Authentication
|
# Authentication
|
||||||
AUTHENTICATION_BACKENDS = (
|
AUTHENTICATION_BACKENDS = (
|
||||||
"social_core.backends.google.GoogleOAuth2",
|
'social_core.backends.google.GoogleOAuth2',
|
||||||
"social_core.backends.facebook.FacebookOAuth2",
|
'social_core.backends.facebook.FacebookOAuth2',
|
||||||
"judge.social_auth.GitHubSecureEmailOAuth2",
|
'judge.social_auth.GitHubSecureEmailOAuth2',
|
||||||
"judge.authentication.CustomModelBackend",
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
)
|
)
|
||||||
|
|
||||||
SOCIAL_AUTH_PIPELINE = (
|
SOCIAL_AUTH_PIPELINE = (
|
||||||
"social_core.pipeline.social_auth.social_details",
|
'social_core.pipeline.social_auth.social_details',
|
||||||
"social_core.pipeline.social_auth.social_uid",
|
'social_core.pipeline.social_auth.social_uid',
|
||||||
"social_core.pipeline.social_auth.auth_allowed",
|
'social_core.pipeline.social_auth.auth_allowed',
|
||||||
"judge.social_auth.verify_email",
|
'judge.social_auth.verify_email',
|
||||||
"social_core.pipeline.social_auth.social_user",
|
'social_core.pipeline.social_auth.social_user',
|
||||||
"social_core.pipeline.user.get_username",
|
'social_core.pipeline.user.get_username',
|
||||||
"social_core.pipeline.social_auth.associate_by_email",
|
'social_core.pipeline.social_auth.associate_by_email',
|
||||||
"judge.social_auth.choose_username",
|
'judge.social_auth.choose_username',
|
||||||
"social_core.pipeline.user.create_user",
|
'social_core.pipeline.user.create_user',
|
||||||
"judge.social_auth.make_profile",
|
'judge.social_auth.make_profile',
|
||||||
"social_core.pipeline.social_auth.associate_user",
|
'social_core.pipeline.social_auth.associate_user',
|
||||||
"social_core.pipeline.social_auth.load_extra_data",
|
'social_core.pipeline.social_auth.load_extra_data',
|
||||||
"social_core.pipeline.user.user_details",
|
'social_core.pipeline.user.user_details',
|
||||||
)
|
)
|
||||||
|
|
||||||
SOCIAL_AUTH_PROTECTED_USER_FIELDS = ["first_name", "last_name"]
|
SOCIAL_AUTH_GITHUB_SECURE_SCOPE = ['user:email']
|
||||||
SOCIAL_AUTH_GOOGLE_OAUTH2_USER_FIELDS = ["email", "username"]
|
SOCIAL_AUTH_FACEBOOK_SCOPE = ['email']
|
||||||
SOCIAL_AUTH_GITHUB_SECURE_SCOPE = ["user:email"]
|
|
||||||
SOCIAL_AUTH_FACEBOOK_SCOPE = ["email"]
|
|
||||||
SOCIAL_AUTH_SLUGIFY_USERNAMES = True
|
SOCIAL_AUTH_SLUGIFY_USERNAMES = True
|
||||||
SOCIAL_AUTH_SLUGIFY_FUNCTION = "judge.social_auth.slugify_username"
|
SOCIAL_AUTH_SLUGIFY_FUNCTION = 'judge.social_auth.slugify_username'
|
||||||
|
|
||||||
JUDGE_AMQP_PATH = None
|
JUDGE_AMQP_PATH = None
|
||||||
|
|
||||||
|
@ -464,38 +492,25 @@ MOSS_API_KEY = None
|
||||||
|
|
||||||
CELERY_WORKER_HIJACK_ROOT_LOGGER = False
|
CELERY_WORKER_HIJACK_ROOT_LOGGER = False
|
||||||
|
|
||||||
|
|
||||||
TESTCASE_VISIBLE_LENGTH = 64
|
|
||||||
|
|
||||||
DATA_UPLOAD_MAX_NUMBER_FIELDS = 10240
|
|
||||||
DATA_UPLOAD_MAX_MEMORY_SIZE = 2621440
|
|
||||||
FILE_UPLOAD_PERMISSIONS = 0o644
|
|
||||||
|
|
||||||
MESSAGES_TO_LOAD = 15
|
|
||||||
|
|
||||||
ML_OUTPUT_PATH = None
|
|
||||||
|
|
||||||
# Use subdomain for organizations
|
|
||||||
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
|
||||||
|
|
||||||
|
TESTCASE_VISIBLE_LENGTH = 60
|
||||||
|
|
||||||
|
DATA_UPLOAD_MAX_NUMBER_FIELDS = 10240
|
||||||
|
DATA_UPLOAD_MAX_MEMORY_SIZE = 2621440
|
||||||
|
|
||||||
|
MESSAGES_TO_LOAD = 15
|
||||||
|
|
||||||
|
ASGI_APPLICATION = 'dmoj.routing.application'
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
||||||
|
'CONFIG': {
|
||||||
|
"hosts": [('0.0.0.0', 6379)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
|
@ -8,8 +8,8 @@ DEFAULT_THROTTLE = (10, 60)
|
||||||
|
|
||||||
|
|
||||||
def new_email():
|
def new_email():
|
||||||
cache.add("error_email_throttle", 0, settings.DMOJ_EMAIL_THROTTLING[1])
|
cache.add('error_email_throttle', 0, settings.DMOJ_EMAIL_THROTTLING[1])
|
||||||
return cache.incr("error_email_throttle")
|
return cache.incr('error_email_throttle')
|
||||||
|
|
||||||
|
|
||||||
class ThrottledEmailHandler(AdminEmailHandler):
|
class ThrottledEmailHandler(AdminEmailHandler):
|
||||||
|
|
1578
dmoj/urls.py
1578
dmoj/urls.py
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,5 @@
|
||||||
import os
|
import os
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings')
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import MySQLdb # noqa: F401, imported for side effect
|
import MySQLdb # noqa: F401, imported for side effect
|
||||||
|
@ -9,8 +8,5 @@ except ImportError:
|
||||||
|
|
||||||
pymysql.install_as_MySQLdb()
|
pymysql.install_as_MySQLdb()
|
||||||
|
|
||||||
from django.core.wsgi import (
|
from django.core.wsgi import get_wsgi_application # noqa: E402, django must be imported here
|
||||||
get_wsgi_application,
|
|
||||||
) # noqa: E402, django must be imported here
|
|
||||||
|
|
||||||
application = get_wsgi_application()
|
application = get_wsgi_application()
|
||||||
|
|
|
@ -2,17 +2,13 @@ import os
|
||||||
|
|
||||||
import gevent.monkey # noqa: I100, gevent must be imported here
|
import gevent.monkey # noqa: I100, gevent must be imported here
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings")
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings')
|
||||||
gevent.monkey.patch_all()
|
gevent.monkey.patch_all()
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
import dmoj_install_pymysql # noqa: F401, I100, I202, imported for side effect
|
import dmoj_install_pymysql # noqa: F401, I100, I202, imported for side effect
|
||||||
|
|
||||||
from django.core.wsgi import (
|
from django.core.wsgi import get_wsgi_application # noqa: E402, I100, I202, django must be imported here
|
||||||
get_wsgi_application,
|
|
||||||
) # noqa: E402, I100, I202, django must be imported here
|
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
import django_2_2_pymysql_patch # noqa: I100, F401, I202, imported for side effect
|
import django_2_2_pymysql_patch # noqa: I100, F401, I202, imported for side effect
|
||||||
|
|
||||||
application = get_wsgi_application()
|
application = get_wsgi_application()
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
import gevent.monkey # noqa: I100, gevent must be imported here
|
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings")
|
|
||||||
gevent.monkey.patch_all()
|
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
|
||||||
import dmoj_install_pymysql # noqa: E402, F401, I100, I202, imported for side effect
|
|
||||||
|
|
||||||
import django # noqa: E402, F401, I100, I202, django must be imported here
|
|
||||||
|
|
||||||
django.setup()
|
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
|
||||||
import django_2_2_pymysql_patch # noqa: E402, I100, F401, I202, imported for side effect
|
|
||||||
|
|
||||||
from judge.bridge.daemon import (
|
|
||||||
judge_daemon,
|
|
||||||
) # noqa: E402, I100, I202, django code must be imported here
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
judge_daemon()
|
|
|
@ -6,7 +6,7 @@ except ImportError:
|
||||||
import dmoj_install_pymysql # noqa: F401, imported for side effect
|
import dmoj_install_pymysql # noqa: F401, imported for side effect
|
||||||
|
|
||||||
# set the default Django settings module for the 'celery' program.
|
# set the default Django settings module for the 'celery' program.
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings")
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings')
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
import django_2_2_pymysql_patch # noqa: I100, F401, I202, imported for side effect
|
import django_2_2_pymysql_patch # noqa: I100, F401, I202, imported for side effect
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import pymysql
|
import pymysql
|
||||||
|
|
||||||
pymysql.install_as_MySQLdb()
|
pymysql.install_as_MySQLdb()
|
||||||
pymysql.version_info = (1, 4, 0, "final", 0)
|
pymysql.version_info = (1, 3, 13, "final", 0)
|
||||||
|
|
11
event_socket_server/__init__.py
Normal file
11
event_socket_server/__init__.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from .base_server import BaseServer
|
||||||
|
from .engines import *
|
||||||
|
from .handler import Handler
|
||||||
|
from .helpers import ProxyProtocolMixin, SizedPacketHandler, ZlibPacketHandler
|
||||||
|
|
||||||
|
|
||||||
|
def get_preferred_engine(choices=('epoll', 'poll', 'select')):
|
||||||
|
for choice in choices:
|
||||||
|
if choice in engines:
|
||||||
|
return engines[choice]
|
||||||
|
return engines['select']
|
169
event_socket_server/base_server.py
Normal file
169
event_socket_server/base_server.py
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from collections import defaultdict, deque
|
||||||
|
from functools import total_ordering
|
||||||
|
from heapq import heappop, heappush
|
||||||
|
|
||||||
|
logger = logging.getLogger('event_socket_server')
|
||||||
|
|
||||||
|
|
||||||
|
class SendMessage(object):
|
||||||
|
__slots__ = ('data', 'callback')
|
||||||
|
|
||||||
|
def __init__(self, data, callback):
|
||||||
|
self.data = data
|
||||||
|
self.callback = callback
|
||||||
|
|
||||||
|
|
||||||
|
@total_ordering
|
||||||
|
class ScheduledJob(object):
|
||||||
|
__slots__ = ('time', 'func', 'args', 'kwargs', 'cancel', 'dispatched')
|
||||||
|
|
||||||
|
def __init__(self, time, func, args, kwargs):
|
||||||
|
self.time = time
|
||||||
|
self.func = func
|
||||||
|
self.args = args
|
||||||
|
self.kwargs = kwargs
|
||||||
|
self.cancel = False
|
||||||
|
self.dispatched = False
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.time == other.time
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
return self.time < other.time
|
||||||
|
|
||||||
|
|
||||||
|
class BaseServer(object):
|
||||||
|
def __init__(self, addresses, client):
|
||||||
|
self._servers = set()
|
||||||
|
for address, port in addresses:
|
||||||
|
info = socket.getaddrinfo(address, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
|
||||||
|
for af, socktype, proto, canonname, sa in info:
|
||||||
|
sock = socket.socket(af, socktype, proto)
|
||||||
|
sock.setblocking(0)
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
sock.bind(sa)
|
||||||
|
self._servers.add(sock)
|
||||||
|
|
||||||
|
self._stop = threading.Event()
|
||||||
|
self._clients = set()
|
||||||
|
self._ClientClass = client
|
||||||
|
self._send_queue = defaultdict(deque)
|
||||||
|
self._job_queue = []
|
||||||
|
self._job_queue_lock = threading.Lock()
|
||||||
|
|
||||||
|
def _serve(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _accept(self, sock):
|
||||||
|
conn, address = sock.accept()
|
||||||
|
conn.setblocking(0)
|
||||||
|
client = self._ClientClass(self, conn)
|
||||||
|
self._clients.add(client)
|
||||||
|
return client
|
||||||
|
|
||||||
|
def schedule(self, delay, func, *args, **kwargs):
|
||||||
|
with self._job_queue_lock:
|
||||||
|
job = ScheduledJob(time.time() + delay, func, args, kwargs)
|
||||||
|
heappush(self._job_queue, job)
|
||||||
|
return job
|
||||||
|
|
||||||
|
def unschedule(self, job):
|
||||||
|
with self._job_queue_lock:
|
||||||
|
if job.dispatched or job.cancel:
|
||||||
|
return False
|
||||||
|
job.cancel = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _register_write(self, client):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _register_read(self, client):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _clean_up_client(self, client, finalize=False):
|
||||||
|
try:
|
||||||
|
del self._send_queue[client.fileno()]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
client.on_close()
|
||||||
|
client._socket.close()
|
||||||
|
if not finalize:
|
||||||
|
self._clients.remove(client)
|
||||||
|
|
||||||
|
def _dispatch_event(self):
|
||||||
|
t = time.time()
|
||||||
|
tasks = []
|
||||||
|
with self._job_queue_lock:
|
||||||
|
while True:
|
||||||
|
dt = self._job_queue[0].time - t if self._job_queue else 1
|
||||||
|
if dt > 0:
|
||||||
|
break
|
||||||
|
task = heappop(self._job_queue)
|
||||||
|
task.dispatched = True
|
||||||
|
if not task.cancel:
|
||||||
|
tasks.append(task)
|
||||||
|
for task in tasks:
|
||||||
|
logger.debug('Dispatching event: %r(*%r, **%r)', task.func, task.args, task.kwargs)
|
||||||
|
task.func(*task.args, **task.kwargs)
|
||||||
|
if not self._job_queue or dt > 1:
|
||||||
|
dt = 1
|
||||||
|
return dt
|
||||||
|
|
||||||
|
def _nonblock_read(self, client):
|
||||||
|
try:
|
||||||
|
data = client._socket.recv(1024)
|
||||||
|
except socket.error:
|
||||||
|
self._clean_up_client(client)
|
||||||
|
else:
|
||||||
|
logger.debug('Read from %s: %d bytes', client.client_address, len(data))
|
||||||
|
if not data:
|
||||||
|
self._clean_up_client(client)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
client._recv_data(data)
|
||||||
|
except Exception:
|
||||||
|
logger.exception('Client recv_data failure')
|
||||||
|
self._clean_up_client(client)
|
||||||
|
|
||||||
|
def _nonblock_write(self, client):
|
||||||
|
fd = client.fileno()
|
||||||
|
queue = self._send_queue[fd]
|
||||||
|
try:
|
||||||
|
top = queue[0]
|
||||||
|
cb = client._socket.send(top.data)
|
||||||
|
top.data = top.data[cb:]
|
||||||
|
logger.debug('Send to %s: %d bytes', client.client_address, cb)
|
||||||
|
if not top.data:
|
||||||
|
logger.debug('Finished sending: %s', client.client_address)
|
||||||
|
if top.callback is not None:
|
||||||
|
logger.debug('Calling callback: %s: %r', client.client_address, top.callback)
|
||||||
|
try:
|
||||||
|
top.callback()
|
||||||
|
except Exception:
|
||||||
|
logger.exception('Client write callback failure')
|
||||||
|
self._clean_up_client(client)
|
||||||
|
return
|
||||||
|
queue.popleft()
|
||||||
|
if not queue:
|
||||||
|
self._register_read(client)
|
||||||
|
del self._send_queue[fd]
|
||||||
|
except socket.error:
|
||||||
|
self._clean_up_client(client)
|
||||||
|
|
||||||
|
def send(self, client, data, callback=None):
|
||||||
|
logger.debug('Writing %d bytes to client %s, callback: %s', len(data), client.client_address, callback)
|
||||||
|
self._send_queue[client.fileno()].append(SendMessage(data, callback))
|
||||||
|
self._register_write(client)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._stop.set()
|
||||||
|
|
||||||
|
def serve_forever(self):
|
||||||
|
self._serve()
|
||||||
|
|
||||||
|
def on_shutdown(self):
|
||||||
|
pass
|
17
event_socket_server/engines/__init__.py
Normal file
17
event_socket_server/engines/__init__.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import select
|
||||||
|
|
||||||
|
__author__ = 'Quantum'
|
||||||
|
engines = {}
|
||||||
|
|
||||||
|
from .select_server import SelectServer # noqa: E402, import not at top for consistency
|
||||||
|
engines['select'] = SelectServer
|
||||||
|
|
||||||
|
if hasattr(select, 'poll'):
|
||||||
|
from .poll_server import PollServer
|
||||||
|
engines['poll'] = PollServer
|
||||||
|
|
||||||
|
if hasattr(select, 'epoll'):
|
||||||
|
from .epoll_server import EpollServer
|
||||||
|
engines['epoll'] = EpollServer
|
||||||
|
|
||||||
|
del select
|
17
event_socket_server/engines/epoll_server.py
Normal file
17
event_socket_server/engines/epoll_server.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import select
|
||||||
|
__author__ = 'Quantum'
|
||||||
|
|
||||||
|
if not hasattr(select, 'epoll'):
|
||||||
|
raise ImportError('System does not support epoll')
|
||||||
|
|
||||||
|
from .poll_server import PollServer # noqa: E402, must be imported here
|
||||||
|
|
||||||
|
|
||||||
|
class EpollServer(PollServer):
|
||||||
|
poll = select.epoll
|
||||||
|
WRITE = select.EPOLLIN | select.EPOLLOUT | select.EPOLLERR | select.EPOLLHUP
|
||||||
|
READ = select.EPOLLIN | select.EPOLLERR | select.EPOLLHUP
|
||||||
|
POLLIN = select.EPOLLIN
|
||||||
|
POLLOUT = select.EPOLLOUT
|
||||||
|
POLL_CLOSE = select.EPOLLHUP | select.EPOLLERR
|
||||||
|
NEED_CLOSE = True
|
97
event_socket_server/engines/poll_server.py
Normal file
97
event_socket_server/engines/poll_server.py
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import errno
|
||||||
|
import logging
|
||||||
|
import select
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from ..base_server import BaseServer
|
||||||
|
|
||||||
|
logger = logging.getLogger('event_socket_server')
|
||||||
|
|
||||||
|
if not hasattr(select, 'poll'):
|
||||||
|
raise ImportError('System does not support poll')
|
||||||
|
|
||||||
|
|
||||||
|
class PollServer(BaseServer):
|
||||||
|
poll = select.poll
|
||||||
|
WRITE = select.POLLIN | select.POLLOUT | select.POLLERR | select.POLLHUP
|
||||||
|
READ = select.POLLIN | select.POLLERR | select.POLLHUP
|
||||||
|
POLLIN = select.POLLIN
|
||||||
|
POLLOUT = select.POLLOUT
|
||||||
|
POLL_CLOSE = select.POLLERR | select.POLLHUP
|
||||||
|
NEED_CLOSE = False
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(PollServer, self).__init__(*args, **kwargs)
|
||||||
|
self._poll = self.poll()
|
||||||
|
self._fdmap = {}
|
||||||
|
self._server_fds = {sock.fileno(): sock for sock in self._servers}
|
||||||
|
self._close_lock = threading.RLock()
|
||||||
|
|
||||||
|
def _register_write(self, client):
|
||||||
|
logger.debug('On write mode: %s', client.client_address)
|
||||||
|
self._poll.modify(client.fileno(), self.WRITE)
|
||||||
|
|
||||||
|
def _register_read(self, client):
|
||||||
|
logger.debug('On read mode: %s', client.client_address)
|
||||||
|
self._poll.modify(client.fileno(), self.READ)
|
||||||
|
|
||||||
|
def _clean_up_client(self, client, finalize=False):
|
||||||
|
logger.debug('Taking close lock: cleanup')
|
||||||
|
with self._close_lock:
|
||||||
|
logger.debug('Cleaning up client: %s, finalize: %d', client.client_address, finalize)
|
||||||
|
fd = client.fileno()
|
||||||
|
try:
|
||||||
|
self._poll.unregister(fd)
|
||||||
|
except IOError as e:
|
||||||
|
if e.errno != errno.ENOENT:
|
||||||
|
raise
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
del self._fdmap[fd]
|
||||||
|
super(PollServer, self)._clean_up_client(client, finalize)
|
||||||
|
|
||||||
|
def _serve(self):
|
||||||
|
for fd, sock in self._server_fds.items():
|
||||||
|
self._poll.register(fd, self.POLLIN)
|
||||||
|
sock.listen(16)
|
||||||
|
try:
|
||||||
|
while not self._stop.is_set():
|
||||||
|
for fd, event in self._poll.poll(self._dispatch_event()):
|
||||||
|
if fd in self._server_fds:
|
||||||
|
client = self._accept(self._server_fds[fd])
|
||||||
|
logger.debug('Accepting: %s', client.client_address)
|
||||||
|
fd = client.fileno()
|
||||||
|
self._poll.register(fd, self.READ)
|
||||||
|
self._fdmap[fd] = client
|
||||||
|
elif event & self.POLL_CLOSE:
|
||||||
|
logger.debug('Client closed: %s', self._fdmap[fd].client_address)
|
||||||
|
self._clean_up_client(self._fdmap[fd])
|
||||||
|
else:
|
||||||
|
logger.debug('Taking close lock: event loop')
|
||||||
|
with self._close_lock:
|
||||||
|
try:
|
||||||
|
client = self._fdmap[fd]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
logger.debug('Client active: %s, read: %d, write: %d',
|
||||||
|
client.client_address,
|
||||||
|
event & self.POLLIN,
|
||||||
|
event & self.POLLOUT)
|
||||||
|
if event & self.POLLIN:
|
||||||
|
logger.debug('Non-blocking read on client: %s', client.client_address)
|
||||||
|
self._nonblock_read(client)
|
||||||
|
# Might be closed in the read handler.
|
||||||
|
if event & self.POLLOUT and fd in self._fdmap:
|
||||||
|
logger.debug('Non-blocking write on client: %s', client.client_address)
|
||||||
|
self._nonblock_write(client)
|
||||||
|
finally:
|
||||||
|
logger.info('Shutting down server')
|
||||||
|
self.on_shutdown()
|
||||||
|
for client in self._clients:
|
||||||
|
self._clean_up_client(client, True)
|
||||||
|
for fd, sock in self._server_fds.items():
|
||||||
|
self._poll.unregister(fd)
|
||||||
|
sock.close()
|
||||||
|
if self.NEED_CLOSE:
|
||||||
|
self._poll.close()
|
49
event_socket_server/engines/select_server.py
Normal file
49
event_socket_server/engines/select_server.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import select
|
||||||
|
|
||||||
|
from ..base_server import BaseServer
|
||||||
|
|
||||||
|
|
||||||
|
class SelectServer(BaseServer):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(SelectServer, self).__init__(*args, **kwargs)
|
||||||
|
self._reads = set(self._servers)
|
||||||
|
self._writes = set()
|
||||||
|
|
||||||
|
def _register_write(self, client):
|
||||||
|
self._writes.add(client)
|
||||||
|
|
||||||
|
def _register_read(self, client):
|
||||||
|
self._writes.remove(client)
|
||||||
|
|
||||||
|
def _clean_up_client(self, client, finalize=False):
|
||||||
|
self._writes.discard(client)
|
||||||
|
self._reads.remove(client)
|
||||||
|
super(SelectServer, self)._clean_up_client(client, finalize)
|
||||||
|
|
||||||
|
def _serve(self, select=select.select):
|
||||||
|
for server in self._servers:
|
||||||
|
server.listen(16)
|
||||||
|
try:
|
||||||
|
while not self._stop.is_set():
|
||||||
|
r, w, x = select(self._reads, self._writes, self._reads, self._dispatch_event())
|
||||||
|
for s in r:
|
||||||
|
if s in self._servers:
|
||||||
|
self._reads.add(self._accept(s))
|
||||||
|
else:
|
||||||
|
self._nonblock_read(s)
|
||||||
|
|
||||||
|
for client in w:
|
||||||
|
self._nonblock_write(client)
|
||||||
|
|
||||||
|
for s in x:
|
||||||
|
s.close()
|
||||||
|
if s in self._servers:
|
||||||
|
raise RuntimeError('Server is in exceptional condition')
|
||||||
|
else:
|
||||||
|
self._clean_up_client(s)
|
||||||
|
finally:
|
||||||
|
self.on_shutdown()
|
||||||
|
for client in self._clients:
|
||||||
|
self._clean_up_client(client, True)
|
||||||
|
for server in self._servers:
|
||||||
|
server.close()
|
27
event_socket_server/handler.py
Normal file
27
event_socket_server/handler.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
__author__ = 'Quantum'
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(object):
|
||||||
|
def __init__(self, server, socket):
|
||||||
|
self._socket = socket
|
||||||
|
self.server = server
|
||||||
|
self.client_address = socket.getpeername()
|
||||||
|
|
||||||
|
def fileno(self):
|
||||||
|
return self._socket.fileno()
|
||||||
|
|
||||||
|
def _recv_data(self, data):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _send(self, data, callback=None):
|
||||||
|
return self.server.send(self, data, callback)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.server._clean_up_client(self)
|
||||||
|
|
||||||
|
def on_close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def socket(self):
|
||||||
|
return self._socket
|
125
event_socket_server/helpers.py
Normal file
125
event_socket_server/helpers.py
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
import struct
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
from judge.utils.unicode import utf8text
|
||||||
|
from .handler import Handler
|
||||||
|
|
||||||
|
size_pack = struct.Struct('!I')
|
||||||
|
|
||||||
|
|
||||||
|
class SizedPacketHandler(Handler):
|
||||||
|
def __init__(self, server, socket):
|
||||||
|
super(SizedPacketHandler, self).__init__(server, socket)
|
||||||
|
self._buffer = b''
|
||||||
|
self._packetlen = 0
|
||||||
|
|
||||||
|
def _packet(self, data):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _format_send(self, data):
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _recv_data(self, data):
|
||||||
|
self._buffer += data
|
||||||
|
while len(self._buffer) >= self._packetlen if self._packetlen else len(self._buffer) >= size_pack.size:
|
||||||
|
if self._packetlen:
|
||||||
|
data = self._buffer[:self._packetlen]
|
||||||
|
self._buffer = self._buffer[self._packetlen:]
|
||||||
|
self._packetlen = 0
|
||||||
|
self._packet(data)
|
||||||
|
else:
|
||||||
|
data = self._buffer[:size_pack.size]
|
||||||
|
self._buffer = self._buffer[size_pack.size:]
|
||||||
|
self._packetlen = size_pack.unpack(data)[0]
|
||||||
|
|
||||||
|
def send(self, data, callback=None):
|
||||||
|
data = self._format_send(data)
|
||||||
|
self._send(size_pack.pack(len(data)) + data, callback)
|
||||||
|
|
||||||
|
|
||||||
|
class ZlibPacketHandler(SizedPacketHandler):
|
||||||
|
def _format_send(self, data):
|
||||||
|
return zlib.compress(data.encode('utf-8'))
|
||||||
|
|
||||||
|
def packet(self, data):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _packet(self, data):
|
||||||
|
try:
|
||||||
|
self.packet(zlib.decompress(data).decode('utf-8'))
|
||||||
|
except zlib.error as e:
|
||||||
|
self.malformed_packet(e)
|
||||||
|
|
||||||
|
def malformed_packet(self, exception):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyProtocolMixin(object):
|
||||||
|
__UNKNOWN_TYPE = 0
|
||||||
|
__PROXY1 = 1
|
||||||
|
__PROXY2 = 2
|
||||||
|
__DATA = 3
|
||||||
|
|
||||||
|
__HEADER2 = b'\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A'
|
||||||
|
__HEADER2_LEN = len(__HEADER2)
|
||||||
|
|
||||||
|
_REAL_IP_SET = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def with_proxy_set(cls, ranges):
|
||||||
|
from netaddr import IPSet, IPGlob
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
globs = []
|
||||||
|
addrs = []
|
||||||
|
for item in ranges:
|
||||||
|
if '*' in item or '-' in item:
|
||||||
|
globs.append(IPGlob(item))
|
||||||
|
else:
|
||||||
|
addrs.append(item)
|
||||||
|
ipset = IPSet(chain(chain.from_iterable(globs), addrs))
|
||||||
|
return type(cls.__name__, (cls,), {'_REAL_IP_SET': ipset})
|
||||||
|
|
||||||
|
def __init__(self, server, socket):
|
||||||
|
super(ProxyProtocolMixin, self).__init__(server, socket)
|
||||||
|
self.__buffer = b''
|
||||||
|
self.__type = (self.__UNKNOWN_TYPE if self._REAL_IP_SET and
|
||||||
|
self.client_address[0] in self._REAL_IP_SET else self.__DATA)
|
||||||
|
|
||||||
|
def __parse_proxy1(self, data):
|
||||||
|
self.__buffer += data
|
||||||
|
index = self.__buffer.find(b'\r\n')
|
||||||
|
if 0 <= index < 106:
|
||||||
|
proxy = data[:index].split()
|
||||||
|
if len(proxy) < 2:
|
||||||
|
return self.close()
|
||||||
|
if proxy[1] == b'TCP4':
|
||||||
|
if len(proxy) != 6:
|
||||||
|
return self.close()
|
||||||
|
self.client_address = (utf8text(proxy[2]), utf8text(proxy[4]))
|
||||||
|
self.server_address = (utf8text(proxy[3]), utf8text(proxy[5]))
|
||||||
|
elif proxy[1] == b'TCP6':
|
||||||
|
self.client_address = (utf8text(proxy[2]), utf8text(proxy[4]), 0, 0)
|
||||||
|
self.server_address = (utf8text(proxy[3]), utf8text(proxy[5]), 0, 0)
|
||||||
|
elif proxy[1] != b'UNKNOWN':
|
||||||
|
return self.close()
|
||||||
|
|
||||||
|
self.__type = self.__DATA
|
||||||
|
super(ProxyProtocolMixin, self)._recv_data(data[index + 2:])
|
||||||
|
elif len(self.__buffer) > 107 or index > 105:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def _recv_data(self, data):
|
||||||
|
if self.__type == self.__DATA:
|
||||||
|
super(ProxyProtocolMixin, self)._recv_data(data)
|
||||||
|
elif self.__type == self.__UNKNOWN_TYPE:
|
||||||
|
if len(data) >= 16 and data[:self.__HEADER2_LEN] == self.__HEADER2:
|
||||||
|
self.close()
|
||||||
|
elif len(data) >= 8 and data[:5] == b'PROXY':
|
||||||
|
self.__type = self.__PROXY1
|
||||||
|
self.__parse_proxy1(data)
|
||||||
|
else:
|
||||||
|
self.__type = self.__DATA
|
||||||
|
super(ProxyProtocolMixin, self)._recv_data(data)
|
||||||
|
else:
|
||||||
|
self.__parse_proxy1(data)
|
94
event_socket_server/test_client.py
Normal file
94
event_socket_server/test_client.py
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import ctypes
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
size_pack = struct.Struct('!I')
|
||||||
|
try:
|
||||||
|
RtlGenRandom = ctypes.windll.advapi32.SystemFunction036
|
||||||
|
except AttributeError:
|
||||||
|
RtlGenRandom = None
|
||||||
|
|
||||||
|
|
||||||
|
def open_connection():
|
||||||
|
sock = socket.create_connection((host, port))
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
def zlibify(data):
|
||||||
|
data = zlib.compress(data.encode('utf-8'))
|
||||||
|
return size_pack.pack(len(data)) + data
|
||||||
|
|
||||||
|
|
||||||
|
def dezlibify(data, skip_head=True):
|
||||||
|
if skip_head:
|
||||||
|
data = data[size_pack.size:]
|
||||||
|
return zlib.decompress(data).decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def random(length):
|
||||||
|
if RtlGenRandom is None:
|
||||||
|
with open('/dev/urandom') as f:
|
||||||
|
return f.read(length)
|
||||||
|
buf = ctypes.create_string_buffer(length)
|
||||||
|
RtlGenRandom(buf, length)
|
||||||
|
return buf.raw
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global host, port
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('-l', '--host', default='localhost')
|
||||||
|
parser.add_argument('-p', '--port', default=9999, type=int)
|
||||||
|
args = parser.parse_args()
|
||||||
|
host, port = args.host, args.port
|
||||||
|
|
||||||
|
print('Opening idle connection:', end=' ')
|
||||||
|
s1 = open_connection()
|
||||||
|
print('Success')
|
||||||
|
print('Opening hello world connection:', end=' ')
|
||||||
|
s2 = open_connection()
|
||||||
|
print('Success')
|
||||||
|
print('Sending Hello, World!', end=' ')
|
||||||
|
s2.sendall(zlibify('Hello, World!'))
|
||||||
|
print('Success')
|
||||||
|
print('Testing blank connection:', end=' ')
|
||||||
|
s3 = open_connection()
|
||||||
|
s3.close()
|
||||||
|
print('Success')
|
||||||
|
result = dezlibify(s2.recv(1024))
|
||||||
|
assert result == 'Hello, World!'
|
||||||
|
print(result)
|
||||||
|
s2.close()
|
||||||
|
print('Large random data test:', end=' ')
|
||||||
|
s4 = open_connection()
|
||||||
|
data = random(1000000)
|
||||||
|
print('Generated', end=' ')
|
||||||
|
s4.sendall(zlibify(data))
|
||||||
|
print('Sent', end=' ')
|
||||||
|
result = ''
|
||||||
|
while len(result) < size_pack.size:
|
||||||
|
result += s4.recv(1024)
|
||||||
|
size = size_pack.unpack(result[:size_pack.size])[0]
|
||||||
|
result = result[size_pack.size:]
|
||||||
|
while len(result) < size:
|
||||||
|
result += s4.recv(1024)
|
||||||
|
print('Received', end=' ')
|
||||||
|
assert dezlibify(result, False) == data
|
||||||
|
print('Success')
|
||||||
|
s4.close()
|
||||||
|
print('Test malformed connection:', end=' ')
|
||||||
|
s5 = open_connection()
|
||||||
|
s5.sendall(data[:100000])
|
||||||
|
s5.close()
|
||||||
|
print('Success')
|
||||||
|
print('Waiting for timeout to close idle connection:', end=' ')
|
||||||
|
time.sleep(6)
|
||||||
|
print('Done')
|
||||||
|
s1.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
54
event_socket_server/test_server.py
Normal file
54
event_socket_server/test_server.py
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
from .engines import engines
|
||||||
|
from .helpers import ProxyProtocolMixin, ZlibPacketHandler
|
||||||
|
|
||||||
|
|
||||||
|
class EchoPacketHandler(ProxyProtocolMixin, ZlibPacketHandler):
|
||||||
|
def __init__(self, server, socket):
|
||||||
|
super(EchoPacketHandler, self).__init__(server, socket)
|
||||||
|
self._gotdata = False
|
||||||
|
self.server.schedule(5, self._kill_if_no_data)
|
||||||
|
|
||||||
|
def _kill_if_no_data(self):
|
||||||
|
if not self._gotdata:
|
||||||
|
print('Inactive client:', self._socket.getpeername())
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def packet(self, data):
|
||||||
|
self._gotdata = True
|
||||||
|
print('Data from %s: %r' % (self._socket.getpeername(), data[:30] if len(data) > 30 else data))
|
||||||
|
self.send(data)
|
||||||
|
|
||||||
|
def on_close(self):
|
||||||
|
self._gotdata = True
|
||||||
|
print('Closed client:', self._socket.getpeername())
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('-l', '--host', action='append')
|
||||||
|
parser.add_argument('-p', '--port', type=int, action='append')
|
||||||
|
parser.add_argument('-e', '--engine', default='select', choices=sorted(engines.keys()))
|
||||||
|
try:
|
||||||
|
import netaddr
|
||||||
|
except ImportError:
|
||||||
|
netaddr = None
|
||||||
|
else:
|
||||||
|
parser.add_argument('-P', '--proxy', action='append')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
class TestServer(engines[args.engine]):
|
||||||
|
def _accept(self, sock):
|
||||||
|
client = super(TestServer, self)._accept(sock)
|
||||||
|
print('New connection:', client.socket.getpeername())
|
||||||
|
return client
|
||||||
|
|
||||||
|
handler = EchoPacketHandler
|
||||||
|
if netaddr is not None and args.proxy:
|
||||||
|
handler = handler.with_proxy_set(args.proxy)
|
||||||
|
server = TestServer(list(zip(args.host, args.port)), handler)
|
||||||
|
server.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -1 +1 @@
|
||||||
default_app_config = "judge.apps.JudgeAppConfig"
|
default_app_config = 'judge.apps.JudgeAppConfig'
|
||||||
|
|
|
@ -1,62 +1,19 @@
|
||||||
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 (
|
from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin
|
||||||
ContestAdmin,
|
from judge.admin.interface import BlogPostAdmin, LicenseAdmin, LogEntryAdmin, NavigationBarAdmin
|
||||||
ContestParticipationAdmin,
|
|
||||||
ContestTagAdmin,
|
|
||||||
ContestsSummaryAdmin,
|
|
||||||
)
|
|
||||||
from judge.admin.interface import (
|
|
||||||
BlogPostAdmin,
|
|
||||||
LicenseAdmin,
|
|
||||||
LogEntryAdmin,
|
|
||||||
NavigationBarAdmin,
|
|
||||||
)
|
|
||||||
from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin
|
from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin
|
||||||
from judge.admin.problem import ProblemAdmin, ProblemPointsVoteAdmin
|
from judge.admin.problem import ProblemAdmin
|
||||||
from judge.admin.profile import ProfileAdmin, UserAdmin
|
from judge.admin.profile import ProfileAdmin
|
||||||
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 (
|
from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin
|
||||||
ProblemGroupAdmin,
|
|
||||||
ProblemTypeAdmin,
|
|
||||||
OfficialContestCategoryAdmin,
|
|
||||||
OfficialContestLocationAdmin,
|
|
||||||
)
|
|
||||||
from judge.admin.ticket import TicketAdmin
|
from judge.admin.ticket import TicketAdmin
|
||||||
from judge.admin.volunteer import VolunteerProblemVoteAdmin
|
from judge.models import BlogPost, Comment, CommentLock, Contest, ContestParticipation, \
|
||||||
from judge.admin.course import CourseAdmin
|
ContestTag, Judge, Language, License, MiscConfig, NavigationBar, Organization, \
|
||||||
from judge.models import (
|
OrganizationRequest, Problem, ProblemGroup, ProblemType, Profile, Submission, Ticket
|
||||||
BlogPost,
|
|
||||||
Comment,
|
|
||||||
CommentLock,
|
|
||||||
Contest,
|
|
||||||
ContestParticipation,
|
|
||||||
ContestTag,
|
|
||||||
Judge,
|
|
||||||
Language,
|
|
||||||
License,
|
|
||||||
MiscConfig,
|
|
||||||
NavigationBar,
|
|
||||||
Organization,
|
|
||||||
OrganizationRequest,
|
|
||||||
Problem,
|
|
||||||
ProblemGroup,
|
|
||||||
ProblemPointsVote,
|
|
||||||
ProblemType,
|
|
||||||
Profile,
|
|
||||||
Submission,
|
|
||||||
Ticket,
|
|
||||||
VolunteerProblemVote,
|
|
||||||
Course,
|
|
||||||
ContestsSummary,
|
|
||||||
OfficialContestCategory,
|
|
||||||
OfficialContestLocation,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(BlogPost, BlogPostAdmin)
|
admin.site.register(BlogPost, BlogPostAdmin)
|
||||||
admin.site.register(Comment, CommentAdmin)
|
admin.site.register(Comment, CommentAdmin)
|
||||||
|
@ -74,15 +31,7 @@ admin.site.register(Organization, OrganizationAdmin)
|
||||||
admin.site.register(OrganizationRequest, OrganizationRequestAdmin)
|
admin.site.register(OrganizationRequest, OrganizationRequestAdmin)
|
||||||
admin.site.register(Problem, ProblemAdmin)
|
admin.site.register(Problem, ProblemAdmin)
|
||||||
admin.site.register(ProblemGroup, ProblemGroupAdmin)
|
admin.site.register(ProblemGroup, ProblemGroupAdmin)
|
||||||
admin.site.register(ProblemPointsVote, ProblemPointsVoteAdmin)
|
|
||||||
admin.site.register(ProblemType, ProblemTypeAdmin)
|
admin.site.register(ProblemType, ProblemTypeAdmin)
|
||||||
admin.site.register(Profile, ProfileAdmin)
|
admin.site.register(Profile, ProfileAdmin)
|
||||||
admin.site.register(Submission, SubmissionAdmin)
|
admin.site.register(Submission, SubmissionAdmin)
|
||||||
admin.site.register(Ticket, TicketAdmin)
|
admin.site.register(Ticket, TicketAdmin)
|
||||||
admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin)
|
|
||||||
admin.site.register(Course, CourseAdmin)
|
|
||||||
admin.site.unregister(User)
|
|
||||||
admin.site.register(User, UserAdmin)
|
|
||||||
admin.site.register(ContestsSummary, ContestsSummaryAdmin)
|
|
||||||
admin.site.register(OfficialContestCategory, OfficialContestCategoryAdmin)
|
|
||||||
admin.site.register(OfficialContestLocation, OfficialContestLocationAdmin)
|
|
||||||
|
|
|
@ -11,71 +11,52 @@ from judge.widgets import AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidg
|
||||||
class CommentForm(ModelForm):
|
class CommentForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
"author": AdminHeavySelect2Widget(data_view="profile_select2"),
|
'author': AdminHeavySelect2Widget(data_view='profile_select2'),
|
||||||
|
'parent': AdminHeavySelect2Widget(data_view='comment_select2'),
|
||||||
}
|
}
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets["body"] = HeavyPreviewAdminPageDownWidget(
|
widgets['body'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('comment_preview'))
|
||||||
preview=reverse_lazy("comment_preview")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CommentAdmin(VersionAdmin):
|
class CommentAdmin(VersionAdmin):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(
|
(None, {'fields': ('author', 'page', 'parent', 'score', 'hidden')}),
|
||||||
None,
|
('Content', {'fields': ('body',)}),
|
||||||
{
|
|
||||||
"fields": (
|
|
||||||
"author",
|
|
||||||
"parent",
|
|
||||||
"score",
|
|
||||||
"hidden",
|
|
||||||
"content_type",
|
|
||||||
"object_id",
|
|
||||||
)
|
)
|
||||||
},
|
list_display = ['author', 'linked_page', 'time']
|
||||||
),
|
search_fields = ['author__user__username', 'page', 'body']
|
||||||
("Content", {"fields": ("body",)}),
|
actions = ['hide_comment', 'unhide_comment']
|
||||||
)
|
list_filter = ['hidden']
|
||||||
list_display = ["author", "linked_object", "time"]
|
|
||||||
search_fields = ["author__user__username", "body"]
|
|
||||||
readonly_fields = ["score", "parent"]
|
|
||||||
actions = ["hide_comment", "unhide_comment"]
|
|
||||||
list_filter = ["hidden"]
|
|
||||||
actions_on_top = True
|
actions_on_top = True
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
form = CommentForm
|
form = CommentForm
|
||||||
date_hierarchy = "time"
|
date_hierarchy = 'time'
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
return Comment.objects.order_by("-time")
|
return Comment.objects.order_by('-time')
|
||||||
|
|
||||||
def hide_comment(self, request, queryset):
|
def hide_comment(self, request, queryset):
|
||||||
count = queryset.update(hidden=True)
|
count = queryset.update(hidden=True)
|
||||||
self.message_user(
|
self.message_user(request, ungettext('%d comment successfully hidden.',
|
||||||
request,
|
'%d comments successfully hidden.',
|
||||||
ungettext(
|
count) % count)
|
||||||
"%d comment successfully hidden.",
|
hide_comment.short_description = _('Hide comments')
|
||||||
"%d comments successfully hidden.",
|
|
||||||
count,
|
|
||||||
)
|
|
||||||
% count,
|
|
||||||
)
|
|
||||||
|
|
||||||
hide_comment.short_description = _("Hide comments")
|
|
||||||
|
|
||||||
def unhide_comment(self, request, queryset):
|
def unhide_comment(self, request, queryset):
|
||||||
count = queryset.update(hidden=False)
|
count = queryset.update(hidden=False)
|
||||||
self.message_user(
|
self.message_user(request, ungettext('%d comment successfully unhidden.',
|
||||||
request,
|
'%d comments successfully unhidden.',
|
||||||
ungettext(
|
count) % count)
|
||||||
"%d comment successfully unhidden.",
|
unhide_comment.short_description = _('Unhide comments')
|
||||||
"%d comments successfully unhidden.",
|
|
||||||
count,
|
|
||||||
)
|
|
||||||
% count,
|
|
||||||
)
|
|
||||||
|
|
||||||
unhide_comment.short_description = _("Unhide comments")
|
def linked_page(self, obj):
|
||||||
|
link = obj.link
|
||||||
|
if link is not None:
|
||||||
|
return format_html('<a href="{0}">{1}</a>', link, obj.page)
|
||||||
|
else:
|
||||||
|
return format_html('{0}', obj.page)
|
||||||
|
linked_page.short_description = _('Associated page')
|
||||||
|
linked_page.admin_order_field = 'page'
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
super(CommentAdmin, self).save_model(request, obj, form, change)
|
super(CommentAdmin, self).save_model(request, obj, form, change)
|
||||||
|
|
|
@ -3,36 +3,18 @@ from django.contrib import admin
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db import connection, transaction
|
from django.db import connection, transaction
|
||||||
from django.db.models import Q, TextField
|
from django.db.models import Q, TextField
|
||||||
from django.forms import ModelForm, ModelMultipleChoiceField, TextInput
|
from django.forms import ModelForm, ModelMultipleChoiceField
|
||||||
from django.http import Http404, HttpResponseRedirect
|
from django.http import Http404, HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.translation import gettext_lazy as _, ungettext
|
from django.utils.translation import gettext_lazy as _, ungettext
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
from reversion_compare.admin import CompareVersionAdmin
|
|
||||||
|
|
||||||
from django_ace import AceWidget
|
from 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, AdminHeavySelect2Widget, AdminPagedownWidget, \
|
||||||
AdminHeavySelect2MultipleWidget,
|
AdminSelect2MultipleWidget, AdminSelect2Widget, HeavyPreviewAdminPageDownWidget
|
||||||
AdminHeavySelect2Widget,
|
|
||||||
AdminPagedownWidget,
|
|
||||||
AdminSelect2MultipleWidget,
|
|
||||||
AdminSelect2Widget,
|
|
||||||
HeavyPreviewAdminPageDownWidget,
|
|
||||||
)
|
|
||||||
from judge.views.contests import recalculate_contest_summary_result
|
|
||||||
from judge.utils.contest import maybe_trigger_contest_rescore
|
|
||||||
|
|
||||||
|
|
||||||
class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
|
class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
|
||||||
|
@ -43,394 +25,197 @@ class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
|
||||||
|
|
||||||
class ContestTagForm(ModelForm):
|
class ContestTagForm(ModelForm):
|
||||||
contests = ModelMultipleChoiceField(
|
contests = ModelMultipleChoiceField(
|
||||||
label=_("Included contests"),
|
label=_('Included contests'),
|
||||||
queryset=Contest.objects.all(),
|
queryset=Contest.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=AdminHeavySelect2MultipleWidget(data_view="contest_select2"),
|
widget=AdminHeavySelect2MultipleWidget(data_view='contest_select2'))
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ContestTagAdmin(admin.ModelAdmin):
|
class ContestTagAdmin(admin.ModelAdmin):
|
||||||
fields = ("name", "color", "description", "contests")
|
fields = ('name', 'color', 'description', 'contests')
|
||||||
list_display = ("name", "color")
|
list_display = ('name', 'color')
|
||||||
actions_on_top = True
|
actions_on_top = True
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
form = ContestTagForm
|
form = ContestTagForm
|
||||||
|
|
||||||
if AdminPagedownWidget is not None:
|
if AdminPagedownWidget is not None:
|
||||||
formfield_overrides = {
|
formfield_overrides = {
|
||||||
TextField: {"widget": AdminPagedownWidget},
|
TextField: {'widget': AdminPagedownWidget},
|
||||||
}
|
}
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
super(ContestTagAdmin, self).save_model(request, obj, form, change)
|
super(ContestTagAdmin, self).save_model(request, obj, form, change)
|
||||||
obj.contests.set(form.cleaned_data["contests"])
|
obj.contests.set(form.cleaned_data['contests'])
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
form = super(ContestTagAdmin, self).get_form(request, obj, **kwargs)
|
form = super(ContestTagAdmin, self).get_form(request, obj, **kwargs)
|
||||||
if obj is not None:
|
if obj is not None:
|
||||||
form.base_fields["contests"].initial = obj.contests.all()
|
form.base_fields['contests'].initial = obj.contests.all()
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
|
||||||
class ContestProblemInlineForm(ModelForm):
|
class ContestProblemInlineForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {'problem': AdminHeavySelect2Widget(data_view='problem_select2')}
|
||||||
"problem": AdminHeavySelect2Widget(data_view="problem_select2"),
|
|
||||||
"hidden_subtasks": TextInput(attrs={"size": "3"}),
|
|
||||||
"points": TextInput(attrs={"size": "1"}),
|
|
||||||
"order": TextInput(attrs={"size": "1"}),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ContestProblemInline(admin.TabularInline):
|
class ContestProblemInline(admin.TabularInline):
|
||||||
model = ContestProblem
|
model = ContestProblem
|
||||||
verbose_name = _("Problem")
|
verbose_name = _('Problem')
|
||||||
verbose_name_plural = "Problems"
|
verbose_name_plural = 'Problems'
|
||||||
fields = (
|
fields = ('problem', 'points', 'partial', 'is_pretested', 'max_submissions', 'output_prefix_override', 'order',
|
||||||
"problem",
|
'rejudge_column')
|
||||||
"points",
|
readonly_fields = ('rejudge_column',)
|
||||||
"partial",
|
|
||||||
"is_pretested",
|
|
||||||
"max_submissions",
|
|
||||||
"hidden_subtasks",
|
|
||||||
"show_testcases",
|
|
||||||
"order",
|
|
||||||
"rejudge_column",
|
|
||||||
)
|
|
||||||
readonly_fields = ("rejudge_column",)
|
|
||||||
form = ContestProblemInlineForm
|
form = ContestProblemInlineForm
|
||||||
|
|
||||||
def rejudge_column(self, obj):
|
def rejudge_column(self, obj):
|
||||||
if obj.id is None:
|
if obj.id is None:
|
||||||
return ""
|
return ''
|
||||||
return format_html(
|
return format_html('<a class="button rejudge-link" href="{}">Rejudge</a>',
|
||||||
'<a class="button rejudge-link" href="{}">Rejudge</a>',
|
reverse('admin:judge_contest_rejudge', args=(obj.contest.id, obj.id)))
|
||||||
reverse("admin:judge_contest_rejudge", args=(obj.contest.id, obj.id)),
|
rejudge_column.short_description = ''
|
||||||
)
|
|
||||||
|
|
||||||
rejudge_column.short_description = ""
|
|
||||||
|
|
||||||
|
|
||||||
class ContestForm(ModelForm):
|
class ContestForm(ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(ContestForm, self).__init__(*args, **kwargs)
|
super(ContestForm, self).__init__(*args, **kwargs)
|
||||||
if "rate_exclude" in self.fields:
|
if 'rate_exclude' in self.fields:
|
||||||
if self.instance and self.instance.id:
|
if self.instance and self.instance.id:
|
||||||
self.fields["rate_exclude"].queryset = Profile.objects.filter(
|
self.fields['rate_exclude'].queryset = \
|
||||||
contest_history__contest=self.instance
|
Profile.objects.filter(contest_history__contest=self.instance).distinct()
|
||||||
).distinct()
|
|
||||||
else:
|
else:
|
||||||
self.fields["rate_exclude"].queryset = Profile.objects.none()
|
self.fields['rate_exclude'].queryset = Profile.objects.none()
|
||||||
self.fields["banned_users"].widget.can_add_related = False
|
self.fields['banned_users'].widget.can_add_related = False
|
||||||
self.fields["view_contest_scoreboard"].widget.can_add_related = False
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super(ContestForm, self).clean()
|
cleaned_data = super(ContestForm, self).clean()
|
||||||
cleaned_data["banned_users"].filter(
|
cleaned_data['banned_users'].filter(current_contest__contest=self.instance).update(current_contest=None)
|
||||||
current_contest__contest=self.instance
|
|
||||||
).update(current_contest=None)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
"authors": AdminHeavySelect2MultipleWidget(data_view="profile_select2"),
|
'organizers': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
|
||||||
"curators": AdminHeavySelect2MultipleWidget(data_view="profile_select2"),
|
'private_contestants': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
|
||||||
"testers": AdminHeavySelect2MultipleWidget(data_view="profile_select2"),
|
attrs={'style': 'width: 100%'}),
|
||||||
"private_contestants": AdminHeavySelect2MultipleWidget(
|
'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2'),
|
||||||
data_view="profile_select2", attrs={"style": "width: 100%"}
|
'tags': AdminSelect2MultipleWidget,
|
||||||
),
|
'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
|
||||||
"organizations": AdminHeavySelect2MultipleWidget(
|
attrs={'style': 'width: 100%'}),
|
||||||
data_view="organization_select2"
|
|
||||||
),
|
|
||||||
"tags": AdminSelect2MultipleWidget,
|
|
||||||
"banned_users": AdminHeavySelect2MultipleWidget(
|
|
||||||
data_view="profile_select2", attrs={"style": "width: 100%"}
|
|
||||||
),
|
|
||||||
"view_contest_scoreboard": AdminHeavySelect2MultipleWidget(
|
|
||||||
data_view="profile_select2", attrs={"style": "width: 100%"}
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets["description"] = HeavyPreviewAdminPageDownWidget(
|
widgets['description'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('contest_preview'))
|
||||||
preview=reverse_lazy("contest_preview")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OfficialContestInlineForm(ModelForm):
|
class ContestAdmin(VersionAdmin):
|
||||||
class Meta:
|
|
||||||
widgets = {
|
|
||||||
"category": AdminSelect2Widget,
|
|
||||||
"location": AdminSelect2Widget,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class OfficialContestInline(admin.StackedInline):
|
|
||||||
fields = (
|
|
||||||
"category",
|
|
||||||
"year",
|
|
||||||
"location",
|
|
||||||
)
|
|
||||||
model = OfficialContest
|
|
||||||
can_delete = True
|
|
||||||
form = OfficialContestInlineForm
|
|
||||||
extra = 0
|
|
||||||
|
|
||||||
|
|
||||||
class ContestAdmin(CompareVersionAdmin):
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {"fields": ("key", "name", "authors", "curators", "testers")}),
|
(None, {'fields': ('key', 'name', 'organizers')}),
|
||||||
(
|
(_('Settings'), {'fields': ('is_visible', 'use_clarifications', 'hide_problem_tags', 'hide_scoreboard',
|
||||||
_("Settings"),
|
'run_pretests_only')}),
|
||||||
{
|
(_('Scheduling'), {'fields': ('start_time', 'end_time', 'time_limit')}),
|
||||||
"fields": (
|
(_('Details'), {'fields': ('description', 'og_image', 'logo_override_image', 'tags', 'summary')}),
|
||||||
"is_visible",
|
(_('Format'), {'fields': ('format_name', 'format_config')}),
|
||||||
"use_clarifications",
|
(_('Rating'), {'fields': ('is_rated', 'rate_all', 'rating_floor', 'rating_ceiling', 'rate_exclude')}),
|
||||||
"hide_problem_tags",
|
(_('Access'), {'fields': ('access_code', 'is_private', 'private_contestants', 'is_organization_private',
|
||||||
"public_scoreboard",
|
'organizations')}),
|
||||||
"scoreboard_visibility",
|
(_('Justice'), {'fields': ('banned_users',)}),
|
||||||
"run_pretests_only",
|
|
||||||
"points_precision",
|
|
||||||
"rate_limit",
|
|
||||||
)
|
)
|
||||||
},
|
list_display = ('key', 'name', 'is_visible', 'is_rated', 'start_time', 'end_time', 'time_limit', 'user_count')
|
||||||
),
|
actions = ['make_visible', 'make_hidden']
|
||||||
(
|
inlines = [ContestProblemInline]
|
||||||
_("Scheduling"),
|
|
||||||
{"fields": ("start_time", "end_time", "time_limit", "freeze_after")},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
_("Details"),
|
|
||||||
{
|
|
||||||
"fields": (
|
|
||||||
"description",
|
|
||||||
"og_image",
|
|
||||||
"logo_override_image",
|
|
||||||
"tags",
|
|
||||||
"summary",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
_("Format"),
|
|
||||||
{"fields": ("format_name", "format_config", "problem_label_script")},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
_("Rating"),
|
|
||||||
{
|
|
||||||
"fields": (
|
|
||||||
"is_rated",
|
|
||||||
"rate_all",
|
|
||||||
"rating_floor",
|
|
||||||
"rating_ceiling",
|
|
||||||
"rate_exclude",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
_("Access"),
|
|
||||||
{
|
|
||||||
"fields": (
|
|
||||||
"access_code",
|
|
||||||
"private_contestants",
|
|
||||||
"organizations",
|
|
||||||
"view_contest_scoreboard",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(_("Justice"), {"fields": ("banned_users",)}),
|
|
||||||
)
|
|
||||||
list_display = (
|
|
||||||
"key",
|
|
||||||
"name",
|
|
||||||
"is_visible",
|
|
||||||
"is_rated",
|
|
||||||
"start_time",
|
|
||||||
"end_time",
|
|
||||||
"time_limit",
|
|
||||||
"user_count",
|
|
||||||
)
|
|
||||||
search_fields = ("key", "name")
|
|
||||||
inlines = [ContestProblemInline, OfficialContestInline]
|
|
||||||
actions_on_top = True
|
actions_on_top = True
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
form = ContestForm
|
form = ContestForm
|
||||||
change_list_template = "admin/judge/contest/change_list.html"
|
change_list_template = 'admin/judge/contest/change_list.html'
|
||||||
filter_horizontal = ["rate_exclude"]
|
filter_horizontal = ['rate_exclude']
|
||||||
date_hierarchy = "start_time"
|
date_hierarchy = 'start_time'
|
||||||
|
|
||||||
def get_actions(self, request):
|
|
||||||
actions = super(ContestAdmin, self).get_actions(request)
|
|
||||||
|
|
||||||
if request.user.has_perm(
|
|
||||||
"judge.change_contest_visibility"
|
|
||||||
) or request.user.has_perm("judge.create_private_contest"):
|
|
||||||
for action in ("make_visible", "make_hidden"):
|
|
||||||
actions[action] = self.get_action(action)
|
|
||||||
|
|
||||||
return actions
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
queryset = Contest.objects.all()
|
queryset = Contest.objects.all()
|
||||||
if request.user.has_perm("judge.edit_all_contest"):
|
if request.user.has_perm('judge.edit_all_contest'):
|
||||||
return queryset
|
return queryset
|
||||||
else:
|
else:
|
||||||
return queryset.filter(
|
return queryset.filter(organizers__id=request.profile.id)
|
||||||
Q(authors=request.profile) | Q(curators=request.profile)
|
|
||||||
).distinct()
|
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
readonly = []
|
readonly = []
|
||||||
if not request.user.has_perm("judge.contest_rating"):
|
if not request.user.has_perm('judge.contest_rating'):
|
||||||
readonly += ["is_rated", "rate_all", "rate_exclude"]
|
readonly += ['is_rated', 'rate_all', 'rate_exclude']
|
||||||
if not request.user.has_perm("judge.contest_access_code"):
|
if not request.user.has_perm('judge.contest_access_code'):
|
||||||
readonly += ["access_code"]
|
readonly += ['access_code']
|
||||||
if not request.user.has_perm("judge.create_private_contest"):
|
if not request.user.has_perm('judge.create_private_contest'):
|
||||||
readonly += [
|
readonly += ['is_private', 'private_contestants', 'is_organization_private', 'organizations']
|
||||||
"private_contestants",
|
|
||||||
"organizations",
|
|
||||||
]
|
|
||||||
if not request.user.has_perm("judge.change_contest_visibility"):
|
|
||||||
readonly += ["is_visible"]
|
|
||||||
if not request.user.has_perm("judge.contest_problem_label"):
|
|
||||||
readonly += ["problem_label_script"]
|
|
||||||
return readonly
|
return readonly
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
|
||||||
# `is_visible` will not appear in `cleaned_data` if user cannot edit it
|
|
||||||
if form.cleaned_data.get("is_visible") and not request.user.has_perm(
|
|
||||||
"judge.change_contest_visibility"
|
|
||||||
):
|
|
||||||
if (
|
|
||||||
not len(form.cleaned_data["organizations"]) > 0
|
|
||||||
and not len(form.cleaned_data["private_contestants"]) > 0
|
|
||||||
):
|
|
||||||
raise PermissionDenied
|
|
||||||
if not request.user.has_perm("judge.create_private_contest"):
|
|
||||||
raise PermissionDenied
|
|
||||||
|
|
||||||
super().save_model(request, obj, form, change)
|
|
||||||
|
|
||||||
def save_related(self, request, form, formsets, change):
|
|
||||||
super().save_related(request, form, formsets, change)
|
|
||||||
# Only rescored if we did not already do so in `save_model`
|
|
||||||
formset_changed = False
|
|
||||||
if any(formset.has_changed() for formset in formsets):
|
|
||||||
formset_changed = True
|
|
||||||
|
|
||||||
maybe_trigger_contest_rescore(form, form.instance, formset_changed)
|
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
def has_change_permission(self, request, obj=None):
|
||||||
if not request.user.has_perm("judge.edit_own_contest"):
|
if not request.user.has_perm('judge.edit_own_contest'):
|
||||||
return False
|
return False
|
||||||
if obj is None:
|
if request.user.has_perm('judge.edit_all_contest') or obj is None:
|
||||||
return True
|
return True
|
||||||
return obj.is_editable_by(request.user)
|
return obj.organizers.filter(id=request.profile.id).exists()
|
||||||
|
|
||||||
def make_visible(self, request, queryset):
|
def make_visible(self, request, queryset):
|
||||||
if not request.user.has_perm("judge.change_contest_visibility"):
|
|
||||||
queryset = queryset.filter(
|
|
||||||
Q(is_private=True) | Q(is_organization_private=True)
|
|
||||||
)
|
|
||||||
count = queryset.update(is_visible=True)
|
count = queryset.update(is_visible=True)
|
||||||
self.message_user(
|
self.message_user(request, ungettext('%d contest successfully marked as visible.',
|
||||||
request,
|
'%d contests successfully marked as visible.',
|
||||||
ungettext(
|
count) % count)
|
||||||
"%d contest successfully marked as visible.",
|
make_visible.short_description = _('Mark contests as visible')
|
||||||
"%d contests successfully marked as visible.",
|
|
||||||
count,
|
|
||||||
)
|
|
||||||
% count,
|
|
||||||
)
|
|
||||||
|
|
||||||
make_visible.short_description = _("Mark contests as visible")
|
|
||||||
|
|
||||||
def make_hidden(self, request, queryset):
|
def make_hidden(self, request, queryset):
|
||||||
if not request.user.has_perm("judge.change_contest_visibility"):
|
count = queryset.update(is_visible=False)
|
||||||
queryset = queryset.filter(
|
self.message_user(request, ungettext('%d contest successfully marked as hidden.',
|
||||||
Q(is_private=True) | Q(is_organization_private=True)
|
'%d contests successfully marked as hidden.',
|
||||||
)
|
count) % count)
|
||||||
count = queryset.update(is_visible=True)
|
make_hidden.short_description = _('Mark contests as hidden')
|
||||||
self.message_user(
|
|
||||||
request,
|
|
||||||
ungettext(
|
|
||||||
"%d contest successfully marked as hidden.",
|
|
||||||
"%d contests successfully marked as hidden.",
|
|
||||||
count,
|
|
||||||
)
|
|
||||||
% count,
|
|
||||||
)
|
|
||||||
|
|
||||||
make_hidden.short_description = _("Mark contests as hidden")
|
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
return [
|
return [
|
||||||
url(r"^rate/all/$", self.rate_all_view, name="judge_contest_rate_all"),
|
url(r'^rate/all/$', self.rate_all_view, name='judge_contest_rate_all'),
|
||||||
url(r"^(\d+)/rate/$", self.rate_view, name="judge_contest_rate"),
|
url(r'^(\d+)/rate/$', self.rate_view, name='judge_contest_rate'),
|
||||||
url(
|
url(r'^(\d+)/judge/(\d+)/$', self.rejudge_view, name='judge_contest_rejudge'),
|
||||||
r"^(\d+)/judge/(\d+)/$", self.rejudge_view, name="judge_contest_rejudge"
|
|
||||||
),
|
|
||||||
] + super(ContestAdmin, self).get_urls()
|
] + super(ContestAdmin, self).get_urls()
|
||||||
|
|
||||||
def rejudge_view(self, request, contest_id, problem_id):
|
def rejudge_view(self, request, contest_id, problem_id):
|
||||||
queryset = ContestSubmission.objects.filter(
|
queryset = ContestSubmission.objects.filter(problem_id=problem_id).select_related('submission')
|
||||||
problem_id=problem_id
|
|
||||||
).select_related("submission")
|
|
||||||
for model in queryset:
|
for model in queryset:
|
||||||
model.submission.judge(rejudge=True)
|
model.submission.judge(rejudge=True)
|
||||||
|
|
||||||
self.message_user(
|
self.message_user(request, ungettext('%d submission was successfully scheduled for rejudging.',
|
||||||
request,
|
'%d submissions were successfully scheduled for rejudging.',
|
||||||
ungettext(
|
len(queryset)) % len(queryset))
|
||||||
"%d submission was successfully scheduled for rejudging.",
|
return HttpResponseRedirect(reverse('admin:judge_contest_change', args=(contest_id,)))
|
||||||
"%d submissions were successfully scheduled for rejudging.",
|
|
||||||
len(queryset),
|
|
||||||
)
|
|
||||||
% len(queryset),
|
|
||||||
)
|
|
||||||
return HttpResponseRedirect(
|
|
||||||
reverse("admin:judge_contest_change", args=(contest_id,))
|
|
||||||
)
|
|
||||||
|
|
||||||
def rate_all_view(self, request):
|
def rate_all_view(self, request):
|
||||||
if not request.user.has_perm("judge.contest_rating"):
|
if not request.user.has_perm('judge.contest_rating'):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
with connection.cursor() as cursor:
|
if connection.vendor == 'sqlite':
|
||||||
cursor.execute("TRUNCATE TABLE `%s`" % Rating._meta.db_table)
|
Rating.objects.all().delete()
|
||||||
|
else:
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute('TRUNCATE TABLE `%s`' % Rating._meta.db_table)
|
||||||
|
cursor.close()
|
||||||
Profile.objects.update(rating=None)
|
Profile.objects.update(rating=None)
|
||||||
for contest in Contest.objects.filter(
|
for contest in Contest.objects.filter(is_rated=True).order_by('end_time'):
|
||||||
is_rated=True, end_time__lte=timezone.now()
|
|
||||||
).order_by("end_time"):
|
|
||||||
rate_contest(contest)
|
rate_contest(contest)
|
||||||
return HttpResponseRedirect(reverse("admin:judge_contest_changelist"))
|
return HttpResponseRedirect(reverse('admin:judge_contest_changelist'))
|
||||||
|
|
||||||
def rate_view(self, request, id):
|
def rate_view(self, request, id):
|
||||||
if not request.user.has_perm("judge.contest_rating"):
|
if not request.user.has_perm('judge.contest_rating'):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
contest = get_object_or_404(Contest, id=id)
|
contest = get_object_or_404(Contest, id=id)
|
||||||
if not contest.is_rated or not contest.ended:
|
if not contest.is_rated:
|
||||||
raise Http404()
|
raise Http404()
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
contest.rate()
|
contest.rate()
|
||||||
return HttpResponseRedirect(
|
return HttpResponseRedirect(request.META.get('HTTP_REFERER', reverse('admin:judge_contest_changelist')))
|
||||||
request.META.get("HTTP_REFERER", reverse("admin:judge_contest_changelist"))
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
def get_form(self, *args, **kwargs):
|
||||||
form = super(ContestAdmin, self).get_form(request, obj, **kwargs)
|
form = super(ContestAdmin, self).get_form(*args, **kwargs)
|
||||||
if "problem_label_script" in form.base_fields:
|
perms = ('edit_own_contest', 'edit_all_contest')
|
||||||
# form.base_fields['problem_label_script'] does not exist when the user has only view permission
|
form.base_fields['organizers'].queryset = Profile.objects.filter(
|
||||||
# on the model.
|
Q(user__is_superuser=True) |
|
||||||
form.base_fields["problem_label_script"].widget = AceWidget(
|
Q(user__groups__permissions__codename__in=perms) |
|
||||||
"lua", request.profile.ace_theme
|
Q(user__user_permissions__codename__in=perms),
|
||||||
)
|
|
||||||
|
|
||||||
perms = ("edit_own_contest", "edit_all_contest")
|
|
||||||
form.base_fields["curators"].queryset = Profile.objects.filter(
|
|
||||||
Q(user__is_superuser=True)
|
|
||||||
| Q(user__groups__permissions__codename__in=perms)
|
|
||||||
| Q(user__user_permissions__codename__in=perms),
|
|
||||||
).distinct()
|
).distinct()
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
@ -438,48 +223,29 @@ class ContestAdmin(CompareVersionAdmin):
|
||||||
class ContestParticipationForm(ModelForm):
|
class ContestParticipationForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
"contest": AdminSelect2Widget(),
|
'contest': AdminSelect2Widget(),
|
||||||
"user": AdminHeavySelect2Widget(data_view="profile_select2"),
|
'user': AdminHeavySelect2Widget(data_view='profile_select2'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ContestParticipationAdmin(admin.ModelAdmin):
|
class ContestParticipationAdmin(admin.ModelAdmin):
|
||||||
fields = ("contest", "user", "real_start", "virtual", "is_disqualified")
|
fields = ('contest', 'user', 'real_start', 'virtual', 'is_disqualified')
|
||||||
list_display = (
|
list_display = ('contest', 'username', 'show_virtual', 'real_start', 'score', 'cumtime')
|
||||||
"contest",
|
actions = ['recalculate_results']
|
||||||
"username",
|
|
||||||
"show_virtual",
|
|
||||||
"real_start",
|
|
||||||
"score",
|
|
||||||
"cumtime",
|
|
||||||
"tiebreaker",
|
|
||||||
)
|
|
||||||
actions = ["recalculate_results"]
|
|
||||||
actions_on_bottom = actions_on_top = True
|
actions_on_bottom = actions_on_top = True
|
||||||
search_fields = ("contest__key", "contest__name", "user__user__username")
|
search_fields = ('contest__key', 'contest__name', 'user__user__username')
|
||||||
form = ContestParticipationForm
|
form = ContestParticipationForm
|
||||||
date_hierarchy = "real_start"
|
date_hierarchy = 'real_start'
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
return (
|
return super(ContestParticipationAdmin, self).get_queryset(request).only(
|
||||||
super(ContestParticipationAdmin, self)
|
'contest__name', 'contest__format_name', 'contest__format_config',
|
||||||
.get_queryset(request)
|
'user__user__username', 'real_start', 'score', 'cumtime', 'virtual',
|
||||||
.only(
|
|
||||||
"contest__name",
|
|
||||||
"contest__format_name",
|
|
||||||
"contest__format_config",
|
|
||||||
"user__user__username",
|
|
||||||
"real_start",
|
|
||||||
"score",
|
|
||||||
"cumtime",
|
|
||||||
"tiebreaker",
|
|
||||||
"virtual",
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
super().save_model(request, obj, form, change)
|
super().save_model(request, obj, form, change)
|
||||||
if form.changed_data and "is_disqualified" in form.changed_data:
|
if form.changed_data and 'is_disqualified' in form.changed_data:
|
||||||
obj.set_disqualified(obj.is_disqualified)
|
obj.set_disqualified(obj.is_disqualified)
|
||||||
|
|
||||||
def recalculate_results(self, request, queryset):
|
def recalculate_results(self, request, queryset):
|
||||||
|
@ -487,48 +253,17 @@ class ContestParticipationAdmin(admin.ModelAdmin):
|
||||||
for participation in queryset:
|
for participation in queryset:
|
||||||
participation.recompute_results()
|
participation.recompute_results()
|
||||||
count += 1
|
count += 1
|
||||||
self.message_user(
|
self.message_user(request, ungettext('%d participation recalculated.',
|
||||||
request,
|
'%d participations recalculated.',
|
||||||
ungettext(
|
count) % count)
|
||||||
"%d participation recalculated.",
|
recalculate_results.short_description = _('Recalculate results')
|
||||||
"%d participations recalculated.",
|
|
||||||
count,
|
|
||||||
)
|
|
||||||
% count,
|
|
||||||
)
|
|
||||||
|
|
||||||
recalculate_results.short_description = _("Recalculate results")
|
|
||||||
|
|
||||||
def username(self, obj):
|
def username(self, obj):
|
||||||
return obj.user.username
|
return obj.user.username
|
||||||
|
username.short_description = _('username')
|
||||||
username.short_description = _("username")
|
username.admin_order_field = 'user__user__username'
|
||||||
username.admin_order_field = "user__user__username"
|
|
||||||
|
|
||||||
def show_virtual(self, obj):
|
def show_virtual(self, obj):
|
||||||
return obj.virtual or "-"
|
return obj.virtual or '-'
|
||||||
|
show_virtual.short_description = _('virtual')
|
||||||
show_virtual.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()
|
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
from django.utils.html import format_html
|
|
||||||
from django.urls import reverse, reverse_lazy
|
|
||||||
from django.utils.translation import gettext, gettext_lazy as _, ungettext
|
|
||||||
from django.forms import ModelForm
|
|
||||||
|
|
||||||
from judge.models import Course, CourseRole
|
|
||||||
from judge.widgets import AdminSelect2MultipleWidget
|
|
||||||
from judge.widgets import (
|
|
||||||
AdminHeavySelect2MultipleWidget,
|
|
||||||
AdminHeavySelect2Widget,
|
|
||||||
HeavyPreviewAdminPageDownWidget,
|
|
||||||
AdminSelect2Widget,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CourseRoleInlineForm(ModelForm):
|
|
||||||
class Meta:
|
|
||||||
widgets = {
|
|
||||||
"user": AdminHeavySelect2Widget(
|
|
||||||
data_view="profile_select2", attrs={"style": "width: 100%"}
|
|
||||||
),
|
|
||||||
"role": AdminSelect2Widget,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class CourseRoleInline(admin.TabularInline):
|
|
||||||
model = CourseRole
|
|
||||||
extra = 1
|
|
||||||
form = CourseRoleInlineForm
|
|
||||||
|
|
||||||
|
|
||||||
class CourseForm(ModelForm):
|
|
||||||
class Meta:
|
|
||||||
widgets = {
|
|
||||||
"organizations": AdminHeavySelect2MultipleWidget(
|
|
||||||
data_view="organization_select2"
|
|
||||||
),
|
|
||||||
"about": HeavyPreviewAdminPageDownWidget(
|
|
||||||
preview=reverse_lazy("blog_preview")
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class CourseAdmin(admin.ModelAdmin):
|
|
||||||
prepopulated_fields = {"slug": ("name",)}
|
|
||||||
inlines = [
|
|
||||||
CourseRoleInline,
|
|
||||||
]
|
|
||||||
list_display = ("name", "is_public", "is_open")
|
|
||||||
search_fields = ("name",)
|
|
||||||
form = CourseForm
|
|
|
@ -6,33 +6,26 @@ from django.utils.html import format_html
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from mptt.admin import DraggableMPTTAdmin
|
from mptt.admin import DraggableMPTTAdmin
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
from reversion_compare.admin import CompareVersionAdmin
|
|
||||||
|
|
||||||
|
|
||||||
from judge.dblock import LockModel
|
from judge.dblock import LockModel
|
||||||
from judge.models import NavigationBar
|
from judge.models import NavigationBar
|
||||||
from judge.widgets import (
|
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
|
||||||
AdminHeavySelect2MultipleWidget,
|
|
||||||
AdminHeavySelect2Widget,
|
|
||||||
HeavyPreviewAdminPageDownWidget,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class NavigationBarAdmin(DraggableMPTTAdmin):
|
class NavigationBarAdmin(DraggableMPTTAdmin):
|
||||||
list_display = DraggableMPTTAdmin.list_display + ("key", "linked_path")
|
list_display = DraggableMPTTAdmin.list_display + ('key', 'linked_path')
|
||||||
fields = ("key", "label", "path", "order", "regex", "parent")
|
fields = ('key', 'label', 'path', 'order', 'regex', 'parent')
|
||||||
list_editable = () # Bug in SortableModelAdmin: 500 without list_editable being set
|
list_editable = () # Bug in SortableModelAdmin: 500 without list_editable being set
|
||||||
mptt_level_indent = 20
|
mptt_level_indent = 20
|
||||||
sortable = "order"
|
sortable = 'order'
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(NavigationBarAdmin, self).__init__(*args, **kwargs)
|
super(NavigationBarAdmin, self).__init__(*args, **kwargs)
|
||||||
self.__save_model_calls = 0
|
self.__save_model_calls = 0
|
||||||
|
|
||||||
def linked_path(self, obj):
|
def linked_path(self, obj):
|
||||||
return format_html('<a href="{0}" target="_blank">{0}</a>', obj.path)
|
return format_html(u'<a href="{0}" target="_blank">{0}</a>', obj.path)
|
||||||
|
linked_path.short_description = _('link path')
|
||||||
linked_path.short_description = _("link path")
|
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
self.__save_model_calls += 1
|
self.__save_model_calls += 1
|
||||||
|
@ -41,9 +34,7 @@ class NavigationBarAdmin(DraggableMPTTAdmin):
|
||||||
def changelist_view(self, request, extra_context=None):
|
def changelist_view(self, request, extra_context=None):
|
||||||
self.__save_model_calls = 0
|
self.__save_model_calls = 0
|
||||||
with NavigationBar.objects.disable_mptt_updates():
|
with NavigationBar.objects.disable_mptt_updates():
|
||||||
result = super(NavigationBarAdmin, self).changelist_view(
|
result = super(NavigationBarAdmin, self).changelist_view(request, extra_context)
|
||||||
request, extra_context
|
|
||||||
)
|
|
||||||
if self.__save_model_calls:
|
if self.__save_model_calls:
|
||||||
with LockModel(write=(NavigationBar,)):
|
with LockModel(write=(NavigationBar,)):
|
||||||
NavigationBar.objects.rebuild()
|
NavigationBar.objects.rebuild()
|
||||||
|
@ -53,106 +44,71 @@ 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:
|
||||||
widgets = {
|
widgets = {
|
||||||
"authors": AdminHeavySelect2MultipleWidget(
|
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||||
data_view="profile_select2", attrs={"style": "width: 100%"}
|
|
||||||
),
|
|
||||||
"organizations": AdminHeavySelect2MultipleWidget(
|
|
||||||
data_view="organization_select2", attrs={"style": "width: 100%"}
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets["content"] = HeavyPreviewAdminPageDownWidget(
|
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('blog_preview'))
|
||||||
preview=reverse_lazy("blog_preview")
|
widgets['summary'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('blog_preview'))
|
||||||
)
|
|
||||||
widgets["summary"] = HeavyPreviewAdminPageDownWidget(
|
|
||||||
preview=reverse_lazy("blog_preview")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BlogPostAdmin(CompareVersionAdmin):
|
class BlogPostAdmin(VersionAdmin):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(
|
(None, {'fields': ('title', 'slug', 'authors', 'visible', 'sticky', 'publish_on')}),
|
||||||
None,
|
(_('Content'), {'fields': ('content', 'og_image')}),
|
||||||
{
|
(_('Summary'), {'classes': ('collapse',), 'fields': ('summary',)}),
|
||||||
"fields": (
|
|
||||||
"title",
|
|
||||||
"slug",
|
|
||||||
"authors",
|
|
||||||
"visible",
|
|
||||||
"sticky",
|
|
||||||
"publish_on",
|
|
||||||
"is_organization_private",
|
|
||||||
"organizations",
|
|
||||||
)
|
)
|
||||||
},
|
prepopulated_fields = {'slug': ('title',)}
|
||||||
),
|
list_display = ('id', 'title', 'visible', 'sticky', 'publish_on')
|
||||||
(_("Content"), {"fields": ("content", "og_image")}),
|
list_display_links = ('id', 'title')
|
||||||
(_("Summary"), {"classes": ("collapse",), "fields": ("summary",)}),
|
ordering = ('-publish_on',)
|
||||||
)
|
|
||||||
prepopulated_fields = {"slug": ("title",)}
|
|
||||||
list_display = ("id", "title", "visible", "sticky", "publish_on")
|
|
||||||
list_display_links = ("id", "title")
|
|
||||||
ordering = ("-publish_on",)
|
|
||||||
form = BlogPostForm
|
form = BlogPostForm
|
||||||
date_hierarchy = "publish_on"
|
date_hierarchy = 'publish_on'
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
def has_change_permission(self, request, obj=None):
|
||||||
return (
|
return (request.user.has_perm('judge.edit_all_post') or
|
||||||
request.user.has_perm("judge.edit_all_post")
|
request.user.has_perm('judge.change_blogpost') and (
|
||||||
or request.user.has_perm("judge.change_blogpost")
|
obj is None or
|
||||||
and (obj is None or obj.authors.filter(id=request.profile.id).exists())
|
obj.authors.filter(id=request.profile.id).exists()))
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SolutionForm(ModelForm):
|
class SolutionForm(ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(SolutionForm, self).__init__(*args, **kwargs)
|
super(SolutionForm, self).__init__(*args, **kwargs)
|
||||||
self.fields["authors"].widget.can_add_related = False
|
self.fields['authors'].widget.can_add_related = False
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
"authors": AdminHeavySelect2MultipleWidget(
|
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||||
data_view="profile_select2", attrs={"style": "width: 100%"}
|
'problem': AdminHeavySelect2Widget(data_view='problem_select2', attrs={'style': 'width: 250px'}),
|
||||||
),
|
|
||||||
"problem": AdminHeavySelect2Widget(
|
|
||||||
data_view="problem_select2", attrs={"style": "width: 250px"}
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets["content"] = HeavyPreviewAdminPageDownWidget(
|
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('solution_preview'))
|
||||||
preview=reverse_lazy("solution_preview")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class LicenseForm(ModelForm):
|
class LicenseForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets = {
|
widgets = {'text': HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('license_preview'))}
|
||||||
"text": HeavyPreviewAdminPageDownWidget(
|
|
||||||
preview=reverse_lazy("license_preview")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class LicenseAdmin(admin.ModelAdmin):
|
class LicenseAdmin(admin.ModelAdmin):
|
||||||
fields = ("key", "link", "name", "display", "icon", "text")
|
fields = ('key', 'link', 'name', 'display', 'icon', 'text')
|
||||||
list_display = ("name", "key")
|
list_display = ('name', 'key')
|
||||||
form = LicenseForm
|
form = LicenseForm
|
||||||
|
|
||||||
|
|
||||||
class UserListFilter(admin.SimpleListFilter):
|
class UserListFilter(admin.SimpleListFilter):
|
||||||
title = _("user")
|
title = _('user')
|
||||||
parameter_name = "user"
|
parameter_name = 'user'
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
return User.objects.filter(is_staff=True).values_list("id", "username")
|
return User.objects.filter(is_staff=True).values_list('id', 'username')
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value():
|
if self.value():
|
||||||
|
@ -161,29 +117,10 @@ class UserListFilter(admin.SimpleListFilter):
|
||||||
|
|
||||||
|
|
||||||
class LogEntryAdmin(admin.ModelAdmin):
|
class LogEntryAdmin(admin.ModelAdmin):
|
||||||
readonly_fields = (
|
readonly_fields = ('user', 'content_type', 'object_id', 'object_repr', 'action_flag', 'change_message')
|
||||||
"user",
|
list_display = ('__str__', 'action_time', 'user', 'content_type', 'object_link')
|
||||||
"content_type",
|
search_fields = ('object_repr', 'change_message')
|
||||||
"object_id",
|
list_filter = (UserListFilter, 'content_type')
|
||||||
"object_repr",
|
|
||||||
"action_flag",
|
|
||||||
"change_message",
|
|
||||||
)
|
|
||||||
list_display = (
|
|
||||||
"__str__",
|
|
||||||
"action_time",
|
|
||||||
"user",
|
|
||||||
"content_type",
|
|
||||||
"object_link",
|
|
||||||
"diff_link",
|
|
||||||
)
|
|
||||||
search_fields = (
|
|
||||||
"object_repr",
|
|
||||||
"change_message",
|
|
||||||
"user__username",
|
|
||||||
"content_type__model",
|
|
||||||
)
|
|
||||||
list_filter = (UserListFilter, "content_type")
|
|
||||||
list_display_links = None
|
list_display_links = None
|
||||||
actions = None
|
actions = None
|
||||||
|
|
||||||
|
@ -202,35 +139,13 @@ class LogEntryAdmin(admin.ModelAdmin):
|
||||||
else:
|
else:
|
||||||
ct = obj.content_type
|
ct = obj.content_type
|
||||||
try:
|
try:
|
||||||
link = format_html(
|
link = format_html('<a href="{1}">{0}</a>', obj.object_repr,
|
||||||
'<a href="{1}">{0}</a>',
|
reverse('admin:%s_%s_change' % (ct.app_label, ct.model), args=(obj.object_id,)))
|
||||||
obj.object_repr,
|
|
||||||
reverse(
|
|
||||||
"admin:%s_%s_change" % (ct.app_label, ct.model),
|
|
||||||
args=(obj.object_id,),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
link = obj.object_repr
|
link = obj.object_repr
|
||||||
return link
|
return link
|
||||||
|
object_link.admin_order_field = 'object_repr'
|
||||||
object_link.admin_order_field = "object_repr"
|
object_link.short_description = _('object')
|
||||||
object_link.short_description = _("object")
|
|
||||||
|
|
||||||
def diff_link(self, obj):
|
|
||||||
if obj.is_deletion():
|
|
||||||
return None
|
|
||||||
ct = obj.content_type
|
|
||||||
try:
|
|
||||||
url = reverse(
|
|
||||||
"admin:%s_%s_history" % (ct.app_label, ct.model), args=(obj.object_id,)
|
|
||||||
)
|
|
||||||
link = format_html('<a href="{1}">{0}</a>', _("Diff"), url)
|
|
||||||
except NoReverseMatch:
|
|
||||||
link = None
|
|
||||||
return link
|
|
||||||
|
|
||||||
diff_link.short_description = _("diff")
|
|
||||||
|
|
||||||
def queryset(self, request):
|
def queryset(self, request):
|
||||||
return super().queryset(request).prefetch_related("content_type")
|
return super().queryset(request).prefetch_related('content_type')
|
||||||
|
|
|
@ -6,94 +6,61 @@ from django.utils.translation import gettext, gettext_lazy as _
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
|
|
||||||
from judge.models import Organization
|
from judge.models import Organization
|
||||||
from judge.widgets import (
|
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
|
||||||
AdminHeavySelect2MultipleWidget,
|
|
||||||
AdminHeavySelect2Widget,
|
|
||||||
HeavyPreviewAdminPageDownWidget,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationForm(ModelForm):
|
class OrganizationForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
"admins": AdminHeavySelect2MultipleWidget(data_view="profile_select2"),
|
'admins': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
|
||||||
"registrant": AdminHeavySelect2Widget(data_view="profile_select2"),
|
'registrant': AdminHeavySelect2Widget(data_view='profile_select2'),
|
||||||
}
|
}
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets["about"] = HeavyPreviewAdminPageDownWidget(
|
widgets['about'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('organization_preview'))
|
||||||
preview=reverse_lazy("organization_preview")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationAdmin(VersionAdmin):
|
class OrganizationAdmin(VersionAdmin):
|
||||||
readonly_fields = ("creation_date",)
|
readonly_fields = ('creation_date',)
|
||||||
fields = (
|
fields = ('name', 'slug', 'short_name', 'is_open', 'about', 'logo_override_image', 'slots', 'registrant',
|
||||||
"name",
|
'creation_date', 'admins')
|
||||||
"slug",
|
list_display = ('name', 'short_name', 'is_open', 'slots', 'registrant', 'show_public')
|
||||||
"short_name",
|
prepopulated_fields = {'slug': ('name',)}
|
||||||
"is_open",
|
|
||||||
"about",
|
|
||||||
"slots",
|
|
||||||
"registrant",
|
|
||||||
"creation_date",
|
|
||||||
"admins",
|
|
||||||
)
|
|
||||||
list_display = (
|
|
||||||
"name",
|
|
||||||
"short_name",
|
|
||||||
"is_open",
|
|
||||||
"creation_date",
|
|
||||||
"registrant",
|
|
||||||
"show_public",
|
|
||||||
)
|
|
||||||
search_fields = ("name", "short_name", "registrant__user__username")
|
|
||||||
prepopulated_fields = {"slug": ("name",)}
|
|
||||||
actions_on_top = True
|
actions_on_top = True
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
form = OrganizationForm
|
form = OrganizationForm
|
||||||
ordering = ["-creation_date"]
|
|
||||||
|
|
||||||
def show_public(self, obj):
|
def show_public(self, obj):
|
||||||
return format_html(
|
return format_html('<a href="{0}" style="white-space:nowrap;">{1}</a>',
|
||||||
'<a href="{0}" style="white-space:nowrap;">{1}</a>',
|
obj.get_absolute_url(), gettext('View on site'))
|
||||||
obj.get_absolute_url(),
|
|
||||||
gettext("View on site"),
|
|
||||||
)
|
|
||||||
|
|
||||||
show_public.short_description = ""
|
show_public.short_description = ''
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
fields = self.readonly_fields
|
fields = self.readonly_fields
|
||||||
if not request.user.has_perm("judge.organization_admin"):
|
if not request.user.has_perm('judge.organization_admin'):
|
||||||
return fields + ("registrant", "admins", "is_open", "slots")
|
return fields + ('registrant', 'admins', 'is_open', 'slots')
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
queryset = Organization.objects.all()
|
queryset = Organization.objects.all()
|
||||||
if request.user.has_perm("judge.edit_all_organization"):
|
if request.user.has_perm('judge.edit_all_organization'):
|
||||||
return queryset
|
return queryset
|
||||||
else:
|
else:
|
||||||
return queryset.filter(admins=request.profile.id)
|
return queryset.filter(admins=request.profile.id)
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
def has_change_permission(self, request, obj=None):
|
||||||
if not request.user.has_perm("judge.change_organization"):
|
if not request.user.has_perm('judge.change_organization'):
|
||||||
return False
|
return False
|
||||||
if request.user.has_perm("judge.edit_all_organization") or obj is None:
|
if request.user.has_perm('judge.edit_all_organization') or obj is None:
|
||||||
return True
|
return True
|
||||||
return obj.admins.filter(id=request.profile.id).exists()
|
return obj.admins.filter(id=request.profile.id).exists()
|
||||||
|
|
||||||
def save_related(self, request, form, formsets, change):
|
|
||||||
super().save_related(request, form, formsets, change)
|
|
||||||
obj = form.instance
|
|
||||||
obj.members.add(*obj.admins.all())
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationRequestAdmin(admin.ModelAdmin):
|
class OrganizationRequestAdmin(admin.ModelAdmin):
|
||||||
list_display = ("username", "organization", "state", "time")
|
list_display = ('username', 'organization', 'state', 'time')
|
||||||
readonly_fields = ("user", "organization")
|
readonly_fields = ('user', 'organization')
|
||||||
|
|
||||||
def username(self, obj):
|
def username(self, obj):
|
||||||
return obj.user.user.username
|
return obj.user.user.username
|
||||||
|
username.short_description = _('username')
|
||||||
username.short_description = _("username")
|
username.admin_order_field = 'user__user__username'
|
||||||
username.admin_order_field = "user__user__username"
|
|
||||||
|
|
|
@ -1,115 +1,54 @@
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin
|
||||||
from django.db import transaction, IntegrityError
|
from django.db import transaction
|
||||||
from django.db.models import Q, Avg, Count
|
from django.db.models import Q
|
||||||
from django.db.models.aggregates import StdDev
|
from django.forms import ModelForm
|
||||||
from django.forms import ModelForm, TextInput
|
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.translation import gettext, gettext_lazy as _, ungettext
|
from django.utils.translation import gettext, gettext_lazy as _, ungettext
|
||||||
from django_ace import AceWidget
|
|
||||||
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 judge.models import LanguageLimit, Problem, ProblemClarification, ProblemTranslation, Profile, Solution
|
||||||
from judge.models import (
|
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminSelect2MultipleWidget, AdminSelect2Widget, \
|
||||||
LanguageLimit,
|
CheckboxSelectMultipleWithSelectAll, HeavyPreviewAdminPageDownWidget, HeavyPreviewPageDownWidget
|
||||||
LanguageTemplate,
|
|
||||||
Problem,
|
|
||||||
ProblemTranslation,
|
|
||||||
Profile,
|
|
||||||
Solution,
|
|
||||||
Notification,
|
|
||||||
)
|
|
||||||
from judge.models.notification import make_notification
|
|
||||||
from judge.widgets import (
|
|
||||||
AdminHeavySelect2MultipleWidget,
|
|
||||||
AdminSelect2MultipleWidget,
|
|
||||||
AdminSelect2Widget,
|
|
||||||
CheckboxSelectMultipleWithSelectAll,
|
|
||||||
HeavyPreviewAdminPageDownWidget,
|
|
||||||
)
|
|
||||||
from judge.utils.problems import user_editable_ids, user_tester_ids
|
|
||||||
|
|
||||||
MEMORY_UNITS = (("KB", "KB"), ("MB", "MB"))
|
|
||||||
|
|
||||||
|
|
||||||
class ProblemForm(ModelForm):
|
class ProblemForm(ModelForm):
|
||||||
change_message = forms.CharField(
|
change_message = forms.CharField(max_length=256, label='Edit reason', required=False)
|
||||||
max_length=256, label="Edit reason", required=False
|
|
||||||
)
|
|
||||||
memory_unit = forms.ChoiceField(choices=MEMORY_UNITS)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(ProblemForm, self).__init__(*args, **kwargs)
|
super(ProblemForm, self).__init__(*args, **kwargs)
|
||||||
self.fields["authors"].widget.can_add_related = False
|
self.fields['authors'].widget.can_add_related = False
|
||||||
self.fields["curators"].widget.can_add_related = False
|
self.fields['curators'].widget.can_add_related = False
|
||||||
self.fields["testers"].widget.can_add_related = False
|
self.fields['testers'].widget.can_add_related = False
|
||||||
self.fields["banned_users"].widget.can_add_related = False
|
self.fields['banned_users'].widget.can_add_related = False
|
||||||
self.fields["change_message"].widget.attrs.update(
|
self.fields['change_message'].widget.attrs.update({
|
||||||
{
|
'placeholder': gettext('Describe the changes you made (optional)'),
|
||||||
"placeholder": gettext("Describe the changes you made (optional)"),
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean_code(self):
|
|
||||||
code = self.cleaned_data.get("code")
|
|
||||||
if self.instance.pk:
|
|
||||||
return code
|
|
||||||
|
|
||||||
if Problem.objects.filter(code=code).exists():
|
|
||||||
raise ValidationError(_("A problem with this code already exists."))
|
|
||||||
|
|
||||||
return code
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
memory_unit = self.cleaned_data.get("memory_unit", "KB")
|
|
||||||
if memory_unit == "MB":
|
|
||||||
self.cleaned_data["memory_limit"] *= 1024
|
|
||||||
date = self.cleaned_data.get("date")
|
|
||||||
if not date or date > timezone.now():
|
|
||||||
self.cleaned_data["date"] = timezone.now()
|
|
||||||
return self.cleaned_data
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
"authors": AdminHeavySelect2MultipleWidget(
|
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||||
data_view="profile_select2", attrs={"style": "width: 100%"}
|
'curators': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||||
),
|
'testers': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||||
"curators": AdminHeavySelect2MultipleWidget(
|
'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
|
||||||
data_view="profile_select2", attrs={"style": "width: 100%"}
|
attrs={'style': 'width: 100%'}),
|
||||||
),
|
'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2',
|
||||||
"testers": AdminHeavySelect2MultipleWidget(
|
attrs={'style': 'width: 100%'}),
|
||||||
data_view="profile_select2", attrs={"style": "width: 100%"}
|
'types': AdminSelect2MultipleWidget,
|
||||||
),
|
'group': AdminSelect2Widget,
|
||||||
"banned_users": AdminHeavySelect2MultipleWidget(
|
|
||||||
data_view="profile_select2", attrs={"style": "width: 100%"}
|
|
||||||
),
|
|
||||||
"organizations": AdminHeavySelect2MultipleWidget(
|
|
||||||
data_view="organization_select2", attrs={"style": "width: 100%"}
|
|
||||||
),
|
|
||||||
"types": AdminSelect2MultipleWidget,
|
|
||||||
"group": AdminSelect2Widget,
|
|
||||||
"memory_limit": TextInput(attrs={"size": "20"}),
|
|
||||||
}
|
}
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets["description"] = HeavyPreviewAdminPageDownWidget(
|
widgets['description'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('problem_preview'))
|
||||||
preview=reverse_lazy("problem_preview")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ProblemCreatorListFilter(admin.SimpleListFilter):
|
class ProblemCreatorListFilter(admin.SimpleListFilter):
|
||||||
title = parameter_name = "creator"
|
title = parameter_name = 'creator'
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
queryset = Profile.objects.exclude(authored_problems=None).values_list(
|
queryset = Profile.objects.exclude(authored_problems=None).values_list('user__username', flat=True)
|
||||||
"user__username", flat=True
|
|
||||||
)
|
|
||||||
return [(name, name) for name in queryset]
|
return [(name, name) for name in queryset]
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
|
@ -119,68 +58,46 @@ class ProblemCreatorListFilter(admin.SimpleListFilter):
|
||||||
|
|
||||||
|
|
||||||
class LanguageLimitInlineForm(ModelForm):
|
class LanguageLimitInlineForm(ModelForm):
|
||||||
memory_unit = forms.ChoiceField(choices=MEMORY_UNITS, label=_("Memory unit"))
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {'language': AdminSelect2Widget}
|
||||||
"language": AdminSelect2Widget,
|
|
||||||
"memory_limit": TextInput(attrs={"size": "10"}),
|
|
||||||
}
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if not self.cleaned_data.get("language"):
|
|
||||||
self.cleaned_data["DELETE"] = True
|
|
||||||
if (
|
|
||||||
self.cleaned_data.get("memory_limit")
|
|
||||||
and self.cleaned_data.get("memory_unit") == "MB"
|
|
||||||
):
|
|
||||||
self.cleaned_data["memory_limit"] *= 1024
|
|
||||||
return self.cleaned_data
|
|
||||||
|
|
||||||
|
|
||||||
class LanguageLimitInline(admin.TabularInline):
|
class LanguageLimitInline(admin.TabularInline):
|
||||||
model = LanguageLimit
|
model = LanguageLimit
|
||||||
fields = ("language", "time_limit", "memory_limit", "memory_unit")
|
fields = ('language', 'time_limit', 'memory_limit')
|
||||||
form = LanguageLimitInlineForm
|
form = LanguageLimitInlineForm
|
||||||
extra = 0
|
|
||||||
|
|
||||||
|
|
||||||
class LanguageTemplateInlineForm(ModelForm):
|
class ProblemClarificationForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
if HeavyPreviewPageDownWidget is not None:
|
||||||
"language": AdminSelect2Widget,
|
widgets = {'description': HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'))}
|
||||||
"source": AceWidget(width="600px", height="200px", toolbar=False),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class LanguageTemplateInline(admin.TabularInline):
|
class ProblemClarificationInline(admin.StackedInline):
|
||||||
model = LanguageTemplate
|
model = ProblemClarification
|
||||||
fields = ("language", "source")
|
fields = ('description',)
|
||||||
form = LanguageTemplateInlineForm
|
form = ProblemClarificationForm
|
||||||
extra = 0
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
class ProblemSolutionForm(ModelForm):
|
class ProblemSolutionForm(ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(ProblemSolutionForm, self).__init__(*args, **kwargs)
|
super(ProblemSolutionForm, self).__init__(*args, **kwargs)
|
||||||
self.fields["authors"].widget.can_add_related = False
|
self.fields['authors'].widget.can_add_related = False
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
"authors": AdminHeavySelect2MultipleWidget(
|
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||||
data_view="profile_select2", attrs={"style": "width: 100%"}
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets["content"] = HeavyPreviewAdminPageDownWidget(
|
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('solution_preview'))
|
||||||
preview=reverse_lazy("solution_preview")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ProblemSolutionInline(admin.StackedInline):
|
class ProblemSolutionInline(admin.StackedInline):
|
||||||
model = Solution
|
model = Solution
|
||||||
fields = ("is_public", "publish_on", "authors", "content")
|
fields = ('is_public', 'publish_on', 'authors', 'content')
|
||||||
form = ProblemSolutionForm
|
form = ProblemSolutionForm
|
||||||
extra = 0
|
extra = 0
|
||||||
|
|
||||||
|
@ -188,300 +105,134 @@ class ProblemSolutionInline(admin.StackedInline):
|
||||||
class ProblemTranslationForm(ModelForm):
|
class ProblemTranslationForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets = {
|
widgets = {'description': HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('problem_preview'))}
|
||||||
"description": HeavyPreviewAdminPageDownWidget(
|
|
||||||
preview=reverse_lazy("problem_preview")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ProblemTranslationInline(admin.StackedInline):
|
class ProblemTranslationInline(admin.StackedInline):
|
||||||
model = ProblemTranslation
|
model = ProblemTranslation
|
||||||
fields = ("language", "name", "description")
|
fields = ('language', 'name', 'description')
|
||||||
form = ProblemTranslationForm
|
form = ProblemTranslationForm
|
||||||
extra = 0
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
class ProblemAdmin(CompareVersionAdmin):
|
class ProblemAdmin(VersionAdmin):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(
|
(None, {
|
||||||
None,
|
'fields': (
|
||||||
{
|
'code', 'name', 'is_public', 'is_manually_managed', 'date', 'authors', 'curators', 'testers',
|
||||||
"fields": (
|
'is_organization_private', 'organizations', 'description', 'license',
|
||||||
"code",
|
|
||||||
"name",
|
|
||||||
"is_public",
|
|
||||||
"organizations",
|
|
||||||
"date",
|
|
||||||
"authors",
|
|
||||||
"curators",
|
|
||||||
"testers",
|
|
||||||
"description",
|
|
||||||
"pdf_description",
|
|
||||||
"license",
|
|
||||||
),
|
),
|
||||||
},
|
}),
|
||||||
),
|
(_('Social Media'), {'classes': ('collapse',), 'fields': ('og_image', 'summary')}),
|
||||||
(
|
(_('Taxonomy'), {'fields': ('types', 'group')}),
|
||||||
_("Social Media"),
|
(_('Points'), {'fields': (('points', 'partial'), 'short_circuit')}),
|
||||||
{"classes": ("collapse",), "fields": ("og_image", "summary")},
|
(_('Limits'), {'fields': ('time_limit', 'memory_limit')}),
|
||||||
),
|
(_('Language'), {'fields': ('allowed_languages',)}),
|
||||||
(_("Taxonomy"), {"fields": ("types", "group")}),
|
(_('Justice'), {'fields': ('banned_users',)}),
|
||||||
(_("Points"), {"fields": (("points", "partial"), "short_circuit")}),
|
(_('History'), {'fields': ('change_message',)}),
|
||||||
(_("Limits"), {"fields": ("time_limit", ("memory_limit", "memory_unit"))}),
|
|
||||||
(_("Language"), {"fields": ("allowed_languages",)}),
|
|
||||||
(_("Justice"), {"fields": ("banned_users",)}),
|
|
||||||
(_("History"), {"fields": ("change_message",)}),
|
|
||||||
)
|
)
|
||||||
list_display = [
|
list_display = ['code', 'name', 'show_authors', 'points', 'is_public', 'show_public']
|
||||||
"code",
|
ordering = ['code']
|
||||||
"name",
|
search_fields = ('code', 'name', 'authors__user__username', 'curators__user__username')
|
||||||
"show_authors",
|
inlines = [LanguageLimitInline, ProblemClarificationInline, ProblemSolutionInline, ProblemTranslationInline]
|
||||||
"date",
|
|
||||||
"points",
|
|
||||||
"is_public",
|
|
||||||
"show_public",
|
|
||||||
]
|
|
||||||
ordering = ["-date"]
|
|
||||||
search_fields = (
|
|
||||||
"code",
|
|
||||||
"name",
|
|
||||||
"authors__user__username",
|
|
||||||
"curators__user__username",
|
|
||||||
)
|
|
||||||
inlines = [
|
|
||||||
LanguageLimitInline,
|
|
||||||
LanguageTemplateInline,
|
|
||||||
ProblemSolutionInline,
|
|
||||||
ProblemTranslationInline,
|
|
||||||
]
|
|
||||||
list_max_show_all = 1000
|
list_max_show_all = 1000
|
||||||
actions_on_top = True
|
actions_on_top = True
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
list_filter = ("is_public", ProblemCreatorListFilter)
|
list_filter = ('is_public', ProblemCreatorListFilter)
|
||||||
form = ProblemForm
|
form = ProblemForm
|
||||||
date_hierarchy = "date"
|
date_hierarchy = 'date'
|
||||||
|
|
||||||
def get_actions(self, request):
|
def get_actions(self, request):
|
||||||
actions = super(ProblemAdmin, self).get_actions(request)
|
actions = super(ProblemAdmin, self).get_actions(request)
|
||||||
|
|
||||||
if request.user.has_perm("judge.change_public_visibility"):
|
if request.user.has_perm('judge.change_public_visibility'):
|
||||||
func, name, desc = self.get_action("make_public")
|
func, name, desc = self.get_action('make_public')
|
||||||
actions[name] = (func, name, desc)
|
actions[name] = (func, name, desc)
|
||||||
|
|
||||||
func, name, desc = self.get_action("make_private")
|
func, name, desc = self.get_action('make_private')
|
||||||
actions[name] = (func, name, desc)
|
actions[name] = (func, name, desc)
|
||||||
|
|
||||||
return actions
|
return actions
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
fields = self.readonly_fields
|
fields = self.readonly_fields
|
||||||
if not request.user.has_perm("judge.change_public_visibility"):
|
if not request.user.has_perm('judge.change_public_visibility'):
|
||||||
fields += ("is_public",)
|
fields += ('is_public',)
|
||||||
|
if not request.user.has_perm('judge.change_manually_managed'):
|
||||||
|
fields += ('is_manually_managed',)
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
def show_authors(self, obj):
|
def show_authors(self, obj):
|
||||||
return ", ".join(map(attrgetter("user.username"), obj.authors.all()))
|
return ', '.join(map(attrgetter('user.username'), obj.authors.all()))
|
||||||
|
|
||||||
show_authors.short_description = _("Authors")
|
show_authors.short_description = _('Authors')
|
||||||
|
|
||||||
def show_public(self, obj):
|
def show_public(self, obj):
|
||||||
return format_html(
|
return format_html('<a href="{1}">{0}</a>', gettext('View on site'), obj.get_absolute_url())
|
||||||
'<a href="{1}">{0}</a>', gettext("View on site"), obj.get_absolute_url()
|
|
||||||
)
|
|
||||||
|
|
||||||
show_public.short_description = ""
|
show_public.short_description = ''
|
||||||
|
|
||||||
def _rescore(self, request, problem_id):
|
def _rescore(self, request, problem_id):
|
||||||
from judge.tasks import rescore_problem
|
from judge.tasks import rescore_problem
|
||||||
|
|
||||||
transaction.on_commit(rescore_problem.s(problem_id).delay)
|
transaction.on_commit(rescore_problem.s(problem_id).delay)
|
||||||
|
|
||||||
def make_public(self, request, queryset):
|
def make_public(self, request, queryset):
|
||||||
count = queryset.update(is_public=True)
|
count = queryset.update(is_public=True)
|
||||||
for problem_id in queryset.values_list("id", flat=True):
|
for problem_id in queryset.values_list('id', flat=True):
|
||||||
self._rescore(request, problem_id)
|
self._rescore(request, problem_id)
|
||||||
self.message_user(
|
self.message_user(request, ungettext('%d problem successfully marked as public.',
|
||||||
request,
|
'%d problems successfully marked as public.',
|
||||||
ungettext(
|
count) % count)
|
||||||
"%d problem successfully marked as public.",
|
|
||||||
"%d problems successfully marked as public.",
|
|
||||||
count,
|
|
||||||
)
|
|
||||||
% count,
|
|
||||||
)
|
|
||||||
|
|
||||||
make_public.short_description = _("Mark problems as public")
|
make_public.short_description = _('Mark problems as public')
|
||||||
|
|
||||||
def make_private(self, request, queryset):
|
def make_private(self, request, queryset):
|
||||||
count = queryset.update(is_public=False)
|
count = queryset.update(is_public=False)
|
||||||
for problem_id in queryset.values_list("id", flat=True):
|
for problem_id in queryset.values_list('id', flat=True):
|
||||||
self._rescore(request, problem_id)
|
self._rescore(request, problem_id)
|
||||||
self.message_user(
|
self.message_user(request, ungettext('%d problem successfully marked as private.',
|
||||||
request,
|
'%d problems successfully marked as private.',
|
||||||
ungettext(
|
count) % count)
|
||||||
"%d problem successfully marked as private.",
|
|
||||||
"%d problems successfully marked as private.",
|
|
||||||
count,
|
|
||||||
)
|
|
||||||
% count,
|
|
||||||
)
|
|
||||||
|
|
||||||
make_private.short_description = _("Mark problems as private")
|
make_private.short_description = _('Mark problems as private')
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
queryset = Problem.objects.prefetch_related("authors__user")
|
queryset = Problem.objects.prefetch_related('authors__user')
|
||||||
if request.user.has_perm("judge.edit_all_problem"):
|
if request.user.has_perm('judge.edit_all_problem'):
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
access = Q()
|
access = Q()
|
||||||
if request.user.has_perm("judge.edit_public_problem"):
|
if request.user.has_perm('judge.edit_public_problem'):
|
||||||
access |= Q(is_public=True)
|
access |= Q(is_public=True)
|
||||||
if request.user.has_perm("judge.edit_own_problem"):
|
if request.user.has_perm('judge.edit_own_problem'):
|
||||||
access |= Q(authors__id=request.profile.id) | Q(
|
access |= Q(authors__id=request.profile.id) | Q(curators__id=request.profile.id)
|
||||||
curators__id=request.profile.id
|
|
||||||
)
|
|
||||||
return queryset.filter(access).distinct() if access else queryset.none()
|
return queryset.filter(access).distinct() if access else queryset.none()
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
def has_change_permission(self, request, obj=None):
|
||||||
if request.user.has_perm("judge.edit_all_problem") or obj is None:
|
if request.user.has_perm('judge.edit_all_problem') or obj is None:
|
||||||
return True
|
return True
|
||||||
if request.user.has_perm("judge.edit_public_problem") and obj.is_public:
|
if request.user.has_perm('judge.edit_public_problem') and obj.is_public:
|
||||||
return True
|
return True
|
||||||
if not request.user.has_perm("judge.edit_own_problem"):
|
if not request.user.has_perm('judge.edit_own_problem'):
|
||||||
return False
|
return False
|
||||||
return obj.is_editor(request.profile)
|
return obj.is_editor(request.profile)
|
||||||
|
|
||||||
def formfield_for_manytomany(self, db_field, request=None, **kwargs):
|
def formfield_for_manytomany(self, db_field, request=None, **kwargs):
|
||||||
if db_field.name == "allowed_languages":
|
if db_field.name == 'allowed_languages':
|
||||||
kwargs["widget"] = CheckboxSelectMultipleWithSelectAll()
|
kwargs['widget'] = CheckboxSelectMultipleWithSelectAll()
|
||||||
return super(ProblemAdmin, self).formfield_for_manytomany(
|
return super(ProblemAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)
|
||||||
db_field, request, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_form(self, *args, **kwargs):
|
def get_form(self, *args, **kwargs):
|
||||||
form = super(ProblemAdmin, self).get_form(*args, **kwargs)
|
form = super(ProblemAdmin, self).get_form(*args, **kwargs)
|
||||||
form.base_fields["authors"].queryset = Profile.objects.all()
|
form.base_fields['authors'].queryset = Profile.objects.all()
|
||||||
return form
|
return form
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
form.changed_data.remove("memory_unit")
|
super(ProblemAdmin, self).save_model(request, obj, form, change)
|
||||||
super().save_model(request, obj, form, change)
|
if form.changed_data and any(f in form.changed_data for f in ('is_public', 'points', 'partial')):
|
||||||
if form.changed_data and any(
|
|
||||||
f in form.changed_data for f in ("is_public", "points", "partial")
|
|
||||||
):
|
|
||||||
self._rescore(request, obj.id)
|
self._rescore(request, obj.id)
|
||||||
|
|
||||||
def save_related(self, request, form, formsets, change):
|
|
||||||
editors = set()
|
|
||||||
testers = set()
|
|
||||||
if "curators" in form.changed_data or "authors" in form.changed_data:
|
|
||||||
editors = set(form.instance.editor_ids)
|
|
||||||
if "testers" in form.changed_data:
|
|
||||||
testers = set(form.instance.tester_ids)
|
|
||||||
|
|
||||||
super().save_related(request, form, formsets, change)
|
|
||||||
obj = form.instance
|
|
||||||
obj.curators.add(request.profile)
|
|
||||||
|
|
||||||
if "curators" in form.changed_data or "authors" in form.changed_data:
|
|
||||||
del obj.editor_ids
|
|
||||||
editors = editors.union(set(obj.editor_ids))
|
|
||||||
if "testers" in form.changed_data:
|
|
||||||
del obj.tester_ids
|
|
||||||
testers = testers.union(set(obj.tester_ids))
|
|
||||||
|
|
||||||
for editor in editors:
|
|
||||||
user_editable_ids.dirty(editor)
|
|
||||||
for tester in testers:
|
|
||||||
user_tester_ids.dirty(tester)
|
|
||||||
|
|
||||||
# Create notification
|
|
||||||
if "is_public" in form.changed_data or "organizations" in form.changed_data:
|
|
||||||
users = set(obj.authors.all())
|
|
||||||
users = users.union(users, set(obj.curators.all()))
|
|
||||||
orgs = []
|
|
||||||
if obj.organizations.count() > 0:
|
|
||||||
for org in obj.organizations.all():
|
|
||||||
users = users.union(users, set(org.admins.all()))
|
|
||||||
orgs.append(org.name)
|
|
||||||
else:
|
|
||||||
admins = Profile.objects.filter(user__is_superuser=True).all()
|
|
||||||
users = users.union(users, admins)
|
|
||||||
link = reverse_lazy("admin:judge_problem_change", args=(obj.id,))
|
|
||||||
html = f'<a href="{link}">{obj.name}</a>'
|
|
||||||
category = "Problem public: " + str(obj.is_public)
|
|
||||||
if orgs:
|
|
||||||
category += " (" + ", ".join(orgs) + ")"
|
|
||||||
make_notification(users, category, html, request.profile)
|
|
||||||
|
|
||||||
def construct_change_message(self, request, form, *args, **kwargs):
|
def construct_change_message(self, request, form, *args, **kwargs):
|
||||||
if form.cleaned_data.get("change_message"):
|
if form.cleaned_data.get('change_message'):
|
||||||
return form.cleaned_data["change_message"]
|
return form.cleaned_data['change_message']
|
||||||
return super(ProblemAdmin, self).construct_change_message(
|
return super(ProblemAdmin, self).construct_change_message(request, form, *args, **kwargs)
|
||||||
request, form, *args, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ProblemPointsVoteAdmin(admin.ModelAdmin):
|
|
||||||
list_display = (
|
|
||||||
"vote_points",
|
|
||||||
"voter",
|
|
||||||
"voter_rating",
|
|
||||||
"voter_point",
|
|
||||||
"problem_name",
|
|
||||||
"problem_code",
|
|
||||||
"problem_points",
|
|
||||||
)
|
|
||||||
search_fields = ("voter__user__username", "problem__code", "problem__name")
|
|
||||||
readonly_fields = (
|
|
||||||
"voter",
|
|
||||||
"problem",
|
|
||||||
"problem_code",
|
|
||||||
"problem_points",
|
|
||||||
"voter_rating",
|
|
||||||
"voter_point",
|
|
||||||
)
|
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
|
||||||
if obj is None:
|
|
||||||
return request.user.has_perm("judge.edit_own_problem")
|
|
||||||
return obj.problem.is_editable_by(request.user)
|
|
||||||
|
|
||||||
def lookup_allowed(self, key, value):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def problem_code(self, obj):
|
|
||||||
return obj.problem.code
|
|
||||||
|
|
||||||
problem_code.short_description = _("Problem code")
|
|
||||||
problem_code.admin_order_field = "problem__code"
|
|
||||||
|
|
||||||
def problem_points(self, obj):
|
|
||||||
return obj.problem.points
|
|
||||||
|
|
||||||
problem_points.short_description = _("Points")
|
|
||||||
problem_points.admin_order_field = "problem__points"
|
|
||||||
|
|
||||||
def problem_name(self, obj):
|
|
||||||
return obj.problem.name
|
|
||||||
|
|
||||||
problem_name.short_description = _("Problem name")
|
|
||||||
problem_name.admin_order_field = "problem__name"
|
|
||||||
|
|
||||||
def voter_rating(self, obj):
|
|
||||||
return obj.voter.rating
|
|
||||||
|
|
||||||
voter_rating.short_description = _("Voter rating")
|
|
||||||
voter_rating.admin_order_field = "voter__rating"
|
|
||||||
|
|
||||||
def voter_point(self, obj):
|
|
||||||
return round(obj.voter.performance_points)
|
|
||||||
|
|
||||||
voter_point.short_description = _("Voter point")
|
|
||||||
voter_point.admin_order_field = "voter__performance_points"
|
|
||||||
|
|
||||||
def vote_points(self, obj):
|
|
||||||
return obj.points
|
|
||||||
|
|
||||||
vote_points.short_description = _("Vote")
|
|
||||||
|
|
|
@ -1,58 +1,41 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.forms import ModelForm, CharField, TextInput
|
from django.forms import ModelForm
|
||||||
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 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 judge.models import Profile, ProfileInfo
|
|
||||||
from judge.widgets import AdminPagedownWidget, AdminSelect2Widget
|
|
||||||
|
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
|
|
||||||
import re
|
from django_ace import AceWidget
|
||||||
|
from judge.models import Profile
|
||||||
|
from judge.widgets import AdminPagedownWidget, AdminSelect2Widget
|
||||||
|
|
||||||
|
|
||||||
class ProfileForm(ModelForm):
|
class ProfileForm(ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(ProfileForm, self).__init__(*args, **kwargs)
|
super(ProfileForm, self).__init__(*args, **kwargs)
|
||||||
if "current_contest" in self.base_fields:
|
if 'current_contest' in self.base_fields:
|
||||||
# form.fields['current_contest'] does not exist when the user has only view permission on the model.
|
# form.fields['current_contest'] does not exist when the user has only view permission on the model.
|
||||||
self.fields[
|
self.fields['current_contest'].queryset = self.instance.contest_history.select_related('contest') \
|
||||||
"current_contest"
|
.only('contest__name', 'user_id', 'virtual')
|
||||||
].queryset = self.instance.contest_history.select_related("contest").only(
|
self.fields['current_contest'].label_from_instance = \
|
||||||
"contest__name", "user_id", "virtual"
|
lambda obj: '%s v%d' % (obj.contest.name, obj.virtual) if obj.virtual else obj.contest.name
|
||||||
)
|
|
||||||
self.fields["current_contest"].label_from_instance = (
|
|
||||||
lambda obj: "%s v%d" % (obj.contest.name, obj.virtual)
|
|
||||||
if obj.virtual
|
|
||||||
else obj.contest.name
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
"timezone": AdminSelect2Widget,
|
'timezone': AdminSelect2Widget,
|
||||||
"language": AdminSelect2Widget,
|
'language': AdminSelect2Widget,
|
||||||
"ace_theme": AdminSelect2Widget,
|
'ace_theme': AdminSelect2Widget,
|
||||||
"current_contest": AdminSelect2Widget,
|
'current_contest': AdminSelect2Widget,
|
||||||
}
|
}
|
||||||
if AdminPagedownWidget is not None:
|
if AdminPagedownWidget is not None:
|
||||||
widgets["about"] = AdminPagedownWidget
|
widgets['about'] = AdminPagedownWidget
|
||||||
|
|
||||||
|
|
||||||
class TimezoneFilter(admin.SimpleListFilter):
|
class TimezoneFilter(admin.SimpleListFilter):
|
||||||
title = _("timezone")
|
title = _('timezone')
|
||||||
parameter_name = "timezone"
|
parameter_name = 'timezone'
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
return (
|
return Profile.objects.values_list('timezone', 'timezone').distinct().order_by('timezone')
|
||||||
Profile.objects.values_list("timezone", "timezone")
|
|
||||||
.distinct()
|
|
||||||
.order_by("timezone")
|
|
||||||
)
|
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value() is None:
|
if self.value() is None:
|
||||||
|
@ -60,168 +43,76 @@ 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', 'display_rank', 'about', 'organizations', 'timezone', 'language', 'ace_theme',
|
||||||
"user",
|
'math_engine', 'last_access', 'ip', 'mute', 'is_unlisted', 'notes', 'is_totp_enabled', 'user_script',
|
||||||
"display_rank",
|
'current_contest')
|
||||||
"about",
|
readonly_fields = ('user',)
|
||||||
"organizations",
|
list_display = ('admin_user_admin', 'email', 'is_totp_enabled', 'timezone_full',
|
||||||
"timezone",
|
'date_joined', 'last_access', 'ip', 'show_public')
|
||||||
"language",
|
ordering = ('user__username',)
|
||||||
"ace_theme",
|
search_fields = ('user__username', 'ip', 'user__email')
|
||||||
"last_access",
|
list_filter = ('language', TimezoneFilter)
|
||||||
"ip",
|
actions = ('recalculate_points',)
|
||||||
"mute",
|
|
||||||
"is_unlisted",
|
|
||||||
"notes",
|
|
||||||
"is_totp_enabled",
|
|
||||||
"current_contest",
|
|
||||||
)
|
|
||||||
readonly_fields = ("user",)
|
|
||||||
list_display = (
|
|
||||||
"admin_user_admin",
|
|
||||||
"email",
|
|
||||||
"is_totp_enabled",
|
|
||||||
"timezone_full",
|
|
||||||
"date_joined",
|
|
||||||
"last_access",
|
|
||||||
"ip",
|
|
||||||
"show_public",
|
|
||||||
)
|
|
||||||
ordering = ("user__username",)
|
|
||||||
search_fields = ("user__username", "ip", "user__email")
|
|
||||||
list_filter = ("language", TimezoneFilter)
|
|
||||||
actions = ("recalculate_points",)
|
|
||||||
actions_on_top = True
|
actions_on_top = True
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
form = ProfileForm
|
form = ProfileForm
|
||||||
inlines = (ProfileInfoInline,)
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
return super(ProfileAdmin, self).get_queryset(request).select_related("user")
|
return super(ProfileAdmin, self).get_queryset(request).select_related('user')
|
||||||
|
|
||||||
def get_fields(self, request, obj=None):
|
def get_fields(self, request, obj=None):
|
||||||
if request.user.has_perm("judge.totp"):
|
if request.user.has_perm('judge.totp'):
|
||||||
fields = list(self.fields)
|
fields = list(self.fields)
|
||||||
fields.insert(fields.index("is_totp_enabled") + 1, "totp_key")
|
fields.insert(fields.index('is_totp_enabled') + 1, 'totp_key')
|
||||||
return tuple(fields)
|
return tuple(fields)
|
||||||
else:
|
else:
|
||||||
return self.fields
|
return self.fields
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
fields = self.readonly_fields
|
fields = self.readonly_fields
|
||||||
if not request.user.has_perm("judge.totp"):
|
if not request.user.has_perm('judge.totp'):
|
||||||
fields += ("is_totp_enabled",)
|
fields += ('is_totp_enabled',)
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
def show_public(self, obj):
|
def show_public(self, obj):
|
||||||
return format_html(
|
return format_html('<a href="{0}" style="white-space:nowrap;">{1}</a>',
|
||||||
'<a href="{0}" style="white-space:nowrap;">{1}</a>',
|
obj.get_absolute_url(), gettext('View on site'))
|
||||||
obj.get_absolute_url(),
|
show_public.short_description = ''
|
||||||
gettext("View on site"),
|
|
||||||
)
|
|
||||||
|
|
||||||
show_public.short_description = ""
|
|
||||||
|
|
||||||
def admin_user_admin(self, obj):
|
def admin_user_admin(self, obj):
|
||||||
return obj.username
|
return obj.username
|
||||||
|
admin_user_admin.admin_order_field = 'user__username'
|
||||||
admin_user_admin.admin_order_field = "user__username"
|
admin_user_admin.short_description = _('User')
|
||||||
admin_user_admin.short_description = _("User")
|
|
||||||
|
|
||||||
def email(self, obj):
|
def email(self, obj):
|
||||||
return obj.email
|
return obj.user.email
|
||||||
|
email.admin_order_field = 'user__email'
|
||||||
email.admin_order_field = "user__email"
|
email.short_description = _('Email')
|
||||||
email.short_description = _("Email")
|
|
||||||
|
|
||||||
def timezone_full(self, obj):
|
def timezone_full(self, obj):
|
||||||
return obj.timezone
|
return obj.timezone
|
||||||
|
timezone_full.admin_order_field = 'timezone'
|
||||||
timezone_full.admin_order_field = "timezone"
|
timezone_full.short_description = _('Timezone')
|
||||||
timezone_full.short_description = _("Timezone")
|
|
||||||
|
|
||||||
def date_joined(self, obj):
|
def date_joined(self, obj):
|
||||||
return obj.user.date_joined
|
return obj.user.date_joined
|
||||||
|
date_joined.admin_order_field = 'user__date_joined'
|
||||||
date_joined.admin_order_field = "user__date_joined"
|
date_joined.short_description = _('date joined')
|
||||||
date_joined.short_description = _("date joined")
|
|
||||||
|
|
||||||
def recalculate_points(self, request, queryset):
|
def recalculate_points(self, request, queryset):
|
||||||
count = 0
|
count = 0
|
||||||
for profile in queryset:
|
for profile in queryset:
|
||||||
profile.calculate_points()
|
profile.calculate_points()
|
||||||
count += 1
|
count += 1
|
||||||
self.message_user(
|
self.message_user(request, ungettext('%d user have scores recalculated.',
|
||||||
request,
|
'%d users have scores recalculated.',
|
||||||
ungettext(
|
count) % count)
|
||||||
"%d user have scores recalculated.",
|
recalculate_points.short_description = _('Recalculate scores')
|
||||||
"%d users have scores recalculated.",
|
|
||||||
count,
|
|
||||||
)
|
|
||||||
% count,
|
|
||||||
)
|
|
||||||
|
|
||||||
recalculate_points.short_description = _("Recalculate scores")
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
|
form = super(ProfileAdmin, self).get_form(request, obj, **kwargs)
|
||||||
|
if 'user_script' in form.base_fields:
|
||||||
class UserForm(UserChangeForm):
|
# form.base_fields['user_script'] does not exist when the user has only view permission on the model.
|
||||||
def __init__(self, *args, **kwargs):
|
form.base_fields['user_script'].widget = AceWidget('javascript', request.profile.ace_theme)
|
||||||
super().__init__(*args, **kwargs)
|
return form
|
||||||
self.fields["username"].help_text = _(
|
|
||||||
"Username can only contain letters, digits, and underscores."
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean_username(self):
|
|
||||||
username = self.cleaned_data.get("username")
|
|
||||||
if not re.match(r"^\w+$", username):
|
|
||||||
raise ValidationError(
|
|
||||||
_("Username can only contain letters, digits, and underscores.")
|
|
||||||
)
|
|
||||||
return username
|
|
||||||
|
|
||||||
|
|
||||||
class UserAdmin(OldUserAdmin):
|
|
||||||
# Customize the fieldsets for adding and editing users
|
|
||||||
form = UserForm
|
|
||||||
fieldsets = (
|
|
||||||
(None, {"fields": ("username", "password")}),
|
|
||||||
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
|
|
||||||
(
|
|
||||||
"Permissions",
|
|
||||||
{
|
|
||||||
"fields": (
|
|
||||||
"is_active",
|
|
||||||
"is_staff",
|
|
||||||
"is_superuser",
|
|
||||||
"groups",
|
|
||||||
"user_permissions",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
("Important dates", {"fields": ("last_login", "date_joined")}),
|
|
||||||
)
|
|
||||||
|
|
||||||
readonly_fields = ("last_login", "date_joined")
|
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
|
||||||
fields = self.readonly_fields
|
|
||||||
if not request.user.is_superuser:
|
|
||||||
fields += (
|
|
||||||
"is_staff",
|
|
||||||
"is_active",
|
|
||||||
"is_superuser",
|
|
||||||
"groups",
|
|
||||||
"user_permissions",
|
|
||||||
)
|
|
||||||
return fields
|
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
|
||||||
return False
|
|
||||||
|
|
|
@ -16,63 +16,41 @@ from judge.widgets import AdminHeavySelect2MultipleWidget, AdminPagedownWidget
|
||||||
|
|
||||||
class LanguageForm(ModelForm):
|
class LanguageForm(ModelForm):
|
||||||
problems = ModelMultipleChoiceField(
|
problems = ModelMultipleChoiceField(
|
||||||
label=_("Disallowed problems"),
|
label=_('Disallowed problems'),
|
||||||
queryset=Problem.objects.all(),
|
queryset=Problem.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_("These problems are NOT allowed to be submitted in this language"),
|
help_text=_('These problems are NOT allowed to be submitted in this language'),
|
||||||
widget=AdminHeavySelect2MultipleWidget(data_view="problem_select2"),
|
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'))
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
if AdminPagedownWidget is not None:
|
if AdminPagedownWidget is not None:
|
||||||
widgets = {"description": AdminPagedownWidget}
|
widgets = {'description': AdminPagedownWidget}
|
||||||
|
|
||||||
|
|
||||||
class LanguageAdmin(VersionAdmin):
|
class LanguageAdmin(VersionAdmin):
|
||||||
fields = (
|
fields = ('key', 'name', 'short_name', 'common_name', 'ace', 'pygments', 'info', 'description',
|
||||||
"key",
|
'template', 'problems')
|
||||||
"name",
|
list_display = ('key', 'name', 'common_name', 'info')
|
||||||
"short_name",
|
|
||||||
"common_name",
|
|
||||||
"ace",
|
|
||||||
"pygments",
|
|
||||||
"info",
|
|
||||||
"description",
|
|
||||||
"template",
|
|
||||||
"problems",
|
|
||||||
)
|
|
||||||
list_display = ("key", "name", "common_name", "info")
|
|
||||||
form = LanguageForm
|
form = LanguageForm
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
super(LanguageAdmin, self).save_model(request, obj, form, change)
|
super(LanguageAdmin, self).save_model(request, obj, form, change)
|
||||||
obj.problem_set.set(
|
obj.problem_set.set(Problem.objects.exclude(id__in=form.cleaned_data['problems'].values('id')))
|
||||||
Problem.objects.exclude(id__in=form.cleaned_data["problems"].values("id"))
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
self.form.base_fields["problems"].initial = (
|
self.form.base_fields['problems'].initial = \
|
||||||
Problem.objects.exclude(id__in=obj.problem_set.values("id")).values_list(
|
Problem.objects.exclude(id__in=obj.problem_set.values('id')).values_list('pk', flat=True) if obj else []
|
||||||
"pk", flat=True
|
|
||||||
)
|
|
||||||
if obj
|
|
||||||
else []
|
|
||||||
)
|
|
||||||
form = super(LanguageAdmin, self).get_form(request, obj, **kwargs)
|
form = super(LanguageAdmin, self).get_form(request, obj, **kwargs)
|
||||||
if obj is not None:
|
if obj is not None:
|
||||||
form.base_fields["template"].widget = AceWidget(
|
form.base_fields['template'].widget = AceWidget(obj.ace, request.profile.ace_theme)
|
||||||
obj.ace, request.profile.ace_theme
|
|
||||||
)
|
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
|
||||||
class GenerateKeyTextInput(TextInput):
|
class GenerateKeyTextInput(TextInput):
|
||||||
def render(self, name, value, attrs=None, renderer=None):
|
def render(self, name, value, attrs=None, renderer=None):
|
||||||
text = super(TextInput, self).render(name, value, attrs)
|
text = super(TextInput, self).render(name, value, attrs)
|
||||||
return mark_safe(
|
return mark_safe(text + format_html(
|
||||||
text
|
'''\
|
||||||
+ format_html(
|
|
||||||
"""\
|
|
||||||
<a href="#" onclick="return false;" class="button" id="id_{0}_regen">Regenerate</a>
|
<a href="#" onclick="return false;" class="button" id="id_{0}_regen">Regenerate</a>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
django.jQuery(document).ready(function ($) {{
|
django.jQuery(document).ready(function ($) {{
|
||||||
|
@ -87,59 +65,37 @@ django.jQuery(document).ready(function ($) {{
|
||||||
}});
|
}});
|
||||||
}});
|
}});
|
||||||
</script>
|
</script>
|
||||||
""",
|
''', name))
|
||||||
name,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class JudgeAdminForm(ModelForm):
|
class JudgeAdminForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {"auth_key": GenerateKeyTextInput}
|
widgets = {'auth_key': GenerateKeyTextInput}
|
||||||
if AdminPagedownWidget is not None:
|
if AdminPagedownWidget is not None:
|
||||||
widgets["description"] = AdminPagedownWidget
|
widgets['description'] = AdminPagedownWidget
|
||||||
|
|
||||||
|
|
||||||
class JudgeAdmin(VersionAdmin):
|
class JudgeAdmin(VersionAdmin):
|
||||||
form = JudgeAdminForm
|
form = JudgeAdminForm
|
||||||
readonly_fields = (
|
readonly_fields = ('created', 'online', 'start_time', 'ping', 'load', 'last_ip', 'runtimes', 'problems')
|
||||||
"created",
|
|
||||||
"online",
|
|
||||||
"start_time",
|
|
||||||
"ping",
|
|
||||||
"load",
|
|
||||||
"last_ip",
|
|
||||||
"runtimes",
|
|
||||||
"problems",
|
|
||||||
)
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {"fields": ("name", "auth_key", "is_blocked")}),
|
(None, {'fields': ('name', 'auth_key', 'is_blocked')}),
|
||||||
(_("Description"), {"fields": ("description",)}),
|
(_('Description'), {'fields': ('description',)}),
|
||||||
(
|
(_('Information'), {'fields': ('created', 'online', 'last_ip', 'start_time', 'ping', 'load')}),
|
||||||
_("Information"),
|
(_('Capabilities'), {'fields': ('runtimes', 'problems')}),
|
||||||
{"fields": ("created", "online", "last_ip", "start_time", "ping", "load")},
|
|
||||||
),
|
|
||||||
(_("Capabilities"), {"fields": ("runtimes", "problems")}),
|
|
||||||
)
|
)
|
||||||
list_display = ("name", "online", "start_time", "ping", "load", "last_ip")
|
list_display = ('name', 'online', 'start_time', 'ping', 'load', 'last_ip')
|
||||||
ordering = ["-online", "name"]
|
ordering = ['-online', 'name']
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
return [
|
return ([url(r'^(\d+)/disconnect/$', self.disconnect_view, name='judge_judge_disconnect'),
|
||||||
url(
|
url(r'^(\d+)/terminate/$', self.terminate_view, name='judge_judge_terminate')] +
|
||||||
r"^(\d+)/disconnect/$",
|
super(JudgeAdmin, self).get_urls())
|
||||||
self.disconnect_view,
|
|
||||||
name="judge_judge_disconnect",
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r"^(\d+)/terminate/$", self.terminate_view, name="judge_judge_terminate"
|
|
||||||
),
|
|
||||||
] + super(JudgeAdmin, self).get_urls()
|
|
||||||
|
|
||||||
def disconnect_judge(self, id, force=False):
|
def disconnect_judge(self, id, force=False):
|
||||||
judge = get_object_or_404(Judge, id=id)
|
judge = get_object_or_404(Judge, id=id)
|
||||||
judge.disconnect(force=force)
|
judge.disconnect(force=force)
|
||||||
return HttpResponseRedirect(reverse("admin:judge_judge_changelist"))
|
return HttpResponseRedirect(reverse('admin:judge_judge_changelist'))
|
||||||
|
|
||||||
def disconnect_view(self, request, id):
|
def disconnect_view(self, request, id):
|
||||||
return self.disconnect_judge(id)
|
return self.disconnect_judge(id)
|
||||||
|
@ -149,7 +105,7 @@ class JudgeAdmin(VersionAdmin):
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
if obj is not None and obj.online:
|
if obj is not None and obj.online:
|
||||||
return self.readonly_fields + ("name",)
|
return self.readonly_fields + ('name',)
|
||||||
return self.readonly_fields
|
return self.readonly_fields
|
||||||
|
|
||||||
def has_delete_permission(self, request, obj=None):
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
@ -160,5 +116,5 @@ class JudgeAdmin(VersionAdmin):
|
||||||
|
|
||||||
if AdminPagedownWidget is not None:
|
if AdminPagedownWidget is not None:
|
||||||
formfield_overrides = {
|
formfield_overrides = {
|
||||||
TextField: {"widget": AdminPagedownWidget},
|
TextField: {'widget': AdminPagedownWidget},
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,365 +13,239 @@ from django.utils.html import format_html
|
||||||
from django.utils.translation import gettext, gettext_lazy as _, pgettext, ungettext
|
from django.utils.translation import gettext, gettext_lazy as _, pgettext, ungettext
|
||||||
|
|
||||||
from django_ace import AceWidget
|
from django_ace import AceWidget
|
||||||
from judge.models import (
|
from judge.models import ContestParticipation, ContestProblem, ContestSubmission, Profile, Submission, \
|
||||||
ContestParticipation,
|
SubmissionSource, SubmissionTestCase
|
||||||
ContestProblem,
|
|
||||||
ContestSubmission,
|
|
||||||
Profile,
|
|
||||||
Submission,
|
|
||||||
SubmissionSource,
|
|
||||||
SubmissionTestCase,
|
|
||||||
)
|
|
||||||
from judge.utils.raw_sql import use_straight_join
|
from judge.utils.raw_sql import use_straight_join
|
||||||
|
|
||||||
|
|
||||||
class SubmissionStatusFilter(admin.SimpleListFilter):
|
class SubmissionStatusFilter(admin.SimpleListFilter):
|
||||||
parameter_name = title = "status"
|
parameter_name = title = 'status'
|
||||||
__lookups = (
|
__lookups = (('None', _('None')), ('NotDone', _('Not done')), ('EX', _('Exceptional'))) + Submission.STATUS
|
||||||
("None", _("None")),
|
|
||||||
("NotDone", _("Not done")),
|
|
||||||
("EX", _("Exceptional")),
|
|
||||||
) + Submission.STATUS
|
|
||||||
__handles = set(map(itemgetter(0), Submission.STATUS))
|
__handles = set(map(itemgetter(0), Submission.STATUS))
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
return self.__lookups
|
return self.__lookups
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value() == "None":
|
if self.value() == 'None':
|
||||||
return queryset.filter(status=None)
|
return queryset.filter(status=None)
|
||||||
elif self.value() == "NotDone":
|
elif self.value() == 'NotDone':
|
||||||
return queryset.exclude(status__in=["D", "IE", "CE", "AB"])
|
return queryset.exclude(status__in=['D', 'IE', 'CE', 'AB'])
|
||||||
elif self.value() == "EX":
|
elif self.value() == 'EX':
|
||||||
return queryset.exclude(status__in=["D", "CE", "G", "AB"])
|
return queryset.exclude(status__in=['D', 'CE', 'G', 'AB'])
|
||||||
elif self.value() in self.__handles:
|
elif self.value() in self.__handles:
|
||||||
return queryset.filter(status=self.value())
|
return queryset.filter(status=self.value())
|
||||||
|
|
||||||
|
|
||||||
class SubmissionResultFilter(admin.SimpleListFilter):
|
class SubmissionResultFilter(admin.SimpleListFilter):
|
||||||
parameter_name = title = "result"
|
parameter_name = title = 'result'
|
||||||
__lookups = (("None", _("None")), ("BAD", _("Unaccepted"))) + Submission.RESULT
|
__lookups = (('None', _('None')), ('BAD', _('Unaccepted'))) + Submission.RESULT
|
||||||
__handles = set(map(itemgetter(0), Submission.RESULT))
|
__handles = set(map(itemgetter(0), Submission.RESULT))
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
return self.__lookups
|
return self.__lookups
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value() == "None":
|
if self.value() == 'None':
|
||||||
return queryset.filter(result=None)
|
return queryset.filter(result=None)
|
||||||
elif self.value() == "BAD":
|
elif self.value() == 'BAD':
|
||||||
return queryset.exclude(result="AC")
|
return queryset.exclude(result='AC')
|
||||||
elif self.value() in self.__handles:
|
elif self.value() in self.__handles:
|
||||||
return queryset.filter(result=self.value())
|
return queryset.filter(result=self.value())
|
||||||
|
|
||||||
|
|
||||||
class SubmissionTestCaseInline(admin.TabularInline):
|
class SubmissionTestCaseInline(admin.TabularInline):
|
||||||
fields = ("case", "batch", "status", "time", "memory", "points", "total")
|
fields = ('case', 'batch', 'status', 'time', 'memory', 'points', 'total')
|
||||||
readonly_fields = ("case", "batch", "total")
|
readonly_fields = ('case', 'batch', 'total')
|
||||||
model = SubmissionTestCase
|
model = SubmissionTestCase
|
||||||
can_delete = False
|
can_delete = False
|
||||||
max_num = 0
|
max_num = 0
|
||||||
|
|
||||||
|
|
||||||
class ContestSubmissionInline(admin.StackedInline):
|
class ContestSubmissionInline(admin.StackedInline):
|
||||||
fields = ("problem", "participation", "points")
|
fields = ('problem', 'participation', 'points')
|
||||||
model = ContestSubmission
|
model = ContestSubmission
|
||||||
|
|
||||||
def get_formset(self, request, obj=None, **kwargs):
|
def get_formset(self, request, obj=None, **kwargs):
|
||||||
kwargs["formfield_callback"] = partial(
|
kwargs['formfield_callback'] = partial(self.formfield_for_dbfield, request=request, obj=obj)
|
||||||
self.formfield_for_dbfield, request=request, obj=obj
|
|
||||||
)
|
|
||||||
return super(ContestSubmissionInline, self).get_formset(request, obj, **kwargs)
|
return super(ContestSubmissionInline, self).get_formset(request, obj, **kwargs)
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
submission = kwargs.pop("obj", None)
|
submission = kwargs.pop('obj', None)
|
||||||
label = None
|
label = None
|
||||||
if submission:
|
if submission:
|
||||||
if db_field.name == "participation":
|
if db_field.name == 'participation':
|
||||||
kwargs["queryset"] = ContestParticipation.objects.filter(
|
kwargs['queryset'] = ContestParticipation.objects.filter(user=submission.user,
|
||||||
user=submission.user, contest__problems=submission.problem
|
contest__problems=submission.problem) \
|
||||||
).only("id", "contest__name")
|
.only('id', 'contest__name')
|
||||||
|
|
||||||
def label(obj):
|
def label(obj):
|
||||||
return obj.contest.name
|
return obj.contest.name
|
||||||
|
elif db_field.name == 'problem':
|
||||||
elif db_field.name == "problem":
|
kwargs['queryset'] = ContestProblem.objects.filter(problem=submission.problem) \
|
||||||
kwargs["queryset"] = ContestProblem.objects.filter(
|
.only('id', 'problem__name', 'contest__name')
|
||||||
problem=submission.problem
|
|
||||||
).only("id", "problem__name", "contest__name")
|
|
||||||
|
|
||||||
def label(obj):
|
def label(obj):
|
||||||
return pgettext("contest problem", "%(problem)s in %(contest)s") % {
|
return pgettext('contest problem', '%(problem)s in %(contest)s') % {
|
||||||
"problem": obj.problem.name,
|
'problem': obj.problem.name, 'contest': obj.contest.name,
|
||||||
"contest": obj.contest.name,
|
|
||||||
}
|
}
|
||||||
|
field = super(ContestSubmissionInline, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
field = super(ContestSubmissionInline, self).formfield_for_dbfield(
|
|
||||||
db_field, **kwargs
|
|
||||||
)
|
|
||||||
if label is not None:
|
if label is not None:
|
||||||
field.label_from_instance = label
|
field.label_from_instance = label
|
||||||
return field
|
return field
|
||||||
|
|
||||||
|
|
||||||
class SubmissionSourceInline(admin.StackedInline):
|
class SubmissionSourceInline(admin.StackedInline):
|
||||||
fields = ("source",)
|
fields = ('source',)
|
||||||
model = SubmissionSource
|
model = SubmissionSource
|
||||||
can_delete = False
|
can_delete = False
|
||||||
extra = 0
|
extra = 0
|
||||||
|
|
||||||
def get_formset(self, request, obj=None, **kwargs):
|
def get_formset(self, request, obj=None, **kwargs):
|
||||||
kwargs.setdefault("widgets", {})["source"] = AceWidget(
|
kwargs.setdefault('widgets', {})['source'] = AceWidget(mode=obj and obj.language.ace,
|
||||||
mode=obj and obj.language.ace, theme=request.profile.ace_theme
|
theme=request.profile.ace_theme)
|
||||||
)
|
|
||||||
return super().get_formset(request, obj, **kwargs)
|
return super().get_formset(request, obj, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class SubmissionAdmin(admin.ModelAdmin):
|
class SubmissionAdmin(admin.ModelAdmin):
|
||||||
readonly_fields = ("user", "problem", "date", "judged_date")
|
readonly_fields = ('user', 'problem', 'date')
|
||||||
fields = (
|
fields = ('user', 'problem', 'date', 'time', 'memory', 'points', 'language', 'status', 'result',
|
||||||
"user",
|
'case_points', 'case_total', 'judged_on', 'error')
|
||||||
"problem",
|
actions = ('judge', 'recalculate_score')
|
||||||
"date",
|
list_display = ('id', 'problem_code', 'problem_name', 'user_column', 'execution_time', 'pretty_memory',
|
||||||
"judged_date",
|
'points', 'language_column', 'status', 'result', 'judge_column')
|
||||||
"time",
|
list_filter = ('language', SubmissionStatusFilter, SubmissionResultFilter)
|
||||||
"memory",
|
search_fields = ('problem__code', 'problem__name', 'user__user__username')
|
||||||
"points",
|
|
||||||
"language",
|
|
||||||
"status",
|
|
||||||
"result",
|
|
||||||
"case_points",
|
|
||||||
"case_total",
|
|
||||||
"judged_on",
|
|
||||||
"error",
|
|
||||||
)
|
|
||||||
actions = ("judge", "recalculate_score")
|
|
||||||
list_display = (
|
|
||||||
"id",
|
|
||||||
"problem_code",
|
|
||||||
"problem_name",
|
|
||||||
"user_column",
|
|
||||||
"execution_time",
|
|
||||||
"pretty_memory",
|
|
||||||
"points",
|
|
||||||
"language_column",
|
|
||||||
"status",
|
|
||||||
"result",
|
|
||||||
"judge_column",
|
|
||||||
)
|
|
||||||
list_filter = ("language", SubmissionStatusFilter, SubmissionResultFilter)
|
|
||||||
search_fields = ("problem__code", "problem__name", "user__user__username")
|
|
||||||
actions_on_top = True
|
actions_on_top = True
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
inlines = [
|
inlines = [SubmissionSourceInline, SubmissionTestCaseInline, ContestSubmissionInline]
|
||||||
SubmissionSourceInline,
|
|
||||||
SubmissionTestCaseInline,
|
|
||||||
ContestSubmissionInline,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
queryset = Submission.objects.select_related(
|
queryset = Submission.objects.select_related('problem', 'user__user', 'language').only(
|
||||||
"problem", "user__user", "language"
|
'problem__code', 'problem__name', 'user__user__username', 'language__name',
|
||||||
).only(
|
'time', 'memory', 'points', 'status', 'result',
|
||||||
"problem__code",
|
|
||||||
"problem__name",
|
|
||||||
"user__user__username",
|
|
||||||
"language__name",
|
|
||||||
"time",
|
|
||||||
"memory",
|
|
||||||
"points",
|
|
||||||
"status",
|
|
||||||
"result",
|
|
||||||
)
|
)
|
||||||
use_straight_join(queryset)
|
use_straight_join(queryset)
|
||||||
if not request.user.has_perm("judge.edit_all_problem"):
|
if not request.user.has_perm('judge.edit_all_problem'):
|
||||||
id = request.profile.id
|
id = request.profile.id
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id)).distinct()
|
||||||
Q(problem__authors__id=id) | Q(problem__curators__id=id)
|
|
||||||
).distinct()
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
def has_add_permission(self, request):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def lookup_allowed(self, key, value):
|
def has_change_permission(self, request, obj=None):
|
||||||
return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in (
|
if not request.user.has_perm('judge.edit_own_problem'):
|
||||||
"problem__code",
|
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 save_model(self, request, obj, form, change):
|
def lookup_allowed(self, key, value):
|
||||||
super().save_model(request, obj, form, change)
|
return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in ('problem__code',)
|
||||||
if "case_points" in form.changed_data or "case_total" in form.changed_data:
|
|
||||||
obj.update_contest()
|
|
||||||
|
|
||||||
def judge(self, request, queryset):
|
def judge(self, request, queryset):
|
||||||
if not request.user.has_perm(
|
if not request.user.has_perm('judge.rejudge_submission') or not request.user.has_perm('judge.edit_own_problem'):
|
||||||
"judge.rejudge_submission"
|
self.message_user(request, gettext('You do not have the permission to rejudge submissions.'),
|
||||||
) or not request.user.has_perm("judge.edit_own_problem"):
|
level=messages.ERROR)
|
||||||
self.message_user(
|
|
||||||
request,
|
|
||||||
gettext("You do not have the permission to rejudge submissions."),
|
|
||||||
level=messages.ERROR,
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
queryset = queryset.order_by("id")
|
queryset = queryset.order_by('id')
|
||||||
if (
|
if not request.user.has_perm('judge.rejudge_submission_lot') and \
|
||||||
not request.user.has_perm("judge.rejudge_submission_lot")
|
queryset.count() > settings.DMOJ_SUBMISSIONS_REJUDGE_LIMIT:
|
||||||
and queryset.count() > settings.DMOJ_SUBMISSIONS_REJUDGE_LIMIT
|
self.message_user(request, gettext('You do not have the permission to rejudge THAT many submissions.'),
|
||||||
):
|
level=messages.ERROR)
|
||||||
self.message_user(
|
|
||||||
request,
|
|
||||||
gettext(
|
|
||||||
"You do not have the permission to rejudge THAT many submissions."
|
|
||||||
),
|
|
||||||
level=messages.ERROR,
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
if not request.user.has_perm("judge.edit_all_problem"):
|
if not request.user.has_perm('judge.edit_all_problem'):
|
||||||
id = request.profile.id
|
id = request.profile.id
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id))
|
||||||
Q(problem__authors__id=id) | Q(problem__curators__id=id)
|
|
||||||
)
|
|
||||||
judged = len(queryset)
|
judged = len(queryset)
|
||||||
for model in queryset:
|
for model in queryset:
|
||||||
model.judge(rejudge=True, batch_rejudge=True)
|
model.judge(rejudge=True, batch_rejudge=True)
|
||||||
self.message_user(
|
self.message_user(request, ungettext('%d submission was successfully scheduled for rejudging.',
|
||||||
request,
|
'%d submissions were successfully scheduled for rejudging.',
|
||||||
ungettext(
|
judged) % judged)
|
||||||
"%d submission was successfully scheduled for rejudging.",
|
judge.short_description = _('Rejudge the selected submissions')
|
||||||
"%d submissions were successfully scheduled for rejudging.",
|
|
||||||
judged,
|
|
||||||
)
|
|
||||||
% judged,
|
|
||||||
)
|
|
||||||
|
|
||||||
judge.short_description = _("Rejudge the selected submissions")
|
|
||||||
|
|
||||||
def recalculate_score(self, request, queryset):
|
def recalculate_score(self, request, queryset):
|
||||||
if not request.user.has_perm("judge.rejudge_submission"):
|
if not request.user.has_perm('judge.rejudge_submission'):
|
||||||
self.message_user(
|
self.message_user(request, gettext('You do not have the permission to rejudge submissions.'),
|
||||||
request,
|
level=messages.ERROR)
|
||||||
gettext("You do not have the permission to rejudge submissions."),
|
|
||||||
level=messages.ERROR,
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
submissions = list(
|
submissions = list(queryset.defer(None).select_related(None).select_related('problem')
|
||||||
queryset.defer(None)
|
.only('points', 'case_points', 'case_total', 'problem__partial', 'problem__points'))
|
||||||
.select_related(None)
|
|
||||||
.select_related("problem")
|
|
||||||
.only(
|
|
||||||
"points",
|
|
||||||
"case_points",
|
|
||||||
"case_total",
|
|
||||||
"problem__partial",
|
|
||||||
"problem__points",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
for submission in submissions:
|
for submission in submissions:
|
||||||
submission.points = round(
|
submission.points = round(submission.case_points / submission.case_total * submission.problem.points
|
||||||
submission.case_points
|
if submission.case_total else 0, 1)
|
||||||
/ submission.case_total
|
if not submission.problem.partial and submission.points < submission.problem.points:
|
||||||
* submission.problem.points
|
|
||||||
if submission.case_total
|
|
||||||
else 0,
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
not submission.problem.partial
|
|
||||||
and submission.points < submission.problem.points
|
|
||||||
):
|
|
||||||
submission.points = 0
|
submission.points = 0
|
||||||
submission.save()
|
submission.save()
|
||||||
submission.update_contest()
|
submission.update_contest()
|
||||||
|
|
||||||
for profile in Profile.objects.filter(
|
for profile in Profile.objects.filter(id__in=queryset.values_list('user_id', flat=True).distinct()):
|
||||||
id__in=queryset.values_list("user_id", flat=True).distinct()
|
|
||||||
):
|
|
||||||
profile.calculate_points()
|
profile.calculate_points()
|
||||||
cache.delete("user_complete:%d" % profile.id)
|
cache.delete('user_complete:%d' % profile.id)
|
||||||
cache.delete("user_attempted:%d" % profile.id)
|
cache.delete('user_attempted:%d' % profile.id)
|
||||||
|
|
||||||
for participation in ContestParticipation.objects.filter(
|
for participation in ContestParticipation.objects.filter(
|
||||||
id__in=queryset.values_list("contest__participation_id")
|
id__in=queryset.values_list('contest__participation_id')).prefetch_related('contest'):
|
||||||
).prefetch_related("contest"):
|
|
||||||
participation.recompute_results()
|
participation.recompute_results()
|
||||||
|
|
||||||
self.message_user(
|
self.message_user(request, ungettext('%d submission were successfully rescored.',
|
||||||
request,
|
'%d submissions were successfully rescored.',
|
||||||
ungettext(
|
len(submissions)) % len(submissions))
|
||||||
"%d submission were successfully rescored.",
|
recalculate_score.short_description = _('Rescore the selected submissions')
|
||||||
"%d submissions were successfully rescored.",
|
|
||||||
len(submissions),
|
|
||||||
)
|
|
||||||
% len(submissions),
|
|
||||||
)
|
|
||||||
|
|
||||||
recalculate_score.short_description = _("Rescore the selected submissions")
|
|
||||||
|
|
||||||
def problem_code(self, obj):
|
def problem_code(self, obj):
|
||||||
return obj.problem.code
|
return obj.problem.code
|
||||||
|
problem_code.short_description = _('Problem code')
|
||||||
problem_code.short_description = _("Problem code")
|
problem_code.admin_order_field = 'problem__code'
|
||||||
problem_code.admin_order_field = "problem__code"
|
|
||||||
|
|
||||||
def problem_name(self, obj):
|
def problem_name(self, obj):
|
||||||
return obj.problem.name
|
return obj.problem.name
|
||||||
|
problem_name.short_description = _('Problem name')
|
||||||
problem_name.short_description = _("Problem name")
|
problem_name.admin_order_field = 'problem__name'
|
||||||
problem_name.admin_order_field = "problem__name"
|
|
||||||
|
|
||||||
def user_column(self, obj):
|
def user_column(self, obj):
|
||||||
return obj.user.user.username
|
return obj.user.user.username
|
||||||
|
user_column.admin_order_field = 'user__user__username'
|
||||||
user_column.admin_order_field = "user__user__username"
|
user_column.short_description = _('User')
|
||||||
user_column.short_description = _("User")
|
|
||||||
|
|
||||||
def execution_time(self, obj):
|
def execution_time(self, obj):
|
||||||
return round(obj.time, 2) if obj.time is not None else "None"
|
return round(obj.time, 2) if obj.time is not None else 'None'
|
||||||
|
execution_time.short_description = _('Time')
|
||||||
execution_time.short_description = _("Time")
|
execution_time.admin_order_field = 'time'
|
||||||
execution_time.admin_order_field = "time"
|
|
||||||
|
|
||||||
def pretty_memory(self, obj):
|
def pretty_memory(self, obj):
|
||||||
memory = obj.memory
|
memory = obj.memory
|
||||||
if memory is None:
|
if memory is None:
|
||||||
return gettext("None")
|
return gettext('None')
|
||||||
if memory < 1000:
|
if memory < 1000:
|
||||||
return gettext("%d KB") % memory
|
return gettext('%d KB') % memory
|
||||||
else:
|
else:
|
||||||
return gettext("%.2f MB") % (memory / 1024)
|
return gettext('%.2f MB') % (memory / 1024)
|
||||||
|
pretty_memory.admin_order_field = 'memory'
|
||||||
pretty_memory.admin_order_field = "memory"
|
pretty_memory.short_description = _('Memory')
|
||||||
pretty_memory.short_description = _("Memory")
|
|
||||||
|
|
||||||
def language_column(self, obj):
|
def language_column(self, obj):
|
||||||
return obj.language.name
|
return obj.language.name
|
||||||
|
language_column.admin_order_field = 'language__name'
|
||||||
language_column.admin_order_field = "language__name"
|
language_column.short_description = _('Language')
|
||||||
language_column.short_description = _("Language")
|
|
||||||
|
|
||||||
def judge_column(self, obj):
|
def judge_column(self, obj):
|
||||||
return format_html(
|
return format_html('<input type="button" value="Rejudge" onclick="location.href=\'{}/judge/\'" />', obj.id)
|
||||||
'<input type="button" value="Rejudge" onclick="location.href=\'{}/judge/\'" />',
|
judge_column.short_description = ''
|
||||||
obj.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
judge_column.short_description = ""
|
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
return [
|
return [
|
||||||
url(r"^(\d+)/judge/$", self.judge_view, name="judge_submission_rejudge"),
|
url(r'^(\d+)/judge/$', self.judge_view, name='judge_submission_rejudge'),
|
||||||
] + super(SubmissionAdmin, self).get_urls()
|
] + super(SubmissionAdmin, self).get_urls()
|
||||||
|
|
||||||
def judge_view(self, request, id):
|
def judge_view(self, request, id):
|
||||||
if not request.user.has_perm(
|
if not request.user.has_perm('judge.rejudge_submission') or not request.user.has_perm('judge.edit_own_problem'):
|
||||||
"judge.rejudge_submission"
|
|
||||||
) or not request.user.has_perm("judge.edit_own_problem"):
|
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
submission = get_object_or_404(Submission, id=id)
|
submission = get_object_or_404(Submission, id=id)
|
||||||
if not request.user.has_perm(
|
if not request.user.has_perm('judge.edit_all_problem') and \
|
||||||
"judge.edit_all_problem"
|
not submission.problem.is_editor(request.profile):
|
||||||
) and not submission.problem.is_editor(request.profile):
|
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
submission.judge(rejudge=True)
|
submission.judge(rejudge=True)
|
||||||
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
|
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
|
||||||
|
|
|
@ -8,59 +8,45 @@ from judge.widgets import AdminHeavySelect2MultipleWidget
|
||||||
|
|
||||||
class ProblemGroupForm(ModelForm):
|
class ProblemGroupForm(ModelForm):
|
||||||
problems = ModelMultipleChoiceField(
|
problems = ModelMultipleChoiceField(
|
||||||
label=_("Included problems"),
|
label=_('Included problems'),
|
||||||
queryset=Problem.objects.all(),
|
queryset=Problem.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_("These problems are included in this group of problems"),
|
help_text=_('These problems are included in this group of problems'),
|
||||||
widget=AdminHeavySelect2MultipleWidget(data_view="problem_select2"),
|
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'))
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ProblemGroupAdmin(admin.ModelAdmin):
|
class ProblemGroupAdmin(admin.ModelAdmin):
|
||||||
fields = ("name", "full_name", "problems")
|
fields = ('name', 'full_name', 'problems')
|
||||||
form = ProblemGroupForm
|
form = ProblemGroupForm
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
super(ProblemGroupAdmin, self).save_model(request, obj, form, change)
|
super(ProblemGroupAdmin, self).save_model(request, obj, form, change)
|
||||||
obj.problem_set.set(form.cleaned_data["problems"])
|
obj.problem_set.set(form.cleaned_data['problems'])
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
self.form.base_fields["problems"].initial = (
|
self.form.base_fields['problems'].initial = [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(ProblemGroupAdmin, self).get_form(request, obj, **kwargs)
|
return super(ProblemGroupAdmin, self).get_form(request, obj, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ProblemTypeForm(ModelForm):
|
class ProblemTypeForm(ModelForm):
|
||||||
problems = ModelMultipleChoiceField(
|
problems = ModelMultipleChoiceField(
|
||||||
label=_("Included problems"),
|
label=_('Included problems'),
|
||||||
queryset=Problem.objects.all(),
|
queryset=Problem.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_("These problems are included in this type of problems"),
|
help_text=_('These problems are included in this type of problems'),
|
||||||
widget=AdminHeavySelect2MultipleWidget(data_view="problem_select2"),
|
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'))
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ProblemTypeAdmin(admin.ModelAdmin):
|
class ProblemTypeAdmin(admin.ModelAdmin):
|
||||||
fields = ("name", "full_name", "problems")
|
fields = ('name', 'full_name', 'problems')
|
||||||
form = ProblemTypeForm
|
form = ProblemTypeForm
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
super(ProblemTypeAdmin, self).save_model(request, obj, form, change)
|
super(ProblemTypeAdmin, self).save_model(request, obj, form, change)
|
||||||
obj.problem_set.set(form.cleaned_data["problems"])
|
obj.problem_set.set(form.cleaned_data['problems'])
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
self.form.base_fields["problems"].initial = (
|
self.form.base_fields['problems'].initial = [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",)
|
|
||||||
|
|
|
@ -4,56 +4,36 @@ from django.forms import ModelForm
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
|
||||||
from judge.models import TicketMessage
|
from judge.models import TicketMessage
|
||||||
from judge.widgets import (
|
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
|
||||||
AdminHeavySelect2MultipleWidget,
|
|
||||||
AdminHeavySelect2Widget,
|
|
||||||
HeavyPreviewAdminPageDownWidget,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TicketMessageForm(ModelForm):
|
class TicketMessageForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
"user": AdminHeavySelect2Widget(
|
'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||||
data_view="profile_select2", attrs={"style": "width: 100%"}
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
if HeavyPreviewAdminPageDownWidget is not None:
|
if HeavyPreviewAdminPageDownWidget is not None:
|
||||||
widgets["body"] = HeavyPreviewAdminPageDownWidget(
|
widgets['body'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('ticket_preview'))
|
||||||
preview=reverse_lazy("ticket_preview")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TicketMessageInline(StackedInline):
|
class TicketMessageInline(StackedInline):
|
||||||
model = TicketMessage
|
model = TicketMessage
|
||||||
form = TicketMessageForm
|
form = TicketMessageForm
|
||||||
fields = ("user", "body")
|
fields = ('user', 'body')
|
||||||
|
|
||||||
|
|
||||||
class TicketForm(ModelForm):
|
class TicketForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
widgets = {
|
widgets = {
|
||||||
"user": AdminHeavySelect2Widget(
|
'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||||
data_view="profile_select2", attrs={"style": "width: 100%"}
|
'assignees': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||||
),
|
|
||||||
"assignees": AdminHeavySelect2MultipleWidget(
|
|
||||||
data_view="profile_select2", attrs={"style": "width: 100%"}
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TicketAdmin(ModelAdmin):
|
class TicketAdmin(ModelAdmin):
|
||||||
fields = (
|
fields = ('title', 'time', 'user', 'assignees', 'content_type', 'object_id', 'notes')
|
||||||
"title",
|
readonly_fields = ('time',)
|
||||||
"time",
|
list_display = ('title', 'user', 'time', 'linked_item')
|
||||||
"user",
|
|
||||||
"assignees",
|
|
||||||
"content_type",
|
|
||||||
"object_id",
|
|
||||||
"notes",
|
|
||||||
)
|
|
||||||
readonly_fields = ("time",)
|
|
||||||
list_display = ("title", "user", "time", "linked_item")
|
|
||||||
inlines = [TicketMessageInline]
|
inlines = [TicketMessageInline]
|
||||||
form = TicketForm
|
form = TicketForm
|
||||||
date_hierarchy = "time"
|
date_hierarchy = 'time'
|
||||||
|
|
|
@ -1,67 +0,0 @@
|
||||||
from operator import attrgetter
|
|
||||||
|
|
||||||
from django.contrib import admin
|
|
||||||
from django.utils.html import format_html
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.translation import gettext, gettext_lazy as _, ungettext
|
|
||||||
from django.forms import ModelForm
|
|
||||||
|
|
||||||
from judge.models import VolunteerProblemVote
|
|
||||||
from judge.widgets import AdminSelect2MultipleWidget
|
|
||||||
|
|
||||||
|
|
||||||
class VolunteerProblemVoteForm(ModelForm):
|
|
||||||
class Meta:
|
|
||||||
widgets = {
|
|
||||||
"types": AdminSelect2MultipleWidget,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class VolunteerProblemVoteAdmin(admin.ModelAdmin):
|
|
||||||
form = VolunteerProblemVoteForm
|
|
||||||
fields = (
|
|
||||||
"voter",
|
|
||||||
"problem_link",
|
|
||||||
"time",
|
|
||||||
"thinking_points",
|
|
||||||
"knowledge_points",
|
|
||||||
"types",
|
|
||||||
"feedback",
|
|
||||||
)
|
|
||||||
readonly_fields = ("time", "problem_link", "voter")
|
|
||||||
list_display = (
|
|
||||||
"voter",
|
|
||||||
"problem_link",
|
|
||||||
"thinking_points",
|
|
||||||
"knowledge_points",
|
|
||||||
"show_types",
|
|
||||||
"feedback",
|
|
||||||
)
|
|
||||||
search_fields = (
|
|
||||||
"voter__user__username",
|
|
||||||
"problem__code",
|
|
||||||
"problem__name",
|
|
||||||
)
|
|
||||||
date_hierarchy = "time"
|
|
||||||
|
|
||||||
def problem_link(self, obj):
|
|
||||||
if self.request.user.is_superuser:
|
|
||||||
url = reverse("admin:judge_problem_change", args=(obj.problem.id,))
|
|
||||||
else:
|
|
||||||
url = reverse("problem_detail", args=(obj.problem.code,))
|
|
||||||
return format_html(f"<a href='{url}'>{obj.problem}</a>")
|
|
||||||
|
|
||||||
problem_link.short_description = _("Problem")
|
|
||||||
problem_link.admin_order_field = "problem__code"
|
|
||||||
|
|
||||||
def show_types(self, obj):
|
|
||||||
return ", ".join(map(attrgetter("name"), obj.types.all()))
|
|
||||||
|
|
||||||
show_types.short_description = _("Types")
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
|
||||||
self.request = request
|
|
||||||
if request.user.is_superuser:
|
|
||||||
return super().get_queryset(request)
|
|
||||||
queryset = VolunteerProblemVote.objects.prefetch_related("voter")
|
|
||||||
return queryset.filter(voter=request.profile).distinct()
|
|
|
@ -4,15 +4,15 @@ from django.utils.translation import gettext_lazy
|
||||||
|
|
||||||
|
|
||||||
class JudgeAppConfig(AppConfig):
|
class JudgeAppConfig(AppConfig):
|
||||||
name = "judge"
|
name = 'judge'
|
||||||
verbose_name = gettext_lazy("Online Judge")
|
verbose_name = gettext_lazy('Online Judge')
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
# WARNING: AS THIS IS NOT A FUNCTIONAL PROGRAMMING LANGUAGE,
|
# WARNING: AS THIS IS NOT A FUNCTIONAL PROGRAMMING LANGUAGE,
|
||||||
# OPERATIONS MAY HAVE SIDE EFFECTS.
|
# OPERATIONS MAY HAVE SIDE EFFECTS.
|
||||||
# DO NOT REMOVE THINKING THE IMPORT IS UNUSED.
|
# DO NOT REMOVE THINKING THE IMPORT IS UNUSED.
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
from . import models, signals, jinja2 # noqa: F401, imported for side effects
|
from . import 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
|
||||||
|
@ -30,7 +30,7 @@ class JudgeAppConfig(AppConfig):
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
try:
|
try:
|
||||||
lang = Language.get_default_language()
|
lang = Language.get_python3()
|
||||||
for user in User.objects.filter(profile=None):
|
for user in User.objects.filter(profile=None):
|
||||||
# These poor profileless users
|
# These poor profileless users
|
||||||
profile = Profile(user=user, language=lang)
|
profile = Profile(user=user, language=lang)
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
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
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
from .djangohandler import DjangoHandler
|
||||||
|
from .djangoserver import DjangoServer
|
||||||
|
from .judgecallback import DjangoJudgeHandler
|
||||||
|
from .judgehandler import JudgeHandler
|
||||||
|
from .judgelist import JudgeList
|
||||||
|
from .judgeserver import JudgeServer
|
|
@ -1,218 +0,0 @@
|
||||||
import logging
|
|
||||||
import socket
|
|
||||||
import struct
|
|
||||||
import zlib
|
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
from netaddr import IPGlob, IPSet
|
|
||||||
|
|
||||||
from judge.utils.unicode import utf8text
|
|
||||||
|
|
||||||
logger = logging.getLogger("judge.bridge")
|
|
||||||
|
|
||||||
size_pack = struct.Struct("!I")
|
|
||||||
assert size_pack.size == 4
|
|
||||||
|
|
||||||
MAX_ALLOWED_PACKET_SIZE = 8 * 1024 * 1024
|
|
||||||
|
|
||||||
|
|
||||||
def proxy_list(human_readable):
|
|
||||||
globs = []
|
|
||||||
addrs = []
|
|
||||||
for item in human_readable:
|
|
||||||
if "*" in item or "-" in item:
|
|
||||||
globs.append(IPGlob(item))
|
|
||||||
else:
|
|
||||||
addrs.append(item)
|
|
||||||
return IPSet(chain(chain.from_iterable(globs), addrs))
|
|
||||||
|
|
||||||
|
|
||||||
class Disconnect(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# socketserver.BaseRequestHandler does all the handling in __init__,
|
|
||||||
# making it impossible to inherit __init__ sanely. While it lets you
|
|
||||||
# use setup(), most tools will complain about uninitialized variables.
|
|
||||||
# This metaclass will allow sane __init__ behaviour while also magically
|
|
||||||
# calling the methods that handles the request.
|
|
||||||
class RequestHandlerMeta(type):
|
|
||||||
def __call__(cls, *args, **kwargs):
|
|
||||||
handler = super().__call__(*args, **kwargs)
|
|
||||||
handler.on_connect()
|
|
||||||
try:
|
|
||||||
handler.handle()
|
|
||||||
except BaseException:
|
|
||||||
logger.exception("Error in base packet handling")
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
handler.on_disconnect()
|
|
||||||
|
|
||||||
|
|
||||||
class ZlibPacketHandler(metaclass=RequestHandlerMeta):
|
|
||||||
proxies = []
|
|
||||||
|
|
||||||
def __init__(self, request, client_address, server):
|
|
||||||
self.request = request
|
|
||||||
self.server = server
|
|
||||||
self.client_address = client_address
|
|
||||||
self.server_address = server.server_address
|
|
||||||
self._initial_tag = None
|
|
||||||
self._got_packet = False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def timeout(self):
|
|
||||||
return self.request.gettimeout()
|
|
||||||
|
|
||||||
@timeout.setter
|
|
||||||
def timeout(self, timeout):
|
|
||||||
self.request.settimeout(timeout or None)
|
|
||||||
|
|
||||||
def read_sized_packet(self, size, initial=None):
|
|
||||||
if size > MAX_ALLOWED_PACKET_SIZE:
|
|
||||||
logger.log(
|
|
||||||
logging.WARNING if self._got_packet else logging.INFO,
|
|
||||||
"Disconnecting client due to too-large message size (%d bytes): %s",
|
|
||||||
size,
|
|
||||||
self.client_address,
|
|
||||||
)
|
|
||||||
raise Disconnect()
|
|
||||||
|
|
||||||
buffer = []
|
|
||||||
remainder = size
|
|
||||||
|
|
||||||
if initial:
|
|
||||||
buffer.append(initial)
|
|
||||||
remainder -= len(initial)
|
|
||||||
assert remainder >= 0
|
|
||||||
|
|
||||||
while remainder:
|
|
||||||
data = self.request.recv(remainder)
|
|
||||||
remainder -= len(data)
|
|
||||||
buffer.append(data)
|
|
||||||
self._on_packet(b"".join(buffer))
|
|
||||||
|
|
||||||
def parse_proxy_protocol(self, line):
|
|
||||||
words = line.split()
|
|
||||||
|
|
||||||
if len(words) < 2:
|
|
||||||
raise Disconnect()
|
|
||||||
|
|
||||||
if words[1] == b"TCP4":
|
|
||||||
if len(words) != 6:
|
|
||||||
raise Disconnect()
|
|
||||||
self.client_address = (utf8text(words[2]), utf8text(words[4]))
|
|
||||||
self.server_address = (utf8text(words[3]), utf8text(words[5]))
|
|
||||||
elif words[1] == b"TCP6":
|
|
||||||
self.client_address = (utf8text(words[2]), utf8text(words[4]), 0, 0)
|
|
||||||
self.server_address = (utf8text(words[3]), utf8text(words[5]), 0, 0)
|
|
||||||
elif words[1] != b"UNKNOWN":
|
|
||||||
raise Disconnect()
|
|
||||||
|
|
||||||
def read_size(self, buffer=b""):
|
|
||||||
while len(buffer) < size_pack.size:
|
|
||||||
recv = self.request.recv(size_pack.size - len(buffer))
|
|
||||||
if not recv:
|
|
||||||
raise Disconnect()
|
|
||||||
buffer += recv
|
|
||||||
return size_pack.unpack(buffer)[0]
|
|
||||||
|
|
||||||
def read_proxy_header(self, buffer=b""):
|
|
||||||
# Max line length for PROXY protocol is 107, and we received 4 already.
|
|
||||||
while b"\r\n" not in buffer:
|
|
||||||
if len(buffer) > 107:
|
|
||||||
raise Disconnect()
|
|
||||||
data = self.request.recv(107)
|
|
||||||
if not data:
|
|
||||||
raise Disconnect()
|
|
||||||
buffer += data
|
|
||||||
return buffer
|
|
||||||
|
|
||||||
def _on_packet(self, data):
|
|
||||||
decompressed = zlib.decompress(data).decode("utf-8")
|
|
||||||
self._got_packet = True
|
|
||||||
self.on_packet(decompressed)
|
|
||||||
|
|
||||||
def on_packet(self, data):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def on_connect(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def on_disconnect(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def on_timeout(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def on_cleanup(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def handle(self):
|
|
||||||
try:
|
|
||||||
tag = self.read_size()
|
|
||||||
self._initial_tag = size_pack.pack(tag)
|
|
||||||
if self.client_address[0] in self.proxies and self._initial_tag == b"PROX":
|
|
||||||
proxy, _, remainder = self.read_proxy_header(
|
|
||||||
self._initial_tag
|
|
||||||
).partition(b"\r\n")
|
|
||||||
self.parse_proxy_protocol(proxy)
|
|
||||||
|
|
||||||
while remainder:
|
|
||||||
while len(remainder) < size_pack.size:
|
|
||||||
self.read_sized_packet(self.read_size(remainder))
|
|
||||||
break
|
|
||||||
|
|
||||||
size = size_pack.unpack(remainder[: size_pack.size])[0]
|
|
||||||
remainder = remainder[size_pack.size :]
|
|
||||||
if len(remainder) <= size:
|
|
||||||
self.read_sized_packet(size, remainder)
|
|
||||||
break
|
|
||||||
|
|
||||||
self._on_packet(remainder[:size])
|
|
||||||
remainder = remainder[size:]
|
|
||||||
else:
|
|
||||||
self.read_sized_packet(tag)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
self.read_sized_packet(self.read_size())
|
|
||||||
except Disconnect:
|
|
||||||
return
|
|
||||||
except zlib.error:
|
|
||||||
if self._got_packet:
|
|
||||||
logger.warning(
|
|
||||||
"Encountered zlib error during packet handling, disconnecting client: %s",
|
|
||||||
self.client_address,
|
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.info(
|
|
||||||
"Potentially wrong protocol (zlib error): %s: %r",
|
|
||||||
self.client_address,
|
|
||||||
self._initial_tag,
|
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
except socket.timeout:
|
|
||||||
if self._got_packet:
|
|
||||||
logger.info("Socket timed out: %s", self.client_address)
|
|
||||||
self.on_timeout()
|
|
||||||
else:
|
|
||||||
logger.info(
|
|
||||||
"Potentially wrong protocol: %s: %r",
|
|
||||||
self.client_address,
|
|
||||||
self._initial_tag,
|
|
||||||
)
|
|
||||||
except socket.error as e:
|
|
||||||
# When a gevent socket is shutdown, gevent cancels all waits, causing recv to raise cancel_wait_ex.
|
|
||||||
if e.__class__.__name__ == "cancel_wait_ex":
|
|
||||||
return
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
self.on_cleanup()
|
|
||||||
|
|
||||||
def send(self, data):
|
|
||||||
compressed = zlib.compress(data.encode("utf-8"))
|
|
||||||
self.request.sendall(size_pack.pack(len(compressed)) + compressed)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self.request.shutdown(socket.SHUT_RDWR)
|
|
|
@ -1,52 +0,0 @@
|
||||||
import logging
|
|
||||||
import signal
|
|
||||||
import threading
|
|
||||||
from functools import partial
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from judge.bridge.django_handler import DjangoHandler
|
|
||||||
from judge.bridge.judge_handler import JudgeHandler
|
|
||||||
from judge.bridge.judge_list import JudgeList
|
|
||||||
from judge.bridge.server import Server
|
|
||||||
from judge.models import Judge, Submission
|
|
||||||
|
|
||||||
logger = logging.getLogger("judge.bridge")
|
|
||||||
|
|
||||||
|
|
||||||
def reset_judges():
|
|
||||||
Judge.objects.update(online=False, ping=None, load=None)
|
|
||||||
|
|
||||||
|
|
||||||
def judge_daemon():
|
|
||||||
reset_judges()
|
|
||||||
Submission.objects.filter(status__in=Submission.IN_PROGRESS_GRADING_STATUS).update(
|
|
||||||
status="IE", result="IE", error=None
|
|
||||||
)
|
|
||||||
judges = JudgeList()
|
|
||||||
|
|
||||||
judge_server = Server(
|
|
||||||
settings.BRIDGED_JUDGE_ADDRESS, partial(JudgeHandler, judges=judges)
|
|
||||||
)
|
|
||||||
django_server = Server(
|
|
||||||
settings.BRIDGED_DJANGO_ADDRESS, partial(DjangoHandler, judges=judges)
|
|
||||||
)
|
|
||||||
|
|
||||||
threading.Thread(target=django_server.serve_forever).start()
|
|
||||||
threading.Thread(target=judge_server.serve_forever).start()
|
|
||||||
|
|
||||||
stop = threading.Event()
|
|
||||||
|
|
||||||
def signal_handler(signum, _):
|
|
||||||
logger.info("Exiting due to %s", signal.Signals(signum).name)
|
|
||||||
stop.set()
|
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
|
||||||
signal.signal(signal.SIGQUIT, signal_handler)
|
|
||||||
signal.signal(signal.SIGTERM, signal_handler)
|
|
||||||
|
|
||||||
try:
|
|
||||||
stop.wait()
|
|
||||||
finally:
|
|
||||||
django_server.shutdown()
|
|
||||||
judge_server.shutdown()
|
|
|
@ -1,66 +0,0 @@
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import struct
|
|
||||||
|
|
||||||
from django import db
|
|
||||||
|
|
||||||
from judge.bridge.base_handler import Disconnect, ZlibPacketHandler
|
|
||||||
|
|
||||||
logger = logging.getLogger("judge.bridge")
|
|
||||||
size_pack = struct.Struct("!I")
|
|
||||||
|
|
||||||
|
|
||||||
class DjangoHandler(ZlibPacketHandler):
|
|
||||||
def __init__(self, request, client_address, server, judges):
|
|
||||||
super().__init__(request, client_address, server)
|
|
||||||
|
|
||||||
self.handlers = {
|
|
||||||
"submission-request": self.on_submission,
|
|
||||||
"terminate-submission": self.on_termination,
|
|
||||||
"disconnect-judge": self.on_disconnect_request,
|
|
||||||
}
|
|
||||||
self.judges = judges
|
|
||||||
|
|
||||||
def send(self, data):
|
|
||||||
super().send(json.dumps(data, separators=(",", ":")))
|
|
||||||
|
|
||||||
def on_packet(self, packet):
|
|
||||||
packet = json.loads(packet)
|
|
||||||
try:
|
|
||||||
result = self.handlers.get(packet.get("name", None), self.on_malformed)(
|
|
||||||
packet
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Error in packet handling (Django-facing)")
|
|
||||||
result = {"name": "bad-request"}
|
|
||||||
self.send(result)
|
|
||||||
raise Disconnect()
|
|
||||||
|
|
||||||
def on_submission(self, data):
|
|
||||||
id = data["submission-id"]
|
|
||||||
problem = data["problem-id"]
|
|
||||||
language = data["language"]
|
|
||||||
source = data["source"]
|
|
||||||
judge_id = data["judge-id"]
|
|
||||||
priority = data["priority"]
|
|
||||||
if not self.judges.check_priority(priority):
|
|
||||||
return {"name": "bad-request"}
|
|
||||||
self.judges.judge(id, problem, language, source, judge_id, priority)
|
|
||||||
return {"name": "submission-received", "submission-id": id}
|
|
||||||
|
|
||||||
def on_termination(self, data):
|
|
||||||
return {
|
|
||||||
"name": "submission-received",
|
|
||||||
"judge-aborted": self.judges.abort(data["submission-id"]),
|
|
||||||
}
|
|
||||||
|
|
||||||
def on_disconnect_request(self, data):
|
|
||||||
judge_id = data["judge-id"]
|
|
||||||
force = data["force"]
|
|
||||||
self.judges.disconnect(judge_id, force=force)
|
|
||||||
|
|
||||||
def on_malformed(self, packet):
|
|
||||||
logger.error("Malformed packet: %s", packet)
|
|
||||||
|
|
||||||
def on_cleanup(self):
|
|
||||||
db.connection.close()
|
|
67
judge/bridge/djangohandler.py
Normal file
67
judge/bridge/djangohandler.py
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from event_socket_server import ZlibPacketHandler
|
||||||
|
|
||||||
|
logger = logging.getLogger('judge.bridge')
|
||||||
|
size_pack = struct.Struct('!I')
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoHandler(ZlibPacketHandler):
|
||||||
|
def __init__(self, server, socket):
|
||||||
|
super(DjangoHandler, self).__init__(server, socket)
|
||||||
|
|
||||||
|
self.handlers = {
|
||||||
|
'submission-request': self.on_submission,
|
||||||
|
'terminate-submission': self.on_termination,
|
||||||
|
'disconnect-judge': self.on_disconnect,
|
||||||
|
}
|
||||||
|
self._to_kill = True
|
||||||
|
# self.server.schedule(5, self._kill_if_no_request)
|
||||||
|
|
||||||
|
def _kill_if_no_request(self):
|
||||||
|
if self._to_kill:
|
||||||
|
logger.info('Killed inactive connection: %s', self._socket.getpeername())
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def _format_send(self, data):
|
||||||
|
return super(DjangoHandler, self)._format_send(json.dumps(data, separators=(',', ':')))
|
||||||
|
|
||||||
|
def packet(self, packet):
|
||||||
|
self._to_kill = False
|
||||||
|
packet = json.loads(packet)
|
||||||
|
try:
|
||||||
|
result = self.handlers.get(packet.get('name', None), self.on_malformed)(packet)
|
||||||
|
except Exception:
|
||||||
|
logger.exception('Error in packet handling (Django-facing)')
|
||||||
|
result = {'name': 'bad-request'}
|
||||||
|
self.send(result, self._schedule_close)
|
||||||
|
|
||||||
|
def _schedule_close(self):
|
||||||
|
self.server.schedule(0, self.close)
|
||||||
|
|
||||||
|
def on_submission(self, data):
|
||||||
|
id = data['submission-id']
|
||||||
|
problem = data['problem-id']
|
||||||
|
language = data['language']
|
||||||
|
source = data['source']
|
||||||
|
priority = data['priority']
|
||||||
|
if not self.server.judges.check_priority(priority):
|
||||||
|
return {'name': 'bad-request'}
|
||||||
|
self.server.judges.judge(id, problem, language, source, priority)
|
||||||
|
return {'name': 'submission-received', 'submission-id': id}
|
||||||
|
|
||||||
|
def on_termination(self, data):
|
||||||
|
return {'name': 'submission-received', 'judge-aborted': self.server.judges.abort(data['submission-id'])}
|
||||||
|
|
||||||
|
def on_disconnect(self, data):
|
||||||
|
judge_id = data['judge-id']
|
||||||
|
force = data['force']
|
||||||
|
self.server.judges.disconnect(judge_id, force=force)
|
||||||
|
|
||||||
|
def on_malformed(self, packet):
|
||||||
|
logger.error('Malformed packet: %s', packet)
|
||||||
|
|
||||||
|
def on_close(self):
|
||||||
|
self._to_kill = False
|
7
judge/bridge/djangoserver.py
Normal file
7
judge/bridge/djangoserver.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from event_socket_server import get_preferred_engine
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoServer(get_preferred_engine()):
|
||||||
|
def __init__(self, judges, *args, **kwargs):
|
||||||
|
super(DjangoServer, self).__init__(*args, **kwargs)
|
||||||
|
self.judges = judges
|
|
@ -1,82 +0,0 @@
|
||||||
import os
|
|
||||||
import socket
|
|
||||||
import struct
|
|
||||||
import time
|
|
||||||
import zlib
|
|
||||||
|
|
||||||
size_pack = struct.Struct("!I")
|
|
||||||
|
|
||||||
|
|
||||||
def open_connection():
|
|
||||||
sock = socket.create_connection((host, port))
|
|
||||||
return sock
|
|
||||||
|
|
||||||
|
|
||||||
def zlibify(data):
|
|
||||||
data = zlib.compress(data.encode("utf-8"))
|
|
||||||
return size_pack.pack(len(data)) + data
|
|
||||||
|
|
||||||
|
|
||||||
def dezlibify(data, skip_head=True):
|
|
||||||
if skip_head:
|
|
||||||
data = data[size_pack.size :]
|
|
||||||
return zlib.decompress(data).decode("utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
global host, port
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("-l", "--host", default="localhost")
|
|
||||||
parser.add_argument("-p", "--port", default=9999, type=int)
|
|
||||||
args = parser.parse_args()
|
|
||||||
host, port = args.host, args.port
|
|
||||||
|
|
||||||
print("Opening idle connection:", end=" ")
|
|
||||||
s1 = open_connection()
|
|
||||||
print("Success")
|
|
||||||
print("Opening hello world connection:", end=" ")
|
|
||||||
s2 = open_connection()
|
|
||||||
print("Success")
|
|
||||||
print("Sending Hello, World!", end=" ")
|
|
||||||
s2.sendall(zlibify("Hello, World!"))
|
|
||||||
print("Success")
|
|
||||||
print("Testing blank connection:", end=" ")
|
|
||||||
s3 = open_connection()
|
|
||||||
s3.close()
|
|
||||||
print("Success")
|
|
||||||
result = dezlibify(s2.recv(1024))
|
|
||||||
assert result == "Hello, World!"
|
|
||||||
print(result)
|
|
||||||
s2.close()
|
|
||||||
print("Large random data test:", end=" ")
|
|
||||||
s4 = open_connection()
|
|
||||||
data = os.urandom(1000000).decode("iso-8859-1")
|
|
||||||
print("Generated", end=" ")
|
|
||||||
s4.sendall(zlibify(data))
|
|
||||||
print("Sent", end=" ")
|
|
||||||
result = b""
|
|
||||||
while len(result) < size_pack.size:
|
|
||||||
result += s4.recv(1024)
|
|
||||||
size = size_pack.unpack(result[: size_pack.size])[0]
|
|
||||||
result = result[size_pack.size :]
|
|
||||||
while len(result) < size:
|
|
||||||
result += s4.recv(1024)
|
|
||||||
print("Received", end=" ")
|
|
||||||
assert dezlibify(result, False) == data
|
|
||||||
print("Success")
|
|
||||||
s4.close()
|
|
||||||
print("Test malformed connection:", end=" ")
|
|
||||||
s5 = open_connection()
|
|
||||||
s5.sendall(data[:100000].encode("utf-8"))
|
|
||||||
s5.close()
|
|
||||||
print("Success")
|
|
||||||
print("Waiting for timeout to close idle connection:", end=" ")
|
|
||||||
time.sleep(6)
|
|
||||||
print("Done")
|
|
||||||
s1.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
|
@ -1,42 +0,0 @@
|
||||||
from judge.bridge.base_handler import ZlibPacketHandler
|
|
||||||
|
|
||||||
|
|
||||||
class EchoPacketHandler(ZlibPacketHandler):
|
|
||||||
def on_connect(self):
|
|
||||||
print("New client:", self.client_address)
|
|
||||||
self.timeout = 5
|
|
||||||
|
|
||||||
def on_timeout(self):
|
|
||||||
print("Inactive client:", self.client_address)
|
|
||||||
|
|
||||||
def on_packet(self, data):
|
|
||||||
self.timeout = None
|
|
||||||
print(
|
|
||||||
"Data from %s: %r"
|
|
||||||
% (self.client_address, data[:30] if len(data) > 30 else data)
|
|
||||||
)
|
|
||||||
self.send(data)
|
|
||||||
|
|
||||||
def on_disconnect(self):
|
|
||||||
print("Closed client:", self.client_address)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
import argparse
|
|
||||||
from judge.bridge.server import Server
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("-l", "--host", action="append")
|
|
||||||
parser.add_argument("-p", "--port", type=int, action="append")
|
|
||||||
parser.add_argument("-P", "--proxy", action="append")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
class Handler(EchoPacketHandler):
|
|
||||||
proxies = args.proxy or []
|
|
||||||
|
|
||||||
server = Server(list(zip(args.host, args.port)), Handler)
|
|
||||||
server.serve_forever()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
|
@ -1,966 +0,0 @@
|
||||||
import hmac
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from collections import deque, namedtuple
|
|
||||||
from operator import itemgetter
|
|
||||||
|
|
||||||
from django import db
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.db.models import F
|
|
||||||
from django.core.cache import cache
|
|
||||||
|
|
||||||
from judge import event_poster as event
|
|
||||||
from judge.bridge.base_handler import ZlibPacketHandler, proxy_list
|
|
||||||
from judge.utils.problems import finished_submission
|
|
||||||
from judge.models import (
|
|
||||||
Judge,
|
|
||||||
Language,
|
|
||||||
LanguageLimit,
|
|
||||||
Problem,
|
|
||||||
RuntimeVersion,
|
|
||||||
Submission,
|
|
||||||
SubmissionTestCase,
|
|
||||||
)
|
|
||||||
from judge.bridge.utils import VanishedSubmission
|
|
||||||
from judge.caching import cache_wrapper
|
|
||||||
|
|
||||||
logger = logging.getLogger("judge.bridge")
|
|
||||||
json_log = logging.getLogger("judge.json.bridge")
|
|
||||||
|
|
||||||
UPDATE_RATE_LIMIT = 5
|
|
||||||
UPDATE_RATE_TIME = 0.5
|
|
||||||
SubmissionData = namedtuple(
|
|
||||||
"SubmissionData",
|
|
||||||
"time memory short_circuit pretests_only contest_no attempt_no user_id",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_connection():
|
|
||||||
db.connection.close_if_unusable_or_obsolete()
|
|
||||||
|
|
||||||
|
|
||||||
class JudgeHandler(ZlibPacketHandler):
|
|
||||||
proxies = proxy_list(settings.BRIDGED_JUDGE_PROXIES or [])
|
|
||||||
|
|
||||||
def __init__(self, request, client_address, server, judges):
|
|
||||||
super().__init__(request, client_address, server)
|
|
||||||
|
|
||||||
self.judges = judges
|
|
||||||
self.handlers = {
|
|
||||||
"grading-begin": self.on_grading_begin,
|
|
||||||
"grading-end": self.on_grading_end,
|
|
||||||
"compile-error": self.on_compile_error,
|
|
||||||
"compile-message": self.on_compile_message,
|
|
||||||
"batch-begin": self.on_batch_begin,
|
|
||||||
"batch-end": self.on_batch_end,
|
|
||||||
"test-case-status": self.on_test_case,
|
|
||||||
"internal-error": self.on_internal_error,
|
|
||||||
"submission-terminated": self.on_submission_terminated,
|
|
||||||
"submission-acknowledged": self.on_submission_acknowledged,
|
|
||||||
"ping-response": self.on_ping_response,
|
|
||||||
"supported-problems": self.on_supported_problems,
|
|
||||||
"handshake": self.on_handshake,
|
|
||||||
}
|
|
||||||
self._working = False
|
|
||||||
self._working_data = {}
|
|
||||||
self._no_response_job = None
|
|
||||||
self.executors = {}
|
|
||||||
self.problems = set()
|
|
||||||
self.latency = None
|
|
||||||
self.time_delta = None
|
|
||||||
self.load = 1e100
|
|
||||||
self.name = None
|
|
||||||
self.batch_id = None
|
|
||||||
self.in_batch = False
|
|
||||||
self._stop_ping = threading.Event()
|
|
||||||
self._ping_average = deque(maxlen=6) # 1 minute average, just like load
|
|
||||||
self._time_delta = deque(maxlen=6)
|
|
||||||
|
|
||||||
# each value is (updates, last reset)
|
|
||||||
self.update_counter = {}
|
|
||||||
self.judge = None
|
|
||||||
self.judge_address = None
|
|
||||||
|
|
||||||
self._submission_cache_id = None
|
|
||||||
self._submission_cache = {}
|
|
||||||
|
|
||||||
def on_connect(self):
|
|
||||||
self.timeout = 15
|
|
||||||
logger.info("Judge connected from: %s", self.client_address)
|
|
||||||
json_log.info(self._make_json_log(action="connect"))
|
|
||||||
|
|
||||||
def on_disconnect(self):
|
|
||||||
self._stop_ping.set()
|
|
||||||
self.judges.remove(self)
|
|
||||||
if self.name is not None:
|
|
||||||
self._disconnected()
|
|
||||||
logger.info(
|
|
||||||
"Judge disconnected from: %s with name %s", self.client_address, self.name
|
|
||||||
)
|
|
||||||
|
|
||||||
json_log.info(
|
|
||||||
self._make_json_log(action="disconnect", info="judge disconnected")
|
|
||||||
)
|
|
||||||
if self._working:
|
|
||||||
self.judges.judge(
|
|
||||||
self._working,
|
|
||||||
self._working_data["problem"],
|
|
||||||
self._working_data["language"],
|
|
||||||
self._working_data["source"],
|
|
||||||
None,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _authenticate(self, id, key):
|
|
||||||
try:
|
|
||||||
judge = Judge.objects.get(name=id)
|
|
||||||
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
|
|
||||||
else:
|
|
||||||
result = hmac.compare_digest(judge.auth_key, key)
|
|
||||||
|
|
||||||
if not result:
|
|
||||||
json_log.warning(
|
|
||||||
self._make_json_log(
|
|
||||||
action="auth", judge=id, info="judge failed authentication"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
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):
|
|
||||||
judge = self.judge = Judge.objects.get(name=self.name)
|
|
||||||
judge.start_time = timezone.now()
|
|
||||||
judge.online = True
|
|
||||||
self._update_judge_problems()
|
|
||||||
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
|
|
||||||
RuntimeVersion.objects.filter(judge=judge).delete()
|
|
||||||
versions = []
|
|
||||||
for lang in judge.runtimes.all():
|
|
||||||
versions += [
|
|
||||||
RuntimeVersion(
|
|
||||||
language=lang,
|
|
||||||
name=name,
|
|
||||||
version=".".join(map(str, version)),
|
|
||||||
priority=idx,
|
|
||||||
judge=judge,
|
|
||||||
)
|
|
||||||
for idx, (name, version) in enumerate(self.executors[lang.key])
|
|
||||||
]
|
|
||||||
RuntimeVersion.objects.bulk_create(versions)
|
|
||||||
judge.last_ip = self.client_address[0]
|
|
||||||
judge.save()
|
|
||||||
self.judge_address = "[%s]:%s" % (
|
|
||||||
self.client_address[0],
|
|
||||||
self.client_address[1],
|
|
||||||
)
|
|
||||||
json_log.info(
|
|
||||||
self._make_json_log(
|
|
||||||
action="auth",
|
|
||||||
info="judge successfully authenticated",
|
|
||||||
executors=list(self.executors.keys()),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _disconnected(self):
|
|
||||||
Judge.objects.filter(id=self.judge.id).update(online=False)
|
|
||||||
RuntimeVersion.objects.filter(judge=self.judge).delete()
|
|
||||||
self.judge.problems.clear()
|
|
||||||
_get_judge_problems.dirty(self.judge)
|
|
||||||
|
|
||||||
def _update_ping(self):
|
|
||||||
try:
|
|
||||||
Judge.objects.filter(name=self.name).update(
|
|
||||||
ping=self.latency, load=self.load
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
# What can I do? I don't want to tie this to MySQL.
|
|
||||||
if (
|
|
||||||
e.__class__.__name__ == "OperationalError"
|
|
||||||
and e.__module__ == "_mysql_exceptions"
|
|
||||||
and e.args[0] == 2006
|
|
||||||
):
|
|
||||||
db.connection.close()
|
|
||||||
|
|
||||||
def send(self, data):
|
|
||||||
super().send(json.dumps(data, separators=(",", ":")))
|
|
||||||
|
|
||||||
def on_handshake(self, packet):
|
|
||||||
if "id" not in packet or "key" not in packet:
|
|
||||||
logger.warning("Malformed handshake: %s", self.client_address)
|
|
||||||
self.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self._authenticate(packet["id"], packet["key"]):
|
|
||||||
logger.warning("Authentication failure: %s", self.client_address)
|
|
||||||
self.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
self.timeout = 60
|
|
||||||
self._update_supported_problems(packet["problems"])
|
|
||||||
self.executors = packet["executors"]
|
|
||||||
self.name = packet["id"]
|
|
||||||
|
|
||||||
self.send({"name": "handshake-success"})
|
|
||||||
logger.info("Judge authenticated: %s (%s)", self.client_address, packet["id"])
|
|
||||||
self.judges.register(self)
|
|
||||||
threading.Thread(target=self._ping_thread).start()
|
|
||||||
self._connected()
|
|
||||||
|
|
||||||
def can_judge(self, problem, executor, judge_id=None):
|
|
||||||
return (
|
|
||||||
problem in self.problems
|
|
||||||
and executor in self.executors
|
|
||||||
and (not judge_id or self.name == judge_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def working(self):
|
|
||||||
return bool(self._working)
|
|
||||||
|
|
||||||
def get_related_submission_data(self, submission):
|
|
||||||
_ensure_connection()
|
|
||||||
|
|
||||||
try:
|
|
||||||
(
|
|
||||||
pid,
|
|
||||||
time,
|
|
||||||
memory,
|
|
||||||
short_circuit,
|
|
||||||
lid,
|
|
||||||
is_pretested,
|
|
||||||
sub_date,
|
|
||||||
uid,
|
|
||||||
part_virtual,
|
|
||||||
part_id,
|
|
||||||
) = (
|
|
||||||
Submission.objects.filter(id=submission).values_list(
|
|
||||||
"problem__id",
|
|
||||||
"problem__time_limit",
|
|
||||||
"problem__memory_limit",
|
|
||||||
"problem__short_circuit",
|
|
||||||
"language__id",
|
|
||||||
"is_pretested",
|
|
||||||
"date",
|
|
||||||
"user__id",
|
|
||||||
"contest__participation__virtual",
|
|
||||||
"contest__participation__id",
|
|
||||||
)
|
|
||||||
).get()
|
|
||||||
except Submission.DoesNotExist:
|
|
||||||
logger.error("Submission vanished: %s", submission)
|
|
||||||
json_log.error(
|
|
||||||
self._make_json_log(
|
|
||||||
sub=self._working,
|
|
||||||
action="request",
|
|
||||||
info="submission vanished when fetching info",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
attempt_no = (
|
|
||||||
Submission.objects.filter(
|
|
||||||
problem__id=pid,
|
|
||||||
contest__participation__id=part_id,
|
|
||||||
user__id=uid,
|
|
||||||
date__lt=sub_date,
|
|
||||||
)
|
|
||||||
.exclude(status__in=("CE", "IE"))
|
|
||||||
.count()
|
|
||||||
+ 1
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
time, memory = (
|
|
||||||
LanguageLimit.objects.filter(problem__id=pid, language__id=lid)
|
|
||||||
.values_list("time_limit", "memory_limit")
|
|
||||||
.get()
|
|
||||||
)
|
|
||||||
except LanguageLimit.DoesNotExist:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return SubmissionData(
|
|
||||||
time=time,
|
|
||||||
memory=memory,
|
|
||||||
short_circuit=short_circuit,
|
|
||||||
pretests_only=is_pretested,
|
|
||||||
contest_no=part_virtual,
|
|
||||||
attempt_no=attempt_no,
|
|
||||||
user_id=uid,
|
|
||||||
)
|
|
||||||
|
|
||||||
def disconnect(self, force=False):
|
|
||||||
if force:
|
|
||||||
# Yank the power out.
|
|
||||||
self.close()
|
|
||||||
else:
|
|
||||||
self.send({"name": "disconnect"})
|
|
||||||
|
|
||||||
def submit(self, id, problem, language, source):
|
|
||||||
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_data = {
|
|
||||||
"problem": problem,
|
|
||||||
"language": language,
|
|
||||||
"source": source,
|
|
||||||
}
|
|
||||||
self._no_response_job = threading.Timer(20, self._kill_if_no_response)
|
|
||||||
self.send(
|
|
||||||
{
|
|
||||||
"name": "submission-request",
|
|
||||||
"submission-id": id,
|
|
||||||
"problem-id": problem,
|
|
||||||
"language": language,
|
|
||||||
"source": source,
|
|
||||||
"time-limit": data.time,
|
|
||||||
"memory-limit": data.memory,
|
|
||||||
"short-circuit": data.short_circuit,
|
|
||||||
"meta": {
|
|
||||||
"pretests-only": data.pretests_only,
|
|
||||||
"in-contest": data.contest_no,
|
|
||||||
"attempt-no": data.attempt_no,
|
|
||||||
"user": data.user_id,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def _kill_if_no_response(self):
|
|
||||||
logger.error(
|
|
||||||
"Judge failed to acknowledge submission: %s: %s", self.name, self._working
|
|
||||||
)
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def on_timeout(self):
|
|
||||||
if self.name:
|
|
||||||
logger.warning("Judge seems dead: %s: %s", self.name, self._working)
|
|
||||||
|
|
||||||
def malformed_packet(self, exception):
|
|
||||||
logger.exception("Judge sent malformed packet: %s", self.name)
|
|
||||||
super(JudgeHandler, self).malformed_packet(exception)
|
|
||||||
|
|
||||||
def on_submission_processing(self, packet):
|
|
||||||
_ensure_connection()
|
|
||||||
|
|
||||||
id = packet["submission-id"]
|
|
||||||
if Submission.objects.filter(id=id).update(status="P", judged_on=self.judge):
|
|
||||||
event.post("sub_%s" % Submission.get_id_secret(id), {"type": "processing"})
|
|
||||||
self._post_update_submission(id, "processing")
|
|
||||||
json_log.info(self._make_json_log(packet, action="processing"))
|
|
||||||
else:
|
|
||||||
logger.warning("Unknown submission: %s", id)
|
|
||||||
json_log.error(
|
|
||||||
self._make_json_log(
|
|
||||||
packet, action="processing", info="unknown submission"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_submission_wrong_acknowledge(self, packet, expected, got):
|
|
||||||
json_log.error(
|
|
||||||
self._make_json_log(
|
|
||||||
packet, action="processing", info="wrong-acknowledge", expected=expected
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Submission.objects.filter(id=expected).update(
|
|
||||||
status="IE", result="IE", error=None
|
|
||||||
)
|
|
||||||
Submission.objects.filter(id=got, status="QU").update(
|
|
||||||
status="IE", result="IE", error=None
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_submission_acknowledged(self, packet):
|
|
||||||
if not packet.get("submission-id", None) == self._working:
|
|
||||||
logger.error(
|
|
||||||
"Wrong acknowledgement: %s: %s, expected: %s",
|
|
||||||
self.name,
|
|
||||||
packet.get("submission-id", None),
|
|
||||||
self._working,
|
|
||||||
)
|
|
||||||
self.on_submission_wrong_acknowledge(
|
|
||||||
packet, self._working, packet.get("submission-id", None)
|
|
||||||
)
|
|
||||||
self.close()
|
|
||||||
logger.info("Submission acknowledged: %d", self._working)
|
|
||||||
if self._no_response_job:
|
|
||||||
self._no_response_job.cancel()
|
|
||||||
self._no_response_job = None
|
|
||||||
self.on_submission_processing(packet)
|
|
||||||
|
|
||||||
def abort(self):
|
|
||||||
self.send({"name": "terminate-submission"})
|
|
||||||
|
|
||||||
def get_current_submission(self):
|
|
||||||
return self._working or None
|
|
||||||
|
|
||||||
def ping(self):
|
|
||||||
self.send({"name": "ping", "when": time.time()})
|
|
||||||
|
|
||||||
def on_packet(self, data):
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
data = json.loads(data)
|
|
||||||
if "name" not in data:
|
|
||||||
raise ValueError
|
|
||||||
except ValueError:
|
|
||||||
self.on_malformed(data)
|
|
||||||
else:
|
|
||||||
handler = self.handlers.get(data["name"], self.on_malformed)
|
|
||||||
handler(data)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Error in packet handling (Judge-side): %s", self.name)
|
|
||||||
self._packet_exception()
|
|
||||||
# You can't crash here because you aren't so sure about the judges
|
|
||||||
# not being malicious or simply malforms. THIS IS A SERVER!
|
|
||||||
|
|
||||||
def _packet_exception(self):
|
|
||||||
json_log.exception(
|
|
||||||
self._make_json_log(sub=self._working, info="packet processing exception")
|
|
||||||
)
|
|
||||||
|
|
||||||
def _submission_is_batch(self, id):
|
|
||||||
if not Submission.objects.filter(id=id).update(batch=True):
|
|
||||||
logger.warning("Unknown submission: %s", id)
|
|
||||||
|
|
||||||
def on_supported_problems(self, packet):
|
|
||||||
logger.info("%s: Updated problem list", self.name)
|
|
||||||
self._update_supported_problems(packet["problems"])
|
|
||||||
|
|
||||||
if not self.working:
|
|
||||||
self.judges.update_problems(self)
|
|
||||||
|
|
||||||
self._update_judge_problems()
|
|
||||||
json_log.info(
|
|
||||||
self._make_json_log(action="update-problems", count=len(self.problems))
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_grading_begin(self, packet):
|
|
||||||
logger.info("%s: Grading has begun on: %s", self.name, packet["submission-id"])
|
|
||||||
self.batch_id = None
|
|
||||||
|
|
||||||
if Submission.objects.filter(id=packet["submission-id"]).update(
|
|
||||||
status="G",
|
|
||||||
is_pretested=packet["pretested"],
|
|
||||||
current_testcase=1,
|
|
||||||
points=0,
|
|
||||||
batch=False,
|
|
||||||
judged_date=timezone.now(),
|
|
||||||
):
|
|
||||||
SubmissionTestCase.objects.filter(
|
|
||||||
submission_id=packet["submission-id"]
|
|
||||||
).delete()
|
|
||||||
event.post(
|
|
||||||
"sub_%s" % Submission.get_id_secret(packet["submission-id"]),
|
|
||||||
{"type": "grading-begin"},
|
|
||||||
)
|
|
||||||
self._post_update_submission(packet["submission-id"], "grading-begin")
|
|
||||||
json_log.info(self._make_json_log(packet, action="grading-begin"))
|
|
||||||
else:
|
|
||||||
logger.warning("Unknown submission: %s", packet["submission-id"])
|
|
||||||
json_log.error(
|
|
||||||
self._make_json_log(
|
|
||||||
packet, action="grading-begin", info="unknown submission"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_grading_end(self, packet):
|
|
||||||
logger.info("%s: Grading has ended on: %s", self.name, packet["submission-id"])
|
|
||||||
self._free_self(packet)
|
|
||||||
self.batch_id = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
submission = Submission.objects.get(id=packet["submission-id"])
|
|
||||||
except Submission.DoesNotExist:
|
|
||||||
logger.warning("Unknown submission: %s", packet["submission-id"])
|
|
||||||
json_log.error(
|
|
||||||
self._make_json_log(
|
|
||||||
packet, action="grading-end", info="unknown submission"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
time = 0
|
|
||||||
memory = 0
|
|
||||||
points = 0.0
|
|
||||||
total = 0
|
|
||||||
status = 0
|
|
||||||
status_codes = ["SC", "AC", "WA", "MLE", "TLE", "IR", "RTE", "OLE"]
|
|
||||||
batches = {} # batch number: (points, total)
|
|
||||||
|
|
||||||
for case in SubmissionTestCase.objects.filter(submission=submission):
|
|
||||||
time += case.time
|
|
||||||
if not case.batch:
|
|
||||||
points += case.points
|
|
||||||
total += case.total
|
|
||||||
else:
|
|
||||||
if case.batch in batches:
|
|
||||||
batches[case.batch][0] += case.points
|
|
||||||
batches[case.batch][1] += case.total
|
|
||||||
else:
|
|
||||||
batches[case.batch] = [case.points, case.total]
|
|
||||||
memory = max(memory, case.memory)
|
|
||||||
i = status_codes.index(case.status)
|
|
||||||
if i > status:
|
|
||||||
status = i
|
|
||||||
|
|
||||||
for i in batches:
|
|
||||||
points += batches[i][0]
|
|
||||||
total += batches[i][1]
|
|
||||||
|
|
||||||
points = points
|
|
||||||
total = total
|
|
||||||
submission.case_points = points
|
|
||||||
submission.case_total = total
|
|
||||||
|
|
||||||
problem = submission.problem
|
|
||||||
sub_points = round(points / total * problem.points if total > 0 else 0, 3)
|
|
||||||
if not problem.partial and sub_points != problem.points:
|
|
||||||
sub_points = 0
|
|
||||||
|
|
||||||
submission.status = "D"
|
|
||||||
submission.time = time
|
|
||||||
submission.memory = memory
|
|
||||||
submission.points = sub_points
|
|
||||||
submission.result = status_codes[status]
|
|
||||||
submission.save()
|
|
||||||
|
|
||||||
json_log.info(
|
|
||||||
self._make_json_log(
|
|
||||||
packet,
|
|
||||||
action="grading-end",
|
|
||||||
time=time,
|
|
||||||
memory=memory,
|
|
||||||
points=sub_points,
|
|
||||||
total=problem.points,
|
|
||||||
result=submission.result,
|
|
||||||
case_points=points,
|
|
||||||
case_total=total,
|
|
||||||
user=submission.user_id,
|
|
||||||
problem=problem.code,
|
|
||||||
finish=True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
submission.user._updating_stats_only = True
|
|
||||||
submission.user.calculate_points()
|
|
||||||
problem._updating_stats_only = True
|
|
||||||
problem.update_stats()
|
|
||||||
submission.update_contest()
|
|
||||||
|
|
||||||
finished_submission(submission)
|
|
||||||
|
|
||||||
event.post(
|
|
||||||
"sub_%s" % submission.id_secret,
|
|
||||||
{
|
|
||||||
"type": "grading-end",
|
|
||||||
"time": time,
|
|
||||||
"memory": memory,
|
|
||||||
"points": float(points),
|
|
||||||
"total": float(problem.points),
|
|
||||||
"result": submission.result,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if hasattr(submission, "contest"):
|
|
||||||
participation = submission.contest.participation
|
|
||||||
event.post("contest_%d" % participation.contest_id, {"type": "update"})
|
|
||||||
self._post_update_submission(submission.id, "grading-end", done=True)
|
|
||||||
|
|
||||||
def on_compile_error(self, packet):
|
|
||||||
logger.info(
|
|
||||||
"%s: Submission failed to compile: %s", self.name, packet["submission-id"]
|
|
||||||
)
|
|
||||||
self._free_self(packet)
|
|
||||||
|
|
||||||
if Submission.objects.filter(id=packet["submission-id"]).update(
|
|
||||||
status="CE", result="CE", error=packet["log"]
|
|
||||||
):
|
|
||||||
event.post(
|
|
||||||
"sub_%s" % Submission.get_id_secret(packet["submission-id"]),
|
|
||||||
{
|
|
||||||
"type": "compile-error",
|
|
||||||
"log": packet["log"],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self._post_update_submission(
|
|
||||||
packet["submission-id"], "compile-error", done=True
|
|
||||||
)
|
|
||||||
json_log.info(
|
|
||||||
self._make_json_log(
|
|
||||||
packet,
|
|
||||||
action="compile-error",
|
|
||||||
log=packet["log"],
|
|
||||||
finish=True,
|
|
||||||
result="CE",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.warning("Unknown submission: %s", packet["submission-id"])
|
|
||||||
json_log.error(
|
|
||||||
self._make_json_log(
|
|
||||||
packet,
|
|
||||||
action="compile-error",
|
|
||||||
info="unknown submission",
|
|
||||||
log=packet["log"],
|
|
||||||
finish=True,
|
|
||||||
result="CE",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_compile_message(self, packet):
|
|
||||||
logger.info(
|
|
||||||
"%s: Submission generated compiler messages: %s",
|
|
||||||
self.name,
|
|
||||||
packet["submission-id"],
|
|
||||||
)
|
|
||||||
|
|
||||||
if Submission.objects.filter(id=packet["submission-id"]).update(
|
|
||||||
error=packet["log"]
|
|
||||||
):
|
|
||||||
event.post(
|
|
||||||
"sub_%s" % Submission.get_id_secret(packet["submission-id"]),
|
|
||||||
{"type": "compile-message"},
|
|
||||||
)
|
|
||||||
json_log.info(
|
|
||||||
self._make_json_log(packet, action="compile-message", log=packet["log"])
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.warning("Unknown submission: %s", packet["submission-id"])
|
|
||||||
json_log.error(
|
|
||||||
self._make_json_log(
|
|
||||||
packet,
|
|
||||||
action="compile-message",
|
|
||||||
info="unknown submission",
|
|
||||||
log=packet["log"],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_internal_error(self, packet):
|
|
||||||
try:
|
|
||||||
raise ValueError("\n\n" + packet["message"])
|
|
||||||
except ValueError:
|
|
||||||
logger.exception(
|
|
||||||
"Judge %s failed while handling submission %s",
|
|
||||||
self.name,
|
|
||||||
packet["submission-id"],
|
|
||||||
)
|
|
||||||
self._free_self(packet)
|
|
||||||
|
|
||||||
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(
|
|
||||||
status="IE", result="IE", error=message
|
|
||||||
):
|
|
||||||
event.post(
|
|
||||||
"sub_%s" % Submission.get_id_secret(id), {"type": "internal-error"}
|
|
||||||
)
|
|
||||||
self._post_update_submission(id, "internal-error", done=True)
|
|
||||||
json_log.info(
|
|
||||||
self._make_json_log(
|
|
||||||
sub=id,
|
|
||||||
action="internal-error",
|
|
||||||
message=message,
|
|
||||||
finish=True,
|
|
||||||
result="IE",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.warning("Unknown submission: %s", id)
|
|
||||||
json_log.error(
|
|
||||||
self._make_json_log(
|
|
||||||
sub=id,
|
|
||||||
action="internal-error",
|
|
||||||
info="unknown submission",
|
|
||||||
message=message,
|
|
||||||
finish=True,
|
|
||||||
result="IE",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_submission_terminated(self, packet):
|
|
||||||
logger.info("%s: Submission aborted: %s", self.name, packet["submission-id"])
|
|
||||||
self._free_self(packet)
|
|
||||||
|
|
||||||
if Submission.objects.filter(id=packet["submission-id"]).update(
|
|
||||||
status="AB", result="AB"
|
|
||||||
):
|
|
||||||
event.post(
|
|
||||||
"sub_%s" % Submission.get_id_secret(packet["submission-id"]),
|
|
||||||
{"type": "aborted-submission"},
|
|
||||||
)
|
|
||||||
self._post_update_submission(
|
|
||||||
packet["submission-id"], "terminated", done=True
|
|
||||||
)
|
|
||||||
json_log.info(
|
|
||||||
self._make_json_log(packet, action="aborted", finish=True, result="AB")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.warning("Unknown submission: %s", packet["submission-id"])
|
|
||||||
json_log.error(
|
|
||||||
self._make_json_log(
|
|
||||||
packet,
|
|
||||||
action="aborted",
|
|
||||||
info="unknown submission",
|
|
||||||
finish=True,
|
|
||||||
result="AB",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_batch_begin(self, packet):
|
|
||||||
logger.info("%s: Batch began on: %s", self.name, packet["submission-id"])
|
|
||||||
self.in_batch = True
|
|
||||||
if self.batch_id is None:
|
|
||||||
self.batch_id = 0
|
|
||||||
self._submission_is_batch(packet["submission-id"])
|
|
||||||
self.batch_id += 1
|
|
||||||
|
|
||||||
json_log.info(
|
|
||||||
self._make_json_log(packet, action="batch-begin", batch=self.batch_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_batch_end(self, packet):
|
|
||||||
self.in_batch = False
|
|
||||||
logger.info("%s: Batch ended on: %s", self.name, packet["submission-id"])
|
|
||||||
json_log.info(
|
|
||||||
self._make_json_log(packet, action="batch-end", batch=self.batch_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_test_case(
|
|
||||||
self,
|
|
||||||
packet,
|
|
||||||
max_feedback=SubmissionTestCase._meta.get_field("feedback").max_length,
|
|
||||||
):
|
|
||||||
logger.info(
|
|
||||||
"%s: %d test case(s) completed on: %s",
|
|
||||||
self.name,
|
|
||||||
len(packet["cases"]),
|
|
||||||
packet["submission-id"],
|
|
||||||
)
|
|
||||||
|
|
||||||
id = packet["submission-id"]
|
|
||||||
updates = packet["cases"]
|
|
||||||
max_position = max(map(itemgetter("position"), updates))
|
|
||||||
sum_points = sum(map(itemgetter("points"), updates))
|
|
||||||
|
|
||||||
if not Submission.objects.filter(id=id).update(
|
|
||||||
current_testcase=max_position + 1, points=F("points") + sum_points
|
|
||||||
):
|
|
||||||
logger.warning("Unknown submission: %s", id)
|
|
||||||
json_log.error(
|
|
||||||
self._make_json_log(
|
|
||||||
packet, action="test-case", info="unknown submission"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
bulk_test_case_updates = []
|
|
||||||
for result in updates:
|
|
||||||
test_case = SubmissionTestCase(submission_id=id, case=result["position"])
|
|
||||||
status = result["status"]
|
|
||||||
if status & 4:
|
|
||||||
test_case.status = "TLE"
|
|
||||||
elif status & 8:
|
|
||||||
test_case.status = "MLE"
|
|
||||||
elif status & 64:
|
|
||||||
test_case.status = "OLE"
|
|
||||||
elif status & 2:
|
|
||||||
test_case.status = "RTE"
|
|
||||||
elif status & 16:
|
|
||||||
test_case.status = "IR"
|
|
||||||
elif status & 1:
|
|
||||||
test_case.status = "WA"
|
|
||||||
elif status & 32:
|
|
||||||
test_case.status = "SC"
|
|
||||||
else:
|
|
||||||
test_case.status = "AC"
|
|
||||||
test_case.time = result["time"]
|
|
||||||
test_case.memory = result["memory"]
|
|
||||||
test_case.points = result["points"]
|
|
||||||
test_case.total = result["total-points"]
|
|
||||||
test_case.batch = self.batch_id if self.in_batch else None
|
|
||||||
test_case.feedback = (result.get("feedback") or "")[:max_feedback]
|
|
||||||
test_case.extended_feedback = result.get("extended-feedback") or ""
|
|
||||||
test_case.output = result["output"]
|
|
||||||
bulk_test_case_updates.append(test_case)
|
|
||||||
|
|
||||||
json_log.info(
|
|
||||||
self._make_json_log(
|
|
||||||
packet,
|
|
||||||
action="test-case",
|
|
||||||
case=test_case.case,
|
|
||||||
batch=test_case.batch,
|
|
||||||
time=test_case.time,
|
|
||||||
memory=test_case.memory,
|
|
||||||
feedback=test_case.feedback,
|
|
||||||
extended_feedback=test_case.extended_feedback,
|
|
||||||
output=test_case.output,
|
|
||||||
points=test_case.points,
|
|
||||||
total=test_case.total,
|
|
||||||
status=test_case.status,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
do_post = True
|
|
||||||
|
|
||||||
if id in self.update_counter:
|
|
||||||
cnt, reset = self.update_counter[id]
|
|
||||||
cnt += 1
|
|
||||||
if time.monotonic() - reset > UPDATE_RATE_TIME:
|
|
||||||
del self.update_counter[id]
|
|
||||||
else:
|
|
||||||
self.update_counter[id] = (cnt, reset)
|
|
||||||
if cnt > UPDATE_RATE_LIMIT:
|
|
||||||
do_post = False
|
|
||||||
if id not in self.update_counter:
|
|
||||||
self.update_counter[id] = (1, time.monotonic())
|
|
||||||
|
|
||||||
if do_post:
|
|
||||||
event.post(
|
|
||||||
"sub_%s" % Submission.get_id_secret(id),
|
|
||||||
{
|
|
||||||
"type": "test-case",
|
|
||||||
"id": max_position,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self._post_update_submission(id, state="test-case")
|
|
||||||
|
|
||||||
SubmissionTestCase.objects.bulk_create(bulk_test_case_updates)
|
|
||||||
|
|
||||||
def on_malformed(self, packet):
|
|
||||||
logger.error("%s: Malformed packet: %s", self.name, packet)
|
|
||||||
json_log.exception(
|
|
||||||
self._make_json_log(sub=self._working, info="malformed json packet")
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_ping_response(self, packet):
|
|
||||||
end = time.time()
|
|
||||||
self._ping_average.append(end - packet["when"])
|
|
||||||
self._time_delta.append((end + packet["when"]) / 2 - packet["time"])
|
|
||||||
self.latency = sum(self._ping_average) / len(self._ping_average)
|
|
||||||
self.time_delta = sum(self._time_delta) / len(self._time_delta)
|
|
||||||
self.load = packet["load"]
|
|
||||||
self._update_ping()
|
|
||||||
|
|
||||||
def _free_self(self, packet):
|
|
||||||
self.judges.on_judge_free(self, packet["submission-id"])
|
|
||||||
|
|
||||||
def _ping_thread(self):
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
self.ping()
|
|
||||||
if self._stop_ping.wait(10):
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Ping error in %s", self.name)
|
|
||||||
self.close()
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _make_json_log(self, packet=None, sub=None, **kwargs):
|
|
||||||
data = {
|
|
||||||
"judge": self.name,
|
|
||||||
"address": self.judge_address,
|
|
||||||
}
|
|
||||||
if sub is None and packet is not None:
|
|
||||||
sub = packet.get("submission-id")
|
|
||||||
if sub is not None:
|
|
||||||
data["submission"] = sub
|
|
||||||
data.update(kwargs)
|
|
||||||
return json.dumps(data)
|
|
||||||
|
|
||||||
def _post_update_submission(self, id, state, done=False):
|
|
||||||
if self._submission_cache_id == id:
|
|
||||||
data = self._submission_cache
|
|
||||||
else:
|
|
||||||
self._submission_cache = data = (
|
|
||||||
Submission.objects.filter(id=id)
|
|
||||||
.values(
|
|
||||||
"problem__is_public",
|
|
||||||
"contest_object__key",
|
|
||||||
"user_id",
|
|
||||||
"problem_id",
|
|
||||||
"status",
|
|
||||||
"language__key",
|
|
||||||
)
|
|
||||||
.get()
|
|
||||||
)
|
|
||||||
self._submission_cache_id = id
|
|
||||||
|
|
||||||
if data["problem__is_public"]:
|
|
||||||
event.post(
|
|
||||||
"submissions",
|
|
||||||
{
|
|
||||||
"type": "done-submission" if done else "update-submission",
|
|
||||||
"state": state,
|
|
||||||
"id": id,
|
|
||||||
"contest": data["contest_object__key"],
|
|
||||||
"user": data["user_id"],
|
|
||||||
"problem": data["problem_id"],
|
|
||||||
"status": data["status"],
|
|
||||||
"language": data["language__key"],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_cleanup(self):
|
|
||||||
db.connection.close()
|
|
||||||
|
|
||||||
|
|
||||||
@cache_wrapper(prefix="gjp", timeout=3600)
|
|
||||||
def _get_judge_problems(judge):
|
|
||||||
return set(judge.problems.values_list("code", flat=True))
|
|
411
judge/bridge/judgecallback.py
Normal file
411
judge/bridge/judgecallback.py
Normal file
|
@ -0,0 +1,411 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from operator import itemgetter
|
||||||
|
|
||||||
|
from django import db
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from judge import event_poster as event
|
||||||
|
from judge.caching import finished_submission
|
||||||
|
from judge.models import Judge, Language, LanguageLimit, Problem, RuntimeVersion, Submission, SubmissionTestCase
|
||||||
|
from .judgehandler import JudgeHandler, SubmissionData
|
||||||
|
|
||||||
|
logger = logging.getLogger('judge.bridge')
|
||||||
|
json_log = logging.getLogger('judge.json.bridge')
|
||||||
|
|
||||||
|
UPDATE_RATE_LIMIT = 5
|
||||||
|
UPDATE_RATE_TIME = 0.5
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_connection():
|
||||||
|
try:
|
||||||
|
db.connection.cursor().execute('SELECT 1').fetchall()
|
||||||
|
except Exception:
|
||||||
|
db.connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoJudgeHandler(JudgeHandler):
|
||||||
|
def __init__(self, server, socket):
|
||||||
|
super(DjangoJudgeHandler, self).__init__(server, socket)
|
||||||
|
|
||||||
|
# each value is (updates, last reset)
|
||||||
|
self.update_counter = {}
|
||||||
|
self.judge = None
|
||||||
|
self.judge_address = None
|
||||||
|
|
||||||
|
self._submission_cache_id = None
|
||||||
|
self._submission_cache = {}
|
||||||
|
|
||||||
|
json_log.info(self._make_json_log(action='connect'))
|
||||||
|
|
||||||
|
def on_close(self):
|
||||||
|
super(DjangoJudgeHandler, self).on_close()
|
||||||
|
json_log.info(self._make_json_log(action='disconnect', info='judge disconnected'))
|
||||||
|
if self._working:
|
||||||
|
Submission.objects.filter(id=self._working).update(status='IE', result='IE')
|
||||||
|
json_log.error(self._make_json_log(sub=self._working, action='close', info='IE due to shutdown on grading'))
|
||||||
|
|
||||||
|
def on_malformed(self, packet):
|
||||||
|
super(DjangoJudgeHandler, self).on_malformed(packet)
|
||||||
|
json_log.exception(self._make_json_log(sub=self._working, info='malformed zlib packet'))
|
||||||
|
|
||||||
|
def _packet_exception(self):
|
||||||
|
json_log.exception(self._make_json_log(sub=self._working, info='packet processing exception'))
|
||||||
|
|
||||||
|
def get_related_submission_data(self, submission):
|
||||||
|
_ensure_connection() # We are called from the django-facing daemon thread. Guess what happens.
|
||||||
|
|
||||||
|
try:
|
||||||
|
pid, time, memory, short_circuit, lid, is_pretested, sub_date, uid, part_virtual, part_id = (
|
||||||
|
Submission.objects.filter(id=submission)
|
||||||
|
.values_list('problem__id', 'problem__time_limit', 'problem__memory_limit',
|
||||||
|
'problem__short_circuit', 'language__id', 'is_pretested', 'date', 'user__id',
|
||||||
|
'contest__participation__virtual', 'contest__participation__id')).get()
|
||||||
|
except Submission.DoesNotExist:
|
||||||
|
logger.error('Submission vanished: %s', submission)
|
||||||
|
json_log.error(self._make_json_log(
|
||||||
|
sub=self._working, action='request',
|
||||||
|
info='submission vanished when fetching info',
|
||||||
|
))
|
||||||
|
return
|
||||||
|
|
||||||
|
attempt_no = Submission.objects.filter(problem__id=pid, contest__participation__id=part_id, user__id=uid,
|
||||||
|
date__lt=sub_date).exclude(status__in=('CE', 'IE')).count() + 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
time, memory = (LanguageLimit.objects.filter(problem__id=pid, language__id=lid)
|
||||||
|
.values_list('time_limit', 'memory_limit').get())
|
||||||
|
except LanguageLimit.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return SubmissionData(
|
||||||
|
time=time,
|
||||||
|
memory=memory,
|
||||||
|
short_circuit=short_circuit,
|
||||||
|
pretests_only=is_pretested,
|
||||||
|
contest_no=part_virtual,
|
||||||
|
attempt_no=attempt_no,
|
||||||
|
user_id=uid,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _authenticate(self, id, key):
|
||||||
|
result = Judge.objects.filter(name=id, auth_key=key, is_blocked=False).exists()
|
||||||
|
if not result:
|
||||||
|
json_log.warning(self._make_json_log(action='auth', judge=id, info='judge failed authentication'))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _connected(self):
|
||||||
|
judge = self.judge = Judge.objects.get(name=self.name)
|
||||||
|
judge.start_time = timezone.now()
|
||||||
|
judge.online = True
|
||||||
|
judge.problems.set(Problem.objects.filter(code__in=list(self.problems.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
|
||||||
|
RuntimeVersion.objects.filter(judge=judge).delete()
|
||||||
|
versions = []
|
||||||
|
for lang in judge.runtimes.all():
|
||||||
|
versions += [
|
||||||
|
RuntimeVersion(language=lang, name=name, version='.'.join(map(str, version)), priority=idx, judge=judge)
|
||||||
|
for idx, (name, version) in enumerate(self.executors[lang.key])
|
||||||
|
]
|
||||||
|
RuntimeVersion.objects.bulk_create(versions)
|
||||||
|
judge.last_ip = self.client_address[0]
|
||||||
|
judge.save()
|
||||||
|
self.judge_address = '[%s]:%s' % (self.client_address[0], self.client_address[1])
|
||||||
|
json_log.info(self._make_json_log(action='auth', info='judge successfully authenticated',
|
||||||
|
executors=list(self.executors.keys())))
|
||||||
|
|
||||||
|
def _disconnected(self):
|
||||||
|
Judge.objects.filter(id=self.judge.id).update(online=False)
|
||||||
|
RuntimeVersion.objects.filter(judge=self.judge).delete()
|
||||||
|
|
||||||
|
def _update_ping(self):
|
||||||
|
try:
|
||||||
|
Judge.objects.filter(name=self.name).update(ping=self.latency, load=self.load)
|
||||||
|
except Exception as e:
|
||||||
|
# What can I do? I don't want to tie this to MySQL.
|
||||||
|
if e.__class__.__name__ == 'OperationalError' and e.__module__ == '_mysql_exceptions' and e.args[0] == 2006:
|
||||||
|
db.connection.close()
|
||||||
|
|
||||||
|
def _post_update_submission(self, id, state, done=False):
|
||||||
|
if self._submission_cache_id == id:
|
||||||
|
data = self._submission_cache
|
||||||
|
else:
|
||||||
|
self._submission_cache = data = Submission.objects.filter(id=id).values(
|
||||||
|
'problem__is_public', 'contest__participation__contest__key',
|
||||||
|
'user_id', 'problem_id', 'status', 'language__key',
|
||||||
|
).get()
|
||||||
|
self._submission_cache_id = id
|
||||||
|
|
||||||
|
if data['problem__is_public']:
|
||||||
|
event.post('submissions', {
|
||||||
|
'type': 'done-submission' if done else 'update-submission',
|
||||||
|
'state': state, 'id': id,
|
||||||
|
'contest': data['contest__participation__contest__key'],
|
||||||
|
'user': data['user_id'], 'problem': data['problem_id'],
|
||||||
|
'status': data['status'], 'language': data['language__key'],
|
||||||
|
})
|
||||||
|
|
||||||
|
def on_submission_processing(self, packet):
|
||||||
|
id = packet['submission-id']
|
||||||
|
if Submission.objects.filter(id=id).update(status='P', judged_on=self.judge):
|
||||||
|
event.post('sub_%s' % Submission.get_id_secret(id), {'type': 'processing'})
|
||||||
|
self._post_update_submission(id, 'processing')
|
||||||
|
json_log.info(self._make_json_log(packet, action='processing'))
|
||||||
|
else:
|
||||||
|
logger.warning('Unknown submission: %s', id)
|
||||||
|
json_log.error(self._make_json_log(packet, action='processing', info='unknown submission'))
|
||||||
|
|
||||||
|
def on_submission_wrong_acknowledge(self, packet, expected, got):
|
||||||
|
json_log.error(self._make_json_log(packet, action='processing', info='wrong-acknowledge', expected=expected))
|
||||||
|
|
||||||
|
def on_grading_begin(self, packet):
|
||||||
|
super(DjangoJudgeHandler, self).on_grading_begin(packet)
|
||||||
|
if Submission.objects.filter(id=packet['submission-id']).update(
|
||||||
|
status='G', is_pretested=packet['pretested'],
|
||||||
|
current_testcase=1, batch=False):
|
||||||
|
SubmissionTestCase.objects.filter(submission_id=packet['submission-id']).delete()
|
||||||
|
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'grading-begin'})
|
||||||
|
self._post_update_submission(packet['submission-id'], 'grading-begin')
|
||||||
|
json_log.info(self._make_json_log(packet, action='grading-begin'))
|
||||||
|
else:
|
||||||
|
logger.warning('Unknown submission: %s', packet['submission-id'])
|
||||||
|
json_log.error(self._make_json_log(packet, action='grading-begin', info='unknown submission'))
|
||||||
|
|
||||||
|
def _submission_is_batch(self, id):
|
||||||
|
if not Submission.objects.filter(id=id).update(batch=True):
|
||||||
|
logger.warning('Unknown submission: %s', id)
|
||||||
|
|
||||||
|
def on_grading_end(self, packet):
|
||||||
|
super(DjangoJudgeHandler, self).on_grading_end(packet)
|
||||||
|
|
||||||
|
try:
|
||||||
|
submission = Submission.objects.get(id=packet['submission-id'])
|
||||||
|
except Submission.DoesNotExist:
|
||||||
|
logger.warning('Unknown submission: %s', packet['submission-id'])
|
||||||
|
json_log.error(self._make_json_log(packet, action='grading-end', info='unknown submission'))
|
||||||
|
return
|
||||||
|
|
||||||
|
time = 0
|
||||||
|
memory = 0
|
||||||
|
points = 0.0
|
||||||
|
total = 0
|
||||||
|
status = 0
|
||||||
|
status_codes = ['SC', 'AC', 'WA', 'MLE', 'TLE', 'IR', 'RTE', 'OLE']
|
||||||
|
batches = {} # batch number: (points, total)
|
||||||
|
|
||||||
|
for case in SubmissionTestCase.objects.filter(submission=submission):
|
||||||
|
time += case.time
|
||||||
|
if not case.batch:
|
||||||
|
points += case.points
|
||||||
|
total += case.total
|
||||||
|
else:
|
||||||
|
if case.batch in batches:
|
||||||
|
batches[case.batch][0] = min(batches[case.batch][0], case.points)
|
||||||
|
batches[case.batch][1] = max(batches[case.batch][1], case.total)
|
||||||
|
else:
|
||||||
|
batches[case.batch] = [case.points, case.total]
|
||||||
|
memory = max(memory, case.memory)
|
||||||
|
i = status_codes.index(case.status)
|
||||||
|
if i > status:
|
||||||
|
status = i
|
||||||
|
|
||||||
|
for i in batches:
|
||||||
|
points += batches[i][0]
|
||||||
|
total += batches[i][1]
|
||||||
|
|
||||||
|
points = round(points, 1)
|
||||||
|
total = round(total, 1)
|
||||||
|
submission.case_points = points
|
||||||
|
submission.case_total = total
|
||||||
|
|
||||||
|
problem = submission.problem
|
||||||
|
sub_points = round(points / total * problem.points if total > 0 else 0, 3)
|
||||||
|
if not problem.partial and sub_points != problem.points:
|
||||||
|
sub_points = 0
|
||||||
|
|
||||||
|
submission.status = 'D'
|
||||||
|
submission.time = time
|
||||||
|
submission.memory = memory
|
||||||
|
submission.points = sub_points
|
||||||
|
submission.result = status_codes[status]
|
||||||
|
submission.save()
|
||||||
|
|
||||||
|
json_log.info(self._make_json_log(
|
||||||
|
packet, action='grading-end', time=time, memory=memory,
|
||||||
|
points=sub_points, total=problem.points, result=submission.result,
|
||||||
|
case_points=points, case_total=total, user=submission.user_id,
|
||||||
|
problem=problem.code, finish=True,
|
||||||
|
))
|
||||||
|
|
||||||
|
submission.user._updating_stats_only = True
|
||||||
|
submission.user.calculate_points()
|
||||||
|
problem._updating_stats_only = True
|
||||||
|
problem.update_stats()
|
||||||
|
submission.update_contest()
|
||||||
|
|
||||||
|
finished_submission(submission)
|
||||||
|
|
||||||
|
event.post('sub_%s' % submission.id_secret, {
|
||||||
|
'type': 'grading-end',
|
||||||
|
'time': time,
|
||||||
|
'memory': memory,
|
||||||
|
'points': float(points),
|
||||||
|
'total': float(problem.points),
|
||||||
|
'result': submission.result,
|
||||||
|
})
|
||||||
|
if hasattr(submission, 'contest'):
|
||||||
|
participation = submission.contest.participation
|
||||||
|
event.post('contest_%d' % participation.contest_id, {'type': 'update'})
|
||||||
|
self._post_update_submission(submission.id, 'grading-end', done=True)
|
||||||
|
|
||||||
|
def on_compile_error(self, packet):
|
||||||
|
super(DjangoJudgeHandler, self).on_compile_error(packet)
|
||||||
|
|
||||||
|
if Submission.objects.filter(id=packet['submission-id']).update(status='CE', result='CE', error=packet['log']):
|
||||||
|
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {
|
||||||
|
'type': 'compile-error',
|
||||||
|
'log': packet['log'],
|
||||||
|
})
|
||||||
|
self._post_update_submission(packet['submission-id'], 'compile-error', done=True)
|
||||||
|
json_log.info(self._make_json_log(packet, action='compile-error', log=packet['log'],
|
||||||
|
finish=True, result='CE'))
|
||||||
|
else:
|
||||||
|
logger.warning('Unknown submission: %s', packet['submission-id'])
|
||||||
|
json_log.error(self._make_json_log(packet, action='compile-error', info='unknown submission',
|
||||||
|
log=packet['log'], finish=True, result='CE'))
|
||||||
|
|
||||||
|
def on_compile_message(self, packet):
|
||||||
|
super(DjangoJudgeHandler, self).on_compile_message(packet)
|
||||||
|
|
||||||
|
if Submission.objects.filter(id=packet['submission-id']).update(error=packet['log']):
|
||||||
|
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'compile-message'})
|
||||||
|
json_log.info(self._make_json_log(packet, action='compile-message', log=packet['log']))
|
||||||
|
else:
|
||||||
|
logger.warning('Unknown submission: %s', packet['submission-id'])
|
||||||
|
json_log.error(self._make_json_log(packet, action='compile-message', info='unknown submission',
|
||||||
|
log=packet['log']))
|
||||||
|
|
||||||
|
def on_internal_error(self, packet):
|
||||||
|
super(DjangoJudgeHandler, self).on_internal_error(packet)
|
||||||
|
|
||||||
|
id = packet['submission-id']
|
||||||
|
if Submission.objects.filter(id=id).update(status='IE', result='IE', error=packet['message']):
|
||||||
|
event.post('sub_%s' % Submission.get_id_secret(id), {'type': 'internal-error'})
|
||||||
|
self._post_update_submission(id, 'internal-error', done=True)
|
||||||
|
json_log.info(self._make_json_log(packet, action='internal-error', message=packet['message'],
|
||||||
|
finish=True, result='IE'))
|
||||||
|
else:
|
||||||
|
logger.warning('Unknown submission: %s', id)
|
||||||
|
json_log.error(self._make_json_log(packet, action='internal-error', info='unknown submission',
|
||||||
|
message=packet['message'], finish=True, result='IE'))
|
||||||
|
|
||||||
|
def on_submission_terminated(self, packet):
|
||||||
|
super(DjangoJudgeHandler, self).on_submission_terminated(packet)
|
||||||
|
|
||||||
|
if Submission.objects.filter(id=packet['submission-id']).update(status='AB', result='AB'):
|
||||||
|
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'aborted-submission'})
|
||||||
|
self._post_update_submission(packet['submission-id'], 'terminated', done=True)
|
||||||
|
json_log.info(self._make_json_log(packet, action='aborted', finish=True, result='AB'))
|
||||||
|
else:
|
||||||
|
logger.warning('Unknown submission: %s', packet['submission-id'])
|
||||||
|
json_log.error(self._make_json_log(packet, action='aborted', info='unknown submission',
|
||||||
|
finish=True, result='AB'))
|
||||||
|
|
||||||
|
def on_batch_begin(self, packet):
|
||||||
|
super(DjangoJudgeHandler, self).on_batch_begin(packet)
|
||||||
|
json_log.info(self._make_json_log(packet, action='batch-begin', batch=self.batch_id))
|
||||||
|
|
||||||
|
def on_batch_end(self, packet):
|
||||||
|
super(DjangoJudgeHandler, self).on_batch_end(packet)
|
||||||
|
json_log.info(self._make_json_log(packet, action='batch-end', batch=self.batch_id))
|
||||||
|
|
||||||
|
def on_test_case(self, packet, max_feedback=SubmissionTestCase._meta.get_field('feedback').max_length):
|
||||||
|
super(DjangoJudgeHandler, self).on_test_case(packet)
|
||||||
|
id = packet['submission-id']
|
||||||
|
updates = packet['cases']
|
||||||
|
max_position = max(map(itemgetter('position'), updates))
|
||||||
|
|
||||||
|
if not Submission.objects.filter(id=id).update(current_testcase=max_position + 1):
|
||||||
|
logger.warning('Unknown submission: %s', id)
|
||||||
|
json_log.error(self._make_json_log(packet, action='test-case', info='unknown submission'))
|
||||||
|
return
|
||||||
|
|
||||||
|
bulk_test_case_updates = []
|
||||||
|
for result in updates:
|
||||||
|
test_case = SubmissionTestCase(submission_id=id, case=result['position'])
|
||||||
|
status = result['status']
|
||||||
|
if status & 4:
|
||||||
|
test_case.status = 'TLE'
|
||||||
|
elif status & 8:
|
||||||
|
test_case.status = 'MLE'
|
||||||
|
elif status & 64:
|
||||||
|
test_case.status = 'OLE'
|
||||||
|
elif status & 2:
|
||||||
|
test_case.status = 'RTE'
|
||||||
|
elif status & 16:
|
||||||
|
test_case.status = 'IR'
|
||||||
|
elif status & 1:
|
||||||
|
test_case.status = 'WA'
|
||||||
|
elif status & 32:
|
||||||
|
test_case.status = 'SC'
|
||||||
|
else:
|
||||||
|
test_case.status = 'AC'
|
||||||
|
test_case.time = result['time']
|
||||||
|
test_case.memory = result['memory']
|
||||||
|
test_case.points = result['points']
|
||||||
|
test_case.total = result['total-points']
|
||||||
|
test_case.batch = self.batch_id if self.in_batch else None
|
||||||
|
test_case.feedback = (result.get('feedback') or '')[:max_feedback]
|
||||||
|
test_case.extended_feedback = result.get('extended-feedback') or ''
|
||||||
|
test_case.output = result['output']
|
||||||
|
bulk_test_case_updates.append(test_case)
|
||||||
|
|
||||||
|
json_log.info(self._make_json_log(
|
||||||
|
packet, action='test-case', case=test_case.case, batch=test_case.batch,
|
||||||
|
time=test_case.time, memory=test_case.memory, feedback=test_case.feedback,
|
||||||
|
extended_feedback=test_case.extended_feedback, output=test_case.output,
|
||||||
|
points=test_case.points, total=test_case.total, status=test_case.status,
|
||||||
|
))
|
||||||
|
|
||||||
|
do_post = True
|
||||||
|
|
||||||
|
if id in self.update_counter:
|
||||||
|
cnt, reset = self.update_counter[id]
|
||||||
|
cnt += 1
|
||||||
|
if time.monotonic() - reset > UPDATE_RATE_TIME:
|
||||||
|
del self.update_counter[id]
|
||||||
|
else:
|
||||||
|
self.update_counter[id] = (cnt, reset)
|
||||||
|
if cnt > UPDATE_RATE_LIMIT:
|
||||||
|
do_post = False
|
||||||
|
if id not in self.update_counter:
|
||||||
|
self.update_counter[id] = (1, time.monotonic())
|
||||||
|
|
||||||
|
if do_post:
|
||||||
|
event.post('sub_%s' % Submission.get_id_secret(id), {
|
||||||
|
'type': 'test-case',
|
||||||
|
'id': max_position,
|
||||||
|
})
|
||||||
|
self._post_update_submission(id, state='test-case')
|
||||||
|
|
||||||
|
SubmissionTestCase.objects.bulk_create(bulk_test_case_updates)
|
||||||
|
|
||||||
|
def on_supported_problems(self, packet):
|
||||||
|
super(DjangoJudgeHandler, self).on_supported_problems(packet)
|
||||||
|
self.judge.problems.set(Problem.objects.filter(code__in=list(self.problems.keys())))
|
||||||
|
json_log.info(self._make_json_log(action='update-problems', count=len(self.problems)))
|
||||||
|
|
||||||
|
def _make_json_log(self, packet=None, sub=None, **kwargs):
|
||||||
|
data = {
|
||||||
|
'judge': self.name,
|
||||||
|
'address': self.judge_address,
|
||||||
|
}
|
||||||
|
if sub is None and packet is not None:
|
||||||
|
sub = packet.get('submission-id')
|
||||||
|
if sub is not None:
|
||||||
|
data['submission'] = sub
|
||||||
|
data.update(kwargs)
|
||||||
|
return json.dumps(data)
|
268
judge/bridge/judgehandler.py
Normal file
268
judge/bridge/judgehandler.py
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from collections import deque, namedtuple
|
||||||
|
|
||||||
|
from event_socket_server import ProxyProtocolMixin, ZlibPacketHandler
|
||||||
|
|
||||||
|
logger = logging.getLogger('judge.bridge')
|
||||||
|
|
||||||
|
SubmissionData = namedtuple('SubmissionData', 'time memory short_circuit pretests_only contest_no attempt_no user_id')
|
||||||
|
|
||||||
|
|
||||||
|
class JudgeHandler(ProxyProtocolMixin, ZlibPacketHandler):
|
||||||
|
def __init__(self, server, socket):
|
||||||
|
super(JudgeHandler, self).__init__(server, socket)
|
||||||
|
|
||||||
|
self.handlers = {
|
||||||
|
'grading-begin': self.on_grading_begin,
|
||||||
|
'grading-end': self.on_grading_end,
|
||||||
|
'compile-error': self.on_compile_error,
|
||||||
|
'compile-message': self.on_compile_message,
|
||||||
|
'batch-begin': self.on_batch_begin,
|
||||||
|
'batch-end': self.on_batch_end,
|
||||||
|
'test-case-status': self.on_test_case,
|
||||||
|
'internal-error': self.on_internal_error,
|
||||||
|
'submission-terminated': self.on_submission_terminated,
|
||||||
|
'submission-acknowledged': self.on_submission_acknowledged,
|
||||||
|
'ping-response': self.on_ping_response,
|
||||||
|
'supported-problems': self.on_supported_problems,
|
||||||
|
'handshake': self.on_handshake,
|
||||||
|
}
|
||||||
|
self._to_kill = True
|
||||||
|
self._working = False
|
||||||
|
self._no_response_job = None
|
||||||
|
self._problems = []
|
||||||
|
self.executors = []
|
||||||
|
self.problems = {}
|
||||||
|
self.latency = None
|
||||||
|
self.time_delta = None
|
||||||
|
self.load = 1e100
|
||||||
|
self.name = None
|
||||||
|
self.batch_id = None
|
||||||
|
self.in_batch = False
|
||||||
|
self._ping_average = deque(maxlen=6) # 1 minute average, just like load
|
||||||
|
self._time_delta = deque(maxlen=6)
|
||||||
|
|
||||||
|
self.server.schedule(15, self._kill_if_no_auth)
|
||||||
|
logger.info('Judge connected from: %s', self.client_address)
|
||||||
|
|
||||||
|
def _kill_if_no_auth(self):
|
||||||
|
if self._to_kill:
|
||||||
|
logger.info('Judge not authenticated: %s', self.client_address)
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def on_close(self):
|
||||||
|
self._to_kill = False
|
||||||
|
if self._no_response_job:
|
||||||
|
self.server.unschedule(self._no_response_job)
|
||||||
|
self.server.judges.remove(self)
|
||||||
|
if self.name is not None:
|
||||||
|
self._disconnected()
|
||||||
|
logger.info('Judge disconnected from: %s', self.client_address)
|
||||||
|
|
||||||
|
def _authenticate(self, id, key):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _connected(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _disconnected(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _update_ping(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _format_send(self, data):
|
||||||
|
return super(JudgeHandler, self)._format_send(json.dumps(data, separators=(',', ':')))
|
||||||
|
|
||||||
|
def on_handshake(self, packet):
|
||||||
|
if 'id' not in packet or 'key' not in packet:
|
||||||
|
logger.warning('Malformed handshake: %s', self.client_address)
|
||||||
|
self.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self._authenticate(packet['id'], packet['key']):
|
||||||
|
logger.warning('Authentication failure: %s', self.client_address)
|
||||||
|
self.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
self._to_kill = False
|
||||||
|
self._problems = packet['problems']
|
||||||
|
self.problems = dict(self._problems)
|
||||||
|
self.executors = packet['executors']
|
||||||
|
self.name = packet['id']
|
||||||
|
|
||||||
|
self.send({'name': 'handshake-success'})
|
||||||
|
logger.info('Judge authenticated: %s (%s)', self.client_address, packet['id'])
|
||||||
|
self.server.judges.register(self)
|
||||||
|
self._connected()
|
||||||
|
|
||||||
|
def can_judge(self, problem, executor):
|
||||||
|
return problem in self.problems and executor in self.executors
|
||||||
|
|
||||||
|
@property
|
||||||
|
def working(self):
|
||||||
|
return bool(self._working)
|
||||||
|
|
||||||
|
def get_related_submission_data(self, submission):
|
||||||
|
return SubmissionData(
|
||||||
|
time=2,
|
||||||
|
memory=16384,
|
||||||
|
short_circuit=False,
|
||||||
|
pretests_only=False,
|
||||||
|
contest_no=None,
|
||||||
|
attempt_no=1,
|
||||||
|
user_id=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def disconnect(self, force=False):
|
||||||
|
if force:
|
||||||
|
# Yank the power out.
|
||||||
|
self.close()
|
||||||
|
else:
|
||||||
|
self.send({'name': 'disconnect'})
|
||||||
|
|
||||||
|
def submit(self, id, problem, language, source):
|
||||||
|
data = self.get_related_submission_data(id)
|
||||||
|
self._working = id
|
||||||
|
self._no_response_job = self.server.schedule(20, self._kill_if_no_response)
|
||||||
|
self.send({
|
||||||
|
'name': 'submission-request',
|
||||||
|
'submission-id': id,
|
||||||
|
'problem-id': problem,
|
||||||
|
'language': language,
|
||||||
|
'source': source,
|
||||||
|
'time-limit': data.time,
|
||||||
|
'memory-limit': data.memory,
|
||||||
|
'short-circuit': data.short_circuit,
|
||||||
|
'meta': {
|
||||||
|
'pretests-only': data.pretests_only,
|
||||||
|
'in-contest': data.contest_no,
|
||||||
|
'attempt-no': data.attempt_no,
|
||||||
|
'user': data.user_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
def _kill_if_no_response(self):
|
||||||
|
logger.error('Judge seems dead: %s: %s', self.name, self._working)
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def malformed_packet(self, exception):
|
||||||
|
logger.exception('Judge sent malformed packet: %s', self.name)
|
||||||
|
super(JudgeHandler, self).malformed_packet(exception)
|
||||||
|
|
||||||
|
def on_submission_processing(self, packet):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_submission_wrong_acknowledge(self, packet, expected, got):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_submission_acknowledged(self, packet):
|
||||||
|
if not packet.get('submission-id', None) == self._working:
|
||||||
|
logger.error('Wrong acknowledgement: %s: %s, expected: %s', self.name, packet.get('submission-id', None),
|
||||||
|
self._working)
|
||||||
|
self.on_submission_wrong_acknowledge(packet, self._working, packet.get('submission-id', None))
|
||||||
|
self.close()
|
||||||
|
logger.info('Submission acknowledged: %d', self._working)
|
||||||
|
if self._no_response_job:
|
||||||
|
self.server.unschedule(self._no_response_job)
|
||||||
|
self._no_response_job = None
|
||||||
|
self.on_submission_processing(packet)
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
self.send({'name': 'terminate-submission'})
|
||||||
|
|
||||||
|
def get_current_submission(self):
|
||||||
|
return self._working or None
|
||||||
|
|
||||||
|
def ping(self):
|
||||||
|
self.send({'name': 'ping', 'when': time.time()})
|
||||||
|
|
||||||
|
def packet(self, data):
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
data = json.loads(data)
|
||||||
|
if 'name' not in data:
|
||||||
|
raise ValueError
|
||||||
|
except ValueError:
|
||||||
|
self.on_malformed(data)
|
||||||
|
else:
|
||||||
|
handler = self.handlers.get(data['name'], self.on_malformed)
|
||||||
|
handler(data)
|
||||||
|
except Exception:
|
||||||
|
logger.exception('Error in packet handling (Judge-side): %s', self.name)
|
||||||
|
self._packet_exception()
|
||||||
|
# You can't crash here because you aren't so sure about the judges
|
||||||
|
# not being malicious or simply malforms. THIS IS A SERVER!
|
||||||
|
|
||||||
|
def _packet_exception(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _submission_is_batch(self, id):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_supported_problems(self, packet):
|
||||||
|
logger.info('%s: Updated problem list', self.name)
|
||||||
|
self._problems = packet['problems']
|
||||||
|
self.problems = dict(self._problems)
|
||||||
|
if not self.working:
|
||||||
|
self.server.judges.update_problems(self)
|
||||||
|
|
||||||
|
def on_grading_begin(self, packet):
|
||||||
|
logger.info('%s: Grading has begun on: %s', self.name, packet['submission-id'])
|
||||||
|
self.batch_id = None
|
||||||
|
|
||||||
|
def on_grading_end(self, packet):
|
||||||
|
logger.info('%s: Grading has ended on: %s', self.name, packet['submission-id'])
|
||||||
|
self._free_self(packet)
|
||||||
|
self.batch_id = None
|
||||||
|
|
||||||
|
def on_compile_error(self, packet):
|
||||||
|
logger.info('%s: Submission failed to compile: %s', self.name, packet['submission-id'])
|
||||||
|
self._free_self(packet)
|
||||||
|
|
||||||
|
def on_compile_message(self, packet):
|
||||||
|
logger.info('%s: Submission generated compiler messages: %s', self.name, packet['submission-id'])
|
||||||
|
|
||||||
|
def on_internal_error(self, packet):
|
||||||
|
try:
|
||||||
|
raise ValueError('\n\n' + packet['message'])
|
||||||
|
except ValueError:
|
||||||
|
logger.exception('Judge %s failed while handling submission %s', self.name, packet['submission-id'])
|
||||||
|
self._free_self(packet)
|
||||||
|
|
||||||
|
def on_submission_terminated(self, packet):
|
||||||
|
logger.info('%s: Submission aborted: %s', self.name, packet['submission-id'])
|
||||||
|
self._free_self(packet)
|
||||||
|
|
||||||
|
def on_batch_begin(self, packet):
|
||||||
|
logger.info('%s: Batch began on: %s', self.name, packet['submission-id'])
|
||||||
|
self.in_batch = True
|
||||||
|
if self.batch_id is None:
|
||||||
|
self.batch_id = 0
|
||||||
|
self._submission_is_batch(packet['submission-id'])
|
||||||
|
self.batch_id += 1
|
||||||
|
|
||||||
|
def on_batch_end(self, packet):
|
||||||
|
self.in_batch = False
|
||||||
|
logger.info('%s: Batch ended on: %s', self.name, packet['submission-id'])
|
||||||
|
|
||||||
|
def on_test_case(self, packet):
|
||||||
|
logger.info('%s: %d test case(s) completed on: %s', self.name, len(packet['cases']), packet['submission-id'])
|
||||||
|
|
||||||
|
def on_malformed(self, packet):
|
||||||
|
logger.error('%s: Malformed packet: %s', self.name, packet)
|
||||||
|
|
||||||
|
def on_ping_response(self, packet):
|
||||||
|
end = time.time()
|
||||||
|
self._ping_average.append(end - packet['when'])
|
||||||
|
self._time_delta.append((end + packet['when']) / 2 - packet['time'])
|
||||||
|
self.latency = sum(self._ping_average) / len(self._ping_average)
|
||||||
|
self.time_delta = sum(self._time_delta) / len(self._time_delta)
|
||||||
|
self.load = packet['load']
|
||||||
|
self._update_ping()
|
||||||
|
|
||||||
|
def _free_self(self, packet):
|
||||||
|
self._working = False
|
||||||
|
self.server.judges.on_judge_free(self, packet['submission-id'])
|
|
@ -3,16 +3,14 @@ from collections import namedtuple
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from threading import RLock
|
from threading import RLock
|
||||||
|
|
||||||
from judge.bridge.utils import VanishedSubmission
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from llist import dllist
|
from llist import dllist
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from pyllist import dllist
|
from pyllist import dllist
|
||||||
|
|
||||||
logger = logging.getLogger("judge.bridge")
|
logger = logging.getLogger('judge.bridge')
|
||||||
|
|
||||||
PriorityMarker = namedtuple("PriorityMarker", "priority")
|
PriorityMarker = namedtuple('PriorityMarker', 'priority')
|
||||||
|
|
||||||
|
|
||||||
class JudgeList(object):
|
class JudgeList(object):
|
||||||
|
@ -20,9 +18,7 @@ class JudgeList(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.queue = dllist()
|
self.queue = dllist()
|
||||||
self.priority = [
|
self.priority = [self.queue.append(PriorityMarker(i)) for i in range(self.priorities)]
|
||||||
self.queue.append(PriorityMarker(i)) for i in range(self.priorities)
|
|
||||||
]
|
|
||||||
self.judges = set()
|
self.judges = set()
|
||||||
self.node_map = {}
|
self.node_map = {}
|
||||||
self.submission_map = {}
|
self.submission_map = {}
|
||||||
|
@ -33,24 +29,14 @@ class JudgeList(object):
|
||||||
node = self.queue.first
|
node = self.queue.first
|
||||||
while node:
|
while node:
|
||||||
if not isinstance(node.value, PriorityMarker):
|
if not isinstance(node.value, PriorityMarker):
|
||||||
id, problem, language, source, judge_id = node.value
|
id, problem, language, source = node.value
|
||||||
if judge.can_judge(problem, language, judge_id):
|
if judge.can_judge(problem, language):
|
||||||
self.submission_map[id] = judge
|
self.submission_map[id] = judge
|
||||||
logger.info(
|
logger.info('Dispatched queued submission %d: %s', id, judge.name)
|
||||||
"Dispatched queued submission %d: %s", id, judge.name
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
judge.submit(id, problem, language, source)
|
judge.submit(id, problem, language, source)
|
||||||
except VanishedSubmission:
|
|
||||||
pass
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception(
|
logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name)
|
||||||
"Failed to dispatch %d (%s, %s) to %s",
|
|
||||||
id,
|
|
||||||
problem,
|
|
||||||
language,
|
|
||||||
judge.name,
|
|
||||||
)
|
|
||||||
self.judges.remove(judge)
|
self.judges.remove(judge)
|
||||||
return
|
return
|
||||||
self.queue.remove(node)
|
self.queue.remove(node)
|
||||||
|
@ -66,7 +52,6 @@ class JudgeList(object):
|
||||||
self._handle_free_judge(judge)
|
self._handle_free_judge(judge)
|
||||||
|
|
||||||
def disconnect(self, judge_id, force=False):
|
def disconnect(self, judge_id, force=False):
|
||||||
with self.lock:
|
|
||||||
for judge in self.judges:
|
for judge in self.judges:
|
||||||
if judge.name == judge_id:
|
if judge.name == judge_id:
|
||||||
judge.disconnect(force=force)
|
judge.disconnect(force=force)
|
||||||
|
@ -90,15 +75,13 @@ class JudgeList(object):
|
||||||
|
|
||||||
def on_judge_free(self, judge, submission):
|
def on_judge_free(self, judge, submission):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
logger.info("Judge available after grading %d: %s", submission, judge.name)
|
logger.info('Judge available after grading %d: %s', submission, judge.name)
|
||||||
del self.submission_map[submission]
|
del self.submission_map[submission]
|
||||||
judge._working = False
|
|
||||||
judge._working_data = {}
|
|
||||||
self._handle_free_judge(judge)
|
self._handle_free_judge(judge)
|
||||||
|
|
||||||
def abort(self, submission):
|
def abort(self, submission):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
logger.info("Abort request: %d", submission)
|
logger.info('Abort request: %d', submission)
|
||||||
try:
|
try:
|
||||||
self.submission_map[submission].abort()
|
self.submission_map[submission].abort()
|
||||||
return True
|
return True
|
||||||
|
@ -115,46 +98,26 @@ class JudgeList(object):
|
||||||
def check_priority(self, priority):
|
def check_priority(self, priority):
|
||||||
return 0 <= priority < self.priorities
|
return 0 <= priority < self.priorities
|
||||||
|
|
||||||
def judge(self, id, problem, language, source, judge_id, priority):
|
def judge(self, id, problem, language, source, priority):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
if id in self.submission_map or id in self.node_map:
|
if id in self.submission_map or id in self.node_map:
|
||||||
# Already judging, don't queue again. This can happen during batch rejudges, rejudges should be
|
# Already judging, don't queue again. This can happen during batch rejudges, rejudges should be
|
||||||
# idempotent.
|
# idempotent.
|
||||||
return
|
return
|
||||||
|
|
||||||
candidates = [
|
candidates = [judge for judge in self.judges if not judge.working and judge.can_judge(problem, language)]
|
||||||
judge
|
logger.info('Free judges: %d', len(candidates))
|
||||||
for judge in self.judges
|
|
||||||
if not judge.working and judge.can_judge(problem, language, judge_id)
|
|
||||||
]
|
|
||||||
if judge_id:
|
|
||||||
logger.info(
|
|
||||||
"Specified judge %s is%savailable",
|
|
||||||
judge_id,
|
|
||||||
" " if candidates else " not ",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.info("Free judges: %d", len(candidates))
|
|
||||||
if candidates:
|
if candidates:
|
||||||
# Schedule the submission on the judge reporting least load.
|
# Schedule the submission on the judge reporting least load.
|
||||||
judge = min(candidates, key=attrgetter("load"))
|
judge = min(candidates, key=attrgetter('load'))
|
||||||
logger.info("Dispatched submission %d to: %s", id, judge.name)
|
logger.info('Dispatched submission %d to: %s', id, judge.name)
|
||||||
self.submission_map[id] = judge
|
self.submission_map[id] = judge
|
||||||
try:
|
try:
|
||||||
judge.submit(id, problem, language, source)
|
judge.submit(id, problem, language, source)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception(
|
logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name)
|
||||||
"Failed to dispatch %d (%s, %s) to %s",
|
|
||||||
id,
|
|
||||||
problem,
|
|
||||||
language,
|
|
||||||
judge.name,
|
|
||||||
)
|
|
||||||
self.judges.discard(judge)
|
self.judges.discard(judge)
|
||||||
return self.judge(id, problem, language, source, judge_id, priority)
|
return self.judge(id, problem, language, source, priority)
|
||||||
else:
|
else:
|
||||||
self.node_map[id] = self.queue.insert(
|
self.node_map[id] = self.queue.insert((id, problem, language, source), self.priority[priority])
|
||||||
(id, problem, language, source, judge_id),
|
logger.info('Queued submission: %d', id)
|
||||||
self.priority[priority],
|
|
||||||
)
|
|
||||||
logger.info("Queued submission: %d", id)
|
|
68
judge/bridge/judgeserver.py
Normal file
68
judge/bridge/judgeserver.py
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
from event_socket_server import get_preferred_engine
|
||||||
|
from judge.models import Judge
|
||||||
|
from .judgelist import JudgeList
|
||||||
|
|
||||||
|
logger = logging.getLogger('judge.bridge')
|
||||||
|
|
||||||
|
|
||||||
|
def reset_judges():
|
||||||
|
Judge.objects.update(online=False, ping=None, load=None)
|
||||||
|
|
||||||
|
|
||||||
|
class JudgeServer(get_preferred_engine()):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(JudgeServer, self).__init__(*args, **kwargs)
|
||||||
|
reset_judges()
|
||||||
|
self.judges = JudgeList()
|
||||||
|
self.ping_judge_thread = threading.Thread(target=self.ping_judge, args=())
|
||||||
|
self.ping_judge_thread.daemon = True
|
||||||
|
self.ping_judge_thread.start()
|
||||||
|
|
||||||
|
def on_shutdown(self):
|
||||||
|
super(JudgeServer, self).on_shutdown()
|
||||||
|
reset_judges()
|
||||||
|
|
||||||
|
def ping_judge(self):
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
for judge in self.judges:
|
||||||
|
judge.ping()
|
||||||
|
time.sleep(10)
|
||||||
|
except Exception:
|
||||||
|
logger.exception('Ping error')
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
from .judgehandler import JudgeHandler
|
||||||
|
|
||||||
|
format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s'
|
||||||
|
logging.basicConfig(format=format)
|
||||||
|
logging.getLogger().setLevel(logging.INFO)
|
||||||
|
handler = logging.FileHandler(os.path.join(os.path.dirname(__file__), 'judgeserver.log'), encoding='utf-8')
|
||||||
|
handler.setFormatter(logging.Formatter(format))
|
||||||
|
handler.setLevel(logging.INFO)
|
||||||
|
logging.getLogger().addHandler(handler)
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='''
|
||||||
|
Runs the bridge between DMOJ website and judges.
|
||||||
|
''')
|
||||||
|
parser.add_argument('judge_host', nargs='+', action='append',
|
||||||
|
help='host to listen for the judge')
|
||||||
|
parser.add_argument('-p', '--judge-port', type=int, action='append',
|
||||||
|
help='port to listen for the judge')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
server = JudgeServer(list(zip(args.judge_host, args.judge_port)), JudgeHandler)
|
||||||
|
server.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -1,32 +0,0 @@
|
||||||
import threading
|
|
||||||
from socketserver import TCPServer, ThreadingMixIn
|
|
||||||
|
|
||||||
|
|
||||||
class ThreadingTCPListener(ThreadingMixIn, TCPServer):
|
|
||||||
allow_reuse_address = True
|
|
||||||
|
|
||||||
|
|
||||||
class Server:
|
|
||||||
def __init__(self, addresses, handler):
|
|
||||||
self.servers = [ThreadingTCPListener(address, handler) for address in addresses]
|
|
||||||
self._shutdown = threading.Event()
|
|
||||||
|
|
||||||
def serve_forever(self):
|
|
||||||
threads = [
|
|
||||||
threading.Thread(target=server.serve_forever) for server in self.servers
|
|
||||||
]
|
|
||||||
for thread in threads:
|
|
||||||
thread.daemon = True
|
|
||||||
thread.start()
|
|
||||||
try:
|
|
||||||
self._shutdown.wait()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
self.shutdown()
|
|
||||||
finally:
|
|
||||||
for thread in threads:
|
|
||||||
thread.join()
|
|
||||||
|
|
||||||
def shutdown(self):
|
|
||||||
for server in self.servers:
|
|
||||||
server.shutdown()
|
|
||||||
self._shutdown.set()
|
|
|
@ -1,2 +0,0 @@
|
||||||
class VanishedSubmission(Exception):
|
|
||||||
pass
|
|
121
judge/caching.py
121
judge/caching.py
|
@ -1,117 +1,10 @@
|
||||||
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.core.handlers.wsgi import WSGIRequest
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from judge.logging import log_debug
|
|
||||||
|
|
||||||
MAX_NUM_CHAR = 50
|
|
||||||
NONE_RESULT = "__None__"
|
|
||||||
|
|
||||||
|
|
||||||
def arg_to_str(arg):
|
def finished_submission(sub):
|
||||||
if hasattr(arg, "id"):
|
keys = ['user_complete:%d' % sub.user_id, 'user_attempted:%s' % sub.user_id]
|
||||||
return str(arg.id)
|
if hasattr(sub, 'contest'):
|
||||||
if isinstance(arg, list) or isinstance(arg, QuerySet):
|
participation = sub.contest.participation
|
||||||
return hashlib.sha1(str(list(arg)).encode()).hexdigest()[:MAX_NUM_CHAR]
|
keys += ['contest_complete:%d' % participation.id]
|
||||||
if len(str(arg)) > MAX_NUM_CHAR:
|
keys += ['contest_attempted:%d' % participation.id]
|
||||||
return str(arg)[:MAX_NUM_CHAR]
|
|
||||||
return str(arg)
|
|
||||||
|
|
||||||
|
|
||||||
def filter_args(args_list):
|
|
||||||
return [x for x in args_list if not isinstance(x, WSGIRequest)]
|
|
||||||
|
|
||||||
|
|
||||||
l0_cache = caches["l0"] if "l0" in caches else None
|
|
||||||
|
|
||||||
|
|
||||||
def cache_wrapper(prefix, timeout=None, expected_type=None):
|
|
||||||
def get_key(func, *args, **kwargs):
|
|
||||||
args_list = list(args)
|
|
||||||
signature_args = list(signature(func).parameters.keys())
|
|
||||||
args_list += [kwargs.get(k) for k in signature_args[len(args) :]]
|
|
||||||
args_list = filter_args(args_list)
|
|
||||||
args_list = [arg_to_str(i) for i in args_list]
|
|
||||||
key = prefix + ":" + ":".join(args_list)
|
|
||||||
key = key.replace(" ", "_")
|
|
||||||
return key
|
|
||||||
|
|
||||||
def _get(key):
|
|
||||||
if not l0_cache:
|
|
||||||
return cache.get(key)
|
|
||||||
result = l0_cache.get(key)
|
|
||||||
if result is None:
|
|
||||||
result = cache.get(key)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _set_l0(key, value):
|
|
||||||
if l0_cache:
|
|
||||||
l0_cache.set(key, value, 30)
|
|
||||||
|
|
||||||
def _set(key, value, timeout):
|
|
||||||
_set_l0(key, value)
|
|
||||||
cache.set(key, value, timeout)
|
|
||||||
|
|
||||||
def decorator(func):
|
|
||||||
def _validate_type(cache_key, result):
|
|
||||||
if expected_type and not isinstance(result, expected_type):
|
|
||||||
data = {
|
|
||||||
"function": f"{func.__module__}.{func.__qualname__}",
|
|
||||||
"result": str(result)[:30],
|
|
||||||
"expected_type": expected_type,
|
|
||||||
"type": type(result),
|
|
||||||
"key": cache_key,
|
|
||||||
}
|
|
||||||
log_debug("invalid_key", data)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
cache_key = get_key(func, *args, **kwargs)
|
|
||||||
result = _get(cache_key)
|
|
||||||
if result is not None and _validate_type(cache_key, result):
|
|
||||||
_set_l0(cache_key, result)
|
|
||||||
if type(result) == str and result == NONE_RESULT:
|
|
||||||
result = None
|
|
||||||
return result
|
|
||||||
result = func(*args, **kwargs)
|
|
||||||
if result is None:
|
|
||||||
cache_result = NONE_RESULT
|
|
||||||
else:
|
|
||||||
cache_result = result
|
|
||||||
_set(cache_key, cache_result, timeout)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def dirty(*args, **kwargs):
|
|
||||||
cache_key = get_key(func, *args, **kwargs)
|
|
||||||
cache.delete(cache_key)
|
|
||||||
if l0_cache:
|
|
||||||
l0_cache.delete(cache_key)
|
|
||||||
|
|
||||||
def prefetch_multi(args_list):
|
|
||||||
keys = []
|
|
||||||
for args in args_list:
|
|
||||||
keys.append(get_key(func, *args))
|
|
||||||
results = cache.get_many(keys)
|
|
||||||
for key, result in results.items():
|
|
||||||
if result is not None:
|
|
||||||
_set_l0(key, result)
|
|
||||||
|
|
||||||
def dirty_multi(args_list):
|
|
||||||
keys = []
|
|
||||||
for args in args_list:
|
|
||||||
keys.append(get_key(func, *args))
|
|
||||||
cache.delete_many(keys)
|
cache.delete_many(keys)
|
||||||
if l0_cache:
|
|
||||||
l0_cache.delete_many(keys)
|
|
||||||
|
|
||||||
wrapper.dirty = dirty
|
|
||||||
wrapper.prefetch_multi = prefetch_multi
|
|
||||||
wrapper.dirty_multi = dirty_multi
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
122
judge/comments.py
Normal file
122
judge/comments.py
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db.models import Count
|
||||||
|
from django.db.models.expressions import F, Value
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
from django.forms import ModelForm
|
||||||
|
from django.http import HttpResponseForbidden, HttpResponseNotFound, HttpResponseRedirect
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from django.views.generic import View
|
||||||
|
from django.views.generic.base import TemplateResponseMixin
|
||||||
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
|
from reversion import revisions
|
||||||
|
from reversion.models import Revision, Version
|
||||||
|
|
||||||
|
from judge.dblock import LockModel
|
||||||
|
from judge.models import Comment, CommentLock, CommentVote
|
||||||
|
from judge.utils.raw_sql import RawSQLColumn, unique_together_left_join
|
||||||
|
from judge.widgets import HeavyPreviewPageDownWidget
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
return (CommentLock.objects.filter(page=self.get_comment_page()).exists() and
|
||||||
|
not self.request.user.has_perm('judge.override_comment_lock'))
|
||||||
|
|
||||||
|
@method_decorator(login_required)
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
page = self.get_comment_page()
|
||||||
|
|
||||||
|
if self.is_comment_locked():
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
|
parent = request.POST.get('parent')
|
||||||
|
if parent:
|
||||||
|
try:
|
||||||
|
parent = int(parent)
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
else:
|
||||||
|
if not Comment.objects.filter(hidden=False, id=parent, page=page).exists():
|
||||||
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
|
form = CommentForm(request, request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
comment = form.save(commit=False)
|
||||||
|
comment.author = request.profile
|
||||||
|
comment.page = page
|
||||||
|
with LockModel(write=(Comment, Revision, Version), read=(ContentType,)), revisions.create_revision():
|
||||||
|
revisions.set_user(request.user)
|
||||||
|
revisions.set_comment(_('Posted comment'))
|
||||||
|
comment.save()
|
||||||
|
return HttpResponseRedirect(request.path)
|
||||||
|
|
||||||
|
context = self.get_context_data(object=self.object, comment_form=form)
|
||||||
|
return self.render_to_response(context)
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
return self.render_to_response(self.get_context_data(
|
||||||
|
object=self.object,
|
||||||
|
comment_form=CommentForm(request, initial={'page': self.get_comment_page(), 'parent': None}),
|
||||||
|
))
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(CommentedDetailView, self).get_context_data(**kwargs)
|
||||||
|
queryset = Comment.objects.filter(hidden=False, page=self.get_comment_page())
|
||||||
|
context['has_comments'] = queryset.exists()
|
||||||
|
context['comment_lock'] = self.is_comment_locked()
|
||||||
|
queryset = queryset.select_related('author__user').defer('author__about').annotate(revisions=Count('versions'))
|
||||||
|
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
queryset = queryset.annotate(vote_score=Coalesce(RawSQLColumn(CommentVote, 'score'), Value(0)))
|
||||||
|
profile = self.request.profile
|
||||||
|
unique_together_left_join(queryset, CommentVote, 'comment', 'voter', profile.id)
|
||||||
|
context['is_new_user'] = (not self.request.user.is_staff and
|
||||||
|
not profile.submission_set.filter(points=F('problem__points')).exists())
|
||||||
|
context['comment_list'] = queryset
|
||||||
|
context['vote_hide_threshold'] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD
|
||||||
|
|
||||||
|
return context
|
|
@ -1,8 +1,5 @@
|
||||||
from judge.contest_format.atcoder import AtCoderContestFormat
|
from judge.contest_format.atcoder import AtCoderContestFormat
|
||||||
from judge.contest_format.default import DefaultContestFormat
|
from judge.contest_format.default import DefaultContestFormat
|
||||||
from judge.contest_format.ecoo import ECOOContestFormat
|
from judge.contest_format.ecoo import ECOOContestFormat
|
||||||
from judge.contest_format.icpc import ICPCContestFormat
|
|
||||||
from judge.contest_format.ioi import IOIContestFormat
|
from judge.contest_format.ioi import IOIContestFormat
|
||||||
from judge.contest_format.new_ioi import NewIOIContestFormat
|
|
||||||
from judge.contest_format.ultimate import UltimateContestFormat
|
|
||||||
from judge.contest_format.registry import choices, formats
|
from judge.contest_format.registry import choices, formats
|
||||||
|
|
|
@ -10,18 +10,18 @@ from django.utils.translation import gettext_lazy
|
||||||
|
|
||||||
from judge.contest_format.default import DefaultContestFormat
|
from judge.contest_format.default import DefaultContestFormat
|
||||||
from judge.contest_format.registry import register_contest_format
|
from judge.contest_format.registry import register_contest_format
|
||||||
from judge.timezone import from_database_time, to_database_time
|
from judge.timezone import from_database_time
|
||||||
from judge.utils.timedelta import nice_repr
|
from judge.utils.timedelta import nice_repr
|
||||||
|
|
||||||
|
|
||||||
@register_contest_format("atcoder")
|
@register_contest_format('atcoder')
|
||||||
class AtCoderContestFormat(DefaultContestFormat):
|
class AtCoderContestFormat(DefaultContestFormat):
|
||||||
name = gettext_lazy("AtCoder")
|
name = gettext_lazy('AtCoder')
|
||||||
config_defaults = {"penalty": 5}
|
config_defaults = {'penalty': 5}
|
||||||
config_validators = {"penalty": lambda x: x >= 0}
|
config_validators = {'penalty': lambda x: x >= 0}
|
||||||
"""
|
'''
|
||||||
penalty: Number of penalty minutes each incorrect submission adds. Defaults to 5.
|
penalty: Number of penalty minutes each incorrect submission adds. Defaults to 5.
|
||||||
"""
|
'''
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config):
|
def validate(cls, config):
|
||||||
|
@ -29,9 +29,7 @@ class AtCoderContestFormat(DefaultContestFormat):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not isinstance(config, dict):
|
if not isinstance(config, dict):
|
||||||
raise ValidationError(
|
raise ValidationError('AtCoder-styled contest expects no config or dict as config')
|
||||||
"AtCoder-styled contest expects no config or dict as config"
|
|
||||||
)
|
|
||||||
|
|
||||||
for key, value in config.items():
|
for key, value in config.items():
|
||||||
if key not in cls.config_defaults:
|
if key not in cls.config_defaults:
|
||||||
|
@ -39,9 +37,7 @@ class AtCoderContestFormat(DefaultContestFormat):
|
||||||
if not isinstance(value, type(cls.config_defaults[key])):
|
if not isinstance(value, type(cls.config_defaults[key])):
|
||||||
raise ValidationError('invalid type for config key "%s"' % key)
|
raise ValidationError('invalid type for config key "%s"' % key)
|
||||||
if not cls.config_validators[key](value):
|
if not cls.config_validators[key](value):
|
||||||
raise ValidationError(
|
raise ValidationError('invalid value "%s" for config key "%s"' % (value, key))
|
||||||
'invalid value "%s" for config key "%s"' % (value, key)
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, contest, config):
|
def __init__(self, contest, config):
|
||||||
self.config = self.config_defaults.copy()
|
self.config = self.config_defaults.copy()
|
||||||
|
@ -54,13 +50,8 @@ class AtCoderContestFormat(DefaultContestFormat):
|
||||||
points = 0
|
points = 0
|
||||||
format_data = {}
|
format_data = {}
|
||||||
|
|
||||||
frozen_time = self.contest.end_time
|
|
||||||
if self.contest.freeze_after:
|
|
||||||
frozen_time = participation.start + self.contest.freeze_after
|
|
||||||
|
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute(
|
cursor.execute('''
|
||||||
"""
|
|
||||||
SELECT MAX(cs.points) as `score`, (
|
SELECT MAX(cs.points) as `score`, (
|
||||||
SELECT MIN(csub.date)
|
SELECT MIN(csub.date)
|
||||||
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
||||||
|
@ -70,29 +61,22 @@ class AtCoderContestFormat(DefaultContestFormat):
|
||||||
FROM judge_contestproblem cp INNER JOIN
|
FROM judge_contestproblem cp INNER JOIN
|
||||||
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
||||||
judge_submission sub ON (sub.id = cs.submission_id)
|
judge_submission sub ON (sub.id = cs.submission_id)
|
||||||
WHERE sub.date < %s
|
|
||||||
GROUP BY cp.id
|
GROUP BY cp.id
|
||||||
""",
|
''', (participation.id, participation.id))
|
||||||
(participation.id, participation.id, to_database_time(frozen_time)),
|
|
||||||
)
|
|
||||||
|
|
||||||
for score, time, prob in cursor.fetchall():
|
for score, time, prob in cursor.fetchall():
|
||||||
time = from_database_time(time)
|
time = from_database_time(time)
|
||||||
dt = (time - participation.start).total_seconds()
|
dt = (time - participation.start).total_seconds()
|
||||||
|
|
||||||
# Compute penalty
|
# Compute penalty
|
||||||
if self.config["penalty"]:
|
if self.config['penalty']:
|
||||||
# An IE can have a submission result of `None`
|
# An IE can have a submission result of `None`
|
||||||
subs = (
|
subs = participation.submissions.exclude(submission__result__isnull=True) \
|
||||||
participation.submissions.exclude(
|
.exclude(submission__result__in=['IE', 'CE']) \
|
||||||
submission__result__isnull=True
|
|
||||||
)
|
|
||||||
.exclude(submission__result__in=["IE", "CE"])
|
|
||||||
.filter(problem_id=prob)
|
.filter(problem_id=prob)
|
||||||
)
|
|
||||||
if score:
|
if score:
|
||||||
prev = subs.filter(submission__date__lte=time).count() - 1
|
prev = subs.filter(submission__date__lte=time).count() - 1
|
||||||
penalty += prev * self.config["penalty"] * 60
|
penalty += prev * self.config['penalty'] * 60
|
||||||
else:
|
else:
|
||||||
# We should always display the penalty, even if the user has a score of 0
|
# We should always display the penalty, even if the user has a score of 0
|
||||||
prev = subs.count()
|
prev = subs.count()
|
||||||
|
@ -102,52 +86,28 @@ class AtCoderContestFormat(DefaultContestFormat):
|
||||||
if score:
|
if score:
|
||||||
cumtime = max(cumtime, dt)
|
cumtime = max(cumtime, dt)
|
||||||
|
|
||||||
format_data[str(prob)] = {"time": dt, "points": score, "penalty": prev}
|
format_data[str(prob)] = {'time': dt, 'points': score, 'penalty': prev}
|
||||||
points += score
|
points += score
|
||||||
|
|
||||||
self.handle_frozen_state(participation, format_data)
|
|
||||||
participation.cumtime = cumtime + penalty
|
participation.cumtime = cumtime + penalty
|
||||||
participation.score = round(points, self.contest.points_precision)
|
participation.score = points
|
||||||
participation.tiebreaker = 0
|
|
||||||
participation.format_data = format_data
|
participation.format_data = format_data
|
||||||
participation.save()
|
participation.save()
|
||||||
|
|
||||||
def display_user_problem(self, participation, contest_problem, show_final=False):
|
def display_user_problem(self, participation, contest_problem):
|
||||||
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
||||||
if format_data:
|
if format_data:
|
||||||
penalty = (
|
penalty = format_html('<small style="color:red"> ({penalty})</small>',
|
||||||
format_html(
|
penalty=floatformat(format_data['penalty'])) if format_data['penalty'] else ''
|
||||||
'<small style="color:red"> ({penalty})</small>',
|
|
||||||
penalty=floatformat(format_data["penalty"]),
|
|
||||||
)
|
|
||||||
if format_data.get("penalty")
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
return format_html(
|
return format_html(
|
||||||
'<td class="{state} problem-score-col"><a data-featherlight="{url}" href="#">{points}{penalty}<div class="solving-time">{time}</div></a></td>',
|
'<td class="{state}"><a href="{url}">{points}{penalty}<div class="solving-time">{time}</div></a></td>',
|
||||||
state=(
|
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
|
||||||
(
|
self.best_solution_state(format_data['points'], contest_problem.points)),
|
||||||
"pretest-"
|
url=reverse('contest_user_submissions',
|
||||||
if self.contest.run_pretests_only
|
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
|
||||||
and contest_problem.is_pretested
|
points=floatformat(format_data['points']),
|
||||||
else ""
|
|
||||||
)
|
|
||||||
+ self.best_solution_state(
|
|
||||||
format_data["points"], contest_problem.points
|
|
||||||
)
|
|
||||||
+ (" frozen" if format_data.get("frozen") else "")
|
|
||||||
),
|
|
||||||
url=reverse(
|
|
||||||
"contest_user_submissions_ajax",
|
|
||||||
args=[
|
|
||||||
self.contest.key,
|
|
||||||
participation.id,
|
|
||||||
contest_problem.problem.code,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
points=floatformat(format_data["points"]),
|
|
||||||
penalty=penalty,
|
penalty=penalty,
|
||||||
time=nice_repr(timedelta(seconds=format_data["time"]), "noday"),
|
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return mark_safe('<td class="problem-score-col"></td>')
|
return mark_safe('<td></td>')
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from abc import ABCMeta, abstractmethod, abstractproperty
|
from abc import ABCMeta, abstractmethod, abstractproperty
|
||||||
from django.db.models import Max
|
|
||||||
|
from django.utils import six
|
||||||
|
|
||||||
|
|
||||||
class abstractclassmethod(classmethod):
|
class abstractclassmethod(classmethod):
|
||||||
|
@ -10,9 +11,7 @@ class abstractclassmethod(classmethod):
|
||||||
super(abstractclassmethod, self).__init__(callable)
|
super(abstractclassmethod, self).__init__(callable)
|
||||||
|
|
||||||
|
|
||||||
class BaseContestFormat(metaclass=ABCMeta):
|
class BaseContestFormat(six.with_metaclass(ABCMeta)):
|
||||||
has_hidden_subtasks = False
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __init__(self, contest, config):
|
def __init__(self, contest, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
@ -50,7 +49,7 @@ class BaseContestFormat(metaclass=ABCMeta):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def display_user_problem(self, participation, contest_problem, show_final):
|
def display_user_problem(self, participation, contest_problem):
|
||||||
"""
|
"""
|
||||||
Returns the HTML fragment to show a user's performance on an individual problem. This is expected to use
|
Returns the HTML fragment to show a user's performance on an individual problem. This is expected to use
|
||||||
information from the format_data field instead of computing it from scratch.
|
information from the format_data field instead of computing it from scratch.
|
||||||
|
@ -62,7 +61,7 @@ class BaseContestFormat(metaclass=ABCMeta):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def display_participation_result(self, participation, show_final):
|
def display_participation_result(self, participation):
|
||||||
"""
|
"""
|
||||||
Returns the HTML fragment to show a user's performance on the whole contest. This is expected to use
|
Returns the HTML fragment to show a user's performance on the whole contest. This is expected to use
|
||||||
information from the format_data field instead of computing it from scratch.
|
information from the format_data field instead of computing it from scratch.
|
||||||
|
@ -83,41 +82,10 @@ class BaseContestFormat(metaclass=ABCMeta):
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_contest_problem_label_script(self):
|
|
||||||
"""
|
|
||||||
Returns the default Lua script to generate contest problem labels.
|
|
||||||
:return: A string, the Lua script.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def best_solution_state(cls, points, total):
|
def best_solution_state(cls, points, total):
|
||||||
if not points:
|
if not points:
|
||||||
return "failed-score"
|
return 'failed-score'
|
||||||
if points == total:
|
if points == total:
|
||||||
return "full-score"
|
return 'full-score'
|
||||||
return "partial-score"
|
return 'partial-score'
|
||||||
|
|
||||||
def handle_frozen_state(self, participation, format_data):
|
|
||||||
hidden_subtasks = {}
|
|
||||||
if hasattr(self, "get_hidden_subtasks"):
|
|
||||||
hidden_subtasks = self.get_hidden_subtasks()
|
|
||||||
|
|
||||||
queryset = participation.submissions.values("problem_id").annotate(
|
|
||||||
time=Max("submission__date")
|
|
||||||
)
|
|
||||||
for result in queryset:
|
|
||||||
problem = str(result["problem_id"])
|
|
||||||
if not (self.contest.freeze_after or hidden_subtasks.get(problem)):
|
|
||||||
continue
|
|
||||||
if format_data.get(problem):
|
|
||||||
is_after_freeze = (
|
|
||||||
self.contest.freeze_after
|
|
||||||
and result["time"]
|
|
||||||
>= self.contest.freeze_after + participation.start
|
|
||||||
)
|
|
||||||
if is_after_freeze or hidden_subtasks.get(problem):
|
|
||||||
format_data[problem]["frozen"] = True
|
|
||||||
else:
|
|
||||||
format_data[problem] = {"time": 0, "points": 0, "frozen": True}
|
|
||||||
|
|
|
@ -13,16 +13,14 @@ from judge.contest_format.registry import register_contest_format
|
||||||
from judge.utils.timedelta import nice_repr
|
from judge.utils.timedelta import nice_repr
|
||||||
|
|
||||||
|
|
||||||
@register_contest_format("default")
|
@register_contest_format('default')
|
||||||
class DefaultContestFormat(BaseContestFormat):
|
class DefaultContestFormat(BaseContestFormat):
|
||||||
name = gettext_lazy("Default")
|
name = gettext_lazy('Default')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config):
|
def validate(cls, config):
|
||||||
if config is not None and (not isinstance(config, dict) or config):
|
if config is not None and (not isinstance(config, dict) or config):
|
||||||
raise ValidationError(
|
raise ValidationError('default contest expects no config or empty dict as config')
|
||||||
"default contest expects no config or empty dict as config"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, contest, config):
|
def __init__(self, contest, config):
|
||||||
super(DefaultContestFormat, self).__init__(contest, config)
|
super(DefaultContestFormat, self).__init__(contest, config)
|
||||||
|
@ -32,84 +30,41 @@ class DefaultContestFormat(BaseContestFormat):
|
||||||
points = 0
|
points = 0
|
||||||
format_data = {}
|
format_data = {}
|
||||||
|
|
||||||
queryset = participation.submissions
|
for result in participation.submissions.values('problem_id').annotate(
|
||||||
|
time=Max('submission__date'), points=Max('points'),
|
||||||
if self.contest.freeze_after:
|
):
|
||||||
queryset = queryset.filter(
|
dt = (result['time'] - participation.start).total_seconds()
|
||||||
submission__date__lt=participation.start + self.contest.freeze_after
|
if result['points']:
|
||||||
)
|
|
||||||
|
|
||||||
queryset = queryset.values("problem_id").annotate(
|
|
||||||
time=Max("submission__date"),
|
|
||||||
points=Max("points"),
|
|
||||||
)
|
|
||||||
|
|
||||||
for result in queryset:
|
|
||||||
dt = (result["time"] - participation.start).total_seconds()
|
|
||||||
if result["points"]:
|
|
||||||
cumtime += dt
|
cumtime += dt
|
||||||
format_data[str(result["problem_id"])] = {
|
format_data[str(result['problem_id'])] = {'time': dt, 'points': result['points']}
|
||||||
"time": dt,
|
points += result['points']
|
||||||
"points": result["points"],
|
|
||||||
}
|
|
||||||
points += result["points"]
|
|
||||||
|
|
||||||
self.handle_frozen_state(participation, format_data)
|
|
||||||
participation.cumtime = max(cumtime, 0)
|
participation.cumtime = max(cumtime, 0)
|
||||||
participation.score = round(points, self.contest.points_precision)
|
participation.score = points
|
||||||
participation.tiebreaker = 0
|
|
||||||
participation.format_data = format_data
|
participation.format_data = format_data
|
||||||
participation.save()
|
participation.save()
|
||||||
|
|
||||||
def display_user_problem(self, participation, contest_problem, show_final=False):
|
def display_user_problem(self, participation, contest_problem):
|
||||||
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
||||||
if format_data:
|
if format_data:
|
||||||
return format_html(
|
return format_html(
|
||||||
'<td class="{state} problem-score-col"><a data-featherlight="{url}" href="#">{points}<div class="solving-time">{time}</div></a></td>',
|
u'<td class="{state}"><a href="{url}">{points}<div class="solving-time">{time}</div></a></td>',
|
||||||
state=(
|
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
|
||||||
(
|
self.best_solution_state(format_data['points'], contest_problem.points)),
|
||||||
"pretest-"
|
url=reverse('contest_user_submissions',
|
||||||
if self.contest.run_pretests_only
|
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
|
||||||
and contest_problem.is_pretested
|
points=floatformat(format_data['points']),
|
||||||
else ""
|
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'),
|
||||||
)
|
|
||||||
+ self.best_solution_state(
|
|
||||||
format_data["points"], contest_problem.points
|
|
||||||
)
|
|
||||||
+ (" frozen" if format_data.get("frozen") else "")
|
|
||||||
),
|
|
||||||
url=reverse(
|
|
||||||
"contest_user_submissions_ajax",
|
|
||||||
args=[
|
|
||||||
self.contest.key,
|
|
||||||
participation.id,
|
|
||||||
contest_problem.problem.code,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
points=floatformat(
|
|
||||||
format_data["points"], -self.contest.points_precision
|
|
||||||
),
|
|
||||||
time=nice_repr(timedelta(seconds=format_data["time"]), "noday"),
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return mark_safe('<td class="problem-score-col"></td>')
|
return mark_safe('<td></td>')
|
||||||
|
|
||||||
def display_participation_result(self, participation, show_final=False):
|
def display_participation_result(self, participation):
|
||||||
return format_html(
|
return format_html(
|
||||||
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
|
u'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
|
||||||
points=floatformat(participation.score, -self.contest.points_precision),
|
points=floatformat(participation.score),
|
||||||
cumtime=nice_repr(timedelta(seconds=participation.cumtime), "noday"),
|
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_problem_breakdown(self, participation, contest_problems):
|
def get_problem_breakdown(self, participation, contest_problems):
|
||||||
return [
|
return [(participation.format_data or {}).get(str(contest_problem.id)) for contest_problem in contest_problems]
|
||||||
(participation.format_data or {}).get(str(contest_problem.id))
|
|
||||||
for contest_problem in contest_problems
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_contest_problem_label_script(self):
|
|
||||||
return """
|
|
||||||
function(n)
|
|
||||||
return tostring(math.floor(n + 1))
|
|
||||||
end
|
|
||||||
"""
|
|
||||||
|
|
|
@ -10,25 +10,21 @@ from django.utils.translation import gettext_lazy
|
||||||
|
|
||||||
from judge.contest_format.default import DefaultContestFormat
|
from judge.contest_format.default import DefaultContestFormat
|
||||||
from judge.contest_format.registry import register_contest_format
|
from judge.contest_format.registry import register_contest_format
|
||||||
from judge.timezone import from_database_time, to_database_time
|
from judge.timezone import from_database_time
|
||||||
from judge.utils.timedelta import nice_repr
|
from judge.utils.timedelta import nice_repr
|
||||||
|
|
||||||
|
|
||||||
@register_contest_format("ecoo")
|
@register_contest_format('ecoo')
|
||||||
class ECOOContestFormat(DefaultContestFormat):
|
class ECOOContestFormat(DefaultContestFormat):
|
||||||
name = gettext_lazy("ECOO")
|
name = gettext_lazy('ECOO')
|
||||||
config_defaults = {"cumtime": False, "first_ac_bonus": 10, "time_bonus": 5}
|
config_defaults = {'cumtime': False, 'first_ac_bonus': 10, 'time_bonus': 5}
|
||||||
config_validators = {
|
config_validators = {'cumtime': lambda x: True, 'first_ac_bonus': lambda x: x >= 0, 'time_bonus': lambda x: x >= 0}
|
||||||
"cumtime": lambda x: True,
|
'''
|
||||||
"first_ac_bonus": lambda x: x >= 0,
|
|
||||||
"time_bonus": lambda x: x >= 0,
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
cumtime: Specify True if cumulative time is to be used in breaking ties. Defaults to False.
|
cumtime: Specify True if cumulative time is to be used in breaking ties. Defaults to False.
|
||||||
first_ac_bonus: The number of points to award if a solution gets AC on its first non-IE/CE run. Defaults to 10.
|
first_ac_bonus: The number of points to award if a solution gets AC on its first non-IE/CE run. Defaults to 10.
|
||||||
time_bonus: Number of minutes to award an extra point for submitting before the contest end.
|
time_bonus: Number of minutes to award an extra point for submitting before the contest end.
|
||||||
Specify 0 to disable. Defaults to 5.
|
Specify 0 to disable. Defaults to 5.
|
||||||
"""
|
'''
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config):
|
def validate(cls, config):
|
||||||
|
@ -36,9 +32,7 @@ class ECOOContestFormat(DefaultContestFormat):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not isinstance(config, dict):
|
if not isinstance(config, dict):
|
||||||
raise ValidationError(
|
raise ValidationError('ECOO-styled contest expects no config or dict as config')
|
||||||
"ECOO-styled contest expects no config or dict as config"
|
|
||||||
)
|
|
||||||
|
|
||||||
for key, value in config.items():
|
for key, value in config.items():
|
||||||
if key not in cls.config_defaults:
|
if key not in cls.config_defaults:
|
||||||
|
@ -46,9 +40,7 @@ class ECOOContestFormat(DefaultContestFormat):
|
||||||
if not isinstance(value, type(cls.config_defaults[key])):
|
if not isinstance(value, type(cls.config_defaults[key])):
|
||||||
raise ValidationError('invalid type for config key "%s"' % key)
|
raise ValidationError('invalid type for config key "%s"' % key)
|
||||||
if not cls.config_validators[key](value):
|
if not cls.config_validators[key](value):
|
||||||
raise ValidationError(
|
raise ValidationError('invalid value "%s" for config key "%s"' % (value, key))
|
||||||
'invalid value "%s" for config key "%s"' % (value, key)
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, contest, config):
|
def __init__(self, contest, config):
|
||||||
self.config = self.config_defaults.copy()
|
self.config = self.config_defaults.copy()
|
||||||
|
@ -60,13 +52,8 @@ class ECOOContestFormat(DefaultContestFormat):
|
||||||
points = 0
|
points = 0
|
||||||
format_data = {}
|
format_data = {}
|
||||||
|
|
||||||
frozen_time = self.contest.end_time
|
|
||||||
if self.contest.freeze_after:
|
|
||||||
frozen_time = participation.start + self.contest.freeze_after
|
|
||||||
|
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute(
|
cursor.execute('''
|
||||||
"""
|
|
||||||
SELECT (
|
SELECT (
|
||||||
SELECT MAX(ccs.points)
|
SELECT MAX(ccs.points)
|
||||||
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
||||||
|
@ -81,92 +68,55 @@ class ECOOContestFormat(DefaultContestFormat):
|
||||||
FROM judge_contestproblem cp INNER JOIN
|
FROM judge_contestproblem cp INNER JOIN
|
||||||
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
||||||
judge_submission sub ON (sub.id = cs.submission_id)
|
judge_submission sub ON (sub.id = cs.submission_id)
|
||||||
WHERE sub.date < %s
|
|
||||||
GROUP BY cp.id
|
GROUP BY cp.id
|
||||||
""",
|
''', (participation.id, participation.id, participation.id))
|
||||||
(
|
|
||||||
participation.id,
|
|
||||||
participation.id,
|
|
||||||
participation.id,
|
|
||||||
to_database_time(frozen_time),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
for score, time, prob, subs, max_score in cursor.fetchall():
|
for score, time, prob, subs, max_score in cursor.fetchall():
|
||||||
time = from_database_time(time)
|
time = from_database_time(time)
|
||||||
dt = (time - participation.start).total_seconds()
|
dt = (time - participation.start).total_seconds()
|
||||||
if self.config["cumtime"]:
|
if self.config['cumtime']:
|
||||||
cumtime += dt
|
cumtime += dt
|
||||||
|
|
||||||
bonus = 0
|
bonus = 0
|
||||||
if score > 0:
|
if score > 0:
|
||||||
# First AC bonus
|
# First AC bonus
|
||||||
if subs == 1 and score == max_score:
|
if subs == 1 and score == max_score:
|
||||||
bonus += self.config["first_ac_bonus"]
|
bonus += self.config['first_ac_bonus']
|
||||||
# Time bonus
|
# Time bonus
|
||||||
if self.config["time_bonus"]:
|
if self.config['time_bonus']:
|
||||||
bonus += (
|
bonus += (participation.end_time - time).total_seconds() // 60 // self.config['time_bonus']
|
||||||
(participation.end_time - time).total_seconds()
|
|
||||||
// 60
|
|
||||||
// self.config["time_bonus"]
|
|
||||||
)
|
|
||||||
points += bonus
|
points += bonus
|
||||||
|
|
||||||
format_data[str(prob)] = {"time": dt, "points": score, "bonus": bonus}
|
format_data[str(prob)] = {'time': dt, 'points': score, 'bonus': bonus}
|
||||||
points += score
|
points += score
|
||||||
|
|
||||||
self.handle_frozen_state(participation, format_data)
|
|
||||||
participation.cumtime = cumtime
|
participation.cumtime = cumtime
|
||||||
participation.score = round(points, self.contest.points_precision)
|
participation.score = points
|
||||||
participation.tiebreaker = 0
|
|
||||||
participation.format_data = format_data
|
participation.format_data = format_data
|
||||||
participation.save()
|
participation.save()
|
||||||
|
|
||||||
def display_user_problem(self, participation, contest_problem, show_final=False):
|
def display_user_problem(self, participation, contest_problem):
|
||||||
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
||||||
if format_data:
|
if format_data:
|
||||||
bonus = (
|
bonus = format_html('<small> +{bonus}</small>',
|
||||||
format_html(
|
bonus=floatformat(format_data['bonus'])) if format_data['bonus'] else ''
|
||||||
"<small> +{bonus}</small>", bonus=floatformat(format_data["bonus"])
|
|
||||||
)
|
|
||||||
if format_data.get("bonus")
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
|
|
||||||
return format_html(
|
return format_html(
|
||||||
'<td class="{state}"><a data-featherlight="{url}" href="#">{points}{bonus}<div class="solving-time">{time}</div></a></td>',
|
'<td class="{state}"><a href="{url}">{points}{bonus}<div class="solving-time">{time}</div></a></td>',
|
||||||
state=(
|
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
|
||||||
(
|
self.best_solution_state(format_data['points'], contest_problem.points)),
|
||||||
"pretest-"
|
url=reverse('contest_user_submissions',
|
||||||
if self.contest.run_pretests_only
|
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
|
||||||
and contest_problem.is_pretested
|
points=floatformat(format_data['points']),
|
||||||
else ""
|
|
||||||
)
|
|
||||||
+ self.best_solution_state(
|
|
||||||
format_data["points"], contest_problem.points
|
|
||||||
)
|
|
||||||
+ (" frozen" if format_data.get("frozen") else "")
|
|
||||||
),
|
|
||||||
url=reverse(
|
|
||||||
"contest_user_submissions_ajax",
|
|
||||||
args=[
|
|
||||||
self.contest.key,
|
|
||||||
participation.id,
|
|
||||||
contest_problem.problem.code,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
points=floatformat(format_data["points"]),
|
|
||||||
bonus=bonus,
|
bonus=bonus,
|
||||||
time=nice_repr(timedelta(seconds=format_data["time"]), "noday"),
|
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return mark_safe("<td></td>")
|
return mark_safe('<td></td>')
|
||||||
|
|
||||||
def display_participation_result(self, participation, show_final=False):
|
def display_participation_result(self, participation):
|
||||||
return format_html(
|
return format_html(
|
||||||
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
|
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
|
||||||
points=floatformat(participation.score),
|
points=floatformat(participation.score),
|
||||||
cumtime=nice_repr(timedelta(seconds=participation.cumtime), "noday")
|
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') if self.config['cumtime'] else '',
|
||||||
if self.config["cumtime"]
|
|
||||||
else "",
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,168 +0,0 @@
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.db import connection
|
|
||||||
from django.template.defaultfilters import floatformat
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.html import format_html
|
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
from django.utils.translation import gettext_lazy
|
|
||||||
|
|
||||||
from judge.contest_format.default import DefaultContestFormat
|
|
||||||
from judge.contest_format.registry import register_contest_format
|
|
||||||
from judge.timezone import from_database_time, to_database_time
|
|
||||||
from judge.utils.timedelta import nice_repr
|
|
||||||
|
|
||||||
|
|
||||||
@register_contest_format("icpc")
|
|
||||||
class ICPCContestFormat(DefaultContestFormat):
|
|
||||||
name = gettext_lazy("ICPC")
|
|
||||||
config_defaults = {"penalty": 20}
|
|
||||||
config_validators = {"penalty": lambda x: x >= 0}
|
|
||||||
"""
|
|
||||||
penalty: Number of penalty minutes each incorrect submission adds. Defaults to 20.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def validate(cls, config):
|
|
||||||
if config is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not isinstance(config, dict):
|
|
||||||
raise ValidationError(
|
|
||||||
"ICPC-styled contest expects no config or dict as config"
|
|
||||||
)
|
|
||||||
|
|
||||||
for key, value in config.items():
|
|
||||||
if key not in cls.config_defaults:
|
|
||||||
raise ValidationError('unknown config key "%s"' % key)
|
|
||||||
if not isinstance(value, type(cls.config_defaults[key])):
|
|
||||||
raise ValidationError('invalid type for config key "%s"' % key)
|
|
||||||
if not cls.config_validators[key](value):
|
|
||||||
raise ValidationError(
|
|
||||||
'invalid value "%s" for config key "%s"' % (value, key)
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, contest, config):
|
|
||||||
self.config = self.config_defaults.copy()
|
|
||||||
self.config.update(config or {})
|
|
||||||
self.contest = contest
|
|
||||||
|
|
||||||
def update_participation(self, participation):
|
|
||||||
cumtime = 0
|
|
||||||
last = 0
|
|
||||||
penalty = 0
|
|
||||||
score = 0
|
|
||||||
format_data = {}
|
|
||||||
|
|
||||||
frozen_time = self.contest.end_time
|
|
||||||
if self.contest.freeze_after:
|
|
||||||
frozen_time = participation.start + self.contest.freeze_after
|
|
||||||
|
|
||||||
with connection.cursor() as cursor:
|
|
||||||
cursor.execute(
|
|
||||||
"""
|
|
||||||
SELECT MAX(cs.points) as `points`, (
|
|
||||||
SELECT MIN(csub.date)
|
|
||||||
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
|
||||||
judge_submission csub ON (csub.id = ccs.submission_id)
|
|
||||||
WHERE ccs.problem_id = cp.id AND ccs.participation_id = %s AND ccs.points = MAX(cs.points)
|
|
||||||
) AS `time`, cp.id AS `prob`
|
|
||||||
FROM judge_contestproblem cp INNER JOIN
|
|
||||||
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
|
||||||
judge_submission sub ON (sub.id = cs.submission_id)
|
|
||||||
WHERE sub.date < %s
|
|
||||||
GROUP BY cp.id
|
|
||||||
""",
|
|
||||||
(participation.id, participation.id, to_database_time(frozen_time)),
|
|
||||||
)
|
|
||||||
|
|
||||||
for points, time, prob in cursor.fetchall():
|
|
||||||
time = from_database_time(time)
|
|
||||||
dt = (time - participation.start).total_seconds()
|
|
||||||
|
|
||||||
# Compute penalty
|
|
||||||
if self.config["penalty"]:
|
|
||||||
# An IE can have a submission result of `None`
|
|
||||||
subs = (
|
|
||||||
participation.submissions.exclude(
|
|
||||||
submission__result__isnull=True
|
|
||||||
)
|
|
||||||
.exclude(submission__result__in=["IE", "CE"])
|
|
||||||
.filter(problem_id=prob)
|
|
||||||
)
|
|
||||||
if points:
|
|
||||||
prev = subs.filter(submission__date__lte=time).count() - 1
|
|
||||||
penalty += prev * self.config["penalty"] * 60
|
|
||||||
else:
|
|
||||||
# We should always display the penalty, even if the user has a score of 0
|
|
||||||
prev = subs.count()
|
|
||||||
else:
|
|
||||||
prev = 0
|
|
||||||
|
|
||||||
if points:
|
|
||||||
cumtime += dt
|
|
||||||
last = max(last, dt)
|
|
||||||
|
|
||||||
format_data[str(prob)] = {"time": dt, "points": points, "penalty": prev}
|
|
||||||
score += points
|
|
||||||
|
|
||||||
self.handle_frozen_state(participation, format_data)
|
|
||||||
participation.cumtime = max(0, cumtime + penalty)
|
|
||||||
participation.score = round(score, self.contest.points_precision)
|
|
||||||
participation.tiebreaker = last # field is sorted from least to greatest
|
|
||||||
participation.format_data = format_data
|
|
||||||
participation.save()
|
|
||||||
|
|
||||||
def display_user_problem(self, participation, contest_problem, show_final=False):
|
|
||||||
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
|
||||||
if format_data:
|
|
||||||
penalty = (
|
|
||||||
format_html(
|
|
||||||
'<small style="color:red"> +{penalty}</small>',
|
|
||||||
penalty=floatformat(format_data["penalty"]),
|
|
||||||
)
|
|
||||||
if format_data.get("penalty")
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
return format_html(
|
|
||||||
'<td class="{state}"><a data-featherlight="{url}" href="#">{points}{penalty}<div class="solving-time">{time}</div></a></td>',
|
|
||||||
state=(
|
|
||||||
(
|
|
||||||
"pretest-"
|
|
||||||
if self.contest.run_pretests_only
|
|
||||||
and contest_problem.is_pretested
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
+ self.best_solution_state(
|
|
||||||
format_data["points"], contest_problem.points
|
|
||||||
)
|
|
||||||
+ (" frozen" if format_data.get("frozen") else "")
|
|
||||||
),
|
|
||||||
url=reverse(
|
|
||||||
"contest_user_submissions_ajax",
|
|
||||||
args=[
|
|
||||||
self.contest.key,
|
|
||||||
participation.id,
|
|
||||||
contest_problem.problem.code,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
points=floatformat(format_data["points"]),
|
|
||||||
penalty=penalty,
|
|
||||||
time=nice_repr(timedelta(seconds=format_data["time"]), "noday"),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return mark_safe("<td></td>")
|
|
||||||
|
|
||||||
def get_contest_problem_label_script(self):
|
|
||||||
return """
|
|
||||||
function(n)
|
|
||||||
n = n + 1
|
|
||||||
ret = ""
|
|
||||||
while n > 0 do
|
|
||||||
ret = string.char((n - 1) % 26 + 65) .. ret
|
|
||||||
n = math.floor((n - 1) / 26)
|
|
||||||
end
|
|
||||||
return ret
|
|
||||||
end
|
|
||||||
"""
|
|
|
@ -12,16 +12,15 @@ from judge.contest_format.default import DefaultContestFormat
|
||||||
from judge.contest_format.registry import register_contest_format
|
from judge.contest_format.registry import register_contest_format
|
||||||
from judge.timezone import from_database_time
|
from judge.timezone import from_database_time
|
||||||
from judge.utils.timedelta import nice_repr
|
from judge.utils.timedelta import nice_repr
|
||||||
from django.db.models import Min, OuterRef, Subquery
|
|
||||||
|
|
||||||
|
|
||||||
@register_contest_format("ioi")
|
@register_contest_format('ioi')
|
||||||
class IOIContestFormat(DefaultContestFormat):
|
class IOIContestFormat(DefaultContestFormat):
|
||||||
name = gettext_lazy("IOI")
|
name = gettext_lazy('IOI')
|
||||||
config_defaults = {"cumtime": False}
|
config_defaults = {'cumtime': False}
|
||||||
"""
|
'''
|
||||||
cumtime: Specify True if time penalties are to be computed. Defaults to False.
|
cumtime: Specify True if time penalties are to be computed. Defaults to False.
|
||||||
"""
|
'''
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, config):
|
def validate(cls, config):
|
||||||
|
@ -29,9 +28,7 @@ class IOIContestFormat(DefaultContestFormat):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not isinstance(config, dict):
|
if not isinstance(config, dict):
|
||||||
raise ValidationError(
|
raise ValidationError('IOI-styled contest expects no config or dict as config')
|
||||||
"IOI-styled contest expects no config or dict as config"
|
|
||||||
)
|
|
||||||
|
|
||||||
for key, value in config.items():
|
for key, value in config.items():
|
||||||
if key not in cls.config_defaults:
|
if key not in cls.config_defaults:
|
||||||
|
@ -46,97 +43,57 @@ class IOIContestFormat(DefaultContestFormat):
|
||||||
|
|
||||||
def update_participation(self, participation):
|
def update_participation(self, participation):
|
||||||
cumtime = 0
|
cumtime = 0
|
||||||
score = 0
|
points = 0
|
||||||
format_data = {}
|
format_data = {}
|
||||||
|
|
||||||
queryset = participation.submissions
|
with connection.cursor() as cursor:
|
||||||
if self.contest.freeze_after:
|
cursor.execute('''
|
||||||
queryset = queryset.filter(
|
SELECT MAX(cs.points) as `score`, (
|
||||||
submission__date__lt=participation.start + self.contest.freeze_after
|
SELECT MIN(csub.date)
|
||||||
)
|
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
||||||
|
judge_submission csub ON (csub.id = ccs.submission_id)
|
||||||
|
WHERE ccs.problem_id = cp.id AND ccs.participation_id = %s AND ccs.points = MAX(cs.points)
|
||||||
|
) AS `time`, cp.id AS `prob`
|
||||||
|
FROM judge_contestproblem cp INNER JOIN
|
||||||
|
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
||||||
|
judge_submission sub ON (sub.id = cs.submission_id)
|
||||||
|
GROUP BY cp.id
|
||||||
|
''', (participation.id, participation.id))
|
||||||
|
|
||||||
queryset = (
|
for score, time, prob in cursor.fetchall():
|
||||||
queryset.values("problem_id")
|
if self.config['cumtime']:
|
||||||
.filter(
|
dt = (from_database_time(time) - participation.start).total_seconds()
|
||||||
points=Subquery(
|
if score:
|
||||||
queryset.filter(problem_id=OuterRef("problem_id"))
|
|
||||||
.order_by("-points")
|
|
||||||
.values("points")[:1]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.annotate(time=Min("submission__date"))
|
|
||||||
.values_list("problem_id", "time", "points")
|
|
||||||
)
|
|
||||||
|
|
||||||
for problem_id, time, points in queryset:
|
|
||||||
if self.config["cumtime"]:
|
|
||||||
dt = (time - participation.start).total_seconds()
|
|
||||||
if points:
|
|
||||||
cumtime += dt
|
cumtime += dt
|
||||||
else:
|
else:
|
||||||
dt = 0
|
dt = 0
|
||||||
|
|
||||||
format_data[str(problem_id)] = {"points": points, "time": dt}
|
format_data[str(prob)] = {'time': dt, 'points': score}
|
||||||
score += points
|
points += score
|
||||||
|
|
||||||
self.handle_frozen_state(participation, format_data)
|
|
||||||
participation.cumtime = max(cumtime, 0)
|
participation.cumtime = max(cumtime, 0)
|
||||||
participation.score = round(score, self.contest.points_precision)
|
participation.score = points
|
||||||
participation.tiebreaker = 0
|
|
||||||
participation.format_data = format_data
|
participation.format_data = format_data
|
||||||
participation.save()
|
participation.save()
|
||||||
|
|
||||||
def display_user_problem(self, participation, contest_problem, show_final=False):
|
def display_user_problem(self, participation, contest_problem):
|
||||||
if show_final:
|
|
||||||
format_data = (participation.format_data_final or {}).get(
|
|
||||||
str(contest_problem.id)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
||||||
if format_data:
|
if format_data:
|
||||||
return format_html(
|
return format_html(
|
||||||
'<td class="{state} problem-score-col"><a data-featherlight="{url}" href="#">{points}<div class="solving-time">{time}</div></a></td>',
|
'<td class="{state}"><a href="{url}">{points}<div class="solving-time">{time}</div></a></td>',
|
||||||
state=(
|
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
|
||||||
(
|
self.best_solution_state(format_data['points'], contest_problem.points)),
|
||||||
"pretest-"
|
url=reverse('contest_user_submissions',
|
||||||
if self.contest.run_pretests_only
|
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
|
||||||
and contest_problem.is_pretested
|
points=floatformat(format_data['points']),
|
||||||
else ""
|
time=nice_repr(timedelta(seconds=format_data['time']), 'noday') if self.config['cumtime'] else '',
|
||||||
)
|
|
||||||
+ self.best_solution_state(
|
|
||||||
format_data["points"], contest_problem.points
|
|
||||||
)
|
|
||||||
+ (" frozen" if format_data.get("frozen") else "")
|
|
||||||
),
|
|
||||||
url=reverse(
|
|
||||||
"contest_user_submissions_ajax",
|
|
||||||
args=[
|
|
||||||
self.contest.key,
|
|
||||||
participation.id,
|
|
||||||
contest_problem.problem.code,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
points=floatformat(
|
|
||||||
format_data["points"], -self.contest.points_precision
|
|
||||||
),
|
|
||||||
time=nice_repr(timedelta(seconds=format_data["time"]), "noday")
|
|
||||||
if self.config["cumtime"]
|
|
||||||
else "",
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return mark_safe('<td class="problem-score-col"></td>')
|
return mark_safe('<td></td>')
|
||||||
|
|
||||||
def display_participation_result(self, participation, show_final=False):
|
def display_participation_result(self, participation):
|
||||||
if show_final:
|
|
||||||
score = participation.score_final
|
|
||||||
cumtime = participation.cumtime_final
|
|
||||||
else:
|
|
||||||
score = participation.score
|
|
||||||
cumtime = participation.cumtime
|
|
||||||
return format_html(
|
return format_html(
|
||||||
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
|
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
|
||||||
points=floatformat(score, -self.contest.points_precision),
|
points=floatformat(participation.score),
|
||||||
cumtime=nice_repr(timedelta(seconds=cumtime), "noday")
|
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') if self.config['cumtime'] else '',
|
||||||
if self.config["cumtime"]
|
|
||||||
else "",
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,173 +0,0 @@
|
||||||
from django.db import connection
|
|
||||||
from django.utils.translation import gettext as _, gettext_lazy
|
|
||||||
|
|
||||||
from judge.contest_format.ioi import IOIContestFormat
|
|
||||||
from judge.contest_format.registry import register_contest_format
|
|
||||||
from judge.timezone import from_database_time, to_database_time
|
|
||||||
|
|
||||||
|
|
||||||
@register_contest_format("ioi16")
|
|
||||||
class NewIOIContestFormat(IOIContestFormat):
|
|
||||||
name = gettext_lazy("New IOI")
|
|
||||||
config_defaults = {"cumtime": False}
|
|
||||||
has_hidden_subtasks = True
|
|
||||||
"""
|
|
||||||
cumtime: Specify True if time penalties are to be computed. Defaults to False.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_hidden_subtasks(self):
|
|
||||||
queryset = self.contest.contest_problems.values_list("id", "hidden_subtasks")
|
|
||||||
res = {}
|
|
||||||
for problem_id, hidden_subtasks in queryset:
|
|
||||||
subtasks = set()
|
|
||||||
if hidden_subtasks:
|
|
||||||
hidden_subtasks = hidden_subtasks.split(",")
|
|
||||||
for i in hidden_subtasks:
|
|
||||||
try:
|
|
||||||
subtasks.add(int(i))
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
res[str(problem_id)] = subtasks
|
|
||||||
return res
|
|
||||||
|
|
||||||
def get_results_by_subtask(self, participation, include_frozen=False):
|
|
||||||
frozen_time = self.contest.end_time
|
|
||||||
if self.contest.freeze_after and not include_frozen:
|
|
||||||
frozen_time = participation.start + self.contest.freeze_after
|
|
||||||
|
|
||||||
with connection.cursor() as cursor:
|
|
||||||
cursor.execute(
|
|
||||||
"""
|
|
||||||
SELECT q.prob,
|
|
||||||
q.prob_points,
|
|
||||||
MIN(q.date) as `date`,
|
|
||||||
q.batch_points,
|
|
||||||
q.total_batch_points,
|
|
||||||
q.batch,
|
|
||||||
q.subid
|
|
||||||
FROM (
|
|
||||||
SELECT cp.id as `prob`,
|
|
||||||
cp.points as `prob_points`,
|
|
||||||
sub.id as `subid`,
|
|
||||||
sub.date as `date`,
|
|
||||||
tc.points as `points`,
|
|
||||||
tc.batch as `batch`,
|
|
||||||
SUM(tc.points) as `batch_points`,
|
|
||||||
SUM(tc.total) as `total_batch_points`
|
|
||||||
FROM judge_contestproblem cp
|
|
||||||
INNER JOIN
|
|
||||||
judge_contestsubmission cs
|
|
||||||
ON (cs.problem_id = cp.id AND cs.participation_id = %s)
|
|
||||||
LEFT OUTER JOIN
|
|
||||||
judge_submission sub
|
|
||||||
ON (sub.id = cs.submission_id AND sub.status = 'D')
|
|
||||||
INNER JOIN judge_submissiontestcase tc
|
|
||||||
ON sub.id = tc.submission_id
|
|
||||||
WHERE sub.date < %s
|
|
||||||
GROUP BY cp.id, tc.batch, sub.id
|
|
||||||
) q
|
|
||||||
INNER JOIN (
|
|
||||||
SELECT prob, batch, MAX(r.batch_points) as max_batch_points
|
|
||||||
FROM (
|
|
||||||
SELECT cp.id as `prob`,
|
|
||||||
tc.batch as `batch`,
|
|
||||||
SUM(tc.points) as `batch_points`
|
|
||||||
FROM judge_contestproblem cp
|
|
||||||
INNER JOIN
|
|
||||||
judge_contestsubmission cs
|
|
||||||
ON (cs.problem_id = cp.id AND cs.participation_id = %s)
|
|
||||||
LEFT OUTER JOIN
|
|
||||||
judge_submission sub
|
|
||||||
ON (sub.id = cs.submission_id AND sub.status = 'D')
|
|
||||||
INNER JOIN judge_submissiontestcase tc
|
|
||||||
ON sub.id = tc.submission_id
|
|
||||||
WHERE sub.date < %s
|
|
||||||
GROUP BY cp.id, tc.batch, sub.id
|
|
||||||
) r
|
|
||||||
GROUP BY prob, batch
|
|
||||||
) p
|
|
||||||
ON p.prob = q.prob AND (p.batch = q.batch OR p.batch is NULL AND q.batch is NULL)
|
|
||||||
WHERE p.max_batch_points = q.batch_points
|
|
||||||
GROUP BY q.prob, q.batch
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
participation.id,
|
|
||||||
to_database_time(frozen_time),
|
|
||||||
participation.id,
|
|
||||||
to_database_time(frozen_time),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return cursor.fetchall()
|
|
||||||
|
|
||||||
def update_participation(self, participation):
|
|
||||||
hidden_subtasks = self.get_hidden_subtasks()
|
|
||||||
|
|
||||||
def calculate_format_data(participation, include_frozen):
|
|
||||||
format_data = {}
|
|
||||||
for (
|
|
||||||
problem_id,
|
|
||||||
problem_points,
|
|
||||||
time,
|
|
||||||
subtask_points,
|
|
||||||
total_subtask_points,
|
|
||||||
subtask,
|
|
||||||
sub_id,
|
|
||||||
) in self.get_results_by_subtask(participation, include_frozen):
|
|
||||||
problem_id = str(problem_id)
|
|
||||||
time = from_database_time(time)
|
|
||||||
if self.config["cumtime"]:
|
|
||||||
dt = (time - participation.start).total_seconds()
|
|
||||||
else:
|
|
||||||
dt = 0
|
|
||||||
|
|
||||||
if format_data.get(problem_id) is None:
|
|
||||||
format_data[problem_id] = {
|
|
||||||
"points": 0,
|
|
||||||
"time": 0,
|
|
||||||
"total_points": 0,
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
subtask not in hidden_subtasks.get(problem_id, set())
|
|
||||||
or include_frozen
|
|
||||||
):
|
|
||||||
format_data[problem_id]["points"] += subtask_points
|
|
||||||
format_data[problem_id]["total_points"] += total_subtask_points
|
|
||||||
format_data[problem_id]["time"] = max(
|
|
||||||
dt, format_data[problem_id]["time"]
|
|
||||||
)
|
|
||||||
format_data[problem_id]["problem_points"] = problem_points
|
|
||||||
|
|
||||||
return format_data
|
|
||||||
|
|
||||||
def recalculate_results(format_data):
|
|
||||||
cumtime = 0
|
|
||||||
score = 0
|
|
||||||
for problem_data in format_data.values():
|
|
||||||
if not problem_data["total_points"]:
|
|
||||||
continue
|
|
||||||
penalty = problem_data["time"]
|
|
||||||
problem_data["points"] = (
|
|
||||||
problem_data["points"]
|
|
||||||
/ problem_data["total_points"]
|
|
||||||
* problem_data["problem_points"]
|
|
||||||
)
|
|
||||||
if self.config["cumtime"] and problem_data["points"]:
|
|
||||||
cumtime += penalty
|
|
||||||
score += problem_data["points"]
|
|
||||||
return score, cumtime
|
|
||||||
|
|
||||||
format_data = calculate_format_data(participation, False)
|
|
||||||
score, cumtime = recalculate_results(format_data)
|
|
||||||
self.handle_frozen_state(participation, format_data)
|
|
||||||
participation.cumtime = max(cumtime, 0)
|
|
||||||
participation.score = round(score, self.contest.points_precision)
|
|
||||||
participation.tiebreaker = 0
|
|
||||||
participation.format_data = format_data
|
|
||||||
|
|
||||||
format_data_final = calculate_format_data(participation, True)
|
|
||||||
score_final, cumtime_final = recalculate_results(format_data_final)
|
|
||||||
participation.cumtime_final = max(cumtime_final, 0)
|
|
||||||
participation.score_final = round(score_final, self.contest.points_precision)
|
|
||||||
participation.format_data_final = format_data_final
|
|
||||||
participation.save()
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from django.utils import six
|
||||||
|
|
||||||
formats = {}
|
formats = {}
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,4 +13,4 @@ def register_contest_format(name):
|
||||||
|
|
||||||
|
|
||||||
def choices():
|
def choices():
|
||||||
return [(key, value.name) for key, value in sorted(formats.items())]
|
return [(key, value.name) for key, value in sorted(six.iteritems(formats))]
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue