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
View 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
View 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

View file

@ -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
View 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
View 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
View file

@ -5,7 +5,6 @@
*.py[co]
*.mo
*~
[a-z0-9]{40,}
dmoj/local_settings.py
resources/style.css
resources/content-description.css
@ -13,7 +12,4 @@ resources/ranks.css
resources/table.css
sass_processed
<desired bridge log path>
node_modules/
package-lock.json
/src

8
.gitmodules vendored Normal file
View 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

View file

@ -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

View file

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>502 bad gateway - LQDOJ</title>
<title>502 bad gateway - DMOJ</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<style type="text/css">
hr {
@ -49,10 +49,14 @@
<br>
<div class="popup">
<div>
<img class="logo" src="logo.svg" alt="LQDOJ">
<img class="logo" src="logo.png" alt="DMOJ">
</div>
<h1 style="width: 100%;">Oops, LQDOJ is down now.</h1>
</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>
</body>
</html>

4547
<desired bridge log path> Normal file

File diff suppressed because it is too large Load diff

1078
README.html Normal file

File diff suppressed because one or more lines are too long

338
README.md
View file

@ -1,327 +1,25 @@
LQDOJ: Le Quy Don Online Judge
===
[![](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ì 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
# online-judge
### 1. Activate virtualenv:
source dmojsite/bin/activate
```
### 2. Remember to change the local_settings
2. Run server:
```bash
python3 manage.py runserver 0.0.0.0:8000
```
### 3. Run server:
python 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)
```bash
python3 manage.py runbridged
```
### 4. Create configure file for judge:
python dmojauto-conf
4. Create a judge (another terminal)
```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.
### 5. Create folder for problems, change the dir in judge conf file and local_settings.py
**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)
```bash
celery -A dmoj_celery worker
```
### 7. Update vietnamese translation:
- go to locale/vi
- 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)
```bash
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)
###8. Run chat server:
docker run -p 6379:6379 -d redis:2.8

View file

@ -2,7 +2,4 @@ from django.apps import AppConfig
class ChatBoxConfig(AppConfig):
name = "chat_box"
def ready(self):
from . import models
name = 'chat_box'

64
chat_box/consumers.py Normal file
View 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

View file

@ -9,43 +9,22 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
("judge", "0100_auto_20200127_0059"),
('judge', '0100_auto_20200127_0059'),
]
operations = [
migrations.CreateModel(
name="Message",
name='Message',
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"time",
models.DateTimeField(auto_now_add=True, verbose_name="posted time"),
),
(
"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",
),
),
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time', models.DateTimeField(auto_now_add=True, verbose_name='posted time')),
('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={
"verbose_name": "message",
"verbose_name_plural": "messages",
"ordering": ("-time",),
'verbose_name': 'message',
'verbose_name_plural': 'messages',
'ordering': ('-time',),
},
),
]

View file

@ -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"),
),
]

View file

@ -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"),
),
]

View file

@ -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"),
),
]

View file

@ -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",
),
),
]

View file

@ -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",
),
),
],
),
]

View file

@ -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"),
),
]

View file

@ -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",
),
),
],
),
]

View file

@ -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",
),
),
]

View file

@ -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")},
),
]

View file

@ -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"
),
),
]

View file

@ -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"
),
),
]

View file

@ -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"
),
),
]

View file

@ -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),
]

View file

@ -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),
]

View file

@ -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")},
),
]

View file

@ -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.models import CASCADE, Q
from django.db.models import CASCADE
from django.utils.translation import gettext_lazy as _
from django.utils.functional import cached_property
from judge.models.profile import Profile
from judge.caching import cache_wrapper
__all__ = ["Message", "Room", "UserRoom", "Ignore"]
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])
__all__ = ['Message']
class Message(models.Model):
author = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE)
time = models.DateTimeField(
verbose_name=_("posted time"), auto_now_add=True, db_index=True
)
body = models.TextField(verbose_name=_("body of comment"), max_length=8192)
hidden = models.BooleanField(verbose_name="is hidden", default=False)
room = models.ForeignKey(
Room, verbose_name="room id", on_delete=CASCADE, default=None, null=True
)
author = models.ForeignKey(Profile, verbose_name=_('user'), on_delete=CASCADE)
time = models.DateTimeField(verbose_name=_('posted time'), auto_now_add=True)
body = models.TextField(verbose_name=_('body of comment'), max_length=8192)
def save(self, *args, **kwargs):
new_message = self.id
self.body = self.body.strip()
super(Message, self).save(*args, **kwargs)
class Meta:
verbose_name = "message"
verbose_name_plural = "messages"
ordering = ("-id",)
indexes = [
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,
}
app_label = 'chat_box'
verbose_name = 'message'
verbose_name_plural = 'messages'
ordering = ('-time',)

7
chat_box/routing.py Normal file
View file

@ -0,0 +1,7 @@
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/', consumers.ChatConsumer),
]

View file

@ -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

View file

@ -1,554 +1,48 @@
from django.utils.translation import gettext as _
from django.views.generic import ListView
from django.http import (
HttpResponse,
JsonResponse,
HttpResponseBadRequest,
HttpResponsePermanentRedirect,
HttpResponseRedirect,
)
from django.http import HttpResponse
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.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):
context_object_name = "message"
template_name = "chat/chat.html"
title = _("LQDOJ Chat")
def __init__(self):
super().__init__()
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
model = Message
context_object_name = 'message'
template_name = 'chat/chat.html'
title = _('Chat Box')
paginate_by = 50
paginator = Paginator(Message.objects.all(), paginate_by)
def get(self, request, *args, **kwargs):
request_room = kwargs["room_id"]
page_size = self.follow_up_page_size
try:
last_id = int(request.GET.get("last_id"))
except Exception:
last_id = 1e15
page_size = self.first_page_size
only_messages = request.GET.get("only_messages")
if request_room:
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:
page = request.GET.get('page')
if (page == None):
return super().get(request, *args, **kwargs)
return render(
request,
"chat/message_list.html",
{
"object_list": self.messages,
"has_next": self.has_next(),
},
)
cur_page = self.paginator.get_page(page)
return HttpResponse(format_messages(cur_page.object_list))
def get_context_data(self, **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
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)

View file

@ -12,6 +12,6 @@ if (2, 2) <= django.VERSION < (3,):
# attribute where the exact query sent to the database is saved.
# See MySQLdb/cursors.py in the source distribution.
# 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

View file

@ -11,12 +11,6 @@
bottom: 0;
}
.ace_editor {
overflow: hidden;
font: 12px/normal 'Fira Code', 'Monaco', 'Menlo', monospace;
direction: 1tr;
}
.django-ace-widget.loading {
display: none;
}
@ -61,4 +55,4 @@
.django-ace-editor-fullscreen .django-ace-max_min {
background-image: url(img/contract.png);
}
}

View file

@ -77,17 +77,15 @@
mode = widget.getAttribute('data-mode'),
theme = widget.getAttribute('data-theme'),
wordwrap = widget.getAttribute('data-wordwrap'),
toolbar = prev(widget);
var main_block = div.parentNode.parentNode;
toolbar = prev(widget),
main_block = toolbar.parentNode;
if (toolbar != null) {
// Toolbar maximize/minimize button
var min_max = toolbar.getElementsByClassName('django-ace-max_min');
min_max[0].onclick = function () {
minimizeMaximize(widget, main_block, editor);
return false;
};
}
// Toolbar maximize/minimize button
var min_max = toolbar.getElementsByClassName('django-ace-max_min');
min_max[0].onclick = function () {
minimizeMaximize(widget, main_block, editor);
return false;
};
editor.getSession().setValue(textarea.value);
@ -162,7 +160,7 @@
]);
window[widget.id] = editor;
setTimeout(() => $(widget).trigger('ace_load', [editor]), 100);
$(widget).trigger('ace_load', [editor]);
}
function init() {

View file

@ -11,33 +11,22 @@ from django.utils.safestring import mark_safe
class AceWidget(forms.Textarea):
def __init__(
self,
mode=None,
theme=None,
wordwrap=False,
width="100%",
height="300px",
no_ace_media=False,
toolbar=True,
*args,
**kwargs
):
def __init__(self, mode=None, theme=None, wordwrap=False, width='100%', height='300px',
no_ace_media=False, *args, **kwargs):
self.mode = mode
self.theme = theme
self.wordwrap = wordwrap
self.width = width
self.height = height
self.ace_media = not no_ace_media
self.toolbar = toolbar
super(AceWidget, self).__init__(*args, **kwargs)
@property
def media(self):
js = [urljoin(settings.ACE_URL, "ace.js")] if self.ace_media else []
js.append("django_ace/widget.js")
js = [urljoin(settings.ACE_URL, 'ace.js')] if self.ace_media else []
js.append('django_ace/widget.js')
css = {
"screen": ["django_ace/widget.css"],
'screen': ['django_ace/widget.css'],
}
return forms.Media(js=js, css=css)
@ -45,32 +34,24 @@ class AceWidget(forms.Textarea):
attrs = attrs or {}
ace_attrs = {
"class": "django-ace-widget loading",
"style": "width:%s; height:%s" % (self.width, self.height),
"id": "ace_%s" % name,
'class': 'django-ace-widget loading',
'style': 'width:%s; height:%s' % (self.width, self.height),
'id': 'ace_%s' % name,
}
if self.mode:
ace_attrs["data-mode"] = self.mode
ace_attrs['data-mode'] = self.mode
if self.theme:
ace_attrs["data-theme"] = self.theme
ace_attrs['data-theme'] = self.theme
if self.wordwrap:
ace_attrs["data-wordwrap"] = "true"
ace_attrs['data-wordwrap'] = 'true'
attrs.update(
style="width: 100%; min-width: 100%; max-width: 100%; resize: none"
)
attrs.update(style='width: 100%; min-width: 100%; max-width: 100%; resize: none')
textarea = super(AceWidget, self).render(name, value, attrs)
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:
toolbar = (
'<div style="width: {}" class="django-ace-toolbar">'
'<a href="#" class="django-ace-max_min"></a>'
"</div>"
).format(self.width)
html = toolbar + html
html = '<div class="django-ace-editor">{}</div>'.format(html)
# add toolbar
html = ('<div class="django-ace-editor"><div style="width: 100%%" class="django-ace-toolbar">'
'<a href="./" class="django-ace-max_min"></a></div>%s</div>') % html
return mark_safe(html)

View file

@ -4,30 +4,24 @@ import socket
from celery import Celery
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
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
if hasattr(settings, "CELERY_RESULT_BACKEND_SECRET"):
if hasattr(settings, 'CELERY_RESULT_BACKEND_SECRET'):
app.conf.result_backend = settings.CELERY_RESULT_BACKEND_SECRET
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()
# Logger to enable errors be reported.
logger = logging.getLogger("judge.celery")
logger = logging.getLogger('judge.celery')
@task_failure.connect()
def celery_failure_log(sender, task_id, exception, traceback, *args, **kwargs):
logger.error(
"Celery Task %s: %s on %s",
sender.name,
task_id,
socket.gethostname(), # noqa: G201
exc_info=(type(exception), exception, traceback),
)
logger.error('Celery Task %s: %s on %s', sender.name, task_id, socket.gethostname(), # noqa: G201
exc_info=(type(exception), exception, traceback))

12
dmoj/routing.py Normal file
View 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
)
),
})

View file

@ -22,7 +22,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# 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!
DEBUG = True
@ -30,10 +30,9 @@ DEBUG = True
ALLOWED_HOSTS = []
SITE_ID = 1
SITE_NAME = "LQDOJ"
SITE_LONG_NAME = "LQDOJ: Le Quy Don Online Judge"
SITE_NAME = 'LQDOJ'
SITE_LONG_NAME = 'LQDOJ: Le Quy Don Online Judge'
SITE_ADMIN_EMAIL = False
SITE_DOMAIN = "lqdoj.edu.vn"
DMOJ_REQUIRE_STAFF_2FA = True
@ -45,11 +44,13 @@ DMOJ_SSL = 0
# Refer to dmoj.ca/post/103-point-system-rework
DMOJ_PP_STEP = 0.95
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"
EXIFTOOL = "/usr/bin/exiftool"
ACE_URL = "//cdnjs.cloudflare.com/ajax/libs/ace/1.1.3"
NODEJS = '/usr/bin/node'
EXIFTOOL = '/usr/bin/exiftool'
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_KEY = None
@ -61,32 +62,26 @@ DMOJ_PROBLEM_MAX_TIME_LIMIT = 60 # seconds
DMOJ_PROBLEM_MIN_MEMORY_LIMIT = 0 # kilobytes
DMOJ_PROBLEM_MAX_MEMORY_LIMIT = 1048576 # kilobytes
DMOJ_PROBLEM_MIN_PROBLEM_POINTS = 0
DMOJ_SUBMISSION_ROOT = "/tmp"
DMOJ_RATING_COLORS = True
DMOJ_RATING_COLORS = False
DMOJ_EMAIL_THROTTLING = (10, 60)
DMOJ_STATS_LANGUAGE_THRESHOLD = 10
DMOJ_SUBMISSIONS_REJUDGE_LIMIT = 10
# 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_CONTEST_COUNT = 7
DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT = 7
DMOJ_TOTP_TOLERANCE_HALF_MINUTES = 1
DMOJ_USER_MAX_ORGANIZATION_COUNT = 10
DMOJ_USER_MAX_ORGANIZATION_ADD = 5
DMOJ_USER_MAX_ORGANIZATION_COUNT = 3
DMOJ_COMMENT_VOTE_HIDE_THRESHOLD = -5
DMOJ_PDF_PROBLEM_CACHE = ""
DMOJ_PDF_PROBLEM_CACHE = ''
DMOJ_PDF_PROBLEM_TEMP_DIR = tempfile.gettempdir()
DMOJ_STATS_SUBMISSION_RESULT_COLORS = {
"TLE": "#a3bcbd",
"AC": "#00a92a",
"WA": "#ed4420",
"CE": "#42586d",
"ERR": "#ffa71c",
'TLE': '#a3bcbd',
'AC': '#00a92a',
'WA': '#ed4420',
'CE': '#42586d',
'ERR': '#ffa71c',
}
DMOJ_PROFILE_IMAGE_ROOT = "profile_images"
DMOJ_ORGANIZATION_IMAGE_ROOT = "organization_images"
DMOJ_TEST_FORMATTER_ROOT = "test_formatter"
MARKDOWN_STYLES = {}
MARKDOWN_DEFAULT_STYLE = {}
@ -94,15 +89,16 @@ MARKDOWN_DEFAULT_STYLE = {}
MATHOID_URL = False
MATHOID_GZIP = False
MATHOID_MML_CACHE = None
MATHOID_CSS_CACHE = "default"
MATHOID_DEFAULT_TYPE = "auto"
MATHOID_CSS_CACHE = 'default'
MATHOID_DEFAULT_TYPE = 'auto'
MATHOID_MML_CACHE_TTL = 86400
MATHOID_CACHE_ROOT = tempfile.gettempdir() + "/mathoidCache"
MATHOID_CACHE_ROOT = ''
MATHOID_CACHE_URL = False
TEXOID_GZIP = False
TEXOID_META_CACHE = "default"
TEXOID_META_CACHE = 'default'
TEXOID_META_CACHE_TTL = 86400
DMOJ_NEWSLETTER_ID_ON_REGISTER = None
BAD_MAIL_PROVIDERS = ()
BAD_MAIL_PROVIDER_REGEX = ()
@ -113,30 +109,27 @@ TIMEZONE_MAP = None
TIMEZONE_DETECT_BACKEND = None
TERMS_OF_SERVICE_URL = None
DEFAULT_USER_LANGUAGE = "PY3"
DEFAULT_USER_LANGUAGE = 'CPP11'
PHANTOMJS = ""
PHANTOMJS = ''
PHANTOMJS_PDF_ZOOM = 0.75
PHANTOMJS_PDF_TIMEOUT = 5.0
PHANTOMJS_PAPER_SIZE = "Letter"
PHANTOMJS_PAPER_SIZE = 'Letter'
SLIMERJS = ""
SLIMERJS = ''
SLIMERJS_PDF_ZOOM = 0.75
SLIMERJS_FIREFOX_PATH = ""
SLIMERJS_PAPER_SIZE = "Letter"
SLIMERJS_FIREFOX_PATH = ''
SLIMERJS_PAPER_SIZE = 'Letter'
PUPPETEER_MODULE = "/usr/lib/node_modules/puppeteer"
PUPPETEER_PAPER_SIZE = "Letter"
USE_SELENIUM = False
SELENIUM_CUSTOM_CHROME_PATH = None
SELENIUM_CHROMEDRIVER_PATH = "chromedriver"
PUPPETEER_MODULE = '/usr/lib/node_modules/puppeteer'
PUPPETEER_PAPER_SIZE = 'Letter'
PYGMENT_THEME = 'pygment-github.css'
INLINE_JQUERY = True
INLINE_FONTAWESOME = True
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"
DMOJ_CANONICAL = ""
JQUERY_JS = '//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js'
FONTAWESOME_CSS = '//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css'
DMOJ_CANONICAL = ''
# Application definition
@ -148,315 +141,350 @@ except ImportError:
pass
else:
del wpadmin
INSTALLED_APPS += ("wpadmin",)
INSTALLED_APPS += ('wpadmin',)
WPADMIN = {
"admin": {
"title": "LQDOJ Admin",
"menu": {
"top": "wpadmin.menu.menus.BasicTopMenu",
"left": "wpadmin.menu.custom.CustomModelLeftMenuWithDashboard",
'admin': {
'title': 'LQDOJ Admin',
'menu': {
'top': 'wpadmin.menu.menus.BasicTopMenu',
'left': 'wpadmin.menu.custom.CustomModelLeftMenuWithDashboard',
},
"custom_menu": [
'custom_menu': [
{
"model": "judge.Problem",
"icon": "fa-question-circle",
"children": [
"judge.ProblemGroup",
"judge.ProblemType",
"judge.ProblemPointsVote",
'model': 'judge.Problem',
'icon': 'fa-question-circle',
'children': [
'judge.ProblemGroup',
'judge.ProblemType',
],
},
{
"model": "judge.Submission",
"icon": "fa-check-square",
"children": [
"judge.Language",
"judge.Judge",
'model': 'judge.Submission',
'icon': 'fa-check-square-o',
'children': [
'judge.Language',
'judge.Judge',
],
},
{
"model": "judge.Contest",
"icon": "fa-bar-chart",
"children": [
"judge.ContestParticipation",
"judge.ContestTag",
'model': 'judge.Contest',
'icon': 'fa-bar-chart',
'children': [
'judge.ContestParticipation',
'judge.ContestTag',
],
},
{
"model": "auth.User",
"icon": "fa-user",
"children": [
"auth.Group",
"registration.RegistrationProfile",
'model': 'auth.User',
'icon': 'fa-user',
'children': [
'auth.Group',
'registration.RegistrationProfile',
],
},
{
"model": "judge.Profile",
"icon": "fa-user-plus",
"children": [
"judge.Organization",
"judge.OrganizationRequest",
'model': 'judge.Profile',
'icon': 'fa-user-plus',
'children': [
'judge.Organization',
'judge.OrganizationRequest',
],
},
{
"model": "judge.NavigationBar",
"icon": "fa-bars",
"children": [
"judge.MiscConfig",
"judge.License",
"sites.Site",
"redirects.Redirect",
'model': 'judge.NavigationBar',
'icon': 'fa-bars',
'children': [
'judge.MiscConfig',
'judge.License',
'sites.Site',
'redirects.Redirect',
],
},
("judge.BlogPost", "fa-rss-square"),
("judge.Ticket", "fa-exclamation-circle"),
("admin.LogEntry", "fa-empire"),
('judge.BlogPost', 'fa-rss-square'),
('judge.Comment', 'fa-comment-o'),
('flatpages.FlatPage', 'fa-file-text-o'),
('judge.Solution', 'fa-pencil'),
],
"dashboard": {
"breadcrumbs": True,
'dashboard': {
'breadcrumbs': True,
},
},
}
INSTALLED_APPS += (
"django.contrib.admin",
"judge",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.flatpages",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.redirects",
"django.contrib.staticfiles",
"django.contrib.sites",
"django.contrib.sitemaps",
"registration",
"mptt",
"reversion",
"reversion_compare",
"django_social_share",
"social_django",
"compressor",
"django_ace",
"pagedown",
"sortedm2m",
"statici18n",
"impersonate",
"django_jinja",
"chat_box",
"django.forms",
'django.contrib.admin',
'judge',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.flatpages',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.redirects',
'django.contrib.staticfiles',
'django.contrib.sites',
'django.contrib.sitemaps',
'registration',
'mptt',
'reversion',
'django_social_share',
'social_django',
'compressor',
'django_ace',
'pagedown',
'sortedm2m',
'statici18n',
'impersonate',
'django_jinja',
'chat_box',
'channels',
)
MIDDLEWARE = (
"judge.middleware.SlowRequestMiddleware",
"judge.middleware.ShortCircuitMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"judge.middleware.DMOJLoginMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"judge.user_log.LogUserAccessMiddleware",
"judge.timezone.TimezoneMiddleware",
"impersonate.middleware.ImpersonateMiddleware",
"judge.middleware.DMOJImpersonationMiddleware",
"judge.middleware.ContestMiddleware",
"judge.middleware.DarkModeMiddleware",
"judge.middleware.SubdomainMiddleware",
"django.contrib.flatpages.middleware.FlatpageFallbackMiddleware",
"judge.social_auth.SocialAuthExceptionMiddleware",
"django.contrib.redirects.middleware.RedirectFallbackMiddleware",
'judge.middleware.ShortCircuitMiddleware',
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'judge.middleware.DMOJLoginMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'judge.user_log.LogUserAccessMiddleware',
'judge.timezone.TimezoneMiddleware',
'impersonate.middleware.ImpersonateMiddleware',
'judge.middleware.DMOJImpersonationMiddleware',
'judge.middleware.ContestMiddleware',
'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
'judge.social_auth.SocialAuthExceptionMiddleware',
'django.contrib.redirects.middleware.RedirectFallbackMiddleware',
)
X_FRAME_OPTIONS = "SAMEORIGIN"
LANGUAGE_COOKIE_AGE = 8640000
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
IMPERSONATE = {
"REQUIRE_SUPERUSER": True,
"DISABLE_LOGGING": True,
"ADMIN_DELETE_PERMISSION": True,
}
IMPERSONATE_REQUIRE_SUPERUSER = True
IMPERSONATE_DISABLE_LOGGING = True
ACCOUNT_ACTIVATION_DAYS = 7
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"
LOGIN_REDIRECT_URL = "/user"
WSGI_APPLICATION = "dmoj.wsgi.application"
ROOT_URLCONF = 'dmoj.urls'
LOGIN_REDIRECT_URL = '/user'
WSGI_APPLICATION = 'dmoj.wsgi.application'
TEMPLATES = [
{
"BACKEND": "django_jinja.backend.Jinja2",
"DIRS": [
os.path.join(BASE_DIR, "templates"),
'BACKEND': 'django_jinja.backend.Jinja2',
'DIRS': [
os.path.join(BASE_DIR, 'templates'),
],
"APP_DIRS": False,
"OPTIONS": {
"match_extension": (".html", ".txt"),
"match_regex": "^(?!admin/)",
"context_processors": [
"django.template.context_processors.media",
"django.template.context_processors.tz",
"django.template.context_processors.i18n",
"django.template.context_processors.request",
"django.contrib.messages.context_processors.messages",
"judge.template_context.comet_location",
"judge.template_context.get_resource",
"judge.template_context.general_info",
"judge.template_context.site",
"judge.template_context.site_name",
"judge.template_context.misc_config",
"social_django.context_processors.backends",
"social_django.context_processors.login_redirect",
'APP_DIRS': False,
'OPTIONS': {
'match_extension': ('.html', '.txt'),
'match_regex': '^(?!admin/)',
'context_processors': [
'django.template.context_processors.media',
'django.template.context_processors.tz',
'django.template.context_processors.i18n',
'django.template.context_processors.request',
'django.contrib.messages.context_processors.messages',
'judge.template_context.comet_location',
'judge.template_context.get_resource',
'judge.template_context.general_info',
'judge.template_context.site',
'judge.template_context.site_name',
'judge.template_context.misc_config',
'judge.template_context.math_setting',
'social_django.context_processors.backends',
'social_django.context_processors.login_redirect',
],
"autoescape": select_autoescape(["html", "xml"]),
"trim_blocks": True,
"lstrip_blocks": True,
"extensions": DEFAULT_EXTENSIONS
+ [
"compressor.contrib.jinja2ext.CompressorExtension",
"judge.jinja2.DMOJExtension",
"judge.jinja2.spaceless.SpacelessExtension",
'autoescape': select_autoescape(['html', 'xml']),
'trim_blocks': True,
'lstrip_blocks': True,
'extensions': DEFAULT_EXTENSIONS + [
'compressor.contrib.jinja2ext.CompressorExtension',
'judge.jinja2.DMOJExtension',
'judge.jinja2.spaceless.SpacelessExtension',
],
},
},
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"APP_DIRS": True,
"DIRS": [
os.path.join(BASE_DIR, "templates"),
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'APP_DIRS': True,
'DIRS': [
os.path.join(BASE_DIR, 'templates'),
],
"OPTIONS": {
"context_processors": [
"django.contrib.auth.context_processors.auth",
"django.template.context_processors.media",
"django.template.context_processors.tz",
"django.template.context_processors.i18n",
"django.template.context_processors.request",
"django.contrib.messages.context_processors.messages",
'OPTIONS': {
'context_processors': [
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.media',
'django.template.context_processors.tz',
'django.template.context_processors.i18n',
'django.template.context_processors.request',
'django.contrib.messages.context_processors.messages',
],
},
},
]
LOCALE_PATHS = [
os.path.join(BASE_DIR, "locale"),
os.path.join(BASE_DIR, 'locale'),
]
LANGUAGES = [
("vi", _("Vietnamese")),
("en", _("English")),
('de', _('German')),
('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
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
},
}
ENABLE_FTS = False
# Bridged configuration
BRIDGED_JUDGE_ADDRESS = [("localhost", 9999)]
BRIDGED_JUDGE_ADDRESS = [('localhost', 9999)]
BRIDGED_JUDGE_PROXIES = None
BRIDGED_DJANGO_ADDRESS = [("localhost", 9998)]
BRIDGED_DJANGO_ADDRESS = [('localhost', 9998)]
BRIDGED_DJANGO_CONNECT = None
BRIDGED_AUTO_CREATE_JUDGE = False
# Event Server configuration
EVENT_DAEMON_USE = False
EVENT_DAEMON_POST = "ws://localhost:9997/"
EVENT_DAEMON_GET = "ws://localhost:9996/"
EVENT_DAEMON_POLL = "/channels/"
EVENT_DAEMON_POST = 'ws://localhost:9997/'
EVENT_DAEMON_GET = 'ws://localhost:9996/'
EVENT_DAEMON_POLL = '/channels/'
EVENT_DAEMON_KEY = None
EVENT_DAEMON_AMQP_EXCHANGE = "dmoj-events"
EVENT_DAEMON_SUBMISSION_KEY = (
"6Sdmkx^%pk@GsifDfXcwX*Y7LRF%RGT8vmFpSxFBT$fwS7trc8raWfN#CSfQuKApx&$B#Gh2L7p%W!Ww"
)
EVENT_DAEMON_AMQP_EXCHANGE = 'dmoj-events'
EVENT_DAEMON_SUBMISSION_KEY = '6Sdmkx^%pk@GsifDfXcwX*Y7LRF%RGT8vmFpSxFBT$fwS7trc8raWfN#CSfQuKApx&$B#Gh2L7p%W!Ww'
# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/
# Whatever you do, this better be one of the entries in `LANGUAGES`.
LANGUAGE_CODE = "vi"
TIME_ZONE = "Asia/Ho_Chi_Minh"
DEFAULT_USER_TIME_ZONE = "Asia/Ho_Chi_Minh"
LANGUAGE_CODE = 'vi'
TIME_ZONE = 'Asia/Ho_Chi_Minh'
DEFAULT_USER_TIME_ZONE = 'Asia/Ho_Chi_Minh'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Cookies
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
# Static files (CSS, JavaScript, Images)
# 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 = (
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
)
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "resources"),
os.path.join(BASE_DIR, 'resources'),
]
STATIC_URL = "/static/"
STATIC_URL = '/static/'
# Define a cache
CACHES = {}
# Authentication
AUTHENTICATION_BACKENDS = (
"social_core.backends.google.GoogleOAuth2",
"social_core.backends.facebook.FacebookOAuth2",
"judge.social_auth.GitHubSecureEmailOAuth2",
"judge.authentication.CustomModelBackend",
'social_core.backends.google.GoogleOAuth2',
'social_core.backends.facebook.FacebookOAuth2',
'judge.social_auth.GitHubSecureEmailOAuth2',
'django.contrib.auth.backends.ModelBackend',
)
SOCIAL_AUTH_PIPELINE = (
"social_core.pipeline.social_auth.social_details",
"social_core.pipeline.social_auth.social_uid",
"social_core.pipeline.social_auth.auth_allowed",
"judge.social_auth.verify_email",
"social_core.pipeline.social_auth.social_user",
"social_core.pipeline.user.get_username",
"social_core.pipeline.social_auth.associate_by_email",
"judge.social_auth.choose_username",
"social_core.pipeline.user.create_user",
"judge.social_auth.make_profile",
"social_core.pipeline.social_auth.associate_user",
"social_core.pipeline.social_auth.load_extra_data",
"social_core.pipeline.user.user_details",
'social_core.pipeline.social_auth.social_details',
'social_core.pipeline.social_auth.social_uid',
'social_core.pipeline.social_auth.auth_allowed',
'judge.social_auth.verify_email',
'social_core.pipeline.social_auth.social_user',
'social_core.pipeline.user.get_username',
'social_core.pipeline.social_auth.associate_by_email',
'judge.social_auth.choose_username',
'social_core.pipeline.user.create_user',
'judge.social_auth.make_profile',
'social_core.pipeline.social_auth.associate_user',
'social_core.pipeline.social_auth.load_extra_data',
'social_core.pipeline.user.user_details',
)
SOCIAL_AUTH_PROTECTED_USER_FIELDS = ["first_name", "last_name"]
SOCIAL_AUTH_GOOGLE_OAUTH2_USER_FIELDS = ["email", "username"]
SOCIAL_AUTH_GITHUB_SECURE_SCOPE = ["user:email"]
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_FUNCTION = "judge.social_auth.slugify_username"
SOCIAL_AUTH_SLUGIFY_FUNCTION = 'judge.social_auth.slugify_username'
JUDGE_AMQP_PATH = None
@ -464,38 +492,25 @@ MOSS_API_KEY = None
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:
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())
except IOError:
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)],
},
},
}

View file

@ -8,8 +8,8 @@ DEFAULT_THROTTLE = (10, 60)
def new_email():
cache.add("error_email_throttle", 0, settings.DMOJ_EMAIL_THROTTLING[1])
return cache.incr("error_email_throttle")
cache.add('error_email_throttle', 0, settings.DMOJ_EMAIL_THROTTLING[1])
return cache.incr('error_email_throttle')
class ThrottledEmailHandler(AdminEmailHandler):

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,5 @@
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings")
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings')
try:
import MySQLdb # noqa: F401, imported for side effect
@ -9,8 +8,5 @@ except ImportError:
pymysql.install_as_MySQLdb()
from django.core.wsgi import (
get_wsgi_application,
) # noqa: E402, django must be imported here
from django.core.wsgi import get_wsgi_application # noqa: E402, django must be imported here
application = get_wsgi_application()

View file

@ -2,17 +2,13 @@ import os
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()
# noinspection PyUnresolvedReferences
import dmoj_install_pymysql # noqa: F401, I100, I202, imported for side effect
from django.core.wsgi import (
get_wsgi_application,
) # noqa: E402, I100, I202, django must be imported here
from django.core.wsgi import get_wsgi_application # noqa: E402, I100, I202, django must be imported here
# noinspection PyUnresolvedReferences
import django_2_2_pymysql_patch # noqa: I100, F401, I202, imported for side effect
application = get_wsgi_application()

View file

@ -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()

View file

@ -6,7 +6,7 @@ except ImportError:
import dmoj_install_pymysql # noqa: F401, imported for side effect
# 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
import django_2_2_pymysql_patch # noqa: I100, F401, I202, imported for side effect

View file

@ -1,4 +1,4 @@
import pymysql
pymysql.install_as_MySQLdb()
pymysql.version_info = (1, 4, 0, "final", 0)
pymysql.version_info = (1, 3, 13, "final", 0)

View 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']

View 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

View 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

View 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

View 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()

View 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()

View 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

View 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)

View 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()

View 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()

View file

@ -1 +1 @@
default_app_config = "judge.apps.JudgeAppConfig"
default_app_config = 'judge.apps.JudgeAppConfig'

View file

@ -1,62 +1,19 @@
from django.contrib import admin
from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import User
from judge.admin.comments import CommentAdmin
from judge.admin.contest import (
ContestAdmin,
ContestParticipationAdmin,
ContestTagAdmin,
ContestsSummaryAdmin,
)
from judge.admin.interface import (
BlogPostAdmin,
LicenseAdmin,
LogEntryAdmin,
NavigationBarAdmin,
)
from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin
from judge.admin.interface import BlogPostAdmin, LicenseAdmin, LogEntryAdmin, NavigationBarAdmin
from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin
from judge.admin.problem import ProblemAdmin, ProblemPointsVoteAdmin
from judge.admin.profile import ProfileAdmin, UserAdmin
from judge.admin.problem import ProblemAdmin
from judge.admin.profile import ProfileAdmin
from judge.admin.runtime import JudgeAdmin, LanguageAdmin
from judge.admin.submission import SubmissionAdmin
from judge.admin.taxon import (
ProblemGroupAdmin,
ProblemTypeAdmin,
OfficialContestCategoryAdmin,
OfficialContestLocationAdmin,
)
from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin
from judge.admin.ticket import TicketAdmin
from judge.admin.volunteer import VolunteerProblemVoteAdmin
from judge.admin.course import CourseAdmin
from judge.models import (
BlogPost,
Comment,
CommentLock,
Contest,
ContestParticipation,
ContestTag,
Judge,
Language,
License,
MiscConfig,
NavigationBar,
Organization,
OrganizationRequest,
Problem,
ProblemGroup,
ProblemPointsVote,
ProblemType,
Profile,
Submission,
Ticket,
VolunteerProblemVote,
Course,
ContestsSummary,
OfficialContestCategory,
OfficialContestLocation,
)
from judge.models import BlogPost, Comment, CommentLock, Contest, ContestParticipation, \
ContestTag, Judge, Language, License, MiscConfig, NavigationBar, Organization, \
OrganizationRequest, Problem, ProblemGroup, ProblemType, Profile, Submission, Ticket
admin.site.register(BlogPost, BlogPostAdmin)
admin.site.register(Comment, CommentAdmin)
@ -74,15 +31,7 @@ admin.site.register(Organization, OrganizationAdmin)
admin.site.register(OrganizationRequest, OrganizationRequestAdmin)
admin.site.register(Problem, ProblemAdmin)
admin.site.register(ProblemGroup, ProblemGroupAdmin)
admin.site.register(ProblemPointsVote, ProblemPointsVoteAdmin)
admin.site.register(ProblemType, ProblemTypeAdmin)
admin.site.register(Profile, ProfileAdmin)
admin.site.register(Submission, SubmissionAdmin)
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)

View file

@ -11,71 +11,52 @@ from judge.widgets import AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidg
class CommentForm(ModelForm):
class Meta:
widgets = {
"author": AdminHeavySelect2Widget(data_view="profile_select2"),
'author': AdminHeavySelect2Widget(data_view='profile_select2'),
'parent': AdminHeavySelect2Widget(data_view='comment_select2'),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets["body"] = HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("comment_preview")
)
widgets['body'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('comment_preview'))
class CommentAdmin(VersionAdmin):
fieldsets = (
(
None,
{
"fields": (
"author",
"parent",
"score",
"hidden",
"content_type",
"object_id",
)
},
),
("Content", {"fields": ("body",)}),
(None, {'fields': ('author', 'page', 'parent', 'score', 'hidden')}),
('Content', {'fields': ('body',)}),
)
list_display = ["author", "linked_object", "time"]
search_fields = ["author__user__username", "body"]
readonly_fields = ["score", "parent"]
actions = ["hide_comment", "unhide_comment"]
list_filter = ["hidden"]
list_display = ['author', 'linked_page', 'time']
search_fields = ['author__user__username', 'page', 'body']
actions = ['hide_comment', 'unhide_comment']
list_filter = ['hidden']
actions_on_top = True
actions_on_bottom = True
form = CommentForm
date_hierarchy = "time"
date_hierarchy = 'time'
def get_queryset(self, request):
return Comment.objects.order_by("-time")
return Comment.objects.order_by('-time')
def hide_comment(self, request, queryset):
count = queryset.update(hidden=True)
self.message_user(
request,
ungettext(
"%d comment successfully hidden.",
"%d comments successfully hidden.",
count,
)
% count,
)
hide_comment.short_description = _("Hide comments")
self.message_user(request, ungettext('%d comment successfully hidden.',
'%d comments successfully hidden.',
count) % count)
hide_comment.short_description = _('Hide comments')
def unhide_comment(self, request, queryset):
count = queryset.update(hidden=False)
self.message_user(
request,
ungettext(
"%d comment successfully unhidden.",
"%d comments successfully unhidden.",
count,
)
% count,
)
self.message_user(request, ungettext('%d comment successfully unhidden.',
'%d comments successfully unhidden.',
count) % count)
unhide_comment.short_description = _('Unhide comments')
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):
super(CommentAdmin, self).save_model(request, obj, form, change)

View file

@ -3,36 +3,18 @@ from django.contrib import admin
from django.core.exceptions import PermissionDenied
from django.db import connection, transaction
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.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _, ungettext
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,
OfficialContest,
)
from judge.models import Contest, ContestProblem, ContestSubmission, Profile, Rating
from judge.ratings import rate_contest
from judge.widgets import (
AdminHeavySelect2MultipleWidget,
AdminHeavySelect2Widget,
AdminPagedownWidget,
AdminSelect2MultipleWidget,
AdminSelect2Widget,
HeavyPreviewAdminPageDownWidget,
)
from judge.views.contests import recalculate_contest_summary_result
from judge.utils.contest import maybe_trigger_contest_rescore
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, AdminPagedownWidget, \
AdminSelect2MultipleWidget, AdminSelect2Widget, HeavyPreviewAdminPageDownWidget
class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
@ -43,394 +25,197 @@ class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
class ContestTagForm(ModelForm):
contests = ModelMultipleChoiceField(
label=_("Included contests"),
label=_('Included contests'),
queryset=Contest.objects.all(),
required=False,
widget=AdminHeavySelect2MultipleWidget(data_view="contest_select2"),
)
widget=AdminHeavySelect2MultipleWidget(data_view='contest_select2'))
class ContestTagAdmin(admin.ModelAdmin):
fields = ("name", "color", "description", "contests")
list_display = ("name", "color")
fields = ('name', 'color', 'description', 'contests')
list_display = ('name', 'color')
actions_on_top = True
actions_on_bottom = True
form = ContestTagForm
if AdminPagedownWidget is not None:
formfield_overrides = {
TextField: {"widget": AdminPagedownWidget},
TextField: {'widget': AdminPagedownWidget},
}
def save_model(self, 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):
form = super(ContestTagAdmin, self).get_form(request, obj, **kwargs)
if obj is not None:
form.base_fields["contests"].initial = obj.contests.all()
form.base_fields['contests'].initial = obj.contests.all()
return form
class ContestProblemInlineForm(ModelForm):
class Meta:
widgets = {
"problem": AdminHeavySelect2Widget(data_view="problem_select2"),
"hidden_subtasks": TextInput(attrs={"size": "3"}),
"points": TextInput(attrs={"size": "1"}),
"order": TextInput(attrs={"size": "1"}),
}
widgets = {'problem': AdminHeavySelect2Widget(data_view='problem_select2')}
class ContestProblemInline(admin.TabularInline):
model = ContestProblem
verbose_name = _("Problem")
verbose_name_plural = "Problems"
fields = (
"problem",
"points",
"partial",
"is_pretested",
"max_submissions",
"hidden_subtasks",
"show_testcases",
"order",
"rejudge_column",
)
readonly_fields = ("rejudge_column",)
verbose_name = _('Problem')
verbose_name_plural = 'Problems'
fields = ('problem', 'points', 'partial', 'is_pretested', 'max_submissions', 'output_prefix_override', 'order',
'rejudge_column')
readonly_fields = ('rejudge_column',)
form = ContestProblemInlineForm
def rejudge_column(self, obj):
if obj.id is None:
return ""
return format_html(
'<a class="button rejudge-link" href="{}">Rejudge</a>',
reverse("admin:judge_contest_rejudge", args=(obj.contest.id, obj.id)),
)
rejudge_column.short_description = ""
return ''
return format_html('<a class="button rejudge-link" href="{}">Rejudge</a>',
reverse('admin:judge_contest_rejudge', args=(obj.contest.id, obj.id)))
rejudge_column.short_description = ''
class ContestForm(ModelForm):
def __init__(self, *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:
self.fields["rate_exclude"].queryset = Profile.objects.filter(
contest_history__contest=self.instance
).distinct()
self.fields['rate_exclude'].queryset = \
Profile.objects.filter(contest_history__contest=self.instance).distinct()
else:
self.fields["rate_exclude"].queryset = Profile.objects.none()
self.fields["banned_users"].widget.can_add_related = False
self.fields["view_contest_scoreboard"].widget.can_add_related = False
self.fields['rate_exclude'].queryset = Profile.objects.none()
self.fields['banned_users'].widget.can_add_related = False
def clean(self):
cleaned_data = super(ContestForm, self).clean()
cleaned_data["banned_users"].filter(
current_contest__contest=self.instance
).update(current_contest=None)
cleaned_data['banned_users'].filter(current_contest__contest=self.instance).update(current_contest=None)
class Meta:
widgets = {
"authors": AdminHeavySelect2MultipleWidget(data_view="profile_select2"),
"curators": AdminHeavySelect2MultipleWidget(data_view="profile_select2"),
"testers": AdminHeavySelect2MultipleWidget(data_view="profile_select2"),
"private_contestants": AdminHeavySelect2MultipleWidget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
"organizations": AdminHeavySelect2MultipleWidget(
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%"}
),
'organizers': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
'private_contestants': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
attrs={'style': 'width: 100%'}),
'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2'),
'tags': AdminSelect2MultipleWidget,
'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
attrs={'style': 'width: 100%'}),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets["description"] = HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("contest_preview")
)
widgets['description'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('contest_preview'))
class OfficialContestInlineForm(ModelForm):
class Meta:
widgets = {
"category": AdminSelect2Widget,
"location": AdminSelect2Widget,
}
class OfficialContestInline(admin.StackedInline):
fields = (
"category",
"year",
"location",
)
model = OfficialContest
can_delete = True
form = OfficialContestInlineForm
extra = 0
class ContestAdmin(CompareVersionAdmin):
class ContestAdmin(VersionAdmin):
fieldsets = (
(None, {"fields": ("key", "name", "authors", "curators", "testers")}),
(
_("Settings"),
{
"fields": (
"is_visible",
"use_clarifications",
"hide_problem_tags",
"public_scoreboard",
"scoreboard_visibility",
"run_pretests_only",
"points_precision",
"rate_limit",
)
},
),
(
_("Scheduling"),
{"fields": ("start_time", "end_time", "time_limit", "freeze_after")},
),
(
_("Details"),
{
"fields": (
"description",
"og_image",
"logo_override_image",
"tags",
"summary",
)
},
),
(
_("Format"),
{"fields": ("format_name", "format_config", "problem_label_script")},
),
(
_("Rating"),
{
"fields": (
"is_rated",
"rate_all",
"rating_floor",
"rating_ceiling",
"rate_exclude",
)
},
),
(
_("Access"),
{
"fields": (
"access_code",
"private_contestants",
"organizations",
"view_contest_scoreboard",
)
},
),
(_("Justice"), {"fields": ("banned_users",)}),
(None, {'fields': ('key', 'name', 'organizers')}),
(_('Settings'), {'fields': ('is_visible', 'use_clarifications', 'hide_problem_tags', 'hide_scoreboard',
'run_pretests_only')}),
(_('Scheduling'), {'fields': ('start_time', 'end_time', 'time_limit')}),
(_('Details'), {'fields': ('description', 'og_image', 'logo_override_image', 'tags', 'summary')}),
(_('Format'), {'fields': ('format_name', 'format_config')}),
(_('Rating'), {'fields': ('is_rated', 'rate_all', 'rating_floor', 'rating_ceiling', 'rate_exclude')}),
(_('Access'), {'fields': ('access_code', 'is_private', 'private_contestants', 'is_organization_private',
'organizations')}),
(_('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]
list_display = ('key', 'name', 'is_visible', 'is_rated', 'start_time', 'end_time', 'time_limit', 'user_count')
actions = ['make_visible', 'make_hidden']
inlines = [ContestProblemInline]
actions_on_top = True
actions_on_bottom = True
form = ContestForm
change_list_template = "admin/judge/contest/change_list.html"
filter_horizontal = ["rate_exclude"]
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
change_list_template = 'admin/judge/contest/change_list.html'
filter_horizontal = ['rate_exclude']
date_hierarchy = 'start_time'
def get_queryset(self, request):
queryset = Contest.objects.all()
if request.user.has_perm("judge.edit_all_contest"):
if request.user.has_perm('judge.edit_all_contest'):
return queryset
else:
return queryset.filter(
Q(authors=request.profile) | Q(curators=request.profile)
).distinct()
return queryset.filter(organizers__id=request.profile.id)
def get_readonly_fields(self, request, obj=None):
readonly = []
if not request.user.has_perm("judge.contest_rating"):
readonly += ["is_rated", "rate_all", "rate_exclude"]
if not request.user.has_perm("judge.contest_access_code"):
readonly += ["access_code"]
if not request.user.has_perm("judge.create_private_contest"):
readonly += [
"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"]
if not request.user.has_perm('judge.contest_rating'):
readonly += ['is_rated', 'rate_all', 'rate_exclude']
if not request.user.has_perm('judge.contest_access_code'):
readonly += ['access_code']
if not request.user.has_perm('judge.create_private_contest'):
readonly += ['is_private', 'private_contestants', 'is_organization_private', 'organizations']
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):
if not request.user.has_perm("judge.edit_own_contest"):
if not request.user.has_perm('judge.edit_own_contest'):
return False
if obj is None:
if request.user.has_perm('judge.edit_all_contest') or obj is None:
return True
return obj.is_editable_by(request.user)
return obj.organizers.filter(id=request.profile.id).exists()
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)
self.message_user(
request,
ungettext(
"%d contest successfully marked as visible.",
"%d contests successfully marked as visible.",
count,
)
% count,
)
make_visible.short_description = _("Mark contests as visible")
self.message_user(request, ungettext('%d contest successfully marked as visible.',
'%d contests successfully marked as visible.',
count) % count)
make_visible.short_description = _('Mark contests as visible')
def make_hidden(self, request, queryset):
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)
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")
count = queryset.update(is_visible=False)
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):
return [
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+)/judge/(\d+)/$", self.rejudge_view, name="judge_contest_rejudge"
),
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+)/judge/(\d+)/$', self.rejudge_view, name='judge_contest_rejudge'),
] + super(ContestAdmin, self).get_urls()
def rejudge_view(self, request, contest_id, problem_id):
queryset = ContestSubmission.objects.filter(
problem_id=problem_id
).select_related("submission")
queryset = ContestSubmission.objects.filter(problem_id=problem_id).select_related('submission')
for model in queryset:
model.submission.judge(rejudge=True)
self.message_user(
request,
ungettext(
"%d submission was successfully scheduled for rejudging.",
"%d submissions were successfully scheduled for rejudging.",
len(queryset),
)
% len(queryset),
)
return HttpResponseRedirect(
reverse("admin:judge_contest_change", args=(contest_id,))
)
self.message_user(request, ungettext('%d submission was successfully scheduled for rejudging.',
'%d submissions were successfully scheduled for rejudging.',
len(queryset)) % len(queryset))
return HttpResponseRedirect(reverse('admin:judge_contest_change', args=(contest_id,)))
def rate_all_view(self, request):
if not request.user.has_perm("judge.contest_rating"):
if not request.user.has_perm('judge.contest_rating'):
raise PermissionDenied()
with transaction.atomic():
with connection.cursor() as cursor:
cursor.execute("TRUNCATE TABLE `%s`" % Rating._meta.db_table)
if connection.vendor == 'sqlite':
Rating.objects.all().delete()
else:
cursor = connection.cursor()
cursor.execute('TRUNCATE TABLE `%s`' % Rating._meta.db_table)
cursor.close()
Profile.objects.update(rating=None)
for contest in Contest.objects.filter(
is_rated=True, end_time__lte=timezone.now()
).order_by("end_time"):
for contest in Contest.objects.filter(is_rated=True).order_by('end_time'):
rate_contest(contest)
return HttpResponseRedirect(reverse("admin:judge_contest_changelist"))
return HttpResponseRedirect(reverse('admin:judge_contest_changelist'))
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()
contest = get_object_or_404(Contest, id=id)
if not contest.is_rated or not contest.ended:
if not contest.is_rated:
raise Http404()
with transaction.atomic():
contest.rate()
return HttpResponseRedirect(
request.META.get("HTTP_REFERER", reverse("admin:judge_contest_changelist"))
)
return HttpResponseRedirect(request.META.get('HTTP_REFERER', reverse('admin:judge_contest_changelist')))
def get_form(self, request, obj=None, **kwargs):
form = super(ContestAdmin, self).get_form(request, obj, **kwargs)
if "problem_label_script" in form.base_fields:
# form.base_fields['problem_label_script'] does not exist when the user has only view permission
# on the model.
form.base_fields["problem_label_script"].widget = AceWidget(
"lua", request.profile.ace_theme
)
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),
def get_form(self, *args, **kwargs):
form = super(ContestAdmin, self).get_form(*args, **kwargs)
perms = ('edit_own_contest', 'edit_all_contest')
form.base_fields['organizers'].queryset = Profile.objects.filter(
Q(user__is_superuser=True) |
Q(user__groups__permissions__codename__in=perms) |
Q(user__user_permissions__codename__in=perms),
).distinct()
return form
@ -438,48 +223,29 @@ class ContestAdmin(CompareVersionAdmin):
class ContestParticipationForm(ModelForm):
class Meta:
widgets = {
"contest": AdminSelect2Widget(),
"user": AdminHeavySelect2Widget(data_view="profile_select2"),
'contest': AdminSelect2Widget(),
'user': AdminHeavySelect2Widget(data_view='profile_select2'),
}
class ContestParticipationAdmin(admin.ModelAdmin):
fields = ("contest", "user", "real_start", "virtual", "is_disqualified")
list_display = (
"contest",
"username",
"show_virtual",
"real_start",
"score",
"cumtime",
"tiebreaker",
)
actions = ["recalculate_results"]
fields = ('contest', 'user', 'real_start', 'virtual', 'is_disqualified')
list_display = ('contest', 'username', 'show_virtual', 'real_start', 'score', 'cumtime')
actions = ['recalculate_results']
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
date_hierarchy = "real_start"
date_hierarchy = 'real_start'
def get_queryset(self, request):
return (
super(ContestParticipationAdmin, self)
.get_queryset(request)
.only(
"contest__name",
"contest__format_name",
"contest__format_config",
"user__user__username",
"real_start",
"score",
"cumtime",
"tiebreaker",
"virtual",
)
return super(ContestParticipationAdmin, self).get_queryset(request).only(
'contest__name', 'contest__format_name', 'contest__format_config',
'user__user__username', 'real_start', 'score', 'cumtime', 'virtual',
)
def save_model(self, 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)
def recalculate_results(self, request, queryset):
@ -487,48 +253,17 @@ class ContestParticipationAdmin(admin.ModelAdmin):
for participation in queryset:
participation.recompute_results()
count += 1
self.message_user(
request,
ungettext(
"%d participation recalculated.",
"%d participations recalculated.",
count,
)
% count,
)
recalculate_results.short_description = _("Recalculate results")
self.message_user(request, ungettext('%d participation recalculated.',
'%d participations recalculated.',
count) % count)
recalculate_results.short_description = _('Recalculate results')
def username(self, obj):
return obj.user.username
username.short_description = _("username")
username.admin_order_field = "user__user__username"
username.short_description = _('username')
username.admin_order_field = 'user__user__username'
def show_virtual(self, obj):
return obj.virtual or "-"
show_virtual.short_description = _("virtual")
show_virtual.admin_order_field = "virtual"
class ContestsSummaryForm(ModelForm):
class Meta:
widgets = {
"contests": AdminHeavySelect2MultipleWidget(
data_view="contest_select2", attrs={"style": "width: 100%"}
),
}
class ContestsSummaryAdmin(admin.ModelAdmin):
fields = ("key", "contests", "scores")
list_display = ("key",)
search_fields = ("key", "contests__key")
form = ContestsSummaryForm
def save_model(self, request, obj, form, change):
super(ContestsSummaryAdmin, self).save_model(request, obj, form, change)
obj.refresh_from_db()
obj.results = recalculate_contest_summary_result(request, obj)
obj.save()
return obj.virtual or '-'
show_virtual.short_description = _('virtual')
show_virtual.admin_order_field = 'virtual'

View file

@ -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

View file

@ -6,33 +6,26 @@ from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from mptt.admin import DraggableMPTTAdmin
from reversion.admin import VersionAdmin
from reversion_compare.admin import CompareVersionAdmin
from judge.dblock import LockModel
from judge.models import NavigationBar
from judge.widgets import (
AdminHeavySelect2MultipleWidget,
AdminHeavySelect2Widget,
HeavyPreviewAdminPageDownWidget,
)
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
class NavigationBarAdmin(DraggableMPTTAdmin):
list_display = DraggableMPTTAdmin.list_display + ("key", "linked_path")
fields = ("key", "label", "path", "order", "regex", "parent")
list_display = DraggableMPTTAdmin.list_display + ('key', 'linked_path')
fields = ('key', 'label', 'path', 'order', 'regex', 'parent')
list_editable = () # Bug in SortableModelAdmin: 500 without list_editable being set
mptt_level_indent = 20
sortable = "order"
sortable = 'order'
def __init__(self, *args, **kwargs):
super(NavigationBarAdmin, self).__init__(*args, **kwargs)
self.__save_model_calls = 0
def linked_path(self, obj):
return format_html('<a href="{0}" target="_blank">{0}</a>', obj.path)
linked_path.short_description = _("link path")
return format_html(u'<a href="{0}" target="_blank">{0}</a>', obj.path)
linked_path.short_description = _('link path')
def save_model(self, request, obj, form, change):
self.__save_model_calls += 1
@ -41,9 +34,7 @@ class NavigationBarAdmin(DraggableMPTTAdmin):
def changelist_view(self, request, extra_context=None):
self.__save_model_calls = 0
with NavigationBar.objects.disable_mptt_updates():
result = super(NavigationBarAdmin, self).changelist_view(
request, extra_context
)
result = super(NavigationBarAdmin, self).changelist_view(request, extra_context)
if self.__save_model_calls:
with LockModel(write=(NavigationBar,)):
NavigationBar.objects.rebuild()
@ -53,106 +44,71 @@ class NavigationBarAdmin(DraggableMPTTAdmin):
class BlogPostForm(ModelForm):
def __init__(self, *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:
widgets = {
"authors": AdminHeavySelect2MultipleWidget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
"organizations": AdminHeavySelect2MultipleWidget(
data_view="organization_select2", attrs={"style": "width: 100%"}
),
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets["content"] = HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("blog_preview")
)
widgets["summary"] = HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("blog_preview")
)
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('blog_preview'))
widgets['summary'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('blog_preview'))
class BlogPostAdmin(CompareVersionAdmin):
class BlogPostAdmin(VersionAdmin):
fieldsets = (
(
None,
{
"fields": (
"title",
"slug",
"authors",
"visible",
"sticky",
"publish_on",
"is_organization_private",
"organizations",
)
},
),
(_("Content"), {"fields": ("content", "og_image")}),
(_("Summary"), {"classes": ("collapse",), "fields": ("summary",)}),
(None, {'fields': ('title', 'slug', 'authors', 'visible', 'sticky', 'publish_on')}),
(_('Content'), {'fields': ('content', 'og_image')}),
(_('Summary'), {'classes': ('collapse',), 'fields': ('summary',)}),
)
prepopulated_fields = {"slug": ("title",)}
list_display = ("id", "title", "visible", "sticky", "publish_on")
list_display_links = ("id", "title")
ordering = ("-publish_on",)
prepopulated_fields = {'slug': ('title',)}
list_display = ('id', 'title', 'visible', 'sticky', 'publish_on')
list_display_links = ('id', 'title')
ordering = ('-publish_on',)
form = BlogPostForm
date_hierarchy = "publish_on"
date_hierarchy = 'publish_on'
def has_change_permission(self, request, obj=None):
return (
request.user.has_perm("judge.edit_all_post")
or request.user.has_perm("judge.change_blogpost")
and (obj is None or obj.authors.filter(id=request.profile.id).exists())
)
return (request.user.has_perm('judge.edit_all_post') or
request.user.has_perm('judge.change_blogpost') and (
obj is None or
obj.authors.filter(id=request.profile.id).exists()))
class SolutionForm(ModelForm):
def __init__(self, *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:
widgets = {
"authors": AdminHeavySelect2MultipleWidget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
"problem": AdminHeavySelect2Widget(
data_view="problem_select2", attrs={"style": "width: 250px"}
),
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
'problem': AdminHeavySelect2Widget(data_view='problem_select2', attrs={'style': 'width: 250px'}),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets["content"] = HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("solution_preview")
)
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('solution_preview'))
class LicenseForm(ModelForm):
class Meta:
if HeavyPreviewAdminPageDownWidget is not None:
widgets = {
"text": HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("license_preview")
)
}
widgets = {'text': HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('license_preview'))}
class LicenseAdmin(admin.ModelAdmin):
fields = ("key", "link", "name", "display", "icon", "text")
list_display = ("name", "key")
fields = ('key', 'link', 'name', 'display', 'icon', 'text')
list_display = ('name', 'key')
form = LicenseForm
class UserListFilter(admin.SimpleListFilter):
title = _("user")
parameter_name = "user"
title = _('user')
parameter_name = 'user'
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):
if self.value():
@ -161,29 +117,10 @@ class UserListFilter(admin.SimpleListFilter):
class LogEntryAdmin(admin.ModelAdmin):
readonly_fields = (
"user",
"content_type",
"object_id",
"object_repr",
"action_flag",
"change_message",
)
list_display = (
"__str__",
"action_time",
"user",
"content_type",
"object_link",
"diff_link",
)
search_fields = (
"object_repr",
"change_message",
"user__username",
"content_type__model",
)
list_filter = (UserListFilter, "content_type")
readonly_fields = ('user', 'content_type', 'object_id', 'object_repr', 'action_flag', 'change_message')
list_display = ('__str__', 'action_time', 'user', 'content_type', 'object_link')
search_fields = ('object_repr', 'change_message')
list_filter = (UserListFilter, 'content_type')
list_display_links = None
actions = None
@ -202,35 +139,13 @@ class LogEntryAdmin(admin.ModelAdmin):
else:
ct = obj.content_type
try:
link = format_html(
'<a href="{1}">{0}</a>',
obj.object_repr,
reverse(
"admin:%s_%s_change" % (ct.app_label, ct.model),
args=(obj.object_id,),
),
)
link = format_html('<a href="{1}">{0}</a>', obj.object_repr,
reverse('admin:%s_%s_change' % (ct.app_label, ct.model), args=(obj.object_id,)))
except NoReverseMatch:
link = obj.object_repr
return link
object_link.admin_order_field = "object_repr"
object_link.short_description = _("object")
def diff_link(self, obj):
if obj.is_deletion():
return None
ct = obj.content_type
try:
url = reverse(
"admin:%s_%s_history" % (ct.app_label, ct.model), args=(obj.object_id,)
)
link = format_html('<a href="{1}">{0}</a>', _("Diff"), url)
except NoReverseMatch:
link = None
return link
diff_link.short_description = _("diff")
object_link.admin_order_field = 'object_repr'
object_link.short_description = _('object')
def queryset(self, request):
return super().queryset(request).prefetch_related("content_type")
return super().queryset(request).prefetch_related('content_type')

View file

@ -6,94 +6,61 @@ from django.utils.translation import gettext, gettext_lazy as _
from reversion.admin import VersionAdmin
from judge.models import Organization
from judge.widgets import (
AdminHeavySelect2MultipleWidget,
AdminHeavySelect2Widget,
HeavyPreviewAdminPageDownWidget,
)
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
class OrganizationForm(ModelForm):
class Meta:
widgets = {
"admins": AdminHeavySelect2MultipleWidget(data_view="profile_select2"),
"registrant": AdminHeavySelect2Widget(data_view="profile_select2"),
'admins': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
'registrant': AdminHeavySelect2Widget(data_view='profile_select2'),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets["about"] = HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("organization_preview")
)
widgets['about'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('organization_preview'))
class OrganizationAdmin(VersionAdmin):
readonly_fields = ("creation_date",)
fields = (
"name",
"slug",
"short_name",
"is_open",
"about",
"slots",
"registrant",
"creation_date",
"admins",
)
list_display = (
"name",
"short_name",
"is_open",
"creation_date",
"registrant",
"show_public",
)
search_fields = ("name", "short_name", "registrant__user__username")
prepopulated_fields = {"slug": ("name",)}
readonly_fields = ('creation_date',)
fields = ('name', 'slug', 'short_name', 'is_open', 'about', 'logo_override_image', 'slots', 'registrant',
'creation_date', 'admins')
list_display = ('name', 'short_name', 'is_open', 'slots', 'registrant', 'show_public')
prepopulated_fields = {'slug': ('name',)}
actions_on_top = True
actions_on_bottom = True
form = OrganizationForm
ordering = ["-creation_date"]
def show_public(self, obj):
return format_html(
'<a href="{0}" style="white-space:nowrap;">{1}</a>',
obj.get_absolute_url(),
gettext("View on site"),
)
return format_html('<a href="{0}" style="white-space:nowrap;">{1}</a>',
obj.get_absolute_url(), gettext('View on site'))
show_public.short_description = ""
show_public.short_description = ''
def get_readonly_fields(self, request, obj=None):
fields = self.readonly_fields
if not request.user.has_perm("judge.organization_admin"):
return fields + ("registrant", "admins", "is_open", "slots")
if not request.user.has_perm('judge.organization_admin'):
return fields + ('registrant', 'admins', 'is_open', 'slots')
return fields
def get_queryset(self, request):
queryset = Organization.objects.all()
if request.user.has_perm("judge.edit_all_organization"):
if request.user.has_perm('judge.edit_all_organization'):
return queryset
else:
return queryset.filter(admins=request.profile.id)
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
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 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):
list_display = ("username", "organization", "state", "time")
readonly_fields = ("user", "organization")
list_display = ('username', 'organization', 'state', 'time')
readonly_fields = ('user', 'organization')
def username(self, obj):
return obj.user.user.username
username.short_description = _("username")
username.admin_order_field = "user__user__username"
username.short_description = _('username')
username.admin_order_field = 'user__user__username'

View file

@ -1,115 +1,54 @@
from operator import attrgetter
from django import forms
from django.contrib import admin, messages
from django.db import transaction, IntegrityError
from django.db.models import Q, Avg, Count
from django.db.models.aggregates import StdDev
from django.forms import ModelForm, TextInput
from django.contrib import admin
from django.db import transaction
from django.db.models import Q
from django.forms import ModelForm
from django.urls import reverse_lazy
from django.utils.html import format_html
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_compare.admin import CompareVersionAdmin
from judge.models import (
LanguageLimit,
LanguageTemplate,
Problem,
ProblemTranslation,
Profile,
Solution,
Notification,
)
from judge.models.notification import make_notification
from judge.widgets import (
AdminHeavySelect2MultipleWidget,
AdminSelect2MultipleWidget,
AdminSelect2Widget,
CheckboxSelectMultipleWithSelectAll,
HeavyPreviewAdminPageDownWidget,
)
from judge.utils.problems import user_editable_ids, user_tester_ids
MEMORY_UNITS = (("KB", "KB"), ("MB", "MB"))
from judge.models import LanguageLimit, Problem, ProblemClarification, ProblemTranslation, Profile, Solution
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminSelect2MultipleWidget, AdminSelect2Widget, \
CheckboxSelectMultipleWithSelectAll, HeavyPreviewAdminPageDownWidget, HeavyPreviewPageDownWidget
class ProblemForm(ModelForm):
change_message = forms.CharField(
max_length=256, label="Edit reason", required=False
)
memory_unit = forms.ChoiceField(choices=MEMORY_UNITS)
change_message = forms.CharField(max_length=256, label='Edit reason', required=False)
def __init__(self, *args, **kwargs):
super(ProblemForm, self).__init__(*args, **kwargs)
self.fields["authors"].widget.can_add_related = False
self.fields["curators"].widget.can_add_related = False
self.fields["testers"].widget.can_add_related = False
self.fields["banned_users"].widget.can_add_related = False
self.fields["change_message"].widget.attrs.update(
{
"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
self.fields['authors'].widget.can_add_related = False
self.fields['curators'].widget.can_add_related = False
self.fields['testers'].widget.can_add_related = False
self.fields['banned_users'].widget.can_add_related = False
self.fields['change_message'].widget.attrs.update({
'placeholder': gettext('Describe the changes you made (optional)'),
})
class Meta:
widgets = {
"authors": AdminHeavySelect2MultipleWidget(
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%"}
),
"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"}),
'authors': AdminHeavySelect2MultipleWidget(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%'}),
'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,
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets["description"] = HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("problem_preview")
)
widgets['description'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('problem_preview'))
class ProblemCreatorListFilter(admin.SimpleListFilter):
title = parameter_name = "creator"
title = parameter_name = 'creator'
def lookups(self, request, model_admin):
queryset = Profile.objects.exclude(authored_problems=None).values_list(
"user__username", flat=True
)
queryset = Profile.objects.exclude(authored_problems=None).values_list('user__username', flat=True)
return [(name, name) for name in queryset]
def queryset(self, request, queryset):
@ -119,68 +58,46 @@ class ProblemCreatorListFilter(admin.SimpleListFilter):
class LanguageLimitInlineForm(ModelForm):
memory_unit = forms.ChoiceField(choices=MEMORY_UNITS, label=_("Memory unit"))
class Meta:
widgets = {
"language": AdminSelect2Widget,
"memory_limit": TextInput(attrs={"size": "10"}),
}
def clean(self):
if not self.cleaned_data.get("language"):
self.cleaned_data["DELETE"] = True
if (
self.cleaned_data.get("memory_limit")
and self.cleaned_data.get("memory_unit") == "MB"
):
self.cleaned_data["memory_limit"] *= 1024
return self.cleaned_data
widgets = {'language': AdminSelect2Widget}
class LanguageLimitInline(admin.TabularInline):
model = LanguageLimit
fields = ("language", "time_limit", "memory_limit", "memory_unit")
fields = ('language', 'time_limit', 'memory_limit')
form = LanguageLimitInlineForm
extra = 0
class LanguageTemplateInlineForm(ModelForm):
class ProblemClarificationForm(ModelForm):
class Meta:
widgets = {
"language": AdminSelect2Widget,
"source": AceWidget(width="600px", height="200px", toolbar=False),
}
if HeavyPreviewPageDownWidget is not None:
widgets = {'description': HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'))}
class LanguageTemplateInline(admin.TabularInline):
model = LanguageTemplate
fields = ("language", "source")
form = LanguageTemplateInlineForm
class ProblemClarificationInline(admin.StackedInline):
model = ProblemClarification
fields = ('description',)
form = ProblemClarificationForm
extra = 0
class ProblemSolutionForm(ModelForm):
def __init__(self, *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:
widgets = {
"authors": AdminHeavySelect2MultipleWidget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets["content"] = HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("solution_preview")
)
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('solution_preview'))
class ProblemSolutionInline(admin.StackedInline):
model = Solution
fields = ("is_public", "publish_on", "authors", "content")
fields = ('is_public', 'publish_on', 'authors', 'content')
form = ProblemSolutionForm
extra = 0
@ -188,300 +105,134 @@ class ProblemSolutionInline(admin.StackedInline):
class ProblemTranslationForm(ModelForm):
class Meta:
if HeavyPreviewAdminPageDownWidget is not None:
widgets = {
"description": HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("problem_preview")
)
}
widgets = {'description': HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('problem_preview'))}
class ProblemTranslationInline(admin.StackedInline):
model = ProblemTranslation
fields = ("language", "name", "description")
fields = ('language', 'name', 'description')
form = ProblemTranslationForm
extra = 0
class ProblemAdmin(CompareVersionAdmin):
class ProblemAdmin(VersionAdmin):
fieldsets = (
(
None,
{
"fields": (
"code",
"name",
"is_public",
"organizations",
"date",
"authors",
"curators",
"testers",
"description",
"pdf_description",
"license",
),
},
),
(
_("Social Media"),
{"classes": ("collapse",), "fields": ("og_image", "summary")},
),
(_("Taxonomy"), {"fields": ("types", "group")}),
(_("Points"), {"fields": (("points", "partial"), "short_circuit")}),
(_("Limits"), {"fields": ("time_limit", ("memory_limit", "memory_unit"))}),
(_("Language"), {"fields": ("allowed_languages",)}),
(_("Justice"), {"fields": ("banned_users",)}),
(_("History"), {"fields": ("change_message",)}),
(None, {
'fields': (
'code', 'name', 'is_public', 'is_manually_managed', 'date', 'authors', 'curators', 'testers',
'is_organization_private', 'organizations', 'description', 'license',
),
}),
(_('Social Media'), {'classes': ('collapse',), 'fields': ('og_image', 'summary')}),
(_('Taxonomy'), {'fields': ('types', 'group')}),
(_('Points'), {'fields': (('points', 'partial'), 'short_circuit')}),
(_('Limits'), {'fields': ('time_limit', 'memory_limit')}),
(_('Language'), {'fields': ('allowed_languages',)}),
(_('Justice'), {'fields': ('banned_users',)}),
(_('History'), {'fields': ('change_message',)}),
)
list_display = [
"code",
"name",
"show_authors",
"date",
"points",
"is_public",
"show_public",
]
ordering = ["-date"]
search_fields = (
"code",
"name",
"authors__user__username",
"curators__user__username",
)
inlines = [
LanguageLimitInline,
LanguageTemplateInline,
ProblemSolutionInline,
ProblemTranslationInline,
]
list_display = ['code', 'name', 'show_authors', 'points', 'is_public', 'show_public']
ordering = ['code']
search_fields = ('code', 'name', 'authors__user__username', 'curators__user__username')
inlines = [LanguageLimitInline, ProblemClarificationInline, ProblemSolutionInline, ProblemTranslationInline]
list_max_show_all = 1000
actions_on_top = True
actions_on_bottom = True
list_filter = ("is_public", ProblemCreatorListFilter)
list_filter = ('is_public', ProblemCreatorListFilter)
form = ProblemForm
date_hierarchy = "date"
date_hierarchy = 'date'
def get_actions(self, request):
actions = super(ProblemAdmin, self).get_actions(request)
if request.user.has_perm("judge.change_public_visibility"):
func, name, desc = self.get_action("make_public")
if request.user.has_perm('judge.change_public_visibility'):
func, name, desc = self.get_action('make_public')
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)
return actions
def get_readonly_fields(self, request, obj=None):
fields = self.readonly_fields
if not request.user.has_perm("judge.change_public_visibility"):
fields += ("is_public",)
if not request.user.has_perm('judge.change_public_visibility'):
fields += ('is_public',)
if not request.user.has_perm('judge.change_manually_managed'):
fields += ('is_manually_managed',)
return fields
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):
return format_html(
'<a href="{1}">{0}</a>', gettext("View on site"), obj.get_absolute_url()
)
return format_html('<a href="{1}">{0}</a>', gettext('View on site'), obj.get_absolute_url())
show_public.short_description = ""
show_public.short_description = ''
def _rescore(self, request, problem_id):
from judge.tasks import rescore_problem
transaction.on_commit(rescore_problem.s(problem_id).delay)
def make_public(self, request, queryset):
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.message_user(
request,
ungettext(
"%d problem successfully marked as public.",
"%d problems successfully marked as public.",
count,
)
% count,
)
self.message_user(request, ungettext('%d problem successfully marked as public.',
'%d problems successfully marked as public.',
count) % count)
make_public.short_description = _("Mark problems as public")
make_public.short_description = _('Mark problems as public')
def make_private(self, request, queryset):
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.message_user(
request,
ungettext(
"%d problem successfully marked as private.",
"%d problems successfully marked as private.",
count,
)
% count,
)
self.message_user(request, ungettext('%d problem successfully marked as private.',
'%d problems successfully marked as private.',
count) % count)
make_private.short_description = _("Mark problems as private")
make_private.short_description = _('Mark problems as private')
def get_queryset(self, request):
queryset = Problem.objects.prefetch_related("authors__user")
if request.user.has_perm("judge.edit_all_problem"):
queryset = Problem.objects.prefetch_related('authors__user')
if request.user.has_perm('judge.edit_all_problem'):
return queryset
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)
if request.user.has_perm("judge.edit_own_problem"):
access |= Q(authors__id=request.profile.id) | Q(
curators__id=request.profile.id
)
if request.user.has_perm('judge.edit_own_problem'):
access |= Q(authors__id=request.profile.id) | Q(curators__id=request.profile.id)
return queryset.filter(access).distinct() if access else queryset.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
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
if not request.user.has_perm("judge.edit_own_problem"):
if not request.user.has_perm('judge.edit_own_problem'):
return False
return obj.is_editor(request.profile)
def formfield_for_manytomany(self, db_field, request=None, **kwargs):
if db_field.name == "allowed_languages":
kwargs["widget"] = CheckboxSelectMultipleWithSelectAll()
return super(ProblemAdmin, self).formfield_for_manytomany(
db_field, request, **kwargs
)
if db_field.name == 'allowed_languages':
kwargs['widget'] = CheckboxSelectMultipleWithSelectAll()
return super(ProblemAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)
def get_form(self, *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
def save_model(self, request, obj, form, change):
form.changed_data.remove("memory_unit")
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")
):
super(ProblemAdmin, self).save_model(request, obj, form, change)
if form.changed_data and any(f in form.changed_data for f in ('is_public', 'points', 'partial')):
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):
if form.cleaned_data.get("change_message"):
return form.cleaned_data["change_message"]
return super(ProblemAdmin, self).construct_change_message(
request, form, *args, **kwargs
)
class ProblemPointsVoteAdmin(admin.ModelAdmin):
list_display = (
"vote_points",
"voter",
"voter_rating",
"voter_point",
"problem_name",
"problem_code",
"problem_points",
)
search_fields = ("voter__user__username", "problem__code", "problem__name")
readonly_fields = (
"voter",
"problem",
"problem_code",
"problem_points",
"voter_rating",
"voter_point",
)
def has_change_permission(self, request, obj=None):
if obj is None:
return request.user.has_perm("judge.edit_own_problem")
return obj.problem.is_editable_by(request.user)
def lookup_allowed(self, key, value):
return True
def problem_code(self, obj):
return obj.problem.code
problem_code.short_description = _("Problem code")
problem_code.admin_order_field = "problem__code"
def problem_points(self, obj):
return obj.problem.points
problem_points.short_description = _("Points")
problem_points.admin_order_field = "problem__points"
def problem_name(self, obj):
return obj.problem.name
problem_name.short_description = _("Problem name")
problem_name.admin_order_field = "problem__name"
def voter_rating(self, obj):
return obj.voter.rating
voter_rating.short_description = _("Voter rating")
voter_rating.admin_order_field = "voter__rating"
def voter_point(self, obj):
return round(obj.voter.performance_points)
voter_point.short_description = _("Voter point")
voter_point.admin_order_field = "voter__performance_points"
def vote_points(self, obj):
return obj.points
vote_points.short_description = _("Vote")
if form.cleaned_data.get('change_message'):
return form.cleaned_data['change_message']
return super(ProblemAdmin, self).construct_change_message(request, form, *args, **kwargs)

View file

@ -1,58 +1,41 @@
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.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
import re
from django_ace import AceWidget
from judge.models import Profile
from judge.widgets import AdminPagedownWidget, AdminSelect2Widget
class ProfileForm(ModelForm):
def __init__(self, *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.
self.fields[
"current_contest"
].queryset = self.instance.contest_history.select_related("contest").only(
"contest__name", "user_id", "virtual"
)
self.fields["current_contest"].label_from_instance = (
lambda obj: "%s v%d" % (obj.contest.name, obj.virtual)
if obj.virtual
else obj.contest.name
)
self.fields['current_contest'].queryset = self.instance.contest_history.select_related('contest') \
.only('contest__name', 'user_id', 'virtual')
self.fields['current_contest'].label_from_instance = \
lambda obj: '%s v%d' % (obj.contest.name, obj.virtual) if obj.virtual else obj.contest.name
class Meta:
widgets = {
"timezone": AdminSelect2Widget,
"language": AdminSelect2Widget,
"ace_theme": AdminSelect2Widget,
"current_contest": AdminSelect2Widget,
'timezone': AdminSelect2Widget,
'language': AdminSelect2Widget,
'ace_theme': AdminSelect2Widget,
'current_contest': AdminSelect2Widget,
}
if AdminPagedownWidget is not None:
widgets["about"] = AdminPagedownWidget
widgets['about'] = AdminPagedownWidget
class TimezoneFilter(admin.SimpleListFilter):
title = _("timezone")
parameter_name = "timezone"
title = _('timezone')
parameter_name = 'timezone'
def lookups(self, request, model_admin):
return (
Profile.objects.values_list("timezone", "timezone")
.distinct()
.order_by("timezone")
)
return Profile.objects.values_list('timezone', 'timezone').distinct().order_by('timezone')
def queryset(self, request, queryset):
if self.value() is None:
@ -60,168 +43,76 @@ class TimezoneFilter(admin.SimpleListFilter):
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):
fields = (
"user",
"display_rank",
"about",
"organizations",
"timezone",
"language",
"ace_theme",
"last_access",
"ip",
"mute",
"is_unlisted",
"notes",
"is_totp_enabled",
"current_contest",
)
readonly_fields = ("user",)
list_display = (
"admin_user_admin",
"email",
"is_totp_enabled",
"timezone_full",
"date_joined",
"last_access",
"ip",
"show_public",
)
ordering = ("user__username",)
search_fields = ("user__username", "ip", "user__email")
list_filter = ("language", TimezoneFilter)
actions = ("recalculate_points",)
fields = ('user', 'display_rank', 'about', 'organizations', 'timezone', 'language', 'ace_theme',
'math_engine', 'last_access', 'ip', 'mute', 'is_unlisted', 'notes', 'is_totp_enabled', 'user_script',
'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_bottom = True
form = ProfileForm
inlines = (ProfileInfoInline,)
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):
if request.user.has_perm("judge.totp"):
if request.user.has_perm('judge.totp'):
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)
else:
return self.fields
def get_readonly_fields(self, request, obj=None):
fields = self.readonly_fields
if not request.user.has_perm("judge.totp"):
fields += ("is_totp_enabled",)
if not request.user.has_perm('judge.totp'):
fields += ('is_totp_enabled',)
return fields
def show_public(self, obj):
return format_html(
'<a href="{0}" style="white-space:nowrap;">{1}</a>',
obj.get_absolute_url(),
gettext("View on site"),
)
show_public.short_description = ""
return format_html('<a href="{0}" style="white-space:nowrap;">{1}</a>',
obj.get_absolute_url(), gettext('View on site'))
show_public.short_description = ''
def admin_user_admin(self, obj):
return obj.username
admin_user_admin.admin_order_field = "user__username"
admin_user_admin.short_description = _("User")
admin_user_admin.admin_order_field = 'user__username'
admin_user_admin.short_description = _('User')
def email(self, obj):
return obj.email
email.admin_order_field = "user__email"
email.short_description = _("Email")
return obj.user.email
email.admin_order_field = 'user__email'
email.short_description = _('Email')
def timezone_full(self, obj):
return obj.timezone
timezone_full.admin_order_field = "timezone"
timezone_full.short_description = _("Timezone")
timezone_full.admin_order_field = 'timezone'
timezone_full.short_description = _('Timezone')
def date_joined(self, obj):
return obj.user.date_joined
date_joined.admin_order_field = "user__date_joined"
date_joined.short_description = _("date joined")
date_joined.admin_order_field = 'user__date_joined'
date_joined.short_description = _('date joined')
def recalculate_points(self, request, queryset):
count = 0
for profile in queryset:
profile.calculate_points()
count += 1
self.message_user(
request,
ungettext(
"%d user have scores recalculated.",
"%d users have scores recalculated.",
count,
)
% count,
)
self.message_user(request, ungettext('%d user have scores recalculated.',
'%d users have scores recalculated.',
count) % count)
recalculate_points.short_description = _('Recalculate scores')
recalculate_points.short_description = _("Recalculate scores")
class UserForm(UserChangeForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["username"].help_text = _(
"Username can only contain letters, digits, and underscores."
)
def clean_username(self):
username = self.cleaned_data.get("username")
if not re.match(r"^\w+$", username):
raise ValidationError(
_("Username can only contain letters, digits, and underscores.")
)
return username
class UserAdmin(OldUserAdmin):
# Customize the fieldsets for adding and editing users
form = UserForm
fieldsets = (
(None, {"fields": ("username", "password")}),
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
(
"Permissions",
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
)
},
),
("Important dates", {"fields": ("last_login", "date_joined")}),
)
readonly_fields = ("last_login", "date_joined")
def get_readonly_fields(self, request, obj=None):
fields = self.readonly_fields
if not request.user.is_superuser:
fields += (
"is_staff",
"is_active",
"is_superuser",
"groups",
"user_permissions",
)
return fields
def has_add_permission(self, request):
return False
def get_form(self, request, obj=None, **kwargs):
form = super(ProfileAdmin, self).get_form(request, obj, **kwargs)
if 'user_script' in form.base_fields:
# form.base_fields['user_script'] does not exist when the user has only view permission on the model.
form.base_fields['user_script'].widget = AceWidget('javascript', request.profile.ace_theme)
return form

View file

@ -16,63 +16,41 @@ from judge.widgets import AdminHeavySelect2MultipleWidget, AdminPagedownWidget
class LanguageForm(ModelForm):
problems = ModelMultipleChoiceField(
label=_("Disallowed problems"),
label=_('Disallowed problems'),
queryset=Problem.objects.all(),
required=False,
help_text=_("These problems are NOT allowed to be submitted in this language"),
widget=AdminHeavySelect2MultipleWidget(data_view="problem_select2"),
)
help_text=_('These problems are NOT allowed to be submitted in this language'),
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'))
class Meta:
if AdminPagedownWidget is not None:
widgets = {"description": AdminPagedownWidget}
widgets = {'description': AdminPagedownWidget}
class LanguageAdmin(VersionAdmin):
fields = (
"key",
"name",
"short_name",
"common_name",
"ace",
"pygments",
"info",
"description",
"template",
"problems",
)
list_display = ("key", "name", "common_name", "info")
fields = ('key', 'name', 'short_name', 'common_name', 'ace', 'pygments', 'info', 'description',
'template', 'problems')
list_display = ('key', 'name', 'common_name', 'info')
form = LanguageForm
def save_model(self, request, obj, form, change):
super(LanguageAdmin, self).save_model(request, obj, form, change)
obj.problem_set.set(
Problem.objects.exclude(id__in=form.cleaned_data["problems"].values("id"))
)
obj.problem_set.set(Problem.objects.exclude(id__in=form.cleaned_data['problems'].values('id')))
def get_form(self, request, obj=None, **kwargs):
self.form.base_fields["problems"].initial = (
Problem.objects.exclude(id__in=obj.problem_set.values("id")).values_list(
"pk", flat=True
)
if obj
else []
)
self.form.base_fields['problems'].initial = \
Problem.objects.exclude(id__in=obj.problem_set.values('id')).values_list('pk', flat=True) if obj else []
form = super(LanguageAdmin, self).get_form(request, obj, **kwargs)
if obj is not None:
form.base_fields["template"].widget = AceWidget(
obj.ace, request.profile.ace_theme
)
form.base_fields['template'].widget = AceWidget(obj.ace, request.profile.ace_theme)
return form
class GenerateKeyTextInput(TextInput):
def render(self, name, value, attrs=None, renderer=None):
text = super(TextInput, self).render(name, value, attrs)
return mark_safe(
text
+ format_html(
"""\
return mark_safe(text + format_html(
'''\
<a href="#" onclick="return false;" class="button" id="id_{0}_regen">Regenerate</a>
<script type="text/javascript">
django.jQuery(document).ready(function ($) {{
@ -87,59 +65,37 @@ django.jQuery(document).ready(function ($) {{
}});
}});
</script>
""",
name,
)
)
''', name))
class JudgeAdminForm(ModelForm):
class Meta:
widgets = {"auth_key": GenerateKeyTextInput}
widgets = {'auth_key': GenerateKeyTextInput}
if AdminPagedownWidget is not None:
widgets["description"] = AdminPagedownWidget
widgets['description'] = AdminPagedownWidget
class JudgeAdmin(VersionAdmin):
form = JudgeAdminForm
readonly_fields = (
"created",
"online",
"start_time",
"ping",
"load",
"last_ip",
"runtimes",
"problems",
)
readonly_fields = ('created', 'online', 'start_time', 'ping', 'load', 'last_ip', 'runtimes', 'problems')
fieldsets = (
(None, {"fields": ("name", "auth_key", "is_blocked")}),
(_("Description"), {"fields": ("description",)}),
(
_("Information"),
{"fields": ("created", "online", "last_ip", "start_time", "ping", "load")},
),
(_("Capabilities"), {"fields": ("runtimes", "problems")}),
(None, {'fields': ('name', 'auth_key', 'is_blocked')}),
(_('Description'), {'fields': ('description',)}),
(_('Information'), {'fields': ('created', 'online', 'last_ip', 'start_time', 'ping', 'load')}),
(_('Capabilities'), {'fields': ('runtimes', 'problems')}),
)
list_display = ("name", "online", "start_time", "ping", "load", "last_ip")
ordering = ["-online", "name"]
list_display = ('name', 'online', 'start_time', 'ping', 'load', 'last_ip')
ordering = ['-online', 'name']
def get_urls(self):
return [
url(
r"^(\d+)/disconnect/$",
self.disconnect_view,
name="judge_judge_disconnect",
),
url(
r"^(\d+)/terminate/$", self.terminate_view, name="judge_judge_terminate"
),
] + super(JudgeAdmin, self).get_urls()
return ([url(r'^(\d+)/disconnect/$', self.disconnect_view, name='judge_judge_disconnect'),
url(r'^(\d+)/terminate/$', self.terminate_view, name='judge_judge_terminate')] +
super(JudgeAdmin, self).get_urls())
def disconnect_judge(self, id, force=False):
judge = get_object_or_404(Judge, id=id)
judge.disconnect(force=force)
return HttpResponseRedirect(reverse("admin:judge_judge_changelist"))
return HttpResponseRedirect(reverse('admin:judge_judge_changelist'))
def disconnect_view(self, request, id):
return self.disconnect_judge(id)
@ -149,7 +105,7 @@ class JudgeAdmin(VersionAdmin):
def get_readonly_fields(self, request, obj=None):
if obj is not None and obj.online:
return self.readonly_fields + ("name",)
return self.readonly_fields + ('name',)
return self.readonly_fields
def has_delete_permission(self, request, obj=None):
@ -160,5 +116,5 @@ class JudgeAdmin(VersionAdmin):
if AdminPagedownWidget is not None:
formfield_overrides = {
TextField: {"widget": AdminPagedownWidget},
TextField: {'widget': AdminPagedownWidget},
}

View file

@ -13,365 +13,239 @@ from django.utils.html import format_html
from django.utils.translation import gettext, gettext_lazy as _, pgettext, ungettext
from django_ace import AceWidget
from judge.models import (
ContestParticipation,
ContestProblem,
ContestSubmission,
Profile,
Submission,
SubmissionSource,
SubmissionTestCase,
)
from judge.models import ContestParticipation, ContestProblem, ContestSubmission, Profile, Submission, \
SubmissionSource, SubmissionTestCase
from judge.utils.raw_sql import use_straight_join
class SubmissionStatusFilter(admin.SimpleListFilter):
parameter_name = title = "status"
__lookups = (
("None", _("None")),
("NotDone", _("Not done")),
("EX", _("Exceptional")),
) + Submission.STATUS
parameter_name = title = 'status'
__lookups = (('None', _('None')), ('NotDone', _('Not done')), ('EX', _('Exceptional'))) + Submission.STATUS
__handles = set(map(itemgetter(0), Submission.STATUS))
def lookups(self, request, model_admin):
return self.__lookups
def queryset(self, request, queryset):
if self.value() == "None":
if self.value() == 'None':
return queryset.filter(status=None)
elif self.value() == "NotDone":
return queryset.exclude(status__in=["D", "IE", "CE", "AB"])
elif self.value() == "EX":
return queryset.exclude(status__in=["D", "CE", "G", "AB"])
elif self.value() == 'NotDone':
return queryset.exclude(status__in=['D', 'IE', 'CE', 'AB'])
elif self.value() == 'EX':
return queryset.exclude(status__in=['D', 'CE', 'G', 'AB'])
elif self.value() in self.__handles:
return queryset.filter(status=self.value())
class SubmissionResultFilter(admin.SimpleListFilter):
parameter_name = title = "result"
__lookups = (("None", _("None")), ("BAD", _("Unaccepted"))) + Submission.RESULT
parameter_name = title = 'result'
__lookups = (('None', _('None')), ('BAD', _('Unaccepted'))) + Submission.RESULT
__handles = set(map(itemgetter(0), Submission.RESULT))
def lookups(self, request, model_admin):
return self.__lookups
def queryset(self, request, queryset):
if self.value() == "None":
if self.value() == 'None':
return queryset.filter(result=None)
elif self.value() == "BAD":
return queryset.exclude(result="AC")
elif self.value() == 'BAD':
return queryset.exclude(result='AC')
elif self.value() in self.__handles:
return queryset.filter(result=self.value())
class SubmissionTestCaseInline(admin.TabularInline):
fields = ("case", "batch", "status", "time", "memory", "points", "total")
readonly_fields = ("case", "batch", "total")
fields = ('case', 'batch', 'status', 'time', 'memory', 'points', 'total')
readonly_fields = ('case', 'batch', 'total')
model = SubmissionTestCase
can_delete = False
max_num = 0
class ContestSubmissionInline(admin.StackedInline):
fields = ("problem", "participation", "points")
fields = ('problem', 'participation', 'points')
model = ContestSubmission
def get_formset(self, request, obj=None, **kwargs):
kwargs["formfield_callback"] = partial(
self.formfield_for_dbfield, request=request, obj=obj
)
kwargs['formfield_callback'] = partial(self.formfield_for_dbfield, request=request, obj=obj)
return super(ContestSubmissionInline, self).get_formset(request, obj, **kwargs)
def formfield_for_dbfield(self, db_field, **kwargs):
submission = kwargs.pop("obj", None)
submission = kwargs.pop('obj', None)
label = None
if submission:
if db_field.name == "participation":
kwargs["queryset"] = ContestParticipation.objects.filter(
user=submission.user, contest__problems=submission.problem
).only("id", "contest__name")
if db_field.name == 'participation':
kwargs['queryset'] = ContestParticipation.objects.filter(user=submission.user,
contest__problems=submission.problem) \
.only('id', 'contest__name')
def label(obj):
return obj.contest.name
elif db_field.name == "problem":
kwargs["queryset"] = ContestProblem.objects.filter(
problem=submission.problem
).only("id", "problem__name", "contest__name")
elif db_field.name == 'problem':
kwargs['queryset'] = ContestProblem.objects.filter(problem=submission.problem) \
.only('id', 'problem__name', 'contest__name')
def label(obj):
return pgettext("contest problem", "%(problem)s in %(contest)s") % {
"problem": obj.problem.name,
"contest": obj.contest.name,
return pgettext('contest problem', '%(problem)s in %(contest)s') % {
'problem': obj.problem.name, 'contest': obj.contest.name,
}
field = super(ContestSubmissionInline, self).formfield_for_dbfield(
db_field, **kwargs
)
field = super(ContestSubmissionInline, self).formfield_for_dbfield(db_field, **kwargs)
if label is not None:
field.label_from_instance = label
return field
class SubmissionSourceInline(admin.StackedInline):
fields = ("source",)
fields = ('source',)
model = SubmissionSource
can_delete = False
extra = 0
def get_formset(self, request, obj=None, **kwargs):
kwargs.setdefault("widgets", {})["source"] = AceWidget(
mode=obj and obj.language.ace, theme=request.profile.ace_theme
)
kwargs.setdefault('widgets', {})['source'] = AceWidget(mode=obj and obj.language.ace,
theme=request.profile.ace_theme)
return super().get_formset(request, obj, **kwargs)
class SubmissionAdmin(admin.ModelAdmin):
readonly_fields = ("user", "problem", "date", "judged_date")
fields = (
"user",
"problem",
"date",
"judged_date",
"time",
"memory",
"points",
"language",
"status",
"result",
"case_points",
"case_total",
"judged_on",
"error",
)
actions = ("judge", "recalculate_score")
list_display = (
"id",
"problem_code",
"problem_name",
"user_column",
"execution_time",
"pretty_memory",
"points",
"language_column",
"status",
"result",
"judge_column",
)
list_filter = ("language", SubmissionStatusFilter, SubmissionResultFilter)
search_fields = ("problem__code", "problem__name", "user__user__username")
readonly_fields = ('user', 'problem', 'date')
fields = ('user', 'problem', 'date', 'time', 'memory', 'points', 'language', 'status', 'result',
'case_points', 'case_total', 'judged_on', 'error')
actions = ('judge', 'recalculate_score')
list_display = ('id', 'problem_code', 'problem_name', 'user_column', 'execution_time', 'pretty_memory',
'points', 'language_column', 'status', 'result', 'judge_column')
list_filter = ('language', SubmissionStatusFilter, SubmissionResultFilter)
search_fields = ('problem__code', 'problem__name', 'user__user__username')
actions_on_top = True
actions_on_bottom = True
inlines = [
SubmissionSourceInline,
SubmissionTestCaseInline,
ContestSubmissionInline,
]
inlines = [SubmissionSourceInline, SubmissionTestCaseInline, ContestSubmissionInline]
def get_queryset(self, request):
queryset = Submission.objects.select_related(
"problem", "user__user", "language"
).only(
"problem__code",
"problem__name",
"user__user__username",
"language__name",
"time",
"memory",
"points",
"status",
"result",
queryset = Submission.objects.select_related('problem', 'user__user', 'language').only(
'problem__code', 'problem__name', 'user__user__username', 'language__name',
'time', 'memory', 'points', 'status', 'result',
)
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
queryset = queryset.filter(
Q(problem__authors__id=id) | Q(problem__curators__id=id)
).distinct()
queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id)).distinct()
return queryset
def has_add_permission(self, request):
return False
def lookup_allowed(self, key, value):
return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in (
"problem__code",
)
def has_change_permission(self, request, obj=None):
if not request.user.has_perm('judge.edit_own_problem'):
return False
if request.user.has_perm('judge.edit_all_problem') or obj is None:
return True
return obj.problem.is_editor(request.profile)
def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
if "case_points" in form.changed_data or "case_total" in form.changed_data:
obj.update_contest()
def lookup_allowed(self, key, value):
return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in ('problem__code',)
def judge(self, request, queryset):
if not request.user.has_perm(
"judge.rejudge_submission"
) or not request.user.has_perm("judge.edit_own_problem"):
self.message_user(
request,
gettext("You do not have the permission to rejudge submissions."),
level=messages.ERROR,
)
if not request.user.has_perm('judge.rejudge_submission') or not request.user.has_perm('judge.edit_own_problem'):
self.message_user(request, gettext('You do not have the permission to rejudge submissions.'),
level=messages.ERROR)
return
queryset = queryset.order_by("id")
if (
not request.user.has_perm("judge.rejudge_submission_lot")
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,
)
queryset = queryset.order_by('id')
if not request.user.has_perm('judge.rejudge_submission_lot') 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)
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
queryset = queryset.filter(
Q(problem__authors__id=id) | Q(problem__curators__id=id)
)
queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id))
judged = len(queryset)
for model in queryset:
model.judge(rejudge=True, batch_rejudge=True)
self.message_user(
request,
ungettext(
"%d submission was successfully scheduled for rejudging.",
"%d submissions were successfully scheduled for rejudging.",
judged,
)
% judged,
)
judge.short_description = _("Rejudge the selected submissions")
self.message_user(request, ungettext('%d submission was successfully scheduled for rejudging.',
'%d submissions were successfully scheduled for rejudging.',
judged) % judged)
judge.short_description = _('Rejudge the selected submissions')
def recalculate_score(self, request, queryset):
if not request.user.has_perm("judge.rejudge_submission"):
self.message_user(
request,
gettext("You do not have the permission to rejudge submissions."),
level=messages.ERROR,
)
if not request.user.has_perm('judge.rejudge_submission'):
self.message_user(request, gettext('You do not have the permission to rejudge submissions.'),
level=messages.ERROR)
return
submissions = list(
queryset.defer(None)
.select_related(None)
.select_related("problem")
.only(
"points",
"case_points",
"case_total",
"problem__partial",
"problem__points",
)
)
submissions = list(queryset.defer(None).select_related(None).select_related('problem')
.only('points', 'case_points', 'case_total', 'problem__partial', 'problem__points'))
for submission in submissions:
submission.points = round(
submission.case_points
/ submission.case_total
* submission.problem.points
if submission.case_total
else 0,
1,
)
if (
not submission.problem.partial
and submission.points < submission.problem.points
):
submission.points = round(submission.case_points / submission.case_total * submission.problem.points
if submission.case_total else 0, 1)
if not submission.problem.partial and submission.points < submission.problem.points:
submission.points = 0
submission.save()
submission.update_contest()
for profile in Profile.objects.filter(
id__in=queryset.values_list("user_id", flat=True).distinct()
):
for profile in Profile.objects.filter(id__in=queryset.values_list('user_id', flat=True).distinct()):
profile.calculate_points()
cache.delete("user_complete:%d" % profile.id)
cache.delete("user_attempted:%d" % profile.id)
cache.delete('user_complete:%d' % profile.id)
cache.delete('user_attempted:%d' % profile.id)
for participation in ContestParticipation.objects.filter(
id__in=queryset.values_list("contest__participation_id")
).prefetch_related("contest"):
id__in=queryset.values_list('contest__participation_id')).prefetch_related('contest'):
participation.recompute_results()
self.message_user(
request,
ungettext(
"%d submission were successfully rescored.",
"%d submissions were successfully rescored.",
len(submissions),
)
% len(submissions),
)
recalculate_score.short_description = _("Rescore the selected submissions")
self.message_user(request, ungettext('%d submission were successfully rescored.',
'%d submissions were successfully rescored.',
len(submissions)) % len(submissions))
recalculate_score.short_description = _('Rescore the selected submissions')
def problem_code(self, obj):
return obj.problem.code
problem_code.short_description = _("Problem code")
problem_code.admin_order_field = "problem__code"
problem_code.short_description = _('Problem code')
problem_code.admin_order_field = 'problem__code'
def problem_name(self, obj):
return obj.problem.name
problem_name.short_description = _("Problem name")
problem_name.admin_order_field = "problem__name"
problem_name.short_description = _('Problem name')
problem_name.admin_order_field = 'problem__name'
def user_column(self, obj):
return obj.user.user.username
user_column.admin_order_field = "user__user__username"
user_column.short_description = _("User")
user_column.admin_order_field = 'user__user__username'
user_column.short_description = _('User')
def execution_time(self, obj):
return round(obj.time, 2) if obj.time is not None else "None"
execution_time.short_description = _("Time")
execution_time.admin_order_field = "time"
return round(obj.time, 2) if obj.time is not None else 'None'
execution_time.short_description = _('Time')
execution_time.admin_order_field = 'time'
def pretty_memory(self, obj):
memory = obj.memory
if memory is None:
return gettext("None")
return gettext('None')
if memory < 1000:
return gettext("%d KB") % memory
return gettext('%d KB') % memory
else:
return gettext("%.2f MB") % (memory / 1024)
pretty_memory.admin_order_field = "memory"
pretty_memory.short_description = _("Memory")
return gettext('%.2f MB') % (memory / 1024)
pretty_memory.admin_order_field = 'memory'
pretty_memory.short_description = _('Memory')
def language_column(self, obj):
return obj.language.name
language_column.admin_order_field = "language__name"
language_column.short_description = _("Language")
language_column.admin_order_field = 'language__name'
language_column.short_description = _('Language')
def judge_column(self, obj):
return format_html(
'<input type="button" value="Rejudge" onclick="location.href=\'{}/judge/\'" />',
obj.id,
)
judge_column.short_description = ""
return format_html('<input type="button" value="Rejudge" onclick="location.href=\'{}/judge/\'" />', obj.id)
judge_column.short_description = ''
def get_urls(self):
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()
def judge_view(self, request, id):
if not request.user.has_perm(
"judge.rejudge_submission"
) or not request.user.has_perm("judge.edit_own_problem"):
if not request.user.has_perm('judge.rejudge_submission') or not request.user.has_perm('judge.edit_own_problem'):
raise PermissionDenied()
submission = get_object_or_404(Submission, id=id)
if not request.user.has_perm(
"judge.edit_all_problem"
) and not submission.problem.is_editor(request.profile):
if not request.user.has_perm('judge.edit_all_problem') and \
not submission.problem.is_editor(request.profile):
raise PermissionDenied()
submission.judge(rejudge=True)
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))

View file

@ -8,59 +8,45 @@ from judge.widgets import AdminHeavySelect2MultipleWidget
class ProblemGroupForm(ModelForm):
problems = ModelMultipleChoiceField(
label=_("Included problems"),
label=_('Included problems'),
queryset=Problem.objects.all(),
required=False,
help_text=_("These problems are included in this group of problems"),
widget=AdminHeavySelect2MultipleWidget(data_view="problem_select2"),
)
help_text=_('These problems are included in this group of problems'),
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'))
class ProblemGroupAdmin(admin.ModelAdmin):
fields = ("name", "full_name", "problems")
fields = ('name', 'full_name', 'problems')
form = ProblemGroupForm
def save_model(self, 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()
def get_form(self, request, obj=None, **kwargs):
self.form.base_fields["problems"].initial = (
[o.pk for o in obj.problem_set.all()] if obj else []
)
self.form.base_fields['problems'].initial = [o.pk for o in obj.problem_set.all()] if obj else []
return super(ProblemGroupAdmin, self).get_form(request, obj, **kwargs)
class ProblemTypeForm(ModelForm):
problems = ModelMultipleChoiceField(
label=_("Included problems"),
label=_('Included problems'),
queryset=Problem.objects.all(),
required=False,
help_text=_("These problems are included in this type of problems"),
widget=AdminHeavySelect2MultipleWidget(data_view="problem_select2"),
)
help_text=_('These problems are included in this type of problems'),
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'))
class ProblemTypeAdmin(admin.ModelAdmin):
fields = ("name", "full_name", "problems")
fields = ('name', 'full_name', 'problems')
form = ProblemTypeForm
def save_model(self, 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()
def get_form(self, request, obj=None, **kwargs):
self.form.base_fields["problems"].initial = (
[o.pk for o in obj.problem_set.all()] if obj else []
)
self.form.base_fields['problems'].initial = [o.pk for o in obj.problem_set.all()] if obj else []
return super(ProblemTypeAdmin, self).get_form(request, obj, **kwargs)
class OfficialContestCategoryAdmin(admin.ModelAdmin):
fields = ("name",)
class OfficialContestLocationAdmin(admin.ModelAdmin):
fields = ("name",)

View file

@ -4,56 +4,36 @@ from django.forms import ModelForm
from django.urls import reverse_lazy
from judge.models import TicketMessage
from judge.widgets import (
AdminHeavySelect2MultipleWidget,
AdminHeavySelect2Widget,
HeavyPreviewAdminPageDownWidget,
)
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
class TicketMessageForm(ModelForm):
class Meta:
widgets = {
"user": AdminHeavySelect2Widget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets["body"] = HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("ticket_preview")
)
widgets['body'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('ticket_preview'))
class TicketMessageInline(StackedInline):
model = TicketMessage
form = TicketMessageForm
fields = ("user", "body")
fields = ('user', 'body')
class TicketForm(ModelForm):
class Meta:
widgets = {
"user": AdminHeavySelect2Widget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
"assignees": AdminHeavySelect2MultipleWidget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
'assignees': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
}
class TicketAdmin(ModelAdmin):
fields = (
"title",
"time",
"user",
"assignees",
"content_type",
"object_id",
"notes",
)
readonly_fields = ("time",)
list_display = ("title", "user", "time", "linked_item")
fields = ('title', 'time', 'user', 'assignees', 'content_type', 'object_id', 'notes')
readonly_fields = ('time',)
list_display = ('title', 'user', 'time', 'linked_item')
inlines = [TicketMessageInline]
form = TicketForm
date_hierarchy = "time"
date_hierarchy = 'time'

View file

@ -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()

View file

@ -4,15 +4,15 @@ from django.utils.translation import gettext_lazy
class JudgeAppConfig(AppConfig):
name = "judge"
verbose_name = gettext_lazy("Online Judge")
name = 'judge'
verbose_name = gettext_lazy('Online Judge')
def ready(self):
# WARNING: AS THIS IS NOT A FUNCTIONAL PROGRAMMING LANGUAGE,
# OPERATIONS MAY HAVE SIDE EFFECTS.
# DO NOT REMOVE THINKING THE IMPORT IS UNUSED.
# 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.admin import FlatPageAdmin
@ -30,7 +30,7 @@ class JudgeAppConfig(AppConfig):
from django.contrib.auth.models import User
try:
lang = Language.get_default_language()
lang = Language.get_python3()
for user in User.objects.filter(profile=None):
# These poor profileless users
profile = Profile(user=user, language=lang)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -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()

View 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

View 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

View file

@ -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()

View file

@ -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()

View file

@ -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))

View 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)

View 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'])

View file

@ -3,16 +3,14 @@ from collections import namedtuple
from operator import attrgetter
from threading import RLock
from judge.bridge.utils import VanishedSubmission
try:
from llist import dllist
except ImportError:
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):
@ -20,9 +18,7 @@ class JudgeList(object):
def __init__(self):
self.queue = dllist()
self.priority = [
self.queue.append(PriorityMarker(i)) for i in range(self.priorities)
]
self.priority = [self.queue.append(PriorityMarker(i)) for i in range(self.priorities)]
self.judges = set()
self.node_map = {}
self.submission_map = {}
@ -33,24 +29,14 @@ class JudgeList(object):
node = self.queue.first
while node:
if not isinstance(node.value, PriorityMarker):
id, problem, language, source, judge_id = node.value
if judge.can_judge(problem, language, judge_id):
id, problem, language, source = node.value
if judge.can_judge(problem, language):
self.submission_map[id] = judge
logger.info(
"Dispatched queued submission %d: %s", id, judge.name
)
logger.info('Dispatched queued submission %d: %s', id, judge.name)
try:
judge.submit(id, problem, language, source)
except VanishedSubmission:
pass
except Exception:
logger.exception(
"Failed to dispatch %d (%s, %s) to %s",
id,
problem,
language,
judge.name,
)
logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name)
self.judges.remove(judge)
return
self.queue.remove(node)
@ -66,10 +52,9 @@ class JudgeList(object):
self._handle_free_judge(judge)
def disconnect(self, judge_id, force=False):
with self.lock:
for judge in self.judges:
if judge.name == judge_id:
judge.disconnect(force=force)
for judge in self.judges:
if judge.name == judge_id:
judge.disconnect(force=force)
def update_problems(self, judge):
with self.lock:
@ -90,15 +75,13 @@ class JudgeList(object):
def on_judge_free(self, judge, submission):
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]
judge._working = False
judge._working_data = {}
self._handle_free_judge(judge)
def abort(self, submission):
with self.lock:
logger.info("Abort request: %d", submission)
logger.info('Abort request: %d', submission)
try:
self.submission_map[submission].abort()
return True
@ -115,46 +98,26 @@ class JudgeList(object):
def check_priority(self, priority):
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:
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
# idempotent.
return
candidates = [
judge
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))
candidates = [judge for judge in self.judges if not judge.working and judge.can_judge(problem, language)]
logger.info('Free judges: %d', len(candidates))
if candidates:
# Schedule the submission on the judge reporting least load.
judge = min(candidates, key=attrgetter("load"))
logger.info("Dispatched submission %d to: %s", id, judge.name)
judge = min(candidates, key=attrgetter('load'))
logger.info('Dispatched submission %d to: %s', id, judge.name)
self.submission_map[id] = judge
try:
judge.submit(id, problem, language, source)
except Exception:
logger.exception(
"Failed to dispatch %d (%s, %s) to %s",
id,
problem,
language,
judge.name,
)
logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name)
self.judges.discard(judge)
return self.judge(id, problem, language, source, judge_id, priority)
return self.judge(id, problem, language, source, priority)
else:
self.node_map[id] = self.queue.insert(
(id, problem, language, source, judge_id),
self.priority[priority],
)
logger.info("Queued submission: %d", id)
self.node_map[id] = self.queue.insert((id, problem, language, source), self.priority[priority])
logger.info('Queued submission: %d', id)

View 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()

View file

@ -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()

View file

@ -1,2 +0,0 @@
class VanishedSubmission(Exception):
pass

View file

@ -1,117 +1,10 @@
from inspect import signature
from django.core.cache import cache, caches
from django.db.models.query import QuerySet
from django.core.handlers.wsgi import WSGIRequest
import hashlib
from judge.logging import log_debug
MAX_NUM_CHAR = 50
NONE_RESULT = "__None__"
from django.core.cache import cache
def arg_to_str(arg):
if hasattr(arg, "id"):
return str(arg.id)
if isinstance(arg, list) or isinstance(arg, QuerySet):
return hashlib.sha1(str(list(arg)).encode()).hexdigest()[:MAX_NUM_CHAR]
if len(str(arg)) > MAX_NUM_CHAR:
return str(arg)[:MAX_NUM_CHAR]
return str(arg)
def filter_args(args_list):
return [x for x in args_list if not isinstance(x, WSGIRequest)]
l0_cache = caches["l0"] if "l0" in caches else None
def cache_wrapper(prefix, timeout=None, expected_type=None):
def get_key(func, *args, **kwargs):
args_list = list(args)
signature_args = list(signature(func).parameters.keys())
args_list += [kwargs.get(k) for k in signature_args[len(args) :]]
args_list = filter_args(args_list)
args_list = [arg_to_str(i) for i in args_list]
key = prefix + ":" + ":".join(args_list)
key = key.replace(" ", "_")
return key
def _get(key):
if not l0_cache:
return cache.get(key)
result = l0_cache.get(key)
if result is None:
result = cache.get(key)
return result
def _set_l0(key, value):
if l0_cache:
l0_cache.set(key, value, 30)
def _set(key, value, timeout):
_set_l0(key, value)
cache.set(key, value, timeout)
def decorator(func):
def _validate_type(cache_key, result):
if expected_type and not isinstance(result, expected_type):
data = {
"function": f"{func.__module__}.{func.__qualname__}",
"result": str(result)[:30],
"expected_type": expected_type,
"type": type(result),
"key": cache_key,
}
log_debug("invalid_key", data)
return False
return True
def wrapper(*args, **kwargs):
cache_key = get_key(func, *args, **kwargs)
result = _get(cache_key)
if result is not None and _validate_type(cache_key, result):
_set_l0(cache_key, result)
if type(result) == str and result == NONE_RESULT:
result = None
return result
result = func(*args, **kwargs)
if result is None:
cache_result = NONE_RESULT
else:
cache_result = result
_set(cache_key, cache_result, timeout)
return result
def dirty(*args, **kwargs):
cache_key = get_key(func, *args, **kwargs)
cache.delete(cache_key)
if l0_cache:
l0_cache.delete(cache_key)
def prefetch_multi(args_list):
keys = []
for args in args_list:
keys.append(get_key(func, *args))
results = cache.get_many(keys)
for key, result in results.items():
if result is not None:
_set_l0(key, result)
def dirty_multi(args_list):
keys = []
for args in args_list:
keys.append(get_key(func, *args))
cache.delete_many(keys)
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
def finished_submission(sub):
keys = ['user_complete:%d' % sub.user_id, 'user_attempted:%s' % sub.user_id]
if hasattr(sub, 'contest'):
participation = sub.contest.participation
keys += ['contest_complete:%d' % participation.id]
keys += ['contest_attempted:%d' % participation.id]
cache.delete_many(keys)

122
judge/comments.py Normal file
View 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

View file

@ -1,8 +1,5 @@
from judge.contest_format.atcoder import AtCoderContestFormat
from judge.contest_format.default import DefaultContestFormat
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.new_ioi import NewIOIContestFormat
from judge.contest_format.ultimate import UltimateContestFormat
from judge.contest_format.registry import choices, formats

View file

@ -10,18 +10,18 @@ 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.timezone import from_database_time
from judge.utils.timedelta import nice_repr
@register_contest_format("atcoder")
@register_contest_format('atcoder')
class AtCoderContestFormat(DefaultContestFormat):
name = gettext_lazy("AtCoder")
config_defaults = {"penalty": 5}
config_validators = {"penalty": lambda x: x >= 0}
"""
name = gettext_lazy('AtCoder')
config_defaults = {'penalty': 5}
config_validators = {'penalty': lambda x: x >= 0}
'''
penalty: Number of penalty minutes each incorrect submission adds. Defaults to 5.
"""
'''
@classmethod
def validate(cls, config):
@ -29,9 +29,7 @@ class AtCoderContestFormat(DefaultContestFormat):
return
if not isinstance(config, dict):
raise ValidationError(
"AtCoder-styled contest expects no config or dict as config"
)
raise ValidationError('AtCoder-styled contest expects no config or dict as config')
for key, value in config.items():
if key not in cls.config_defaults:
@ -39,9 +37,7 @@ class AtCoderContestFormat(DefaultContestFormat):
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)
)
raise ValidationError('invalid value "%s" for config key "%s"' % (value, key))
def __init__(self, contest, config):
self.config = self.config_defaults.copy()
@ -54,13 +50,8 @@ class AtCoderContestFormat(DefaultContestFormat):
points = 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(
"""
cursor.execute('''
SELECT MAX(cs.points) as `score`, (
SELECT MIN(csub.date)
FROM judge_contestsubmission ccs LEFT OUTER JOIN
@ -70,29 +61,22 @@ class AtCoderContestFormat(DefaultContestFormat):
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)),
)
''', (participation.id, participation.id))
for score, time, prob in cursor.fetchall():
time = from_database_time(time)
dt = (time - participation.start).total_seconds()
# Compute penalty
if self.config["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)
)
subs = participation.submissions.exclude(submission__result__isnull=True) \
.exclude(submission__result__in=['IE', 'CE']) \
.filter(problem_id=prob)
if score:
prev = subs.filter(submission__date__lte=time).count() - 1
penalty += prev * self.config["penalty"] * 60
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()
@ -102,52 +86,28 @@ class AtCoderContestFormat(DefaultContestFormat):
if score:
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
self.handle_frozen_state(participation, format_data)
participation.cumtime = cumtime + penalty
participation.score = round(points, self.contest.points_precision)
participation.tiebreaker = 0
participation.score = points
participation.format_data = format_data
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))
if format_data:
penalty = (
format_html(
'<small style="color:red"> ({penalty})</small>',
penalty=floatformat(format_data["penalty"]),
)
if format_data.get("penalty")
else ""
)
penalty = format_html('<small style="color:red"> ({penalty})</small>',
penalty=floatformat(format_data['penalty'])) if format_data['penalty'] else ''
return format_html(
'<td class="{state} problem-score-col"><a data-featherlight="{url}" href="#">{points}{penalty}<div class="solving-time">{time}</div></a></td>',
state=(
(
"pretest-"
if self.contest.run_pretests_only
and contest_problem.is_pretested
else ""
)
+ 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"]),
'<td class="{state}"><a href="{url}">{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)),
url=reverse('contest_user_submissions',
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
points=floatformat(format_data['points']),
penalty=penalty,
time=nice_repr(timedelta(seconds=format_data["time"]), "noday"),
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'),
)
else:
return mark_safe('<td class="problem-score-col"></td>')
return mark_safe('<td></td>')

View file

@ -1,5 +1,6 @@
from abc import ABCMeta, abstractmethod, abstractproperty
from django.db.models import Max
from django.utils import six
class abstractclassmethod(classmethod):
@ -10,9 +11,7 @@ class abstractclassmethod(classmethod):
super(abstractclassmethod, self).__init__(callable)
class BaseContestFormat(metaclass=ABCMeta):
has_hidden_subtasks = False
class BaseContestFormat(six.with_metaclass(ABCMeta)):
@abstractmethod
def __init__(self, contest, config):
self.config = config
@ -50,7 +49,7 @@ class BaseContestFormat(metaclass=ABCMeta):
raise NotImplementedError()
@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
information from the format_data field instead of computing it from scratch.
@ -62,7 +61,7 @@ class BaseContestFormat(metaclass=ABCMeta):
raise NotImplementedError()
@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
information from the format_data field instead of computing it from scratch.
@ -83,41 +82,10 @@ class BaseContestFormat(metaclass=ABCMeta):
"""
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
def best_solution_state(cls, points, total):
if not points:
return "failed-score"
return 'failed-score'
if points == total:
return "full-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}
return 'full-score'
return 'partial-score'

View file

@ -13,16 +13,14 @@ from judge.contest_format.registry import register_contest_format
from judge.utils.timedelta import nice_repr
@register_contest_format("default")
@register_contest_format('default')
class DefaultContestFormat(BaseContestFormat):
name = gettext_lazy("Default")
name = gettext_lazy('Default')
@classmethod
def validate(cls, config):
if config is not None and (not isinstance(config, dict) or config):
raise ValidationError(
"default contest expects no config or empty dict as config"
)
raise ValidationError('default contest expects no config or empty dict as config')
def __init__(self, contest, config):
super(DefaultContestFormat, self).__init__(contest, config)
@ -32,84 +30,41 @@ class DefaultContestFormat(BaseContestFormat):
points = 0
format_data = {}
queryset = participation.submissions
if self.contest.freeze_after:
queryset = queryset.filter(
submission__date__lt=participation.start + self.contest.freeze_after
)
queryset = queryset.values("problem_id").annotate(
time=Max("submission__date"),
points=Max("points"),
)
for result in queryset:
dt = (result["time"] - participation.start).total_seconds()
if result["points"]:
for result in participation.submissions.values('problem_id').annotate(
time=Max('submission__date'), points=Max('points'),
):
dt = (result['time'] - participation.start).total_seconds()
if result['points']:
cumtime += dt
format_data[str(result["problem_id"])] = {
"time": dt,
"points": result["points"],
}
points += result["points"]
format_data[str(result['problem_id'])] = {'time': dt, 'points': result['points']}
points += result['points']
self.handle_frozen_state(participation, format_data)
participation.cumtime = max(cumtime, 0)
participation.score = round(points, self.contest.points_precision)
participation.tiebreaker = 0
participation.score = points
participation.format_data = format_data
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))
if format_data:
return format_html(
'<td class="{state} problem-score-col"><a data-featherlight="{url}" href="#">{points}<div class="solving-time">{time}</div></a></td>',
state=(
(
"pretest-"
if self.contest.run_pretests_only
and contest_problem.is_pretested
else ""
)
+ 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"),
u'<td class="{state}"><a href="{url}">{points}<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)),
url=reverse('contest_user_submissions',
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
points=floatformat(format_data['points']),
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'),
)
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(
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
points=floatformat(participation.score, -self.contest.points_precision),
cumtime=nice_repr(timedelta(seconds=participation.cumtime), "noday"),
u'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
points=floatformat(participation.score),
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday'),
)
def get_problem_breakdown(self, participation, contest_problems):
return [
(participation.format_data or {}).get(str(contest_problem.id))
for contest_problem in contest_problems
]
def get_contest_problem_label_script(self):
return """
function(n)
return tostring(math.floor(n + 1))
end
"""
return [(participation.format_data or {}).get(str(contest_problem.id)) for contest_problem in contest_problems]

View file

@ -10,25 +10,21 @@ 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.timezone import from_database_time
from judge.utils.timedelta import nice_repr
@register_contest_format("ecoo")
@register_contest_format('ecoo')
class ECOOContestFormat(DefaultContestFormat):
name = gettext_lazy("ECOO")
config_defaults = {"cumtime": False, "first_ac_bonus": 10, "time_bonus": 5}
config_validators = {
"cumtime": lambda x: True,
"first_ac_bonus": lambda x: x >= 0,
"time_bonus": lambda x: x >= 0,
}
"""
name = gettext_lazy('ECOO')
config_defaults = {'cumtime': False, 'first_ac_bonus': 10, 'time_bonus': 5}
config_validators = {'cumtime': lambda x: True, 'first_ac_bonus': lambda x: x >= 0, 'time_bonus': lambda x: x >= 0}
'''
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.
time_bonus: Number of minutes to award an extra point for submitting before the contest end.
Specify 0 to disable. Defaults to 5.
"""
'''
@classmethod
def validate(cls, config):
@ -36,9 +32,7 @@ class ECOOContestFormat(DefaultContestFormat):
return
if not isinstance(config, dict):
raise ValidationError(
"ECOO-styled contest expects no config or dict as config"
)
raise ValidationError('ECOO-styled contest expects no config or dict as config')
for key, value in config.items():
if key not in cls.config_defaults:
@ -46,9 +40,7 @@ class ECOOContestFormat(DefaultContestFormat):
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)
)
raise ValidationError('invalid value "%s" for config key "%s"' % (value, key))
def __init__(self, contest, config):
self.config = self.config_defaults.copy()
@ -60,13 +52,8 @@ class ECOOContestFormat(DefaultContestFormat):
points = 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(
"""
cursor.execute('''
SELECT (
SELECT MAX(ccs.points)
FROM judge_contestsubmission ccs LEFT OUTER JOIN
@ -81,92 +68,55 @@ class ECOOContestFormat(DefaultContestFormat):
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,
participation.id,
to_database_time(frozen_time),
),
)
''', (participation.id, participation.id, participation.id))
for score, time, prob, subs, max_score in cursor.fetchall():
time = from_database_time(time)
dt = (time - participation.start).total_seconds()
if self.config["cumtime"]:
if self.config['cumtime']:
cumtime += dt
bonus = 0
if score > 0:
# First AC bonus
if subs == 1 and score == max_score:
bonus += self.config["first_ac_bonus"]
bonus += self.config['first_ac_bonus']
# Time bonus
if self.config["time_bonus"]:
bonus += (
(participation.end_time - time).total_seconds()
// 60
// self.config["time_bonus"]
)
if self.config['time_bonus']:
bonus += (participation.end_time - time).total_seconds() // 60 // self.config['time_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
self.handle_frozen_state(participation, format_data)
participation.cumtime = cumtime
participation.score = round(points, self.contest.points_precision)
participation.tiebreaker = 0
participation.score = points
participation.format_data = format_data
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))
if format_data:
bonus = (
format_html(
"<small> +{bonus}</small>", bonus=floatformat(format_data["bonus"])
)
if format_data.get("bonus")
else ""
)
bonus = format_html('<small> +{bonus}</small>',
bonus=floatformat(format_data['bonus'])) if format_data['bonus'] else ''
return format_html(
'<td class="{state}"><a data-featherlight="{url}" href="#">{points}{bonus}<div class="solving-time">{time}</div></a></td>',
state=(
(
"pretest-"
if self.contest.run_pretests_only
and contest_problem.is_pretested
else ""
)
+ 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"]),
'<td class="{state}"><a href="{url}">{points}{bonus}<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)),
url=reverse('contest_user_submissions',
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
points=floatformat(format_data['points']),
bonus=bonus,
time=nice_repr(timedelta(seconds=format_data["time"]), "noday"),
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'),
)
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(
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
points=floatformat(participation.score),
cumtime=nice_repr(timedelta(seconds=participation.cumtime), "noday")
if self.config["cumtime"]
else "",
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') if self.config['cumtime'] else '',
)

View file

@ -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
"""

View file

@ -12,16 +12,15 @@ from judge.contest_format.default import DefaultContestFormat
from judge.contest_format.registry import register_contest_format
from judge.timezone import from_database_time
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):
name = gettext_lazy("IOI")
config_defaults = {"cumtime": False}
"""
name = gettext_lazy('IOI')
config_defaults = {'cumtime': False}
'''
cumtime: Specify True if time penalties are to be computed. Defaults to False.
"""
'''
@classmethod
def validate(cls, config):
@ -29,9 +28,7 @@ class IOIContestFormat(DefaultContestFormat):
return
if not isinstance(config, dict):
raise ValidationError(
"IOI-styled contest expects no config or dict as config"
)
raise ValidationError('IOI-styled contest expects no config or dict as config')
for key, value in config.items():
if key not in cls.config_defaults:
@ -46,97 +43,57 @@ class IOIContestFormat(DefaultContestFormat):
def update_participation(self, participation):
cumtime = 0
score = 0
points = 0
format_data = {}
queryset = participation.submissions
if self.contest.freeze_after:
queryset = queryset.filter(
submission__date__lt=participation.start + self.contest.freeze_after
)
with connection.cursor() as cursor:
cursor.execute('''
SELECT MAX(cs.points) as `score`, (
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 = (
queryset.values("problem_id")
.filter(
points=Subquery(
queryset.filter(problem_id=OuterRef("problem_id"))
.order_by("-points")
.values("points")[:1]
)
)
.annotate(time=Min("submission__date"))
.values_list("problem_id", "time", "points")
)
for score, time, prob in cursor.fetchall():
if self.config['cumtime']:
dt = (from_database_time(time) - participation.start).total_seconds()
if score:
cumtime += dt
else:
dt = 0
for problem_id, time, points in queryset:
if self.config["cumtime"]:
dt = (time - participation.start).total_seconds()
if points:
cumtime += dt
else:
dt = 0
format_data[str(prob)] = {'time': dt, 'points': score}
points += score
format_data[str(problem_id)] = {"points": points, "time": dt}
score += points
self.handle_frozen_state(participation, format_data)
participation.cumtime = max(cumtime, 0)
participation.score = round(score, self.contest.points_precision)
participation.tiebreaker = 0
participation.score = points
participation.format_data = format_data
participation.save()
def display_user_problem(self, participation, contest_problem, show_final=False):
if show_final:
format_data = (participation.format_data_final or {}).get(
str(contest_problem.id)
)
else:
format_data = (participation.format_data or {}).get(str(contest_problem.id))
def display_user_problem(self, participation, contest_problem):
format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data:
return format_html(
'<td class="{state} problem-score-col"><a data-featherlight="{url}" href="#">{points}<div class="solving-time">{time}</div></a></td>',
state=(
(
"pretest-"
if self.contest.run_pretests_only
and contest_problem.is_pretested
else ""
)
+ 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 "",
'<td class="{state}"><a href="{url}">{points}<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)),
url=reverse('contest_user_submissions',
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
points=floatformat(format_data['points']),
time=nice_repr(timedelta(seconds=format_data['time']), 'noday') if self.config['cumtime'] 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):
if show_final:
score = participation.score_final
cumtime = participation.cumtime_final
else:
score = participation.score
cumtime = participation.cumtime
def display_participation_result(self, participation):
return format_html(
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
points=floatformat(score, -self.contest.points_precision),
cumtime=nice_repr(timedelta(seconds=cumtime), "noday")
if self.config["cumtime"]
else "",
points=floatformat(participation.score),
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') if self.config['cumtime'] else '',
)

View file

@ -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()

View file

@ -1,3 +1,5 @@
from django.utils import six
formats = {}
@ -11,4 +13,4 @@ def register_contest_format(name):
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