Compare commits

..

1 commit

Author SHA1 Message Date
cuom1999
9bc44bd65c Tmp 2022-06-02 16:55:00 -05:00
2753 changed files with 24064 additions and 97991 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

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

@ -49,7 +49,7 @@
<br> <br>
<div class="popup"> <div class="popup">
<div> <div>
<img class="logo" src="logo.svg" alt="LQDOJ"> <img class="logo" src="logo.png" alt="LQDOJ">
</div> </div>
<h1 style="width: 100%;">Oops, LQDOJ is down now.</h1> <h1 style="width: 100%;">Oops, LQDOJ is down now.</h1>
</div> </div>

232
README.md
View file

@ -17,12 +17,11 @@ Supported languages:
- Assembly (x64) - Assembly (x64)
- AWK - AWK
- C - C
- C++03 / C++11 / C++14 / C++17 / C++20 - C++03 / C++11 / C++14 / C++17
- Java 11 - Java 11
- Pascal - Pascal
- Perl - Perl
- Python 2 / Python 3 - Python 2 / Python 3
- PyPy 2 / PyPy 3
Support plagiarism detection via [Stanford MOSS](https://theory.stanford.edu/~aiken/moss/). Support plagiarism detection via [Stanford MOSS](https://theory.stanford.edu/~aiken/moss/).
@ -31,196 +30,10 @@ Support plagiarism detection via [Stanford MOSS](https://theory.stanford.edu/~ai
Most of the setup are the same as DMOJ installations. You can view the installation guide of DMOJ here: https://docs.dmoj.ca/#/site/installation. Most of the setup are the same as DMOJ installations. You can view the installation guide of DMOJ here: https://docs.dmoj.ca/#/site/installation.
There is one minor change: Instead of `git clone https://github.com/DMOJ/site.git`, you clone this repo `git clone https://github.com/LQDJudge/online-judge.git`. There is one minor change: Instead of `git clone https://github.com/DMOJ/site.git`, you clone this repo `git clone https://github.com/LQDJudge/online-judge.git`.
### Additional Steps in Production:
- Bước 1: cài các thư viện cần thiết 1. To use newsletter (email sending), go to admin and create a newsletter.
- $ ở đây nghĩa là sudo. Ví dụ dòng đầu nghĩa là chạy lệnh `sudo apt update` 2. Change the domain name and website name in Admin page: Navigation Bars/Sites
```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: ### Some frequent difficulties when installation:
@ -228,21 +41,6 @@ $ dmoj -c judge.yml localhost
2. Missing the problem folder in `local_settings.py`. You need to create a folder to contain all problem packages and configure in `local_settings.py`. 2. Missing the problem folder in `local_settings.py`. You need to create a folder to contain all problem packages and configure in `local_settings.py`.
3. Missing static folder in `local_settings.py`. Similar to problem folder, make sure to configure `STATIC_FILES` inside `local_settings.py`. 3. Missing static folder in `local_settings.py`. Similar to problem folder, make sure to configure `STATIC_FILES` inside `local_settings.py`.
4. Missing configure file for judges. Each judge must have a seperate configure file. To create this file, you can run `python dmojauto-conf`. Checkout all sample files here https://github.com/DMOJ/docs/blob/master/sample_files. 4. Missing configure file for judges. Each judge must have a seperate configure file. To create this file, you can run `python dmojauto-conf`. Checkout all sample files here https://github.com/DMOJ/docs/blob/master/sample_files.
5. Missing timezone data for SQL. If you're using Ubuntu and you're following DMOJ's installation guide for the server, and you are getting the error mentioned in https://github.com/LQDJudge/online-judge/issues/45, then you can follow this method to fix:
```
mysql
-- You may have to do this if you haven't set root password for MySQL, replace mypass with your password
-- SET PASSWORD FOR 'root'@'localhost' = PASSWORD('mypass');
-- FLUSH PRIVILEGES;
exit
mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -D mysql -u root -p
mysql -u root -p -e "flush tables;" mysql
```
6. Missing the chat secret key, you must generate a Fernet key, and assign a variable in `local_settings.py` like this
```python
CHAT_SECRET_KEY = "81HqDtbqAywKSOumSxxxxxxxxxxxxxxxxx="
```
## Usage ## Usage
@ -255,12 +53,12 @@ source dmojsite/bin/activate
2. Run server: 2. Run server:
```bash ```bash
python3 manage.py runserver 0.0.0.0:8000 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) 3. Create a bridge (this is opened in a different terminal with the second step if you are using the same machine)
```bash ```bash
python3 manage.py runbridged python manage.py runbridged
``` ```
4. Create a judge (another terminal) 4. Create a judge (another terminal)
@ -281,22 +79,20 @@ celery -A dmoj_celery worker
node websocket/daemon.js node websocket/daemon.js
``` ```
7. To use subdomain for each organization, go to admin page -> navigation bar -> sites, add domain name (e.g, "localhost:8000"). Then go to add `USE_SUBDOMAIN = True` to local_settings.py.
## Deploy ## Deploy
Most of the steps are similar to Django tutorials. Here are two usual steps: Most of the steps are similar to Django tutorials. Here are two usual steps:
1. Update vietnamese translation: 1. Update vietnamese translation:
- If you add any new phrases in the code, ```python3 manage.py makemessages``` - If you add any new phrases in the code, ```python manage.py makemessages```
- go to `locale/vi` - go to `locale/vi`
- modify `.po` file - modify `.po` file
- ```python3 manage.py compilemessages``` - ```python manage.py compilemessages```
- ```python3 manage.py compilejsi18n``` - ```python manage.py compilejsi18n```
2. Update styles (using SASS) 2. Update styles (using SASS)
- Change .css/.scss files in `resources` folder - Change .css/.scss files in `resources` folder
- ```./make_style.sh && python3 manage.py collectstatic``` - ```./make_style && python manage.py collectstatic```
- Sometimes you need to press `Ctrl + F5` to see the new user interface in browser. - Sometimes you need to `Ctrl + F5` to see the new user interface in browser.
## Screenshots ## Screenshots
@ -304,8 +100,7 @@ Most of the steps are similar to Django tutorials. Here are two usual steps:
Leaderboard with information about contest rating, performance points and real name of all users. Leaderboard with information about contest rating, performance points and real name of all users.
![](https://raw.githubusercontent.com/emladevops/LQDOJ-image/main/brave_SK67WA26FA.png#gh-light-mode-only) ![](https://i.imgur.com/ampxHXM.png)
![](https://raw.githubusercontent.com/emladevops/LQDOJ-image/main/brave_cmqqCnwaFc.png#gh-dark-mode-only)
### Admin dashboard ### Admin dashboard
@ -323,5 +118,4 @@ You can write the problems' statement in Markdown with LaTeX figures and formula
Users can communicate with each other and can see who's online. Users can communicate with each other and can see who's online.
![](https://raw.githubusercontent.com/emladevops/LQDOJ-image/main/brave_kPsC5bJluc.png#gh-light-mode-only) ![](https://i.imgur.com/y9SGCgl.png)
![](https://raw.githubusercontent.com/emladevops/LQDOJ-image/main/brave_AtrEzXzEAx.png#gh-dark-mode-only)

View file

@ -3,6 +3,3 @@ from django.apps import AppConfig
class ChatBoxConfig(AppConfig): class ChatBoxConfig(AppConfig):
name = "chat_box" name = "chat_box"
def ready(self):
from . import models

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,14 +1,12 @@
from django.db import models from django.db import models
from django.db.models import CASCADE, Q from django.db.models import CASCADE
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.functional import cached_property
from judge.models.profile import Profile from judge.models.profile import Profile
from judge.caching import cache_wrapper
__all__ = ["Message", "Room", "UserRoom", "Ignore"] __all__ = ["Message"]
class Room(models.Model): class Room(models.Model):
@ -18,44 +16,20 @@ class Room(models.Model):
user_two = models.ForeignKey( user_two = models.ForeignKey(
Profile, related_name="user_two", verbose_name="user 2", on_delete=CASCADE Profile, related_name="user_two", verbose_name="user 2", on_delete=CASCADE
) )
last_msg_time = models.DateTimeField(
verbose_name=_("last seen"), null=True, db_index=True
)
class Meta:
app_label = "chat_box"
unique_together = ("user_one", "user_two")
@cached_property
def _cached_info(self):
return get_room_info(self.id)
def contain(self, profile): def contain(self, profile):
return profile.id in [self.user_one_id, self.user_two_id] return self.user_one == profile or self.user_two == profile
def other_user(self, profile): def other_user(self, profile):
return self.user_one if profile == self.user_two else self.user_two return self.user_one if profile == self.user_two else self.user_two
def other_user_id(self, profile):
user_ids = [self.user_one_id, self.user_two_id]
return sum(user_ids) - profile.id
def users(self): def users(self):
return [self.user_one, self.user_two] return [self.user_one, self.user_two]
def last_message_body(self):
return self._cached_info["last_message"]
@classmethod
def prefetch_room_cache(self, room_ids):
get_room_info.prefetch_multi([(i,) for i in room_ids])
class Message(models.Model): class Message(models.Model):
author = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE) author = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE)
time = models.DateTimeField( time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True)
verbose_name=_("posted time"), auto_now_add=True, db_index=True
)
body = models.TextField(verbose_name=_("body of comment"), max_length=8192) body = models.TextField(verbose_name=_("body of comment"), max_length=8192)
hidden = models.BooleanField(verbose_name="is hidden", default=False) hidden = models.BooleanField(verbose_name="is hidden", default=False)
room = models.ForeignKey( room = models.ForeignKey(
@ -63,17 +37,15 @@ class Message(models.Model):
) )
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
new_message = self.id
self.body = self.body.strip() self.body = self.body.strip()
super(Message, self).save(*args, **kwargs) super(Message, self).save(*args, **kwargs)
class Meta: class Meta:
app_label = "chat_box"
verbose_name = "message" verbose_name = "message"
verbose_name_plural = "messages" verbose_name_plural = "messages"
ordering = ("-id",) ordering = ("-time",)
indexes = [
models.Index(fields=["hidden", "room", "-id"]),
]
app_label = "chat_box"
class UserRoom(models.Model): class UserRoom(models.Model):
@ -82,52 +54,35 @@ class UserRoom(models.Model):
Room, verbose_name="room id", on_delete=CASCADE, default=None, null=True Room, verbose_name="room id", on_delete=CASCADE, default=None, null=True
) )
last_seen = models.DateTimeField(verbose_name=_("last seen"), auto_now_add=True) last_seen = models.DateTimeField(verbose_name=_("last seen"), auto_now_add=True)
unread_count = models.IntegerField(default=0, db_index=True)
class Meta:
unique_together = ("user", "room")
app_label = "chat_box"
class Ignore(models.Model): class Ignore(models.Model):
user = models.OneToOneField( user = models.ForeignKey(
Profile, Profile,
related_name="ignored_chat_users", related_name="ignored_chat_users",
verbose_name=_("user"), verbose_name=_("user"),
on_delete=CASCADE, on_delete=CASCADE,
db_index=True,
) )
ignored_users = models.ManyToManyField(Profile) ignored_users = models.ManyToManyField(Profile)
class Meta:
app_label = "chat_box"
@classmethod @classmethod
def is_ignored(self, current_user, new_friend): def is_ignored(self, current_user, new_friend):
try: try:
return current_user.ignored_chat_users.ignored_users.filter( return (
id=new_friend.id current_user.ignored_chat_users.get()
).exists() .ignored_users.filter(id=new_friend.id)
.exists()
)
except: except:
return False return False
@classmethod @classmethod
def get_ignored_users(self, user): def get_ignored_users(self, user):
try: try:
return self.objects.get(user=user).ignored_users.all() return self.objects.filter(user=user)[0].ignored_users.all()
except: except:
return Profile.objects.none() return Profile.objects.none()
@classmethod
def get_ignored_rooms(self, user):
try:
ignored_users = self.objects.get(user=user).ignored_users.all()
return Room.objects.filter(Q(user_one=user) | Q(user_two=user)).filter(
Q(user_one__in=ignored_users) | Q(user_two__in=ignored_users)
)
except:
return Room.objects.none()
@classmethod @classmethod
def add_ignore(self, current_user, friend): def add_ignore(self, current_user, friend):
ignore, created = self.objects.get_or_create(user=current_user) ignore, created = self.objects.get_or_create(user=current_user)
@ -144,11 +99,3 @@ class Ignore(models.Model):
self.remove_ignore(current_user, friend) self.remove_ignore(current_user, friend)
else: else:
self.add_ignore(current_user, friend) self.add_ignore(current_user, friend)
@cache_wrapper(prefix="Rinfo")
def get_room_info(room_id):
last_msg = Message.objects.filter(room_id=room_id).first()
return {
"last_message": last_msg.body if last_msg else None,
}

View file

@ -1,14 +1,6 @@
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
import hmac
import hashlib
from django.conf import settings from django.conf import settings
from django.db.models import OuterRef, Count, Subquery, IntegerField, Q
from django.db.models.functions import Coalesce
from chat_box.models import Ignore, Message, UserRoom, Room
from judge.caching import cache_wrapper
secret_key = settings.CHAT_SECRET_KEY secret_key = settings.CHAT_SECRET_KEY
fernet = Fernet(secret_key) fernet = Fernet(secret_key)
@ -26,26 +18,3 @@ def decrypt_url(message_encrypted):
return int(creator_id), int(other_id) return int(creator_id), int(other_id)
except Exception as e: except Exception as e:
return None, None return None, None
def encrypt_channel(channel):
return (
hmac.new(
settings.CHAT_SECRET_KEY.encode(),
channel.encode(),
hashlib.sha512,
).hexdigest()[:16]
+ "%s" % channel
)
@cache_wrapper(prefix="gub")
def get_unread_boxes(profile):
ignored_rooms = Ignore.get_ignored_rooms(profile)
unread_boxes = (
UserRoom.objects.filter(user=profile, unread_count__gt=0)
.exclude(room__in=ignored_rooms)
.count()
)
return unread_boxes

View file

@ -21,57 +21,43 @@ from django.db.models import (
Exists, Exists,
Count, Count,
IntegerField, IntegerField,
F,
Max,
) )
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.utils import timezone from django.utils import timezone
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.urls import reverse from django.urls import reverse
import datetime
from judge import event_poster as event from judge import event_poster as event
from judge.jinja2.gravatar import gravatar from judge.jinja2.gravatar import gravatar
from judge.models import Friend from judge.models import Friend
from chat_box.models import Message, Profile, Room, UserRoom, Ignore, get_room_info from chat_box.models import Message, Profile, Room, UserRoom, Ignore
from chat_box.utils import encrypt_url, decrypt_url, encrypt_channel, get_unread_boxes from chat_box.utils import encrypt_url, decrypt_url
from reversion import revisions import json
class ChatView(ListView): class ChatView(ListView):
context_object_name = "message" context_object_name = "message"
template_name = "chat/chat.html" template_name = "chat/chat.html"
title = _("LQDOJ Chat") title = _("Chat Box")
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.room_id = None self.room_id = None
self.room = None self.room = None
self.paginate_by = 50
self.messages = None self.messages = None
self.first_page_size = 20 # only for first request self.paginator = None
self.follow_up_page_size = 50
def get_queryset(self): def get_queryset(self):
return self.messages return self.messages
def has_next(self):
try:
msg = Message.objects.filter(room=self.room_id).earliest("id")
except Exception as e:
return False
return msg not in self.messages
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
request_room = kwargs["room_id"] request_room = kwargs["room_id"]
page_size = self.follow_up_page_size page = request.GET.get("page")
try:
last_id = int(request.GET.get("last_id"))
except Exception:
last_id = 1e15
page_size = self.first_page_size
only_messages = request.GET.get("only_messages")
if request_room: if request_room:
try: try:
@ -83,21 +69,23 @@ class ChatView(ListView):
else: else:
request_room = None request_room = None
if request_room != self.room_id or not self.messages:
self.room_id = request_room self.room_id = request_room
self.messages = ( self.messages = Message.objects.filter(hidden=False, room=self.room_id)
Message.objects.filter(hidden=False, room=self.room_id, id__lt=last_id) self.paginator = Paginator(self.messages, self.paginate_by)
.select_related("author")
.only("body", "time", "author__rating", "author__display_rank")[:page_size] if page == None:
) update_last_seen(request, **kwargs)
if not only_messages:
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
cur_page = self.paginator.get_page(page)
return render( return render(
request, request,
"chat/message_list.html", "chat/message_list.html",
{ {
"object_list": self.messages, "object_list": cur_page.object_list,
"has_next": self.has_next(), "num_pages": self.paginator.num_pages,
}, },
) )
@ -106,14 +94,9 @@ class ChatView(ListView):
context["title"] = self.title context["title"] = self.title
context["last_msg"] = event.last() context["last_msg"] = event.last()
context["status_sections"] = get_status_context(self.request.profile) context["status_sections"] = get_status_context(self.request)
context["room"] = self.room_id context["room"] = self.room_id
context["has_next"] = self.has_next()
context["unread_count_lobby"] = get_unread_count(None, self.request.profile) context["unread_count_lobby"] = get_unread_count(None, self.request.profile)
context["chat_channel"] = encrypt_channel(
"chat_" + str(self.request.profile.id)
)
context["chat_lobby_channel"] = encrypt_channel("chat_lobby")
if self.room: if self.room:
users_room = [self.room.user_one, self.room.user_two] users_room = [self.room.user_one, self.room.user_two]
users_room.remove(self.request.profile) users_room.remove(self.request.profile)
@ -137,89 +120,36 @@ def delete_message(request):
ret = {"delete": "done"} ret = {"delete": "done"}
if request.method == "GET": if request.method == "GET":
return HttpResponseBadRequest() return JsonResponse(ret)
if request.user.is_staff:
try: try:
messid = int(request.POST.get("message")) messid = int(request.POST.get("message"))
mess = Message.objects.get(id=messid) mess = Message.objects.get(id=messid)
except: except:
return HttpResponseBadRequest() return HttpResponseBadRequest()
if not request.user.is_staff and request.profile != mess.author:
return HttpResponseBadRequest()
mess.hidden = True mess.hidden = True
mess.save() mess.save()
return JsonResponse(ret) return JsonResponse(ret)
def mute_message(request):
ret = {"mute": "done"}
if request.method == "GET":
return HttpResponseBadRequest()
if not request.user.is_staff:
return HttpResponseBadRequest()
try:
messid = int(request.POST.get("message"))
mess = Message.objects.get(id=messid)
except:
return HttpResponseBadRequest()
with revisions.create_revision():
revisions.set_comment(_("Mute chat") + ": " + mess.body)
revisions.set_user(request.user)
mess.author.mute = True
mess.author.save()
Message.objects.filter(room=None, author=mess.author).update(hidden=True)
return JsonResponse(ret) return JsonResponse(ret)
def check_valid_message(request, room):
if not room and len(request.POST["body"]) > 200:
return False
if not can_access_room(request, room) or request.profile.mute:
return False
last_msg = Message.objects.filter(room=room).first()
if (
last_msg
and last_msg.author == request.profile
and last_msg.body == request.POST["body"].strip()
):
return False
if not room:
four_last_msg = Message.objects.filter(room=room).order_by("-id")[:4]
if len(four_last_msg) >= 4:
same_author = all(msg.author == request.profile for msg in four_last_msg)
time_diff = timezone.now() - four_last_msg[3].time
if same_author and time_diff.total_seconds() < 300:
return False
return True
@login_required @login_required
def post_message(request): def post_message(request):
ret = {"msg": "posted"} ret = {"msg": "posted"}
if request.method != "POST": if request.method != "POST":
return HttpResponseBadRequest() return HttpResponseBadRequest()
if len(request.POST["body"]) > 5000 or len(request.POST["body"].strip()) == 0: if len(request.POST["body"]) > 5000:
return HttpResponseBadRequest() return HttpResponseBadRequest()
room = None room = None
if request.POST["room"]: if request.POST["room"]:
room = Room.objects.get(id=request.POST["room"]) room = Room.objects.get(id=request.POST["room"])
if not check_valid_message(request, room): if not can_access_room(request, room) or request.profile.mute:
return HttpResponseBadRequest() return HttpResponseBadRequest()
new_message = Message(author=request.profile, body=request.POST["body"], room=room) new_message = Message(author=request.profile, body=request.POST["body"], room=room)
@ -227,7 +157,7 @@ def post_message(request):
if not room: if not room:
event.post( event.post(
encrypt_channel("chat_lobby"), "chat_lobby",
{ {
"type": "lobby", "type": "lobby",
"author_id": request.profile.id, "author_id": request.profile.id,
@ -237,13 +167,9 @@ def post_message(request):
}, },
) )
else: else:
get_room_info.dirty(room.id)
room.last_msg_time = new_message.time
room.save()
for user in room.users(): for user in room.users():
event.post( event.post(
encrypt_channel("chat_" + str(user.id)), "chat_" + str(user.id),
{ {
"type": "private", "type": "private",
"author_id": request.profile.id, "author_id": request.profile.id,
@ -252,17 +178,14 @@ def post_message(request):
"tmp_id": request.POST.get("tmp_id"), "tmp_id": request.POST.get("tmp_id"),
}, },
) )
if user != request.profile:
UserRoom.objects.filter(user=user, room=room).update(
unread_count=F("unread_count") + 1
)
get_unread_boxes.dirty(user)
return JsonResponse(ret) return JsonResponse(ret)
def can_access_room(request, room): def can_access_room(request, room):
return not room or room.contain(request.profile) return (
not room or room.user_one == request.profile or room.user_two == request.profile
)
@login_required @login_required
@ -278,7 +201,7 @@ def chat_message_ajax(request):
try: try:
message = Message.objects.filter(hidden=False).get(id=message_id) message = Message.objects.filter(hidden=False).get(id=message_id)
room = message.room room = message.room
if not can_access_room(request, room): if room and not room.contain(request.profile):
return HttpResponse("Unauthorized", status=401) return HttpResponse("Unauthorized", status=401)
except Message.DoesNotExist: except Message.DoesNotExist:
return HttpResponseBadRequest() return HttpResponseBadRequest()
@ -301,35 +224,35 @@ def update_last_seen(request, **kwargs):
room_id = request.POST.get("room") room_id = request.POST.get("room")
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
try: try:
profile = request.profile profile = request.profile
room = None room = None
if room_id: if room_id:
room = Room.objects.filter(id=int(room_id)).first() room = Room.objects.get(id=int(room_id))
except Room.DoesNotExist: except Room.DoesNotExist:
return HttpResponseBadRequest() return HttpResponseBadRequest()
except Exception as e:
return HttpResponseBadRequest()
if not can_access_room(request, room): if room and not room.contain(profile):
return HttpResponseBadRequest() return HttpResponseBadRequest()
user_room, _ = UserRoom.objects.get_or_create(user=profile, room=room) user_room, _ = UserRoom.objects.get_or_create(user=profile, room=room)
user_room.last_seen = timezone.now() user_room.last_seen = timezone.now()
user_room.unread_count = 0
user_room.save() user_room.save()
get_unread_boxes.dirty(profile)
return JsonResponse({"msg": "updated"}) return JsonResponse({"msg": "updated"})
def get_online_count(): def get_online_count():
last_5_minutes = timezone.now() - timezone.timedelta(minutes=5) last_two_minutes = timezone.now() - timezone.timedelta(minutes=2)
return Profile.objects.filter(last_access__gte=last_5_minutes).count() return Profile.objects.filter(last_access__gte=last_two_minutes).count()
def get_user_online_status(user): def get_user_online_status(user):
time_diff = timezone.now() - user.last_access time_diff = timezone.now() - user.last_access
is_online = time_diff <= timezone.timedelta(minutes=5) is_online = time_diff <= timezone.timedelta(minutes=2)
return is_online return is_online
@ -366,66 +289,47 @@ def user_online_status_ajax(request):
) )
def get_online_status(profile, other_profile_ids, rooms=None): def get_online_status(request_user, queryset, rooms=None):
if not other_profile_ids: if not queryset:
return None return None
Profile.prefetch_profile_cache(other_profile_ids) last_two_minutes = timezone.now() - timezone.timedelta(minutes=2)
joined_ids = ",".join([str(id) for id in other_profile_ids])
other_profiles = Profile.objects.raw(
f"SELECT * from judge_profile where id in ({joined_ids}) order by field(id,{joined_ids})"
)
last_5_minutes = timezone.now() - timezone.timedelta(minutes=5)
ret = [] ret = []
if rooms:
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: if rooms:
user_dict.update( unread_count = get_unread_count(rooms, request_user)
{ count = {}
"unread_count": count.get(other_profile.id), for i in unread_count:
"last_msg": last_msg.get(other_profile.id), count[i["other_user"]] = i["unread_count"]
"room": room_of_user.get(other_profile.id),
} for user in queryset:
) is_online = False
user_dict["url"] = encrypt_url(profile.id, other_profile.id) if user.last_access >= last_two_minutes:
is_online = True
user_dict = {"user": user, "is_online": is_online}
if rooms and user.id in count:
user_dict["unread_count"] = count[user.id]
user_dict["url"] = encrypt_url(request_user.id, user.id)
ret.append(user_dict) ret.append(user_dict)
return ret return ret
def get_status_context(profile, include_ignored=False): def get_status_context(request, include_ignored=False):
if include_ignored: if include_ignored:
ignored_users = [] ignored_users = Profile.objects.none()
queryset = Profile.objects queryset = Profile.objects
else: else:
ignored_users = list( ignored_users = Ignore.get_ignored_users(request.profile)
Ignore.get_ignored_users(profile).values_list("id", flat=True)
)
queryset = Profile.objects.exclude(id__in=ignored_users) queryset = Profile.objects.exclude(id__in=ignored_users)
last_5_minutes = timezone.now() - timezone.timedelta(minutes=5) last_two_minutes = timezone.now() - timezone.timedelta(minutes=2)
recent_profile = ( recent_profile = (
Room.objects.filter(Q(user_one=profile) | Q(user_two=profile)) Room.objects.filter(Q(user_one=request.profile) | Q(user_two=request.profile))
.annotate( .annotate(
last_msg_time=Subquery(
Message.objects.filter(room=OuterRef("pk")).values("time")[:1]
),
other_user=Case( other_user=Case(
When(user_one=profile, then="user_two"), When(user_one=request.profile, then="user_two"),
default="user_one", default="user_one",
), ),
) )
@ -435,24 +339,50 @@ def get_status_context(profile, include_ignored=False):
.values("other_user", "id")[:20] .values("other_user", "id")[:20]
) )
recent_profile_ids = [str(i["other_user"]) for i in recent_profile] recent_profile_id = [str(i["other_user"]) for i in recent_profile]
joined_id = ",".join(recent_profile_id)
recent_rooms = [int(i["id"]) for i in recent_profile] recent_rooms = [int(i["id"]) for i in recent_profile]
Room.prefetch_room_cache(recent_rooms) recent_list = None
if joined_id:
recent_list = Profile.objects.raw(
f"SELECT * from judge_profile where id in ({joined_id}) order by field(id,{joined_id})"
)
friend_list = (
Friend.get_friend_profiles(request.profile)
.exclude(id__in=recent_profile_id)
.exclude(id__in=ignored_users)
.order_by("-last_access")
)
admin_list = ( admin_list = (
queryset.filter(display_rank="admin") queryset.filter(display_rank="admin")
.exclude(id__in=recent_profile_ids) .exclude(id__in=friend_list)
.values_list("id", flat=True) .exclude(id__in=recent_profile_id)
)
all_user_status = (
queryset.filter(display_rank="user", last_access__gte=last_two_minutes)
.annotate(is_online=Case(default=True, output_field=BooleanField()))
.order_by("-rating")
.exclude(id__in=friend_list)
.exclude(id__in=admin_list)
.exclude(id__in=recent_profile_id)[:30]
) )
return [ return [
{ {
"title": _("Recent"), "title": "Recent",
"user_list": get_online_status(profile, recent_profile_ids, recent_rooms), "user_list": get_online_status(request.profile, recent_list, recent_rooms),
}, },
{ {
"title": _("Admin"), "title": "Following",
"user_list": get_online_status(profile, admin_list), "user_list": get_online_status(request.profile, friend_list),
},
{
"title": "Admin",
"user_list": get_online_status(request.profile, admin_list),
},
{
"title": "Other",
"user_list": get_online_status(request.profile, all_user_status),
}, },
] ]
@ -463,7 +393,7 @@ def online_status_ajax(request):
request, request,
"chat/online_status.html", "chat/online_status.html",
{ {
"status_sections": get_status_context(request.profile), "status_sections": get_status_context(request),
"unread_count_lobby": get_unread_count(None, request.profile), "unread_count_lobby": get_unread_count(None, request.profile),
}, },
) )
@ -487,6 +417,7 @@ def get_or_create_room(request):
return HttpResponseBadRequest() return HttpResponseBadRequest()
request_id, other_id = decrypt_url(decrypted_other_id) request_id, other_id = decrypt_url(decrypted_other_id)
if not other_id or not request_id or request_id != request.profile.id: if not other_id or not request_id or request_id != request.profile.id:
return HttpResponseBadRequest() return HttpResponseBadRequest()
@ -507,36 +438,55 @@ def get_or_create_room(request):
user_room.last_seen = timezone.now() user_room.last_seen = timezone.now()
user_room.save() user_room.save()
room_url = reverse("chat", kwargs={"room_id": room.id})
if request.method == "GET": if request.method == "GET":
return JsonResponse( return JsonResponse({"room": room.id, "other_user_id": other_user.id})
{ return HttpResponseRedirect(reverse("chat", kwargs={"room_id": room.id}))
"room": room.id,
"other_user_id": other_user.id,
"url": room_url,
}
)
return HttpResponseRedirect(room_url)
def get_unread_count(rooms, user): def get_unread_count(rooms, user):
if rooms: if rooms:
return UserRoom.objects.filter( mess = (
user=user, room__in=rooms, unread_count__gt=0 Message.objects.filter(
).values("unread_count", "room") room=OuterRef("room"), time__gte=OuterRef("last_seen")
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(author=user)
.exclude(hidden=True) .order_by()
.count() .values("room")
.annotate(unread_count=Count("pk"))
.values("unread_count")
) )
return res return (
UserRoom.objects.filter(user=user, room__in=rooms)
.annotate(
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
other_user=Case(
When(room__user_one=user, then="room__user_two"),
default="room__user_one",
),
)
.filter(unread_count__gte=1)
.values("other_user", "unread_count")
)
else: # lobby
mess = (
Message.objects.filter(room__isnull=True, time__gte=OuterRef("last_seen"))
.exclude(author=user)
.order_by()
.values("room")
.annotate(unread_count=Count("pk"))
.values("unread_count")
)
res = (
UserRoom.objects.filter(user=user, room__isnull=True)
.annotate(
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
)
.values_list("unread_count", flat=True)
)
return res[0] if len(res) else 0
@login_required @login_required
@ -552,3 +502,32 @@ def toggle_ignore(request, **kwargs):
Ignore.toggle_ignore(request.profile, other_user) Ignore.toggle_ignore(request.profile, other_user)
next_url = request.GET.get("next", "/") next_url = request.GET.get("next", "/")
return HttpResponseRedirect(next_url) return HttpResponseRedirect(next_url)
@login_required
def get_unread_boxes(request):
if request.method != "GET":
return HttpResponseBadRequest()
ignored_users = Ignore.get_ignored_users(request.profile)
mess = (
Message.objects.filter(room=OuterRef("room"), time__gte=OuterRef("last_seen"))
.exclude(author=request.profile)
.exclude(author__in=ignored_users)
.order_by()
.values("room")
.annotate(unread_count=Count("pk"))
.values("unread_count")
)
unread_boxes = (
UserRoom.objects.filter(user=request.profile, room__isnull=False)
.annotate(
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
)
.filter(unread_count__gte=1)
.count()
)
return JsonResponse({"unread_boxes": unread_boxes})

View file

@ -11,12 +11,6 @@
bottom: 0; bottom: 0;
} }
.ace_editor {
overflow: hidden;
font: 12px/normal 'Fira Code', 'Monaco', 'Menlo', monospace;
direction: 1tr;
}
.django-ace-widget.loading { .django-ace-widget.loading {
display: none; display: none;
} }

View file

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

View file

@ -19,7 +19,6 @@ class AceWidget(forms.Textarea):
width="100%", width="100%",
height="300px", height="300px",
no_ace_media=False, no_ace_media=False,
toolbar=True,
*args, *args,
**kwargs **kwargs
): ):
@ -29,7 +28,6 @@ class AceWidget(forms.Textarea):
self.width = width self.width = width
self.height = height self.height = height
self.ace_media = not no_ace_media self.ace_media = not no_ace_media
self.toolbar = toolbar
super(AceWidget, self).__init__(*args, **kwargs) super(AceWidget, self).__init__(*args, **kwargs)
@property @property
@ -63,14 +61,10 @@ class AceWidget(forms.Textarea):
html = "<div%s><div></div></div>%s" % (flatatt(ace_attrs), textarea) html = "<div%s><div></div></div>%s" % (flatatt(ace_attrs), textarea)
if self.toolbar: # add toolbar
toolbar = ( html = (
'<div style="width: {}" class="django-ace-toolbar">' '<div class="django-ace-editor"><div style="width: 100%%" class="django-ace-toolbar">'
'<a href="#" class="django-ace-max_min"></a>' '<a href="./" class="django-ace-max_min"></a></div>%s</div>'
"</div>" ) % html
).format(self.width)
html = toolbar + html
html = '<div class="django-ace-editor">{}</div>'.format(html)
return mark_safe(html) return mark_safe(html)

14
dmoj/decorators.py Normal file
View file

@ -0,0 +1,14 @@
import time
def timeit(method):
def timed(*args, **kw):
ts = time.time()
result = method(*args, **kw)
te = time.time()
if "log_time" in kw:
name = kw.get("log_name", method.__name__.upper())
kw["log_time"][name] = int((te - ts) * 1000)
return result
return timed

View file

@ -33,7 +33,6 @@ SITE_ID = 1
SITE_NAME = "LQDOJ" SITE_NAME = "LQDOJ"
SITE_LONG_NAME = "LQDOJ: Le Quy Don Online Judge" SITE_LONG_NAME = "LQDOJ: Le Quy Don Online Judge"
SITE_ADMIN_EMAIL = False SITE_ADMIN_EMAIL = False
SITE_DOMAIN = "lqdoj.edu.vn"
DMOJ_REQUIRE_STAFF_2FA = True DMOJ_REQUIRE_STAFF_2FA = True
@ -50,6 +49,10 @@ DMOJ_PP_BONUS_FUNCTION = lambda n: 300 * (1 - 0.997**n) # noqa: E731
NODEJS = "/usr/bin/node" NODEJS = "/usr/bin/node"
EXIFTOOL = "/usr/bin/exiftool" EXIFTOOL = "/usr/bin/exiftool"
ACE_URL = "//cdnjs.cloudflare.com/ajax/libs/ace/1.1.3" ACE_URL = "//cdnjs.cloudflare.com/ajax/libs/ace/1.1.3"
SELECT2_JS_URL = "//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js"
DEFAULT_SELECT2_CSS = (
"//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css"
)
DMOJ_CAMO_URL = None DMOJ_CAMO_URL = None
DMOJ_CAMO_KEY = None DMOJ_CAMO_KEY = None
@ -61,7 +64,6 @@ DMOJ_PROBLEM_MAX_TIME_LIMIT = 60 # seconds
DMOJ_PROBLEM_MIN_MEMORY_LIMIT = 0 # kilobytes DMOJ_PROBLEM_MIN_MEMORY_LIMIT = 0 # kilobytes
DMOJ_PROBLEM_MAX_MEMORY_LIMIT = 1048576 # kilobytes DMOJ_PROBLEM_MAX_MEMORY_LIMIT = 1048576 # kilobytes
DMOJ_PROBLEM_MIN_PROBLEM_POINTS = 0 DMOJ_PROBLEM_MIN_PROBLEM_POINTS = 0
DMOJ_SUBMISSION_ROOT = "/tmp"
DMOJ_RATING_COLORS = True DMOJ_RATING_COLORS = True
DMOJ_EMAIL_THROTTLING = (10, 60) DMOJ_EMAIL_THROTTLING = (10, 60)
DMOJ_STATS_LANGUAGE_THRESHOLD = 10 DMOJ_STATS_LANGUAGE_THRESHOLD = 10
@ -73,7 +75,6 @@ DMOJ_BLOG_NEW_CONTEST_COUNT = 7
DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT = 7 DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT = 7
DMOJ_TOTP_TOLERANCE_HALF_MINUTES = 1 DMOJ_TOTP_TOLERANCE_HALF_MINUTES = 1
DMOJ_USER_MAX_ORGANIZATION_COUNT = 10 DMOJ_USER_MAX_ORGANIZATION_COUNT = 10
DMOJ_USER_MAX_ORGANIZATION_ADD = 5
DMOJ_COMMENT_VOTE_HIDE_THRESHOLD = -5 DMOJ_COMMENT_VOTE_HIDE_THRESHOLD = -5
DMOJ_PDF_PROBLEM_CACHE = "" DMOJ_PDF_PROBLEM_CACHE = ""
DMOJ_PDF_PROBLEM_TEMP_DIR = tempfile.gettempdir() DMOJ_PDF_PROBLEM_TEMP_DIR = tempfile.gettempdir()
@ -84,9 +85,6 @@ DMOJ_STATS_SUBMISSION_RESULT_COLORS = {
"CE": "#42586d", "CE": "#42586d",
"ERR": "#ffa71c", "ERR": "#ffa71c",
} }
DMOJ_PROFILE_IMAGE_ROOT = "profile_images"
DMOJ_ORGANIZATION_IMAGE_ROOT = "organization_images"
DMOJ_TEST_FORMATTER_ROOT = "test_formatter"
MARKDOWN_STYLES = {} MARKDOWN_STYLES = {}
MARKDOWN_DEFAULT_STYLE = {} MARKDOWN_DEFAULT_STYLE = {}
@ -103,6 +101,7 @@ MATHOID_CACHE_URL = False
TEXOID_GZIP = False TEXOID_GZIP = False
TEXOID_META_CACHE = "default" TEXOID_META_CACHE = "default"
TEXOID_META_CACHE_TTL = 86400 TEXOID_META_CACHE_TTL = 86400
DMOJ_NEWSLETTER_ID_ON_REGISTER = 1
BAD_MAIL_PROVIDERS = () BAD_MAIL_PROVIDERS = ()
BAD_MAIL_PROVIDER_REGEX = () BAD_MAIL_PROVIDER_REGEX = ()
@ -132,10 +131,13 @@ USE_SELENIUM = False
SELENIUM_CUSTOM_CHROME_PATH = None SELENIUM_CUSTOM_CHROME_PATH = None
SELENIUM_CHROMEDRIVER_PATH = "chromedriver" SELENIUM_CHROMEDRIVER_PATH = "chromedriver"
PYGMENT_THEME = "pygment-github.css"
INLINE_JQUERY = True INLINE_JQUERY = True
INLINE_FONTAWESOME = True INLINE_FONTAWESOME = True
JQUERY_JS = "//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js" JQUERY_JS = "//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"
FONTAWESOME_CSS = "//cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" FONTAWESOME_CSS = (
"//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
)
DMOJ_CANONICAL = "" DMOJ_CANONICAL = ""
# Application definition # Application definition
@ -169,7 +171,7 @@ else:
}, },
{ {
"model": "judge.Submission", "model": "judge.Submission",
"icon": "fa-check-square", "icon": "fa-check-square-o",
"children": [ "children": [
"judge.Language", "judge.Language",
"judge.Judge", "judge.Judge",
@ -210,8 +212,10 @@ else:
], ],
}, },
("judge.BlogPost", "fa-rss-square"), ("judge.BlogPost", "fa-rss-square"),
("judge.Comment", "fa-comment-o"),
("judge.Ticket", "fa-exclamation-circle"), ("judge.Ticket", "fa-exclamation-circle"),
("admin.LogEntry", "fa-empire"), ("flatpages.FlatPage", "fa-file-text-o"),
("judge.Solution", "fa-pencil"),
], ],
"dashboard": { "dashboard": {
"breadcrumbs": True, "breadcrumbs": True,
@ -245,15 +249,15 @@ INSTALLED_APPS += (
"impersonate", "impersonate",
"django_jinja", "django_jinja",
"chat_box", "chat_box",
"newsletter",
"django.forms", "django.forms",
) )
MIDDLEWARE = ( MIDDLEWARE = (
"judge.middleware.SlowRequestMiddleware",
"judge.middleware.ShortCircuitMiddleware", "judge.middleware.ShortCircuitMiddleware",
"django.middleware.common.CommonMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware", "django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"judge.middleware.DMOJLoginMiddleware", "judge.middleware.DMOJLoginMiddleware",
@ -264,28 +268,22 @@ MIDDLEWARE = (
"impersonate.middleware.ImpersonateMiddleware", "impersonate.middleware.ImpersonateMiddleware",
"judge.middleware.DMOJImpersonationMiddleware", "judge.middleware.DMOJImpersonationMiddleware",
"judge.middleware.ContestMiddleware", "judge.middleware.ContestMiddleware",
"judge.middleware.DarkModeMiddleware",
"judge.middleware.SubdomainMiddleware",
"django.contrib.flatpages.middleware.FlatpageFallbackMiddleware", "django.contrib.flatpages.middleware.FlatpageFallbackMiddleware",
"judge.social_auth.SocialAuthExceptionMiddleware", "judge.social_auth.SocialAuthExceptionMiddleware",
"django.contrib.redirects.middleware.RedirectFallbackMiddleware", "django.contrib.redirects.middleware.RedirectFallbackMiddleware",
) )
X_FRAME_OPTIONS = "SAMEORIGIN"
LANGUAGE_COOKIE_AGE = 8640000
FORM_RENDERER = "django.forms.renderers.TemplatesSetting" FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
IMPERSONATE = { IMPERSONATE_REQUIRE_SUPERUSER = True
"REQUIRE_SUPERUSER": True, IMPERSONATE_DISABLE_LOGGING = True
"DISABLE_LOGGING": True,
"ADMIN_DELETE_PERMISSION": True,
}
ACCOUNT_ACTIVATION_DAYS = 7 ACCOUNT_ACTIVATION_DAYS = 7
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{ {
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
}, },
@ -325,6 +323,7 @@ TEMPLATES = [
"judge.template_context.site", "judge.template_context.site",
"judge.template_context.site_name", "judge.template_context.site_name",
"judge.template_context.misc_config", "judge.template_context.misc_config",
"judge.template_context.math_setting",
"social_django.context_processors.backends", "social_django.context_processors.backends",
"social_django.context_processors.login_redirect", "social_django.context_processors.login_redirect",
], ],
@ -363,10 +362,60 @@ LOCALE_PATHS = [
] ]
LANGUAGES = [ LANGUAGES = [
("vi", _("Vietnamese")), ("de", _("German")),
("en", _("English")), ("en", _("English")),
("es", _("Spanish")),
("fr", _("French")),
("hr", _("Croatian")),
("hu", _("Hungarian")),
("ja", _("Japanese")),
("ko", _("Korean")),
("pt", _("Brazilian Portuguese")),
("ro", _("Romanian")),
("ru", _("Russian")),
("sr-latn", _("Serbian (Latin)")),
("tr", _("Turkish")),
("vi", _("Vietnamese")),
("zh-hans", _("Simplified Chinese")),
("zh-hant", _("Traditional Chinese")),
] ]
MARKDOWN_ADMIN_EDITABLE_STYLE = {
"safe_mode": False,
"use_camo": True,
"texoid": True,
"math": True,
}
MARKDOWN_DEFAULT_STYLE = {
"safe_mode": True,
"nofollow": True,
"use_camo": True,
"math": True,
}
MARKDOWN_USER_LARGE_STYLE = {
"safe_mode": True,
"nofollow": True,
"use_camo": True,
"math": True,
}
MARKDOWN_STYLES = {
"comment": MARKDOWN_DEFAULT_STYLE,
"self-description": MARKDOWN_USER_LARGE_STYLE,
"problem": MARKDOWN_ADMIN_EDITABLE_STYLE,
"contest": MARKDOWN_ADMIN_EDITABLE_STYLE,
"language": MARKDOWN_ADMIN_EDITABLE_STYLE,
"license": MARKDOWN_ADMIN_EDITABLE_STYLE,
"judge": MARKDOWN_ADMIN_EDITABLE_STYLE,
"blog": MARKDOWN_ADMIN_EDITABLE_STYLE,
"solution": MARKDOWN_ADMIN_EDITABLE_STYLE,
"contest_tag": MARKDOWN_ADMIN_EDITABLE_STYLE,
"organization-about": MARKDOWN_USER_LARGE_STYLE,
"ticket": MARKDOWN_USER_LARGE_STYLE,
}
# Database # Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases # https://docs.djangoproject.com/en/1.11/ref/settings/#databases
@ -384,7 +433,6 @@ BRIDGED_JUDGE_ADDRESS = [("localhost", 9999)]
BRIDGED_JUDGE_PROXIES = None BRIDGED_JUDGE_PROXIES = None
BRIDGED_DJANGO_ADDRESS = [("localhost", 9998)] BRIDGED_DJANGO_ADDRESS = [("localhost", 9998)]
BRIDGED_DJANGO_CONNECT = None BRIDGED_DJANGO_CONNECT = None
BRIDGED_AUTO_CREATE_JUDGE = False
# Event Server configuration # Event Server configuration
EVENT_DAEMON_USE = False EVENT_DAEMON_USE = False
@ -432,7 +480,7 @@ AUTHENTICATION_BACKENDS = (
"social_core.backends.google.GoogleOAuth2", "social_core.backends.google.GoogleOAuth2",
"social_core.backends.facebook.FacebookOAuth2", "social_core.backends.facebook.FacebookOAuth2",
"judge.social_auth.GitHubSecureEmailOAuth2", "judge.social_auth.GitHubSecureEmailOAuth2",
"judge.authentication.CustomModelBackend", "django.contrib.auth.backends.ModelBackend",
) )
SOCIAL_AUTH_PIPELINE = ( SOCIAL_AUTH_PIPELINE = (
@ -473,26 +521,19 @@ FILE_UPLOAD_PERMISSIONS = 0o644
MESSAGES_TO_LOAD = 15 MESSAGES_TO_LOAD = 15
ML_OUTPUT_PATH = None NEWSLETTER_CONFIRM_EMAIL = False
# Use subdomain for organizations # Amount of seconds to wait between each email. Here 100ms is used.
USE_SUBDOMAIN = False NEWSLETTER_EMAIL_DELAY = 0.1
# Chat # Amount of seconds to wait between each batch. Here one minute is used.
CHAT_SECRET_KEY = "QUdVFsxk6f5-Hd8g9BXv81xMqvIZFRqMl-KbRzztW-U=" NEWSLETTER_BATCH_DELAY = 60
# Nginx # Number of emails in one batch
META_REMOTE_ADDRESS_KEY = "REMOTE_ADDR" NEWSLETTER_BATCH_SIZE = 100
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Chunk upload
CHUNK_UPLOAD_DIR = "/tmp/chunk_upload_tmp"
# Rate limit
RL_VOTE = "200/h"
RL_COMMENT = "30/h"
# Google form to request name
REGISTER_NAME_URL = None
try: try:
with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f: with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f:

View file

@ -12,9 +12,16 @@ from django.utils.functional import lazystr
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import RedirectView from django.views.generic import RedirectView
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.conf.urls.static import static as url_static
from judge.feed import (
AtomBlogFeed,
AtomCommentFeed,
AtomProblemFeed,
BlogFeed,
CommentFeed,
ProblemFeed,
)
from judge.forms import CustomAuthenticationForm from judge.forms import CustomAuthenticationForm
from judge.sitemap import ( from judge.sitemap import (
BlogPostSitemap, BlogPostSitemap,
@ -36,8 +43,6 @@ from judge.views import (
language, language,
license, license,
mailgun, mailgun,
markdown_editor,
test_formatter,
notification, notification,
organization, organization,
preview, preview,
@ -53,19 +58,9 @@ from judge.views import (
totp, totp,
user, user,
volunteer, volunteer,
pagevote,
bookmark,
widgets, widgets,
internal, internal,
resolver,
course,
email,
custom_file_upload,
) )
from judge import authentication
from judge.views.test_formatter import test_formatter
from judge.views.problem_data import ( from judge.views.problem_data import (
ProblemDataView, ProblemDataView,
ProblemSubmissionDiff, ProblemSubmissionDiff,
@ -77,6 +72,7 @@ from judge.views.register import ActivationView, RegistrationView
from judge.views.select2 import ( from judge.views.select2 import (
AssigneeSelect2View, AssigneeSelect2View,
ChatUserSearchSelect2View, ChatUserSearchSelect2View,
CommentSelect2View,
ContestSelect2View, ContestSelect2View,
ContestUserSearchSelect2View, ContestUserSearchSelect2View,
OrganizationSelect2View, OrganizationSelect2View,
@ -84,7 +80,6 @@ from judge.views.select2 import (
TicketUserSelect2View, TicketUserSelect2View,
UserSearchSelect2View, UserSearchSelect2View,
UserSelect2View, UserSelect2View,
ProblemAuthorSearchSelect2View,
) )
admin.autodiscover() admin.autodiscover()
@ -104,19 +99,19 @@ register_patterns = [
# confusing 404. # confusing 404.
url( url(
r"^activate/(?P<activation_key>\w+)/$", r"^activate/(?P<activation_key>\w+)/$",
ActivationView.as_view(title=_("Activation key invalid")), ActivationView.as_view(title="Activation key invalid"),
name="registration_activate", name="registration_activate",
), ),
url( url(
r"^register/$", r"^register/$",
RegistrationView.as_view(title=_("Register")), RegistrationView.as_view(title="Register"),
name="registration_register", name="registration_register",
), ),
url( url(
r"^register/complete/$", r"^register/complete/$",
TitledTemplateView.as_view( TitledTemplateView.as_view(
template_name="registration/registration_complete.html", template_name="registration/registration_complete.html",
title=_("Registration Completed"), title="Registration Completed",
), ),
name="registration_complete", name="registration_complete",
), ),
@ -124,7 +119,7 @@ register_patterns = [
r"^register/closed/$", r"^register/closed/$",
TitledTemplateView.as_view( TitledTemplateView.as_view(
template_name="registration/registration_closed.html", template_name="registration/registration_closed.html",
title=_("Registration not allowed"), title="Registration not allowed",
), ),
name="registration_disallowed", name="registration_disallowed",
), ),
@ -141,7 +136,9 @@ register_patterns = [
url(r"^logout/$", user.UserLogoutView.as_view(), name="auth_logout"), url(r"^logout/$", user.UserLogoutView.as_view(), name="auth_logout"),
url( url(
r"^password/change/$", r"^password/change/$",
authentication.CustomPasswordChangeView.as_view(), auth_views.PasswordChangeView.as_view(
template_name="registration/password_change_form.html",
),
name="password_change", name="password_change",
), ),
url( url(
@ -181,17 +178,6 @@ register_patterns = [
), ),
name="password_reset_done", name="password_reset_done",
), ),
url(r"^email/change/$", email.email_change_view, name="email_change"),
url(
r"^email/change/verify/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$",
email.verify_email_view,
name="email_change_verify",
),
url(
r"^email/change/pending$",
email.email_change_pending_view,
name="email_change_pending",
),
url(r"^social/error/$", register.social_auth_error, name="social_auth_error"), url(r"^social/error/$", register.social_auth_error, name="social_auth_error"),
url(r"^2fa/$", totp.TOTPLoginView.as_view(), name="login_2fa"), url(r"^2fa/$", totp.TOTPLoginView.as_view(), name="login_2fa"),
url(r"^2fa/enable/$", totp.TOTPEnableView.as_view(), name="enable_2fa"), url(r"^2fa/enable/$", totp.TOTPEnableView.as_view(), name="enable_2fa"),
@ -215,7 +201,6 @@ def paged_list_view(view, name, **kwargs):
urlpatterns = [ urlpatterns = [
url("", include("pagedown.urls")),
url( url(
r"^$", r"^$",
blog.PostList.as_view(template_name="home.html", title=_("Home")), blog.PostList.as_view(template_name="home.html", title=_("Home")),
@ -223,7 +208,6 @@ urlpatterns = [
name="home", name="home",
), ),
url(r"^500/$", exception), url(r"^500/$", exception),
url(r"^toggle_darkmode/$", user.toggle_darkmode, name="toggle_darkmode"),
url(r"^admin/", admin.site.urls), url(r"^admin/", admin.site.urls),
url(r"^i18n/", include("django.conf.urls.i18n")), url(r"^i18n/", include("django.conf.urls.i18n")),
url(r"^accounts/", include(register_patterns)), url(r"^accounts/", include(register_patterns)),
@ -240,19 +224,18 @@ urlpatterns = [
url(r"^problems/", paged_list_view(problem.ProblemList, "problem_list")), url(r"^problems/", paged_list_view(problem.ProblemList, "problem_list")),
url(r"^problems/random/$", problem.RandomProblem.as_view(), name="problem_random"), url(r"^problems/random/$", problem.RandomProblem.as_view(), name="problem_random"),
url( url(
r"^problems/feed/$", r"^problems/feed/",
problem.ProblemFeed.as_view(feed_type="for_you"), paged_list_view(problem.ProblemFeed, "problem_feed", feed_type="for_you"),
name="problem_feed",
), ),
url( url(
r"^problems/feed/new/$", r"^problems/feed/new/",
problem.ProblemFeed.as_view(feed_type="new"), paged_list_view(problem.ProblemFeed, "problem_feed_new", feed_type="new"),
name="problem_feed_new",
), ),
url( url(
r"^problems/feed/volunteer/$", r"^problems/feed/volunteer/",
problem.ProblemFeed.as_view(feed_type="volunteer"), paged_list_view(
name="problem_feed_volunteer", problem.ProblemFeed, "problem_feed_volunteer", feed_type="volunteer"
),
), ),
url( url(
r"^problem/(?P<problem>[^/]+)", r"^problem/(?P<problem>[^/]+)",
@ -271,11 +254,6 @@ urlpatterns = [
problem.ProblemPdfView.as_view(), problem.ProblemPdfView.as_view(),
name="problem_pdf", name="problem_pdf",
), ),
url(
r"^/pdf_description$",
problem.ProblemPdfDescriptionView.as_view(),
name="problem_pdf_description",
),
url(r"^/clone", problem.ProblemClone.as_view(), name="problem_clone"), url(r"^/clone", problem.ProblemClone.as_view(), name="problem_clone"),
url(r"^/submit$", problem.problem_submit, name="problem_submit"), url(r"^/submit$", problem.problem_submit, name="problem_submit"),
url( url(
@ -380,8 +358,9 @@ urlpatterns = [
paged_list_view(submission.AllUserSubmissions, "all_user_submissions"), paged_list_view(submission.AllUserSubmissions, "all_user_submissions"),
), ),
url( url(
r"^submissions/friends/", r"^src/(?P<submission>\d+)$",
paged_list_view(submission.AllFriendSubmissions, "all_friend_submissions"), submission.SubmissionSource.as_view(),
name="submission_source",
), ),
url( url(
r"^src/(?P<submission>\d+)/raw$", r"^src/(?P<submission>\d+)/raw$",
@ -398,41 +377,10 @@ urlpatterns = [
name="submission_status", name="submission_status",
), ),
url(r"^/abort$", submission.abort_submission, name="submission_abort"), url(r"^/abort$", submission.abort_submission, name="submission_abort"),
url(r"^/html$", submission.single_submission),
] ]
), ),
), ),
url(
r"^test_formatter/",
include(
[
url(
r"^$",
login_required(test_formatter.TestFormatter.as_view()),
name="test_formatter",
),
url(
r"^edit_page$",
login_required(test_formatter.EditTestFormatter.as_view()),
name="test_formatter_edit",
),
url(
r"^download_page$",
login_required(test_formatter.DownloadTestFormatter.as_view()),
name="test_formatter_download",
),
]
),
),
url(
r"^markdown_editor/",
markdown_editor.MarkdownEditor.as_view(),
name="markdown_editor",
),
url(
r"^submission_source_file/(?P<filename>(\w|\.)+)",
submission.SubmissionSourceFileView.as_view(),
name="submission_source_file",
),
url( url(
r"^users/", r"^users/",
include( include(
@ -452,7 +400,6 @@ urlpatterns = [
), ),
url(r"^user$", user.UserAboutPage.as_view(), name="user_page"), url(r"^user$", user.UserAboutPage.as_view(), name="user_page"),
url(r"^edit/profile/$", user.edit_profile, name="user_edit_profile"), url(r"^edit/profile/$", user.edit_profile, name="user_edit_profile"),
url(r"^user/bookmarks", user.UserBookMarkPage.as_view(), name="user_bookmark"),
url( url(
r"^user/(?P<user>\w+)", r"^user/(?P<user>\w+)",
include( include(
@ -487,7 +434,6 @@ urlpatterns = [
reverse("all_user_submissions", args=[user]) reverse("all_user_submissions", args=[user])
), ),
), ),
url(r"^/toggle_follow/", user.toggle_follow, name="user_toggle_follow"),
url( url(
r"^/$", r"^/$",
lambda _, user: HttpResponsePermanentRedirect( lambda _, user: HttpResponsePermanentRedirect(
@ -497,15 +443,9 @@ urlpatterns = [
] ]
), ),
), ),
url(r"^pagevotes/upvote/$", pagevote.upvote_page, name="pagevote_upvote"),
url(r"^pagevotes/downvote/$", pagevote.downvote_page, name="pagevote_downvote"),
url(r"^bookmarks/dobookmark/$", bookmark.dobookmark_page, name="dobookmark"),
url(r"^bookmarks/undobookmark/$", bookmark.undobookmark_page, name="undobookmark"),
url(r"^comments/upvote/$", comment.upvote_comment, name="comment_upvote"), url(r"^comments/upvote/$", comment.upvote_comment, name="comment_upvote"),
url(r"^comments/downvote/$", comment.downvote_comment, name="comment_downvote"), url(r"^comments/downvote/$", comment.downvote_comment, name="comment_downvote"),
url(r"^comments/hide/$", comment.comment_hide, name="comment_hide"), url(r"^comments/hide/$", comment.comment_hide, name="comment_hide"),
url(r"^comments/get_replies/$", comment.get_replies, name="comment_get_replies"),
url(r"^comments/show_more/$", comment.get_show_more, name="comment_show_more"),
url( url(
r"^comments/(?P<id>\d+)/", r"^comments/(?P<id>\d+)/",
include( include(
@ -535,58 +475,6 @@ urlpatterns = [
), ),
), ),
url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")), url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")),
url(
r"^contests/summary/(?P<key>\w+)/",
paged_list_view(contests.ContestsSummaryView, "contests_summary"),
),
url(
r"^contests/official",
paged_list_view(contests.OfficialContestList, "official_contest_list"),
),
url(r"^courses/", paged_list_view(course.CourseList, "course_list")),
url(
r"^course/(?P<slug>[\w-]*)",
include(
[
url(r"^$", course.CourseDetail.as_view(), name="course_detail"),
url(
r"^/lesson/(?P<id>\d+)$",
course.CourseLessonDetail.as_view(),
name="course_lesson_detail",
),
url(
r"^/edit_lessons$",
course.EditCourseLessonsView.as_view(),
name="edit_course_lessons",
),
url(
r"^/grades$",
course.CourseStudentResults.as_view(),
name="course_grades",
),
url(
r"^/grades/lesson/(?P<id>\d+)$",
course.CourseStudentResultsLesson.as_view(),
name="course_grades_lesson",
),
url(
r"^/add_contest$",
course.AddCourseContest.as_view(),
name="add_course_contest",
),
url(
r"^/edit_contest/(?P<contest>\w+)$",
course.EditCourseContest.as_view(),
name="edit_course_contest",
),
url(
r"^/contests$",
course.CourseContestList.as_view(),
name="course_contest_list",
),
]
),
),
url( url(
r"^contests/(?P<year>\d+)/(?P<month>\d+)/$", r"^contests/(?P<year>\d+)/(?P<month>\d+)/$",
contests.ContestCalendar.as_view(), contests.ContestCalendar.as_view(),
@ -625,13 +513,20 @@ urlpatterns = [
name="contest_ranking", name="contest_ranking",
), ),
url( url(
r"^/final_ranking/$", r"^/ranking/ajax$",
contests.ContestFinalRanking.as_view(), contests.contest_ranking_ajax,
name="contest_final_ranking", name="contest_ranking_ajax",
), ),
url(r"^/join$", contests.ContestJoin.as_view(), name="contest_join"), url(r"^/join$", contests.ContestJoin.as_view(), name="contest_join"),
url(r"^/leave$", contests.ContestLeave.as_view(), name="contest_leave"), url(r"^/leave$", contests.ContestLeave.as_view(), name="contest_leave"),
url(r"^/stats$", contests.ContestStats.as_view(), name="contest_stats"), url(r"^/stats$", contests.ContestStats.as_view(), name="contest_stats"),
url(
r"^/rank/(?P<problem>\w+)/",
paged_list_view(
ranked_submission.ContestRankedSubmission,
"contest_ranked_submissions",
),
),
url( url(
r"^/submissions/(?P<user>\w+)/(?P<problem>\w+)", r"^/submissions/(?P<user>\w+)/(?P<problem>\w+)",
paged_list_view( paged_list_view(
@ -639,19 +534,12 @@ urlpatterns = [
), ),
), ),
url( url(
r"^/submissions/(?P<participation>\d+)/(?P<problem>\w+)/ajax", r"^/submissions/(?P<user>\w+)/(?P<problem>\w+)/ajax",
paged_list_view( paged_list_view(
submission.UserContestSubmissionsAjax, submission.UserContestSubmissionsAjax,
"contest_user_submissions_ajax", "contest_user_submissions_ajax",
), ),
), ),
url(
r"^/submissions",
paged_list_view(
submission.ContestSubmissions,
"contest_submissions",
),
),
url( url(
r"^/participations$", r"^/participations$",
contests.ContestParticipationList.as_view(), contests.ContestParticipationList.as_view(),
@ -691,11 +579,6 @@ urlpatterns = [
organization.OrganizationList.as_view(), organization.OrganizationList.as_view(),
name="organization_list", name="organization_list",
), ),
url(
r"^organizations/add/$",
organization.AddOrganization.as_view(),
name="organization_add",
),
url( url(
r"^organization/(?P<pk>\d+)-(?P<slug>[\w-]*)", r"^organization/(?P<pk>\d+)-(?P<slug>[\w-]*)",
include( include(
@ -706,39 +589,19 @@ urlpatterns = [
name="organization_home", name="organization_home",
), ),
url( url(
r"^/users/", r"^/users$",
paged_list_view( organization.OrganizationUsers.as_view(),
organization.OrganizationUsers, name="organization_users",
"organization_users",
),
), ),
url( url(
r"^/problems/", r"^/problems$",
paged_list_view( organization.OrganizationProblems.as_view(),
organization.OrganizationProblems, "organization_problems" name="organization_problems",
),
), ),
url( url(
r"^/contests/", r"^/contests$",
paged_list_view( organization.OrganizationContests.as_view(),
organization.OrganizationContests, "organization_contests" name="organization_contests",
),
),
url(
r"^/contest/add",
organization.AddOrganizationContest.as_view(),
name="organization_contest_add",
),
url(
r"^/contest/edit/(?P<contest>\w+)",
organization.EditOrganizationContest.as_view(),
name="organization_contest_edit",
),
url(
r"^/submissions/",
paged_list_view(
organization.OrganizationSubmissions, "organization_submissions"
),
), ),
url( url(
r"^/join$", r"^/join$",
@ -847,7 +710,7 @@ urlpatterns = [
] ]
), ),
), ),
url(r"^blog/", blog.PostList.as_view(), name="blog_post_list"), url(r"^blog/", paged_list_view(blog.PostList, "blog_post_list")),
url(r"^post/(?P<id>\d+)-(?P<slug>.*)$", blog.PostView.as_view(), name="blog_post"), url(r"^post/(?P<id>\d+)-(?P<slug>.*)$", blog.PostView.as_view(), name="blog_post"),
url(r"^license/(?P<key>[-\w.]+)$", license.LicenseDetail.as_view(), name="license"), url(r"^license/(?P<key>[-\w.]+)$", license.LicenseDetail.as_view(), name="license"),
url( url(
@ -917,11 +780,6 @@ urlpatterns = [
AssigneeSelect2View.as_view(), AssigneeSelect2View.as_view(),
name="ticket_assignee_select2_ajax", name="ticket_assignee_select2_ajax",
), ),
url(
r"^problem_authors$",
ProblemAuthorSearchSelect2View.as_view(),
name="problem_authors_select2_ajax",
),
] ]
), ),
), ),
@ -980,6 +838,19 @@ urlpatterns = [
] ]
), ),
), ),
url(
r"^feed/",
include(
[
url(r"^problems/rss/$", ProblemFeed(), name="problem_rss"),
url(r"^problems/atom/$", AtomProblemFeed(), name="problem_atom"),
url(r"^comment/rss/$", CommentFeed(), name="comment_rss"),
url(r"^comment/atom/$", AtomCommentFeed(), name="comment_atom"),
url(r"^blog/rss/$", BlogFeed(), name="blog_rss"),
url(r"^blog/atom/$", AtomBlogFeed(), name="blog_atom"),
]
),
),
url( url(
r"^stats/", r"^stats/",
include( include(
@ -988,19 +859,27 @@ urlpatterns = [
"^language/", "^language/",
include( include(
[ [
url("^$", stats.language, name="language_stats"),
url( url(
"^$", "^data/all/$",
stats.StatLanguage.as_view(), stats.language_data,
name="language_stats", name="language_stats_data_all",
),
]
),
), ),
url( url(
"^site/", "^data/ac/$",
include( stats.ac_language_data,
[ name="language_stats_data_ac",
url("^$", stats.StatSite.as_view(), name="site_stats"), ),
url(
"^data/status/$",
stats.status_data,
name="stats_data_status",
),
url(
"^data/ac_rate/$",
stats.ac_rate,
name="language_stats_data_ac_rate",
),
] ]
), ),
), ),
@ -1080,6 +959,9 @@ urlpatterns = [
url( url(
r"^contest/$", ContestSelect2View.as_view(), name="contest_select2" r"^contest/$", ContestSelect2View.as_view(), name="contest_select2"
), ),
url(
r"^comment/$", CommentSelect2View.as_view(), name="comment_select2"
),
] ]
), ),
), ),
@ -1115,7 +997,6 @@ urlpatterns = [
name="chat", name="chat",
), ),
url(r"^delete/$", chat.delete_message, name="delete_chat_message"), url(r"^delete/$", chat.delete_message, name="delete_chat_message"),
url(r"^mute/$", chat.mute_message, name="mute_chat_message"),
url(r"^post/$", chat.post_message, name="post_chat_message"), url(r"^post/$", chat.post_message, name="post_chat_message"),
url(r"^ajax$", chat.chat_message_ajax, name="chat_message_ajax"), url(r"^ajax$", chat.chat_message_ajax, name="chat_message_ajax"),
url( url(
@ -1143,6 +1024,11 @@ urlpatterns = [
chat.toggle_ignore, chat.toggle_ignore,
name="toggle_ignore", name="toggle_ignore",
), ),
url(
r"^get_unread_boxes$",
chat.get_unread_boxes,
name="get_unread_boxes",
),
] ]
), ),
), ),
@ -1155,37 +1041,13 @@ urlpatterns = [
internal.InternalProblem.as_view(), internal.InternalProblem.as_view(),
name="internal_problem", name="internal_problem",
), ),
url(
r"^problem_votes$",
internal.get_problem_votes,
name="internal_problem_votes",
),
url(
r"^request_time$",
internal.InternalRequestTime.as_view(),
name="internal_request_time",
),
url(
r"^request_time_detail$",
internal.InternalRequestTimeDetail.as_view(),
name="internal_request_time_detail",
),
url(
r"^internal_slow_request$",
internal.InternalSlowRequest.as_view(),
name="internal_slow_request",
),
url(
r"^internal_slow_request_detail$",
internal.InternalSlowRequestDetail.as_view(),
name="internal_slow_request_detail",
),
] ]
), ),
), ),
url( url(
r"^notifications/", r"^notifications/",
paged_list_view(notification.NotificationList, "notification"), login_required(notification.NotificationList.as_view()),
name="notification",
), ),
url( url(
r"^import_users/", r"^import_users/",
@ -1214,12 +1076,7 @@ urlpatterns = [
] ]
), ),
), ),
url(r"^resolver/(?P<contest>\w+)", resolver.Resolver.as_view(), name="resolver"), ]
url(r"^upload/$", custom_file_upload.file_upload, name="custom_file_upload"),
] + url_static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# if hasattr(settings, "INTERNAL_IPS"):
# urlpatterns.append(url("__debug__/", include("debug_toolbar.urls")))
favicon_paths = [ favicon_paths = [
"apple-touch-icon-180x180.png", "apple-touch-icon-180x180.png",
@ -1243,7 +1100,6 @@ favicon_paths = [
"favicon-32x32.png", "favicon-32x32.png",
"favicon-16x16.png", "favicon-16x16.png",
"android-chrome-192x192.png", "android-chrome-192x192.png",
"android-chrome-512x512.png",
"android-chrome-48x48.png", "android-chrome-48x48.png",
"mstile-310x150.png", "mstile-310x150.png",
"apple-touch-icon-144x144.png", "apple-touch-icon-144x144.png",
@ -1263,5 +1119,7 @@ handler404 = "judge.views.error.error404"
handler403 = "judge.views.error.error403" handler403 = "judge.views.error.error403"
handler500 = "judge.views.error.error500" handler500 = "judge.views.error.error500"
if "newsletter" in settings.INSTALLED_APPS:
urlpatterns.append(url(r"^newsletter/", include("newsletter.urls")))
if "impersonate" in settings.INSTALLED_APPS: if "impersonate" in settings.INSTALLED_APPS:
urlpatterns.append(url(r"^impersonate/", include("impersonate.urls"))) urlpatterns.append(url(r"^impersonate/", include("impersonate.urls")))

View file

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

View file

@ -1,14 +1,8 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.admin.models import LogEntry from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import User
from judge.admin.comments import CommentAdmin from judge.admin.comments import CommentAdmin
from judge.admin.contest import ( from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin
ContestAdmin,
ContestParticipationAdmin,
ContestTagAdmin,
ContestsSummaryAdmin,
)
from judge.admin.interface import ( from judge.admin.interface import (
BlogPostAdmin, BlogPostAdmin,
LicenseAdmin, LicenseAdmin,
@ -17,18 +11,12 @@ from judge.admin.interface import (
) )
from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin
from judge.admin.problem import ProblemAdmin, ProblemPointsVoteAdmin from judge.admin.problem import ProblemAdmin, ProblemPointsVoteAdmin
from judge.admin.profile import ProfileAdmin, UserAdmin from judge.admin.profile import ProfileAdmin
from judge.admin.runtime import JudgeAdmin, LanguageAdmin from judge.admin.runtime import JudgeAdmin, LanguageAdmin
from judge.admin.submission import SubmissionAdmin from judge.admin.submission import SubmissionAdmin
from judge.admin.taxon import ( from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin
ProblemGroupAdmin,
ProblemTypeAdmin,
OfficialContestCategoryAdmin,
OfficialContestLocationAdmin,
)
from judge.admin.ticket import TicketAdmin from judge.admin.ticket import TicketAdmin
from judge.admin.volunteer import VolunteerProblemVoteAdmin from judge.admin.volunteer import VolunteerProblemVoteAdmin
from judge.admin.course import CourseAdmin
from judge.models import ( from judge.models import (
BlogPost, BlogPost,
Comment, Comment,
@ -51,10 +39,6 @@ from judge.models import (
Submission, Submission,
Ticket, Ticket,
VolunteerProblemVote, VolunteerProblemVote,
Course,
ContestsSummary,
OfficialContestCategory,
OfficialContestLocation,
) )
@ -80,9 +64,3 @@ admin.site.register(Profile, ProfileAdmin)
admin.site.register(Submission, SubmissionAdmin) admin.site.register(Submission, SubmissionAdmin)
admin.site.register(Ticket, TicketAdmin) admin.site.register(Ticket, TicketAdmin)
admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin) admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin)
admin.site.register(Course, 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

@ -12,6 +12,7 @@ class CommentForm(ModelForm):
class Meta: class Meta:
widgets = { widgets = {
"author": AdminHeavySelect2Widget(data_view="profile_select2"), "author": AdminHeavySelect2Widget(data_view="profile_select2"),
"parent": AdminHeavySelect2Widget(data_view="comment_select2"),
} }
if HeavyPreviewAdminPageDownWidget is not None: if HeavyPreviewAdminPageDownWidget is not None:
widgets["body"] = HeavyPreviewAdminPageDownWidget( widgets["body"] = HeavyPreviewAdminPageDownWidget(
@ -21,24 +22,12 @@ class CommentForm(ModelForm):
class CommentAdmin(VersionAdmin): class CommentAdmin(VersionAdmin):
fieldsets = ( fieldsets = (
( (None, {"fields": ("author", "page", "parent", "score", "hidden")}),
None,
{
"fields": (
"author",
"parent",
"score",
"hidden",
"content_type",
"object_id",
)
},
),
("Content", {"fields": ("body",)}), ("Content", {"fields": ("body",)}),
) )
list_display = ["author", "linked_object", "time"] list_display = ["author", "linked_page", "time"]
search_fields = ["author__user__username", "body"] search_fields = ["author__user__username", "page", "body"]
readonly_fields = ["score", "parent"] readonly_fields = ["score"]
actions = ["hide_comment", "unhide_comment"] actions = ["hide_comment", "unhide_comment"]
list_filter = ["hidden"] list_filter = ["hidden"]
actions_on_top = True actions_on_top = True
@ -77,6 +66,16 @@ class CommentAdmin(VersionAdmin):
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): def save_model(self, request, obj, form, change):
super(CommentAdmin, self).save_model(request, obj, form, change) super(CommentAdmin, self).save_model(request, obj, form, change)
if obj.hidden: if obj.hidden:

View file

@ -3,7 +3,7 @@ from django.contrib import admin
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import connection, transaction from django.db import connection, transaction
from django.db.models import Q, TextField from django.db.models import Q, TextField
from django.forms import ModelForm, ModelMultipleChoiceField, TextInput from django.forms import ModelForm, ModelMultipleChoiceField
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
@ -14,14 +14,7 @@ from reversion.admin import VersionAdmin
from reversion_compare.admin import CompareVersionAdmin from reversion_compare.admin import CompareVersionAdmin
from django_ace import AceWidget from django_ace import AceWidget
from judge.models import ( from judge.models import Contest, ContestProblem, ContestSubmission, Profile, Rating
Contest,
ContestProblem,
ContestSubmission,
Profile,
Rating,
OfficialContest,
)
from judge.ratings import rate_contest from judge.ratings import rate_contest
from judge.widgets import ( from judge.widgets import (
AdminHeavySelect2MultipleWidget, AdminHeavySelect2MultipleWidget,
@ -31,8 +24,6 @@ from judge.widgets import (
AdminSelect2Widget, AdminSelect2Widget,
HeavyPreviewAdminPageDownWidget, HeavyPreviewAdminPageDownWidget,
) )
from judge.views.contests import recalculate_contest_summary_result
from judge.utils.contest import maybe_trigger_contest_rescore
class AdminHeavySelect2Widget(AdminHeavySelect2Widget): class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
@ -75,12 +66,7 @@ class ContestTagAdmin(admin.ModelAdmin):
class ContestProblemInlineForm(ModelForm): class ContestProblemInlineForm(ModelForm):
class Meta: class Meta:
widgets = { widgets = {"problem": AdminHeavySelect2Widget(data_view="problem_select2")}
"problem": AdminHeavySelect2Widget(data_view="problem_select2"),
"hidden_subtasks": TextInput(attrs={"size": "3"}),
"points": TextInput(attrs={"size": "1"}),
"order": TextInput(attrs={"size": "1"}),
}
class ContestProblemInline(admin.TabularInline): class ContestProblemInline(admin.TabularInline):
@ -93,8 +79,7 @@ class ContestProblemInline(admin.TabularInline):
"partial", "partial",
"is_pretested", "is_pretested",
"max_submissions", "max_submissions",
"hidden_subtasks", "output_prefix_override",
"show_testcases",
"order", "order",
"rejudge_column", "rejudge_column",
) )
@ -157,26 +142,6 @@ class ContestForm(ModelForm):
) )
class OfficialContestInlineForm(ModelForm):
class Meta:
widgets = {
"category": AdminSelect2Widget,
"location": AdminSelect2Widget,
}
class OfficialContestInline(admin.StackedInline):
fields = (
"category",
"year",
"location",
)
model = OfficialContest
can_delete = True
form = OfficialContestInlineForm
extra = 0
class ContestAdmin(CompareVersionAdmin): class ContestAdmin(CompareVersionAdmin):
fieldsets = ( fieldsets = (
(None, {"fields": ("key", "name", "authors", "curators", "testers")}), (None, {"fields": ("key", "name", "authors", "curators", "testers")}),
@ -187,18 +152,13 @@ class ContestAdmin(CompareVersionAdmin):
"is_visible", "is_visible",
"use_clarifications", "use_clarifications",
"hide_problem_tags", "hide_problem_tags",
"public_scoreboard",
"scoreboard_visibility", "scoreboard_visibility",
"run_pretests_only", "run_pretests_only",
"points_precision", "points_precision",
"rate_limit",
) )
}, },
), ),
( (_("Scheduling"), {"fields": ("start_time", "end_time", "time_limit")}),
_("Scheduling"),
{"fields": ("start_time", "end_time", "time_limit", "freeze_after")},
),
( (
_("Details"), _("Details"),
{ {
@ -232,7 +192,9 @@ class ContestAdmin(CompareVersionAdmin):
{ {
"fields": ( "fields": (
"access_code", "access_code",
"is_private",
"private_contestants", "private_contestants",
"is_organization_private",
"organizations", "organizations",
"view_contest_scoreboard", "view_contest_scoreboard",
) )
@ -251,7 +213,7 @@ class ContestAdmin(CompareVersionAdmin):
"user_count", "user_count",
) )
search_fields = ("key", "name") search_fields = ("key", "name")
inlines = [ContestProblemInline, OfficialContestInline] inlines = [ContestProblemInline]
actions_on_top = True actions_on_top = True
actions_on_bottom = True actions_on_bottom = True
form = ContestForm form = ContestForm
@ -287,7 +249,9 @@ class ContestAdmin(CompareVersionAdmin):
readonly += ["access_code"] readonly += ["access_code"]
if not request.user.has_perm("judge.create_private_contest"): if not request.user.has_perm("judge.create_private_contest"):
readonly += [ readonly += [
"is_private",
"private_contestants", "private_contestants",
"is_organization_private",
"organizations", "organizations",
] ]
if not request.user.has_perm("judge.change_contest_visibility"): if not request.user.has_perm("judge.change_contest_visibility"):
@ -302,8 +266,8 @@ class ContestAdmin(CompareVersionAdmin):
"judge.change_contest_visibility" "judge.change_contest_visibility"
): ):
if ( if (
not len(form.cleaned_data["organizations"]) > 0 not form.cleaned_data["is_private"]
and not len(form.cleaned_data["private_contestants"]) > 0 and not form.cleaned_data["is_organization_private"]
): ):
raise PermissionDenied raise PermissionDenied
if not request.user.has_perm("judge.create_private_contest"): if not request.user.has_perm("judge.create_private_contest"):
@ -311,14 +275,23 @@ class ContestAdmin(CompareVersionAdmin):
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
# We need this flag because `save_related` deals with the inlines, but does not know if we have already rescored
self._rescored = False
if form.changed_data and any(
f in form.changed_data for f in ("format_config", "format_name")
):
self._rescore(obj.key)
self._rescored = True
def save_related(self, request, form, formsets, change): def save_related(self, request, form, formsets, change):
super().save_related(request, form, formsets, change) super().save_related(request, form, formsets, change)
# Only rescored if we did not already do so in `save_model` # Only rescored if we did not already do so in `save_model`
formset_changed = False if not self._rescored and any(formset.has_changed() for formset in formsets):
if any(formset.has_changed() for formset in formsets): self._rescore(form.cleaned_data["key"])
formset_changed = True obj = form.instance
obj.is_organization_private = obj.organizations.count() > 0
maybe_trigger_contest_rescore(form, form.instance, formset_changed) obj.is_private = obj.private_contestants.count() > 0
obj.save()
def has_change_permission(self, request, obj=None): def has_change_permission(self, request, obj=None):
if not request.user.has_perm("judge.edit_own_contest"): if not request.user.has_perm("judge.edit_own_contest"):
@ -327,6 +300,11 @@ class ContestAdmin(CompareVersionAdmin):
return True return True
return obj.is_editable_by(request.user) return obj.is_editable_by(request.user)
def _rescore(self, contest_key):
from judge.tasks import rescore_contest
transaction.on_commit(rescore_contest.s(contest_key).delay)
def make_visible(self, request, queryset): def make_visible(self, request, queryset):
if not request.user.has_perm("judge.change_contest_visibility"): if not request.user.has_perm("judge.change_contest_visibility"):
queryset = queryset.filter( queryset = queryset.filter(
@ -510,25 +488,3 @@ class ContestParticipationAdmin(admin.ModelAdmin):
show_virtual.short_description = _("virtual") show_virtual.short_description = _("virtual")
show_virtual.admin_order_field = "virtual" show_virtual.admin_order_field = "virtual"
class ContestsSummaryForm(ModelForm):
class Meta:
widgets = {
"contests": AdminHeavySelect2MultipleWidget(
data_view="contest_select2", attrs={"style": "width: 100%"}
),
}
class ContestsSummaryAdmin(admin.ModelAdmin):
fields = ("key", "contests", "scores")
list_display = ("key",)
search_fields = ("key", "contests__key")
form = ContestsSummaryForm
def save_model(self, request, obj, form, change):
super(ContestsSummaryAdmin, self).save_model(request, obj, form, change)
obj.refresh_from_db()
obj.results = recalculate_contest_summary_result(request, obj)
obj.save()

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

@ -53,7 +53,6 @@ class NavigationBarAdmin(DraggableMPTTAdmin):
class BlogPostForm(ModelForm): class BlogPostForm(ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(BlogPostForm, self).__init__(*args, **kwargs) super(BlogPostForm, self).__init__(*args, **kwargs)
if "authors" in self.fields:
self.fields["authors"].widget.can_add_related = False self.fields["authors"].widget.can_add_related = False
class Meta: class Meta:
@ -177,12 +176,7 @@ class LogEntryAdmin(admin.ModelAdmin):
"object_link", "object_link",
"diff_link", "diff_link",
) )
search_fields = ( search_fields = ("object_repr", "change_message")
"object_repr",
"change_message",
"user__username",
"content_type__model",
)
list_filter = (UserListFilter, "content_type") list_filter = (UserListFilter, "content_type")
list_display_links = None list_display_links = None
actions = None actions = None

View file

@ -33,6 +33,7 @@ class OrganizationAdmin(VersionAdmin):
"short_name", "short_name",
"is_open", "is_open",
"about", "about",
"logo_override_image",
"slots", "slots",
"registrant", "registrant",
"creation_date", "creation_date",
@ -42,16 +43,14 @@ class OrganizationAdmin(VersionAdmin):
"name", "name",
"short_name", "short_name",
"is_open", "is_open",
"creation_date", "slots",
"registrant", "registrant",
"show_public", "show_public",
) )
search_fields = ("name", "short_name", "registrant__user__username")
prepopulated_fields = {"slug": ("name",)} prepopulated_fields = {"slug": ("name",)}
actions_on_top = True actions_on_top = True
actions_on_bottom = True actions_on_bottom = True
form = OrganizationForm form = OrganizationForm
ordering = ["-creation_date"]
def show_public(self, obj): def show_public(self, obj):
return format_html( return format_html(

View file

@ -1,49 +1,41 @@
from operator import attrgetter from operator import attrgetter
import statistics
from django import forms from django import forms
from django.contrib import admin, messages from django.contrib import admin
from django.db import transaction, IntegrityError from django.db import transaction
from django.db.models import Q, Avg, Count from django.db.models import Q, Avg, Count
from django.db.models.aggregates import StdDev from django.db.models.aggregates import StdDev
from django.forms import ModelForm, TextInput from django.forms import ModelForm
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import gettext, gettext_lazy as _, ungettext from django.utils.translation import gettext, gettext_lazy as _, ungettext
from django_ace import AceWidget
from django.utils import timezone
from django.core.exceptions import ValidationError
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from reversion_compare.admin import CompareVersionAdmin from reversion_compare.admin import CompareVersionAdmin
from judge.models import ( from judge.models import (
LanguageLimit, LanguageLimit,
LanguageTemplate,
Problem, Problem,
ProblemClarification,
ProblemTranslation, ProblemTranslation,
Profile, Profile,
Solution, Solution,
Notification,
) )
from judge.models.notification import make_notification
from judge.widgets import ( from judge.widgets import (
AdminHeavySelect2MultipleWidget, AdminHeavySelect2MultipleWidget,
AdminSelect2MultipleWidget, AdminSelect2MultipleWidget,
AdminSelect2Widget, AdminSelect2Widget,
CheckboxSelectMultipleWithSelectAll, CheckboxSelectMultipleWithSelectAll,
HeavyPreviewAdminPageDownWidget, HeavyPreviewAdminPageDownWidget,
HeavyPreviewPageDownWidget,
) )
from judge.utils.problems import user_editable_ids, user_tester_ids
MEMORY_UNITS = (("KB", "KB"), ("MB", "MB"))
class ProblemForm(ModelForm): class ProblemForm(ModelForm):
change_message = forms.CharField( change_message = forms.CharField(
max_length=256, label="Edit reason", required=False max_length=256, label="Edit reason", required=False
) )
memory_unit = forms.ChoiceField(choices=MEMORY_UNITS)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ProblemForm, self).__init__(*args, **kwargs) super(ProblemForm, self).__init__(*args, **kwargs)
@ -57,25 +49,6 @@ class ProblemForm(ModelForm):
} }
) )
def clean_code(self):
code = self.cleaned_data.get("code")
if self.instance.pk:
return code
if Problem.objects.filter(code=code).exists():
raise ValidationError(_("A problem with this code already exists."))
return code
def clean(self):
memory_unit = self.cleaned_data.get("memory_unit", "KB")
if memory_unit == "MB":
self.cleaned_data["memory_limit"] *= 1024
date = self.cleaned_data.get("date")
if not date or date > timezone.now():
self.cleaned_data["date"] = timezone.now()
return self.cleaned_data
class Meta: class Meta:
widgets = { widgets = {
"authors": AdminHeavySelect2MultipleWidget( "authors": AdminHeavySelect2MultipleWidget(
@ -95,7 +68,6 @@ class ProblemForm(ModelForm):
), ),
"types": AdminSelect2MultipleWidget, "types": AdminSelect2MultipleWidget,
"group": AdminSelect2Widget, "group": AdminSelect2Widget,
"memory_limit": TextInput(attrs={"size": "20"}),
} }
if HeavyPreviewAdminPageDownWidget is not None: if HeavyPreviewAdminPageDownWidget is not None:
widgets["description"] = HeavyPreviewAdminPageDownWidget( widgets["description"] = HeavyPreviewAdminPageDownWidget(
@ -119,44 +91,30 @@ class ProblemCreatorListFilter(admin.SimpleListFilter):
class LanguageLimitInlineForm(ModelForm): class LanguageLimitInlineForm(ModelForm):
memory_unit = forms.ChoiceField(choices=MEMORY_UNITS, label=_("Memory unit"))
class Meta: class Meta:
widgets = { widgets = {"language": AdminSelect2Widget}
"language": AdminSelect2Widget,
"memory_limit": TextInput(attrs={"size": "10"}),
}
def clean(self):
if not self.cleaned_data.get("language"):
self.cleaned_data["DELETE"] = True
if (
self.cleaned_data.get("memory_limit")
and self.cleaned_data.get("memory_unit") == "MB"
):
self.cleaned_data["memory_limit"] *= 1024
return self.cleaned_data
class LanguageLimitInline(admin.TabularInline): class LanguageLimitInline(admin.TabularInline):
model = LanguageLimit model = LanguageLimit
fields = ("language", "time_limit", "memory_limit", "memory_unit") fields = ("language", "time_limit", "memory_limit")
form = LanguageLimitInlineForm form = LanguageLimitInlineForm
extra = 0
class LanguageTemplateInlineForm(ModelForm): class ProblemClarificationForm(ModelForm):
class Meta: class Meta:
if HeavyPreviewPageDownWidget is not None:
widgets = { widgets = {
"language": AdminSelect2Widget, "description": HeavyPreviewPageDownWidget(
"source": AceWidget(width="600px", height="200px", toolbar=False), preview=reverse_lazy("comment_preview")
)
} }
class LanguageTemplateInline(admin.TabularInline): class ProblemClarificationInline(admin.StackedInline):
model = LanguageTemplate model = ProblemClarification
fields = ("language", "source") fields = ("description",)
form = LanguageTemplateInlineForm form = ProblemClarificationForm
extra = 0 extra = 0
@ -211,13 +169,14 @@ class ProblemAdmin(CompareVersionAdmin):
"code", "code",
"name", "name",
"is_public", "is_public",
"organizations", "is_manually_managed",
"date", "date",
"authors", "authors",
"curators", "curators",
"testers", "testers",
"is_organization_private",
"organizations",
"description", "description",
"pdf_description",
"license", "license",
), ),
}, },
@ -228,7 +187,7 @@ class ProblemAdmin(CompareVersionAdmin):
), ),
(_("Taxonomy"), {"fields": ("types", "group")}), (_("Taxonomy"), {"fields": ("types", "group")}),
(_("Points"), {"fields": (("points", "partial"), "short_circuit")}), (_("Points"), {"fields": (("points", "partial"), "short_circuit")}),
(_("Limits"), {"fields": ("time_limit", ("memory_limit", "memory_unit"))}), (_("Limits"), {"fields": ("time_limit", "memory_limit")}),
(_("Language"), {"fields": ("allowed_languages",)}), (_("Language"), {"fields": ("allowed_languages",)}),
(_("Justice"), {"fields": ("banned_users",)}), (_("Justice"), {"fields": ("banned_users",)}),
(_("History"), {"fields": ("change_message",)}), (_("History"), {"fields": ("change_message",)}),
@ -237,12 +196,15 @@ class ProblemAdmin(CompareVersionAdmin):
"code", "code",
"name", "name",
"show_authors", "show_authors",
"date",
"points", "points",
"vote_cnt",
"vote_mean",
"vote_median",
"vote_std",
"is_public", "is_public",
"show_public", "show_public",
] ]
ordering = ["-date"] ordering = ["code"]
search_fields = ( search_fields = (
"code", "code",
"name", "name",
@ -251,7 +213,7 @@ class ProblemAdmin(CompareVersionAdmin):
) )
inlines = [ inlines = [
LanguageLimitInline, LanguageLimitInline,
LanguageTemplateInline, ProblemClarificationInline,
ProblemSolutionInline, ProblemSolutionInline,
ProblemTranslationInline, ProblemTranslationInline,
] ]
@ -278,6 +240,8 @@ class ProblemAdmin(CompareVersionAdmin):
fields = self.readonly_fields fields = self.readonly_fields
if not request.user.has_perm("judge.change_public_visibility"): if not request.user.has_perm("judge.change_public_visibility"):
fields += ("is_public",) fields += ("is_public",)
if not request.user.has_perm("judge.change_manually_managed"):
fields += ("is_manually_managed",)
return fields return fields
def show_authors(self, obj): def show_authors(self, obj):
@ -331,6 +295,11 @@ class ProblemAdmin(CompareVersionAdmin):
def get_queryset(self, request): def get_queryset(self, request):
queryset = Problem.objects.prefetch_related("authors__user") queryset = Problem.objects.prefetch_related("authors__user")
queryset = queryset.annotate(
_vote_mean=Avg("problem_points_votes__points"),
_vote_std=StdDev("problem_points_votes__points"),
_vote_cnt=Count("problem_points_votes__points"),
)
if request.user.has_perm("judge.edit_all_problem"): if request.user.has_perm("judge.edit_all_problem"):
return queryset return queryset
@ -365,56 +334,12 @@ class ProblemAdmin(CompareVersionAdmin):
return form return form
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
form.changed_data.remove("memory_unit")
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
if form.changed_data and any( if form.changed_data and any(
f in form.changed_data for f in ("is_public", "points", "partial") f in form.changed_data for f in ("is_public", "points", "partial")
): ):
self._rescore(request, obj.id) self._rescore(request, obj.id)
def save_related(self, request, form, formsets, change):
editors = set()
testers = set()
if "curators" in form.changed_data or "authors" in form.changed_data:
editors = set(form.instance.editor_ids)
if "testers" in form.changed_data:
testers = set(form.instance.tester_ids)
super().save_related(request, form, formsets, change)
obj = form.instance
obj.curators.add(request.profile)
if "curators" in form.changed_data or "authors" in form.changed_data:
del obj.editor_ids
editors = editors.union(set(obj.editor_ids))
if "testers" in form.changed_data:
del obj.tester_ids
testers = testers.union(set(obj.tester_ids))
for editor in editors:
user_editable_ids.dirty(editor)
for tester in testers:
user_tester_ids.dirty(tester)
# Create notification
if "is_public" in form.changed_data or "organizations" in form.changed_data:
users = set(obj.authors.all())
users = users.union(users, set(obj.curators.all()))
orgs = []
if obj.organizations.count() > 0:
for org in obj.organizations.all():
users = users.union(users, set(org.admins.all()))
orgs.append(org.name)
else:
admins = Profile.objects.filter(user__is_superuser=True).all()
users = users.union(users, admins)
link = reverse_lazy("admin:judge_problem_change", args=(obj.id,))
html = f'<a href="{link}">{obj.name}</a>'
category = "Problem public: " + str(obj.is_public)
if orgs:
category += " (" + ", ".join(orgs) + ")"
make_notification(users, category, html, request.profile)
def construct_change_message(self, request, form, *args, **kwargs): def construct_change_message(self, request, form, *args, **kwargs):
if form.cleaned_data.get("change_message"): if form.cleaned_data.get("change_message"):
return form.cleaned_data["change_message"] return form.cleaned_data["change_message"]
@ -422,6 +347,25 @@ class ProblemAdmin(CompareVersionAdmin):
request, form, *args, **kwargs request, form, *args, **kwargs
) )
def vote_mean(self, obj):
return round(obj._vote_mean, 1) if obj._vote_mean is not None else None
vote_mean.admin_order_field = "_vote_mean"
def vote_std(self, obj):
return round(obj._vote_std, 1) if obj._vote_std is not None else None
vote_std.admin_order_field = "_vote_std"
def vote_cnt(self, obj):
return obj._vote_cnt
vote_cnt.admin_order_field = "_vote_cnt"
def vote_median(self, obj):
votes = obj.problem_points_votes.values_list("points", flat=True)
return statistics.median(votes) if votes else None
class ProblemPointsVoteAdmin(admin.ModelAdmin): class ProblemPointsVoteAdmin(admin.ModelAdmin):
list_display = ( list_display = (

View file

@ -1,19 +1,12 @@
from django.contrib import admin from django.contrib import admin
from django.forms import ModelForm, CharField, TextInput from django.forms import ModelForm
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import gettext, gettext_lazy as _, ungettext from django.utils.translation import gettext, gettext_lazy as _, ungettext
from django.contrib.auth.admin import UserAdmin as OldUserAdmin
from django.core.exceptions import ValidationError
from django.contrib.auth.forms import UserChangeForm
from django_ace import AceWidget
from judge.models import Profile, ProfileInfo
from judge.widgets import AdminPagedownWidget, AdminSelect2Widget
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
import re from django_ace import AceWidget
from judge.models import Profile
from judge.widgets import AdminPagedownWidget, AdminSelect2Widget
class ProfileForm(ModelForm): class ProfileForm(ModelForm):
@ -60,13 +53,6 @@ class TimezoneFilter(admin.SimpleListFilter):
return queryset.filter(timezone=self.value()) return queryset.filter(timezone=self.value())
class ProfileInfoInline(admin.StackedInline):
model = ProfileInfo
can_delete = False
verbose_name_plural = "profile info"
fk_name = "profile"
class ProfileAdmin(VersionAdmin): class ProfileAdmin(VersionAdmin):
fields = ( fields = (
"user", "user",
@ -76,12 +62,15 @@ class ProfileAdmin(VersionAdmin):
"timezone", "timezone",
"language", "language",
"ace_theme", "ace_theme",
"math_engine",
"last_access", "last_access",
"ip", "ip",
"mute", "mute",
"is_unlisted", "is_unlisted",
"is_banned_problem_voting",
"notes", "notes",
"is_totp_enabled", "is_totp_enabled",
"user_script",
"current_contest", "current_contest",
) )
readonly_fields = ("user",) readonly_fields = ("user",)
@ -102,7 +91,6 @@ class ProfileAdmin(VersionAdmin):
actions_on_top = True actions_on_top = True
actions_on_bottom = True actions_on_bottom = True
form = ProfileForm form = ProfileForm
inlines = (ProfileInfoInline,)
def get_queryset(self, request): def get_queryset(self, request):
return super(ProfileAdmin, self).get_queryset(request).select_related("user") return super(ProfileAdmin, self).get_queryset(request).select_related("user")
@ -137,7 +125,7 @@ class ProfileAdmin(VersionAdmin):
admin_user_admin.short_description = _("User") admin_user_admin.short_description = _("User")
def email(self, obj): def email(self, obj):
return obj.email return obj.user.email
email.admin_order_field = "user__email" email.admin_order_field = "user__email"
email.short_description = _("Email") email.short_description = _("Email")
@ -171,57 +159,11 @@ class ProfileAdmin(VersionAdmin):
recalculate_points.short_description = _("Recalculate scores") recalculate_points.short_description = _("Recalculate scores")
def get_form(self, request, obj=None, **kwargs):
class UserForm(UserChangeForm): form = super(ProfileAdmin, self).get_form(request, obj, **kwargs)
def __init__(self, *args, **kwargs): if "user_script" in form.base_fields:
super().__init__(*args, **kwargs) # form.base_fields['user_script'] does not exist when the user has only view permission on the model.
self.fields["username"].help_text = _( form.base_fields["user_script"].widget = AceWidget(
"Username can only contain letters, digits, and underscores." "javascript", request.profile.ace_theme
) )
return form
def clean_username(self):
username = self.cleaned_data.get("username")
if not re.match(r"^\w+$", username):
raise ValidationError(
_("Username can only contain letters, digits, and underscores.")
)
return username
class UserAdmin(OldUserAdmin):
# Customize the fieldsets for adding and editing users
form = UserForm
fieldsets = (
(None, {"fields": ("username", "password")}),
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
(
"Permissions",
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
)
},
),
("Important dates", {"fields": ("last_login", "date_joined")}),
)
readonly_fields = ("last_login", "date_joined")
def get_readonly_fields(self, request, obj=None):
fields = self.readonly_fields
if not request.user.is_superuser:
fields += (
"is_staff",
"is_active",
"is_superuser",
"groups",
"user_permissions",
)
return fields
def has_add_permission(self, request):
return False

View file

@ -194,16 +194,18 @@ class SubmissionAdmin(admin.ModelAdmin):
def has_add_permission(self, request): def has_add_permission(self, request):
return False return False
def has_change_permission(self, request, obj=None):
if not request.user.has_perm("judge.edit_own_problem"):
return False
if request.user.has_perm("judge.edit_all_problem") or obj is None:
return True
return obj.problem.is_editor(request.profile)
def lookup_allowed(self, key, value): def lookup_allowed(self, key, value):
return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in ( return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in (
"problem__code", "problem__code",
) )
def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
if "case_points" in form.changed_data or "case_total" in form.changed_data:
obj.update_contest()
def judge(self, request, queryset): def judge(self, request, queryset):
if not request.user.has_perm( if not request.user.has_perm(
"judge.rejudge_submission" "judge.rejudge_submission"

View file

@ -56,11 +56,3 @@ class ProblemTypeAdmin(admin.ModelAdmin):
[o.pk for o in obj.problem_set.all()] if obj else [] [o.pk for o in obj.problem_set.all()] if obj else []
) )
return super(ProblemTypeAdmin, self).get_form(request, obj, **kwargs) return super(ProblemTypeAdmin, self).get_form(request, obj, **kwargs)
class OfficialContestCategoryAdmin(admin.ModelAdmin):
fields = ("name",)
class OfficialContestLocationAdmin(admin.ModelAdmin):
fields = ("name",)

View file

@ -1,67 +1,34 @@
from operator import attrgetter
from django.contrib import admin from django.contrib import admin
from django.utils.html import format_html from django.utils.html import format_html
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext, gettext_lazy as _, ungettext from django.utils.translation import gettext, gettext_lazy as _, ungettext
from django.forms import ModelForm
from judge.models import VolunteerProblemVote from judge.models import VolunteerProblemVote
from judge.widgets import AdminSelect2MultipleWidget
class VolunteerProblemVoteForm(ModelForm):
class Meta:
widgets = {
"types": AdminSelect2MultipleWidget,
}
class VolunteerProblemVoteAdmin(admin.ModelAdmin): class VolunteerProblemVoteAdmin(admin.ModelAdmin):
form = VolunteerProblemVoteForm
fields = ( fields = (
"voter",
"problem",
"time",
"thinking_points",
"knowledge_points",
"feedback",
)
readonly_fields = ("time", "problem", "voter")
list_display = (
"voter", "voter",
"problem_link", "problem_link",
"time", "time",
"thinking_points", "thinking_points",
"knowledge_points", "knowledge_points",
"types",
"feedback", "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" date_hierarchy = "time"
def problem_link(self, obj): def problem_link(self, obj):
if self.request.user.is_superuser:
url = reverse("admin:judge_problem_change", args=(obj.problem.id,)) url = reverse("admin:judge_problem_change", args=(obj.problem.id,))
else: return format_html(f"<a href='{url}'>{obj.problem.code}</a>")
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.short_description = _("Problem")
problem_link.admin_order_field = "problem__code" 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

@ -12,7 +12,7 @@ class JudgeAppConfig(AppConfig):
# OPERATIONS MAY HAVE SIDE EFFECTS. # OPERATIONS MAY HAVE SIDE EFFECTS.
# DO NOT REMOVE THINKING THE IMPORT IS UNUSED. # DO NOT REMOVE THINKING THE IMPORT IS UNUSED.
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
from . import models, signals, jinja2 # noqa: F401, imported for side effects from . import signals, jinja2 # noqa: F401, imported for side effects
from django.contrib.flatpages.models import FlatPage from django.contrib.flatpages.models import FlatPage
from django.contrib.flatpages.admin import FlatPageAdmin from django.contrib.flatpages.admin import FlatPageAdmin

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

@ -145,9 +145,6 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta):
def on_timeout(self): def on_timeout(self):
pass pass
def on_cleanup(self):
pass
def handle(self): def handle(self):
try: try:
tag = self.read_size() tag = self.read_size()
@ -207,8 +204,6 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta):
if e.__class__.__name__ == "cancel_wait_ex": if e.__class__.__name__ == "cancel_wait_ex":
return return
raise raise
finally:
self.on_cleanup()
def send(self, data): def send(self, data):
compressed = zlib.compress(data.encode("utf-8")) compressed = zlib.compress(data.encode("utf-8"))

View file

@ -2,8 +2,6 @@ import json
import logging import logging
import struct import struct
from django import db
from judge.bridge.base_handler import Disconnect, ZlibPacketHandler from judge.bridge.base_handler import Disconnect, ZlibPacketHandler
logger = logging.getLogger("judge.bridge") logger = logging.getLogger("judge.bridge")
@ -62,5 +60,5 @@ class DjangoHandler(ZlibPacketHandler):
def on_malformed(self, packet): def on_malformed(self, packet):
logger.error("Malformed packet: %s", packet) logger.error("Malformed packet: %s", packet)
def on_cleanup(self): def on_close(self):
db.connection.close() self._to_kill = False

View file

@ -10,11 +10,10 @@ from django import db
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.db.models import F from django.db.models import F
from django.core.cache import cache
from judge import event_poster as event from judge import event_poster as event
from judge.bridge.base_handler import ZlibPacketHandler, proxy_list from judge.bridge.base_handler import ZlibPacketHandler, proxy_list
from judge.utils.problems import finished_submission from judge.caching import finished_submission
from judge.models import ( from judge.models import (
Judge, Judge,
Language, Language,
@ -24,8 +23,6 @@ from judge.models import (
Submission, Submission,
SubmissionTestCase, SubmissionTestCase,
) )
from judge.bridge.utils import VanishedSubmission
from judge.caching import cache_wrapper
logger = logging.getLogger("judge.bridge") logger = logging.getLogger("judge.bridge")
json_log = logging.getLogger("judge.json.bridge") json_log = logging.getLogger("judge.json.bridge")
@ -39,7 +36,10 @@ SubmissionData = namedtuple(
def _ensure_connection(): def _ensure_connection():
db.connection.close_if_unusable_or_obsolete() try:
db.connection.cursor().execute("SELECT 1").fetchall()
except Exception:
db.connection.close()
class JudgeHandler(ZlibPacketHandler): class JudgeHandler(ZlibPacketHandler):
@ -65,10 +65,10 @@ class JudgeHandler(ZlibPacketHandler):
"handshake": self.on_handshake, "handshake": self.on_handshake,
} }
self._working = False self._working = False
self._working_data = {}
self._no_response_job = None self._no_response_job = None
self._problems = []
self.executors = {} self.executors = {}
self.problems = set() self.problems = {}
self.latency = None self.latency = None
self.time_delta = None self.time_delta = None
self.load = 1e100 self.load = 1e100
@ -94,6 +94,12 @@ class JudgeHandler(ZlibPacketHandler):
def on_disconnect(self): def on_disconnect(self):
self._stop_ping.set() self._stop_ping.set()
if self._working:
logger.error(
"Judge %s disconnected while handling submission %s",
self.name,
self._working,
)
self.judges.remove(self) self.judges.remove(self)
if self.name is not None: if self.name is not None:
self._disconnected() self._disconnected()
@ -105,29 +111,21 @@ class JudgeHandler(ZlibPacketHandler):
self._make_json_log(action="disconnect", info="judge disconnected") self._make_json_log(action="disconnect", info="judge disconnected")
) )
if self._working: if self._working:
self.judges.judge( Submission.objects.filter(id=self._working).update(
self._working, status="IE", result="IE", error=""
self._working_data["problem"], )
self._working_data["language"], json_log.error(
self._working_data["source"], self._make_json_log(
None, sub=self._working,
0, action="close",
info="IE due to shutdown on grading",
)
) )
def _authenticate(self, id, key): def _authenticate(self, id, key):
try: try:
judge = Judge.objects.get(name=id) judge = Judge.objects.get(name=id, is_blocked=False)
except Judge.DoesNotExist: except Judge.DoesNotExist:
if settings.BRIDGED_AUTO_CREATE_JUDGE:
judge = Judge()
judge.name = id
judge.auth_key = key
judge.save()
result = True
else:
result = False
else:
if judge.is_blocked:
result = False result = False
else: else:
result = hmac.compare_digest(judge.auth_key, key) result = hmac.compare_digest(judge.auth_key, key)
@ -140,52 +138,11 @@ class JudgeHandler(ZlibPacketHandler):
) )
return result return result
def _update_supported_problems(self, problem_packet):
# problem_packet is a dict {code: mtimes} from judge-server
self.problems = set(p for p, _ in problem_packet)
def _update_judge_problems(self):
chunk_size = 500
target_problem_codes = self.problems
current_problems = _get_judge_problems(self.judge)
updated = False
problems_to_add = list(target_problem_codes - current_problems)
problems_to_remove = list(current_problems - target_problem_codes)
if problems_to_add:
for i in range(0, len(problems_to_add), chunk_size):
chunk = problems_to_add[i : i + chunk_size]
problem_ids = Problem.objects.filter(code__in=chunk).values_list(
"id", flat=True
)
if not problem_ids:
continue
logger.info("%s: Add %d problems", self.name, len(problem_ids))
self.judge.problems.add(*problem_ids)
updated = True
if problems_to_remove:
for i in range(0, len(problems_to_remove), chunk_size):
chunk = problems_to_remove[i : i + chunk_size]
problem_ids = Problem.objects.filter(code__in=chunk).values_list(
"id", flat=True
)
if not problem_ids:
continue
logger.info("%s: Remove %d problems", self.name, len(problem_ids))
self.judge.problems.remove(*problem_ids)
updated = True
if updated:
_get_judge_problems.dirty(self.judge)
def _connected(self): def _connected(self):
judge = self.judge = Judge.objects.get(name=self.name) judge = self.judge = Judge.objects.get(name=self.name)
judge.start_time = timezone.now() judge.start_time = timezone.now()
judge.online = True judge.online = True
self._update_judge_problems() judge.problems.set(Problem.objects.filter(code__in=list(self.problems.keys())))
judge.runtimes.set(Language.objects.filter(key__in=list(self.executors.keys()))) judge.runtimes.set(Language.objects.filter(key__in=list(self.executors.keys())))
# Delete now in case we somehow crashed and left some over from the last connection # Delete now in case we somehow crashed and left some over from the last connection
@ -220,8 +177,6 @@ class JudgeHandler(ZlibPacketHandler):
def _disconnected(self): def _disconnected(self):
Judge.objects.filter(id=self.judge.id).update(online=False) Judge.objects.filter(id=self.judge.id).update(online=False)
RuntimeVersion.objects.filter(judge=self.judge).delete() RuntimeVersion.objects.filter(judge=self.judge).delete()
self.judge.problems.clear()
_get_judge_problems.dirty(self.judge)
def _update_ping(self): def _update_ping(self):
try: try:
@ -252,7 +207,8 @@ class JudgeHandler(ZlibPacketHandler):
return return
self.timeout = 60 self.timeout = 60
self._update_supported_problems(packet["problems"]) self._problems = packet["problems"]
self.problems = dict(self._problems)
self.executors = packet["executors"] self.executors = packet["executors"]
self.name = packet["id"] self.name = packet["id"]
@ -353,15 +309,7 @@ class JudgeHandler(ZlibPacketHandler):
def submit(self, id, problem, language, source): def submit(self, id, problem, language, source):
data = self.get_related_submission_data(id) data = self.get_related_submission_data(id)
if not data:
self._update_internal_error_submission(id, "Submission vanished")
raise VanishedSubmission()
self._working = id self._working = id
self._working_data = {
"problem": problem,
"language": language,
"source": source,
}
self._no_response_job = threading.Timer(20, self._kill_if_no_response) self._no_response_job = threading.Timer(20, self._kill_if_no_response)
self.send( self.send(
{ {
@ -480,12 +428,14 @@ class JudgeHandler(ZlibPacketHandler):
def on_supported_problems(self, packet): def on_supported_problems(self, packet):
logger.info("%s: Updated problem list", self.name) logger.info("%s: Updated problem list", self.name)
self._update_supported_problems(packet["problems"]) self._problems = packet["problems"]
self.problems = dict(self._problems)
if not self.working: if not self.working:
self.judges.update_problems(self) self.judges.update_problems(self)
self._update_judge_problems() self.judge.problems.set(
Problem.objects.filter(code__in=list(self.problems.keys()))
)
json_log.info( json_log.info(
self._make_json_log(action="update-problems", count=len(self.problems)) self._make_json_log(action="update-problems", count=len(self.problems))
) )
@ -550,8 +500,8 @@ class JudgeHandler(ZlibPacketHandler):
total += case.total total += case.total
else: else:
if case.batch in batches: if case.batch in batches:
batches[case.batch][0] += case.points batches[case.batch][0] = min(batches[case.batch][0], case.points)
batches[case.batch][1] += case.total batches[case.batch][1] = max(batches[case.batch][1], case.total)
else: else:
batches[case.batch] = [case.points, case.total] batches[case.batch] = [case.points, case.total]
memory = max(memory, case.memory) memory = max(memory, case.memory)
@ -563,8 +513,8 @@ class JudgeHandler(ZlibPacketHandler):
points += batches[i][0] points += batches[i][0]
total += batches[i][1] total += batches[i][1]
points = points points = round(points, 1)
total = total total = round(total, 1)
submission.case_points = points submission.case_points = points
submission.case_total = total submission.case_total = total
@ -702,11 +652,8 @@ class JudgeHandler(ZlibPacketHandler):
self._free_self(packet) self._free_self(packet)
id = packet["submission-id"] id = packet["submission-id"]
self._update_internal_error_submission(id, packet["message"])
def _update_internal_error_submission(self, id, message):
if Submission.objects.filter(id=id).update( if Submission.objects.filter(id=id).update(
status="IE", result="IE", error=message status="IE", result="IE", error=packet["message"]
): ):
event.post( event.post(
"sub_%s" % Submission.get_id_secret(id), {"type": "internal-error"} "sub_%s" % Submission.get_id_secret(id), {"type": "internal-error"}
@ -714,9 +661,9 @@ class JudgeHandler(ZlibPacketHandler):
self._post_update_submission(id, "internal-error", done=True) self._post_update_submission(id, "internal-error", done=True)
json_log.info( json_log.info(
self._make_json_log( self._make_json_log(
sub=id, packet,
action="internal-error", action="internal-error",
message=message, message=packet["message"],
finish=True, finish=True,
result="IE", result="IE",
) )
@ -725,10 +672,10 @@ class JudgeHandler(ZlibPacketHandler):
logger.warning("Unknown submission: %s", id) logger.warning("Unknown submission: %s", id)
json_log.error( json_log.error(
self._make_json_log( self._make_json_log(
sub=id, packet,
action="internal-error", action="internal-error",
info="unknown submission", info="unknown submission",
message=message, message=packet["message"],
finish=True, finish=True,
result="IE", result="IE",
) )
@ -956,11 +903,3 @@ class JudgeHandler(ZlibPacketHandler):
"language": data["language__key"], "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

@ -3,8 +3,6 @@ from collections import namedtuple
from operator import attrgetter from operator import attrgetter
from threading import RLock from threading import RLock
from judge.bridge.utils import VanishedSubmission
try: try:
from llist import dllist from llist import dllist
except ImportError: except ImportError:
@ -41,8 +39,6 @@ class JudgeList(object):
) )
try: try:
judge.submit(id, problem, language, source) judge.submit(id, problem, language, source)
except VanishedSubmission:
pass
except Exception: except Exception:
logger.exception( logger.exception(
"Failed to dispatch %d (%s, %s) to %s", "Failed to dispatch %d (%s, %s) to %s",
@ -93,7 +89,6 @@ class JudgeList(object):
logger.info("Judge available after grading %d: %s", submission, judge.name) logger.info("Judge available after grading %d: %s", submission, judge.name)
del self.submission_map[submission] del self.submission_map[submission]
judge._working = False judge._working = False
judge._working_data = {}
self._handle_free_judge(judge) self._handle_free_judge(judge)
def abort(self, submission): def abort(self, submission):

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

196
judge/comments.py Normal file
View file

@ -0,0 +1,196 @@
from django import forms
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db.models import Count
from django.db.models.expressions import F, Value
from django.db.models.functions import Coalesce
from django.forms import ModelForm
from django.http import (
HttpResponseForbidden,
HttpResponseNotFound,
HttpResponseRedirect,
)
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
from django.views.generic import View
from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin
from reversion import revisions
from reversion.models import Revision, Version
from judge.dblock import LockModel
from judge.models import Comment, CommentLock, CommentVote, Notification
from judge.utils.raw_sql import RawSQLColumn, unique_together_left_join
from judge.widgets import HeavyPreviewPageDownWidget
from judge.jinja2.reference import get_user_from_text
def add_mention_notifications(comment):
user_referred = get_user_from_text(comment.body).exclude(id=comment.author.id)
for user in user_referred:
notification_ref = Notification(owner=user, comment=comment, category="Mention")
notification_ref.save()
def del_mention_notifications(comment):
query = {"comment": comment, "category": "Mention"}
Notification.objects.filter(**query).delete()
class CommentForm(ModelForm):
class Meta:
model = Comment
fields = ["body", "parent"]
widgets = {
"parent": forms.HiddenInput(),
}
if HeavyPreviewPageDownWidget is not None:
widgets["body"] = HeavyPreviewPageDownWidget(
preview=reverse_lazy("comment_preview"),
preview_timeout=1000,
hide_preview_button=True,
)
def __init__(self, request, *args, **kwargs):
self.request = request
super(CommentForm, self).__init__(*args, **kwargs)
self.fields["body"].widget.attrs.update({"placeholder": _("Comment body")})
def clean(self):
if self.request is not None and self.request.user.is_authenticated:
profile = self.request.profile
if profile.mute:
raise ValidationError(_("Your part is silent, little toad."))
elif (
not self.request.user.is_staff
and not profile.submission_set.filter(
points=F("problem__points")
).exists()
):
raise ValidationError(
_(
"You need to have solved at least one problem "
"before your voice can be heard."
)
)
return super(CommentForm, self).clean()
class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
comment_page = None
def get_comment_page(self):
if self.comment_page is None:
raise NotImplementedError()
return self.comment_page
def is_comment_locked(self):
if self.request.user.has_perm("judge.override_comment_lock"):
return False
return CommentLock.objects.filter(page=self.get_comment_page()).exists() or (
self.request.in_contest
and self.request.participation.contest.use_clarifications
)
@method_decorator(login_required)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
page = self.get_comment_page()
if self.is_comment_locked():
return HttpResponseForbidden()
parent = request.POST.get("parent")
if parent:
try:
parent = int(parent)
except ValueError:
return HttpResponseNotFound()
else:
if not Comment.objects.filter(
hidden=False, id=parent, page=page
).exists():
return HttpResponseNotFound()
form = CommentForm(request, request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.author = request.profile
comment.page = page
with LockModel(
write=(Comment, Revision, Version), read=(ContentType,)
), revisions.create_revision():
revisions.set_user(request.user)
revisions.set_comment(_("Posted comment"))
comment.save()
# add notification for reply
if comment.parent and comment.parent.author != comment.author:
notification_reply = Notification(
owner=comment.parent.author, comment=comment, category="Reply"
)
notification_reply.save()
# add notification for page authors
page_authors = comment.page_object.authors.all()
for user in page_authors:
if user == comment.author:
continue
notification = Notification(
owner=user, comment=comment, category="Comment"
)
notification.save()
# except Exception:
# pass
add_mention_notifications(comment)
return HttpResponseRedirect(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

@ -3,6 +3,4 @@ from judge.contest_format.default import DefaultContestFormat
from judge.contest_format.ecoo import ECOOContestFormat from judge.contest_format.ecoo import ECOOContestFormat
from judge.contest_format.icpc import ICPCContestFormat from judge.contest_format.icpc import ICPCContestFormat
from judge.contest_format.ioi import IOIContestFormat from judge.contest_format.ioi import IOIContestFormat
from judge.contest_format.new_ioi import NewIOIContestFormat
from judge.contest_format.ultimate import UltimateContestFormat
from judge.contest_format.registry import choices, formats from judge.contest_format.registry import choices, formats

View file

@ -10,7 +10,7 @@ from django.utils.translation import gettext_lazy
from judge.contest_format.default import DefaultContestFormat from judge.contest_format.default import DefaultContestFormat
from judge.contest_format.registry import register_contest_format from judge.contest_format.registry import register_contest_format
from judge.timezone import from_database_time, to_database_time from judge.timezone import from_database_time
from judge.utils.timedelta import nice_repr from judge.utils.timedelta import nice_repr
@ -54,10 +54,6 @@ class AtCoderContestFormat(DefaultContestFormat):
points = 0 points = 0
format_data = {} format_data = {}
frozen_time = self.contest.end_time
if self.contest.freeze_after:
frozen_time = participation.start + self.contest.freeze_after
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute( cursor.execute(
""" """
@ -70,10 +66,9 @@ class AtCoderContestFormat(DefaultContestFormat):
FROM judge_contestproblem cp INNER JOIN FROM judge_contestproblem cp INNER JOIN
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
judge_submission sub ON (sub.id = cs.submission_id) judge_submission sub ON (sub.id = cs.submission_id)
WHERE sub.date < %s
GROUP BY cp.id GROUP BY cp.id
""", """,
(participation.id, participation.id, to_database_time(frozen_time)), (participation.id, participation.id),
) )
for score, time, prob in cursor.fetchall(): for score, time, prob in cursor.fetchall():
@ -105,14 +100,13 @@ class AtCoderContestFormat(DefaultContestFormat):
format_data[str(prob)] = {"time": dt, "points": score, "penalty": prev} format_data[str(prob)] = {"time": dt, "points": score, "penalty": prev}
points += score points += score
self.handle_frozen_state(participation, format_data)
participation.cumtime = cumtime + penalty participation.cumtime = cumtime + penalty
participation.score = round(points, self.contest.points_precision) participation.score = points
participation.tiebreaker = 0 participation.tiebreaker = 0
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()
def display_user_problem(self, participation, contest_problem, show_final=False): def display_user_problem(self, participation, contest_problem):
format_data = (participation.format_data or {}).get(str(contest_problem.id)) format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data: if format_data:
penalty = ( penalty = (
@ -120,11 +114,11 @@ class AtCoderContestFormat(DefaultContestFormat):
'<small style="color:red"> ({penalty})</small>', '<small style="color:red"> ({penalty})</small>',
penalty=floatformat(format_data["penalty"]), penalty=floatformat(format_data["penalty"]),
) )
if format_data.get("penalty") if format_data["penalty"]
else "" else ""
) )
return format_html( return format_html(
'<td class="{state} problem-score-col"><a data-featherlight="{url}" href="#">{points}{penalty}<div class="solving-time">{time}</div></a></td>', '<td class="{state} problem-score-col"><a href="{url}">{points}{penalty}<div class="solving-time">{time}</div></a></td>',
state=( state=(
( (
"pretest-" "pretest-"
@ -135,13 +129,12 @@ class AtCoderContestFormat(DefaultContestFormat):
+ self.best_solution_state( + self.best_solution_state(
format_data["points"], contest_problem.points format_data["points"], contest_problem.points
) )
+ (" frozen" if format_data.get("frozen") else "")
), ),
url=reverse( url=reverse(
"contest_user_submissions_ajax", "contest_user_submissions",
args=[ args=[
self.contest.key, self.contest.key,
participation.id, participation.user.user.username,
contest_problem.problem.code, contest_problem.problem.code,
], ],
), ),

View file

@ -1,5 +1,6 @@
from abc import ABCMeta, abstractmethod, abstractproperty from abc import ABCMeta, abstractmethod, abstractproperty
from django.db.models import Max
from django.utils import six
class abstractclassmethod(classmethod): class abstractclassmethod(classmethod):
@ -10,9 +11,7 @@ class abstractclassmethod(classmethod):
super(abstractclassmethod, self).__init__(callable) super(abstractclassmethod, self).__init__(callable)
class BaseContestFormat(metaclass=ABCMeta): class BaseContestFormat(six.with_metaclass(ABCMeta)):
has_hidden_subtasks = False
@abstractmethod @abstractmethod
def __init__(self, contest, config): def __init__(self, contest, config):
self.config = config self.config = config
@ -50,7 +49,7 @@ class BaseContestFormat(metaclass=ABCMeta):
raise NotImplementedError() raise NotImplementedError()
@abstractmethod @abstractmethod
def display_user_problem(self, participation, contest_problem, show_final): def display_user_problem(self, participation, contest_problem):
""" """
Returns the HTML fragment to show a user's performance on an individual problem. This is expected to use Returns the HTML fragment to show a user's performance on an individual problem. This is expected to use
information from the format_data field instead of computing it from scratch. information from the format_data field instead of computing it from scratch.
@ -62,7 +61,7 @@ class BaseContestFormat(metaclass=ABCMeta):
raise NotImplementedError() raise NotImplementedError()
@abstractmethod @abstractmethod
def display_participation_result(self, participation, show_final): def display_participation_result(self, participation):
""" """
Returns the HTML fragment to show a user's performance on the whole contest. This is expected to use Returns the HTML fragment to show a user's performance on the whole contest. This is expected to use
information from the format_data field instead of computing it from scratch. information from the format_data field instead of computing it from scratch.
@ -98,26 +97,3 @@ class BaseContestFormat(metaclass=ABCMeta):
if points == total: if points == total:
return "full-score" return "full-score"
return "partial-score" return "partial-score"
def handle_frozen_state(self, participation, format_data):
hidden_subtasks = {}
if hasattr(self, "get_hidden_subtasks"):
hidden_subtasks = self.get_hidden_subtasks()
queryset = participation.submissions.values("problem_id").annotate(
time=Max("submission__date")
)
for result in queryset:
problem = str(result["problem_id"])
if not (self.contest.freeze_after or hidden_subtasks.get(problem)):
continue
if format_data.get(problem):
is_after_freeze = (
self.contest.freeze_after
and result["time"]
>= self.contest.freeze_after + participation.start
)
if is_after_freeze or hidden_subtasks.get(problem):
format_data[problem]["frozen"] = True
else:
format_data[problem] = {"time": 0, "points": 0, "frozen": True}

View file

@ -32,19 +32,10 @@ class DefaultContestFormat(BaseContestFormat):
points = 0 points = 0
format_data = {} format_data = {}
queryset = participation.submissions for result in participation.submissions.values("problem_id").annotate(
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"), time=Max("submission__date"),
points=Max("points"), points=Max("points"),
) ):
for result in queryset:
dt = (result["time"] - participation.start).total_seconds() dt = (result["time"] - participation.start).total_seconds()
if result["points"]: if result["points"]:
cumtime += dt cumtime += dt
@ -54,14 +45,13 @@ class DefaultContestFormat(BaseContestFormat):
} }
points += result["points"] points += result["points"]
self.handle_frozen_state(participation, format_data)
participation.cumtime = max(cumtime, 0) participation.cumtime = max(cumtime, 0)
participation.score = round(points, self.contest.points_precision) participation.score = points
participation.tiebreaker = 0 participation.tiebreaker = 0
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()
def display_user_problem(self, participation, contest_problem, show_final=False): def display_user_problem(self, participation, contest_problem):
format_data = (participation.format_data or {}).get(str(contest_problem.id)) format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data: if format_data:
return format_html( return format_html(
@ -76,13 +66,12 @@ class DefaultContestFormat(BaseContestFormat):
+ self.best_solution_state( + self.best_solution_state(
format_data["points"], contest_problem.points format_data["points"], contest_problem.points
) )
+ (" frozen" if format_data.get("frozen") else "")
), ),
url=reverse( url=reverse(
"contest_user_submissions_ajax", "contest_user_submissions_ajax",
args=[ args=[
self.contest.key, self.contest.key,
participation.id, participation.user.user.username,
contest_problem.problem.code, contest_problem.problem.code,
], ],
), ),
@ -94,7 +83,7 @@ class DefaultContestFormat(BaseContestFormat):
else: else:
return mark_safe('<td class="problem-score-col"></td>') return mark_safe('<td class="problem-score-col"></td>')
def display_participation_result(self, participation, show_final=False): def display_participation_result(self, participation):
return format_html( return format_html(
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>', '<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
points=floatformat(participation.score, -self.contest.points_precision), points=floatformat(participation.score, -self.contest.points_precision),

View file

@ -10,7 +10,7 @@ from django.utils.translation import gettext_lazy
from judge.contest_format.default import DefaultContestFormat from judge.contest_format.default import DefaultContestFormat
from judge.contest_format.registry import register_contest_format from judge.contest_format.registry import register_contest_format
from judge.timezone import from_database_time, to_database_time from judge.timezone import from_database_time
from judge.utils.timedelta import nice_repr from judge.utils.timedelta import nice_repr
@ -60,10 +60,6 @@ class ECOOContestFormat(DefaultContestFormat):
points = 0 points = 0
format_data = {} format_data = {}
frozen_time = self.contest.end_time
if self.contest.freeze_after:
frozen_time = participation.start + self.contest.freeze_after
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute( cursor.execute(
""" """
@ -81,15 +77,9 @@ class ECOOContestFormat(DefaultContestFormat):
FROM judge_contestproblem cp INNER JOIN FROM judge_contestproblem cp INNER JOIN
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
judge_submission sub ON (sub.id = cs.submission_id) judge_submission sub ON (sub.id = cs.submission_id)
WHERE sub.date < %s
GROUP BY cp.id GROUP BY cp.id
""", """,
( (participation.id, participation.id, participation.id),
participation.id,
participation.id,
participation.id,
to_database_time(frozen_time),
),
) )
for score, time, prob, subs, max_score in cursor.fetchall(): for score, time, prob, subs, max_score in cursor.fetchall():
@ -115,26 +105,25 @@ class ECOOContestFormat(DefaultContestFormat):
format_data[str(prob)] = {"time": dt, "points": score, "bonus": bonus} format_data[str(prob)] = {"time": dt, "points": score, "bonus": bonus}
points += score points += score
self.handle_frozen_state(participation, format_data)
participation.cumtime = cumtime participation.cumtime = cumtime
participation.score = round(points, self.contest.points_precision) participation.score = points
participation.tiebreaker = 0 participation.tiebreaker = 0
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()
def display_user_problem(self, participation, contest_problem, show_final=False): def display_user_problem(self, participation, contest_problem):
format_data = (participation.format_data or {}).get(str(contest_problem.id)) format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data: if format_data:
bonus = ( bonus = (
format_html( format_html(
"<small> +{bonus}</small>", bonus=floatformat(format_data["bonus"]) "<small> +{bonus}</small>", bonus=floatformat(format_data["bonus"])
) )
if format_data.get("bonus") if format_data["bonus"]
else "" else ""
) )
return format_html( return format_html(
'<td class="{state}"><a data-featherlight="{url}" href="#">{points}{bonus}<div class="solving-time">{time}</div></a></td>', '<td class="{state}"><a href="{url}">{points}{bonus}<div class="solving-time">{time}</div></a></td>',
state=( state=(
( (
"pretest-" "pretest-"
@ -145,13 +134,12 @@ class ECOOContestFormat(DefaultContestFormat):
+ self.best_solution_state( + self.best_solution_state(
format_data["points"], contest_problem.points format_data["points"], contest_problem.points
) )
+ (" frozen" if format_data.get("frozen") else "")
), ),
url=reverse( url=reverse(
"contest_user_submissions_ajax", "contest_user_submissions",
args=[ args=[
self.contest.key, self.contest.key,
participation.id, participation.user.user.username,
contest_problem.problem.code, contest_problem.problem.code,
], ],
), ),
@ -162,7 +150,7 @@ class ECOOContestFormat(DefaultContestFormat):
else: else:
return mark_safe("<td></td>") return mark_safe("<td></td>")
def display_participation_result(self, participation, show_final=False): def display_participation_result(self, participation):
return format_html( return format_html(
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>', '<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
points=floatformat(participation.score), points=floatformat(participation.score),

View file

@ -10,7 +10,7 @@ from django.utils.translation import gettext_lazy
from judge.contest_format.default import DefaultContestFormat from judge.contest_format.default import DefaultContestFormat
from judge.contest_format.registry import register_contest_format from judge.contest_format.registry import register_contest_format
from judge.timezone import from_database_time, to_database_time from judge.timezone import from_database_time
from judge.utils.timedelta import nice_repr from judge.utils.timedelta import nice_repr
@ -55,10 +55,6 @@ class ICPCContestFormat(DefaultContestFormat):
score = 0 score = 0
format_data = {} format_data = {}
frozen_time = self.contest.end_time
if self.contest.freeze_after:
frozen_time = participation.start + self.contest.freeze_after
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute( cursor.execute(
""" """
@ -71,10 +67,9 @@ class ICPCContestFormat(DefaultContestFormat):
FROM judge_contestproblem cp INNER JOIN FROM judge_contestproblem cp INNER JOIN
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
judge_submission sub ON (sub.id = cs.submission_id) judge_submission sub ON (sub.id = cs.submission_id)
WHERE sub.date < %s
GROUP BY cp.id GROUP BY cp.id
""", """,
(participation.id, participation.id, to_database_time(frozen_time)), (participation.id, participation.id),
) )
for points, time, prob in cursor.fetchall(): for points, time, prob in cursor.fetchall():
@ -107,26 +102,25 @@ class ICPCContestFormat(DefaultContestFormat):
format_data[str(prob)] = {"time": dt, "points": points, "penalty": prev} format_data[str(prob)] = {"time": dt, "points": points, "penalty": prev}
score += points score += points
self.handle_frozen_state(participation, format_data)
participation.cumtime = max(0, cumtime + penalty) participation.cumtime = max(0, cumtime + penalty)
participation.score = round(score, self.contest.points_precision) participation.score = score
participation.tiebreaker = last # field is sorted from least to greatest participation.tiebreaker = last # field is sorted from least to greatest
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()
def display_user_problem(self, participation, contest_problem, show_final=False): def display_user_problem(self, participation, contest_problem):
format_data = (participation.format_data or {}).get(str(contest_problem.id)) format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data: if format_data:
penalty = ( penalty = (
format_html( format_html(
'<small style="color:red"> +{penalty}</small>', '<small style="color:red"> ({penalty})</small>',
penalty=floatformat(format_data["penalty"]), penalty=floatformat(format_data["penalty"]),
) )
if format_data.get("penalty") if format_data["penalty"]
else "" else ""
) )
return format_html( return format_html(
'<td class="{state}"><a data-featherlight="{url}" href="#">{points}{penalty}<div class="solving-time">{time}</div></a></td>', '<td class="{state}"><a href="{url}">{points}{penalty}<div class="solving-time">{time}</div></a></td>',
state=( state=(
( (
"pretest-" "pretest-"
@ -137,13 +131,12 @@ class ICPCContestFormat(DefaultContestFormat):
+ self.best_solution_state( + self.best_solution_state(
format_data["points"], contest_problem.points format_data["points"], contest_problem.points
) )
+ (" frozen" if format_data.get("frozen") else "")
), ),
url=reverse( url=reverse(
"contest_user_submissions_ajax", "contest_user_submissions",
args=[ args=[
self.contest.key, self.contest.key,
participation.id, participation.user.user.username,
contest_problem.problem.code, contest_problem.problem.code,
], ],
), ),

View file

@ -12,7 +12,6 @@ from judge.contest_format.default import DefaultContestFormat
from judge.contest_format.registry import register_contest_format from judge.contest_format.registry import register_contest_format
from judge.timezone import from_database_time from judge.timezone import from_database_time
from judge.utils.timedelta import nice_repr from judge.utils.timedelta import nice_repr
from django.db.models import Min, OuterRef, Subquery
@register_contest_format("ioi") @register_contest_format("ioi")
@ -46,56 +45,50 @@ class IOIContestFormat(DefaultContestFormat):
def update_participation(self, participation): def update_participation(self, participation):
cumtime = 0 cumtime = 0
score = 0 points = 0
format_data = {} format_data = {}
queryset = participation.submissions with connection.cursor() as cursor:
if self.contest.freeze_after: cursor.execute(
queryset = queryset.filter( """
submission__date__lt=participation.start + self.contest.freeze_after 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 = ( for score, time, prob in cursor.fetchall():
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 problem_id, time, points in queryset:
if self.config["cumtime"]: if self.config["cumtime"]:
dt = (time - participation.start).total_seconds() dt = (
if points: from_database_time(time) - participation.start
).total_seconds()
if score:
cumtime += dt cumtime += dt
else: else:
dt = 0 dt = 0
format_data[str(problem_id)] = {"points": points, "time": dt} format_data[str(prob)] = {"time": dt, "points": score}
score += points points += score
self.handle_frozen_state(participation, format_data)
participation.cumtime = max(cumtime, 0) participation.cumtime = max(cumtime, 0)
participation.score = round(score, self.contest.points_precision) participation.score = points
participation.tiebreaker = 0 participation.tiebreaker = 0
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()
def display_user_problem(self, participation, contest_problem, show_final=False): def display_user_problem(self, participation, contest_problem):
if show_final:
format_data = (participation.format_data_final or {}).get(
str(contest_problem.id)
)
else:
format_data = (participation.format_data or {}).get(str(contest_problem.id)) format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data: if format_data:
return format_html( return format_html(
'<td class="{state} problem-score-col"><a data-featherlight="{url}" href="#">{points}<div class="solving-time">{time}</div></a></td>', '<td class="{state} problem-score-col"><a href="{url}">{points}<div class="solving-time">{time}</div></a></td>',
state=( state=(
( (
"pretest-" "pretest-"
@ -106,19 +99,16 @@ class IOIContestFormat(DefaultContestFormat):
+ self.best_solution_state( + self.best_solution_state(
format_data["points"], contest_problem.points format_data["points"], contest_problem.points
) )
+ (" frozen" if format_data.get("frozen") else "")
), ),
url=reverse( url=reverse(
"contest_user_submissions_ajax", "contest_user_submissions",
args=[ args=[
self.contest.key, self.contest.key,
participation.id, participation.user.user.username,
contest_problem.problem.code, contest_problem.problem.code,
], ],
), ),
points=floatformat( points=floatformat(format_data["points"]),
format_data["points"], -self.contest.points_precision
),
time=nice_repr(timedelta(seconds=format_data["time"]), "noday") time=nice_repr(timedelta(seconds=format_data["time"]), "noday")
if self.config["cumtime"] if self.config["cumtime"]
else "", else "",
@ -126,17 +116,11 @@ class IOIContestFormat(DefaultContestFormat):
else: else:
return mark_safe('<td class="problem-score-col"></td>') return mark_safe('<td class="problem-score-col"></td>')
def display_participation_result(self, participation, show_final=False): def display_participation_result(self, participation):
if show_final:
score = participation.score_final
cumtime = participation.cumtime_final
else:
score = participation.score
cumtime = participation.cumtime
return format_html( return format_html(
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>', '<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
points=floatformat(score, -self.contest.points_precision), points=floatformat(participation.score),
cumtime=nice_repr(timedelta(seconds=cumtime), "noday") cumtime=nice_repr(timedelta(seconds=participation.cumtime), "noday")
if self.config["cumtime"] if self.config["cumtime"]
else "", 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 = {} formats = {}
@ -11,4 +13,4 @@ def register_contest_format(name):
def choices(): def choices():
return [(key, value.name) for key, value in sorted(formats.items())] return [(key, value.name) for key, value in sorted(six.iteritems(formats))]

View file

@ -1,55 +0,0 @@
from django.utils.translation import gettext_lazy
from judge.contest_format.ioi import IOIContestFormat
from judge.contest_format.registry import register_contest_format
from django.db.models import Min, OuterRef, Subquery
# This contest format only counts last submission for each problem.
@register_contest_format("ultimate")
class UltimateContestFormat(IOIContestFormat):
name = gettext_lazy("Ultimate")
def update_participation(self, participation):
cumtime = 0
score = 0
format_data = {}
queryset = participation.submissions
if self.contest.freeze_after:
queryset = queryset.filter(
submission__date__lt=participation.start + self.contest.freeze_after
)
queryset = (
queryset.values("problem_id")
.filter(
id=Subquery(
queryset.filter(problem_id=OuterRef("problem_id"))
.order_by("-id")
.values("id")[:1]
)
)
.values_list("problem_id", "submission__date", "points")
)
for problem_id, time, points in queryset:
if self.config["cumtime"]:
dt = (time - participation.start).total_seconds()
if points:
cumtime += dt
else:
dt = 0
format_data[str(problem_id)] = {
"time": dt,
"points": points,
}
score += points
self.handle_frozen_state(participation, format_data)
participation.cumtime = max(cumtime, 0)
participation.score = round(score, self.contest.points_precision)
participation.tiebreaker = 0
participation.format_data = format_data
participation.save()

View file

@ -1,22 +0,0 @@
from django.utils.translation import gettext_lazy as _, ngettext
def custom_trans():
return [
# Password reset
ngettext(
"This password is too short. It must contain at least %(min_length)d character.",
"This password is too short. It must contain at least %(min_length)d characters.",
0,
),
ngettext(
"Your password must contain at least %(min_length)d character.",
"Your password must contain at least %(min_length)d characters.",
0,
),
_("The two password fields didnt match."),
_("Your password cant be entirely numeric."),
# Navbar
_("Bug Report"),
_("Courses"),
]

View file

@ -16,7 +16,7 @@ class EventPoster(object):
def _connect(self): def _connect(self):
self._conn = pika.BlockingConnection( self._conn = pika.BlockingConnection(
pika.URLParameters(settings.EVENT_DAEMON_AMQP), pika.URLParameters(settings.EVENT_DAEMON_AMQP)
) )
self._chan = self._conn.channel() self._chan = self._conn.channel()
@ -25,7 +25,7 @@ class EventPoster(object):
id = int(time() * 1000000) id = int(time() * 1000000)
self._chan.basic_publish( self._chan.basic_publish(
self._exchange, self._exchange,
"#", "",
json.dumps({"id": id, "channel": channel, "message": message}), json.dumps({"id": id, "channel": channel, "message": message}),
) )
return id return id

120
judge/feed.py Normal file
View file

@ -0,0 +1,120 @@
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.contrib.syndication.views import Feed
from django.core.cache import cache
from django.utils import timezone
from django.utils.feedgenerator import Atom1Feed
from judge.jinja2.markdown import markdown
from judge.models import BlogPost, Comment, Problem
import re
# https://lsimons.wordpress.com/2011/03/17/stripping-illegal-characters-out-of-xml-in-python/
def escape_xml_illegal_chars(val, replacement="?"):
_illegal_xml_chars_RE = re.compile(
"[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]"
)
return _illegal_xml_chars_RE.sub(replacement, val)
class ProblemFeed(Feed):
title = "Recently Added %s Problems" % settings.SITE_NAME
link = "/"
description = (
"The latest problems added on the %s website" % settings.SITE_LONG_NAME
)
def items(self):
return (
Problem.objects.filter(is_public=True, is_organization_private=False)
.defer("description")
.order_by("-date", "-id")[:25]
)
def item_title(self, problem):
return problem.name
def item_description(self, problem):
key = "problem_feed:%d" % problem.id
desc = cache.get(key)
if desc is None:
desc = str(markdown(problem.description, "problem"))[:500] + "..."
desc = escape_xml_illegal_chars(desc)
cache.set(key, desc, 86400)
return desc
def item_pubdate(self, problem):
return problem.date
item_updateddate = item_pubdate
class AtomProblemFeed(ProblemFeed):
feed_type = Atom1Feed
subtitle = ProblemFeed.description
class CommentFeed(Feed):
title = "Latest %s Comments" % settings.SITE_NAME
link = "/"
description = "The latest comments on the %s website" % settings.SITE_LONG_NAME
def items(self):
return Comment.most_recent(AnonymousUser(), 25)
def item_title(self, comment):
return "%s -> %s" % (comment.author.user.username, comment.page_title)
def item_description(self, comment):
key = "comment_feed:%d" % comment.id
desc = cache.get(key)
if desc is None:
desc = str(markdown(comment.body, "comment"))
desc = escape_xml_illegal_chars(desc)
cache.set(key, desc, 86400)
return desc
def item_pubdate(self, comment):
return comment.time
item_updateddate = item_pubdate
class AtomCommentFeed(CommentFeed):
feed_type = Atom1Feed
subtitle = CommentFeed.description
class BlogFeed(Feed):
title = "Latest %s Blog Posts" % settings.SITE_NAME
link = "/"
description = "The latest blog posts from the %s" % settings.SITE_LONG_NAME
def items(self):
return BlogPost.objects.filter(
visible=True, publish_on__lte=timezone.now()
).order_by("-sticky", "-publish_on")
def item_title(self, post):
return post.title
def item_description(self, post):
key = "blog_feed:%d" % post.id
summary = cache.get(key)
if summary is None:
summary = str(markdown(post.summary or post.content, "blog"))
summary = escape_xml_illegal_chars(summary)
cache.set(key, summary, 86400)
return summary
def item_pubdate(self, post):
return post.publish_on
item_updateddate = item_pubdate
class AtomBlogFeed(BlogFeed):
feed_type = Atom1Feed
subtitle = BlogFeed.description

View file

@ -8,6 +8,7 @@
"ip": "10.0.2.2", "ip": "10.0.2.2",
"language": 1, "language": 1,
"last_access": "2017-12-02T08:57:10.093Z", "last_access": "2017-12-02T08:57:10.093Z",
"math_engine": "auto",
"mute": false, "mute": false,
"organizations": [ "organizations": [
1 1
@ -17,7 +18,8 @@
"problem_count": 0, "problem_count": 0,
"rating": null, "rating": null,
"timezone": "America/Toronto", "timezone": "America/Toronto",
"user": 1 "user": 1,
"user_script": ""
}, },
"model": "judge.profile", "model": "judge.profile",
"pk": 1 "pk": 1
@ -145,8 +147,25 @@
}, },
{ {
"fields": { "fields": {
"domain": "localhost:8000", "author": 1,
"name": "LQDOJ" "body": "This is your first comment!",
"hidden": false,
"level": 0,
"lft": 1,
"page": "b:1",
"parent": null,
"rght": 2,
"score": 0,
"time": "2017-12-02T08:46:54.007Z",
"tree_id": 1
},
"model": "judge.comment",
"pk": 1
},
{
"fields": {
"domain": "localhost:8081",
"name": "DMOJ: Modern Online Judge"
}, },
"model": "sites.site", "model": "sites.site",
"pk": 1 "pk": 1

View file

@ -1,27 +1,14 @@
import os
import secrets
from operator import attrgetter from operator import attrgetter
import pyotp
import time
import datetime
import pyotp
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
from django.core.exceptions import ValidationError, ObjectDoesNotExist from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db.models import Q from django.db.models import Q
from django.forms import ( from django.forms import CharField, ChoiceField, Form, ModelForm
CharField, from django.urls import reverse_lazy
ChoiceField,
Form,
ModelForm,
formset_factory,
BaseModelFormSet,
FileField,
)
from django.urls import reverse_lazy, reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils import timezone from django.utils import timezone
@ -29,7 +16,6 @@ from django_ace import AceWidget
from judge.models import ( from judge.models import (
Contest, Contest,
Language, Language,
TestFormatterModel,
Organization, Organization,
PrivateMessage, PrivateMessage,
Problem, Problem,
@ -37,22 +23,14 @@ from judge.models import (
Profile, Profile,
Submission, Submission,
BlogPost, BlogPost,
ContestProblem,
TestFormatterModel,
ProfileInfo,
) )
from judge.utils.subscription import newsletter_id
from judge.widgets import ( from judge.widgets import (
HeavyPreviewPageDownWidget, HeavyPreviewPageDownWidget,
MathJaxPagedownWidget,
PagedownWidget, PagedownWidget,
Select2MultipleWidget, Select2MultipleWidget,
Select2Widget, Select2Widget,
HeavySelect2MultipleWidget,
HeavySelect2Widget,
Select2MultipleWidget,
DateTimePickerWidget,
ImageWidget,
DatePickerWidget,
) )
@ -62,70 +40,63 @@ def fix_unicode(string, unsafe=tuple("\u202a\u202b\u202d\u202e")):
) )
class UserForm(ModelForm):
class Meta:
model = User
fields = [
"first_name",
"last_name",
]
class ProfileInfoForm(ModelForm):
class Meta:
model = ProfileInfo
fields = ["tshirt_size", "date_of_birth", "address"]
widgets = {
"tshirt_size": Select2Widget(attrs={"style": "width:100%"}),
"date_of_birth": DatePickerWidget,
"address": forms.TextInput(attrs={"style": "width:100%"}),
}
class ProfileForm(ModelForm): class ProfileForm(ModelForm):
if newsletter_id is not None:
newsletter = forms.BooleanField(
label=_("Subscribe to contest updates"), initial=False, required=False
)
test_site = forms.BooleanField(
label=_("Enable experimental features"), initial=False, required=False
)
class Meta: class Meta:
model = Profile model = Profile
fields = [ fields = [
"about", "about",
"organizations",
"timezone", "timezone",
"language", "language",
"ace_theme", "ace_theme",
"profile_image", "user_script",
"css_background",
] ]
widgets = { widgets = {
"user_script": AceWidget(theme="github"),
"timezone": Select2Widget(attrs={"style": "width:200px"}), "timezone": Select2Widget(attrs={"style": "width:200px"}),
"language": Select2Widget(attrs={"style": "width:200px"}), "language": Select2Widget(attrs={"style": "width:200px"}),
"ace_theme": Select2Widget(attrs={"style": "width:200px"}), "ace_theme": Select2Widget(attrs={"style": "width:200px"}),
"profile_image": ImageWidget,
"css_background": forms.TextInput(),
} }
has_math_config = bool(settings.MATHOID_URL)
if has_math_config:
fields.append("math_engine")
widgets["math_engine"] = Select2Widget(attrs={"style": "width:200px"})
if HeavyPreviewPageDownWidget is not None: if HeavyPreviewPageDownWidget is not None:
widgets["about"] = HeavyPreviewPageDownWidget( widgets["about"] = HeavyPreviewPageDownWidget(
preview=reverse_lazy("profile_preview"), preview=reverse_lazy("profile_preview"),
attrs={"style": "max-width:700px;min-width:700px;width:700px"}, attrs={"style": "max-width:700px;min-width:700px;width:700px"},
) )
def clean(self):
organizations = self.cleaned_data.get("organizations") or []
max_orgs = settings.DMOJ_USER_MAX_ORGANIZATION_COUNT
if sum(org.is_open for org in organizations) > max_orgs:
raise ValidationError(
_("You may not be part of more than {count} public groups.").format(
count=max_orgs
)
)
return self.cleaned_data
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
user = kwargs.pop("user", None) user = kwargs.pop("user", None)
super(ProfileForm, self).__init__(*args, **kwargs) super(ProfileForm, self).__init__(*args, **kwargs)
self.fields["profile_image"].required = False if not user.has_perm("judge.edit_all_organization"):
self.fields["organizations"].queryset = Organization.objects.filter(
def clean_profile_image(self): Q(is_open=True) | Q(id__in=user.profile.organizations.all()),
profile_image = self.cleaned_data.get("profile_image")
if profile_image:
if profile_image.size > 5 * 1024 * 1024:
raise ValidationError(
_("File size exceeds the maximum allowed limit of 5MB.")
) )
return profile_image
def file_size_validator(file):
limit = 10 * 1024 * 1024
if file.size > limit:
raise ValidationError("File too large. Size should not exceed 10MB.")
class ProblemSubmitForm(ModelForm): class ProblemSubmitForm(ModelForm):
@ -133,13 +104,9 @@ class ProblemSubmitForm(ModelForm):
max_length=65536, widget=AceWidget(theme="twilight", no_ace_media=True) max_length=65536, widget=AceWidget(theme="twilight", no_ace_media=True)
) )
judge = ChoiceField(choices=(), widget=forms.HiddenInput(), required=False) judge = ChoiceField(choices=(), widget=forms.HiddenInput(), required=False)
source_file = FileField(required=False, validators=[file_size_validator])
def __init__(self, *args, judge_choices=(), request=None, problem=None, **kwargs): def __init__(self, *args, judge_choices=(), **kwargs):
super(ProblemSubmitForm, self).__init__(*args, **kwargs) super(ProblemSubmitForm, self).__init__(*args, **kwargs)
self.source_file_name = None
self.request = request
self.problem = problem
self.fields["language"].empty_label = None self.fields["language"].empty_label = None
self.fields["language"].label_from_instance = attrgetter("display_name") self.fields["language"].label_from_instance = attrgetter("display_name")
self.fields["language"].queryset = Language.objects.filter( self.fields["language"].queryset = Language.objects.filter(
@ -152,36 +119,6 @@ class ProblemSubmitForm(ModelForm):
) )
self.fields["judge"].choices = judge_choices self.fields["judge"].choices = judge_choices
def allow_url_as_source(self):
key = self.cleaned_data["language"].key
filename = self.files["source_file"].name
if key == "OUTPUT" and self.problem.data_files.output_only:
return filename.endswith(".zip")
if key == "SCAT":
return filename.endswith(".sb3")
return False
def clean(self):
if "source_file" in self.files:
if self.allow_url_as_source():
filename = self.files["source_file"].name
now = datetime.datetime.now()
timestamp = str(int(time.mktime(now.timetuple())))
self.source_file_name = (
timestamp + secrets.token_hex(5) + "." + filename.split(".")[-1]
)
filepath = os.path.join(
settings.DMOJ_SUBMISSION_ROOT, self.source_file_name
)
with open(filepath, "wb+") as destination:
for chunk in self.files["source_file"].chunks():
destination.write(chunk)
self.cleaned_data["source"] = self.request.build_absolute_uri(
reverse("submission_source_file", args=(self.source_file_name,))
)
del self.files["source_file"]
return self.cleaned_data
class Meta: class Meta:
model = Submission model = Submission
fields = ["language"] fields = ["language"]
@ -190,176 +127,13 @@ class ProblemSubmitForm(ModelForm):
class EditOrganizationForm(ModelForm): class EditOrganizationForm(ModelForm):
class Meta: class Meta:
model = Organization model = Organization
fields = [ fields = ["about", "logo_override_image", "admins", "is_open"]
"name", widgets = {"admins": Select2MultipleWidget()}
"slug",
"short_name",
"about",
"organization_image",
"admins",
"is_open",
]
widgets = {
"admins": Select2MultipleWidget(),
"organization_image": ImageWidget,
}
if HeavyPreviewPageDownWidget is not None: if HeavyPreviewPageDownWidget is not None:
widgets["about"] = HeavyPreviewPageDownWidget( widgets["about"] = HeavyPreviewPageDownWidget(
preview=reverse_lazy("organization_preview") preview=reverse_lazy("organization_preview")
) )
def __init__(self, *args, **kwargs):
super(EditOrganizationForm, self).__init__(*args, **kwargs)
self.fields["organization_image"].required = False
def clean_organization_image(self):
organization_image = self.cleaned_data.get("organization_image")
if organization_image:
if organization_image.size > 5 * 1024 * 1024:
raise ValidationError(
_("File size exceeds the maximum allowed limit of 5MB.")
)
return organization_image
class AddOrganizationForm(ModelForm):
class Meta:
model = Organization
fields = [
"name",
"slug",
"short_name",
"about",
"organization_image",
"is_open",
]
widgets = {}
if HeavyPreviewPageDownWidget is not None:
widgets["about"] = HeavyPreviewPageDownWidget(
preview=reverse_lazy("organization_preview")
)
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request", None)
super(AddOrganizationForm, self).__init__(*args, **kwargs)
self.fields["organization_image"].required = False
def save(self, commit=True):
res = super(AddOrganizationForm, self).save(commit=False)
res.registrant = self.request.profile
if commit:
res.save()
return res
class AddOrganizationContestForm(ModelForm):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request", None)
super(AddOrganizationContestForm, self).__init__(*args, **kwargs)
def save(self, commit=True):
contest = super(AddOrganizationContestForm, self).save(commit=False)
old_save_m2m = self.save_m2m
def save_m2m():
for i, problem in enumerate(self.cleaned_data["problems"]):
contest_problem = ContestProblem(
contest=contest, problem=problem, points=100, order=i + 1
)
contest_problem.save()
contest.contest_problems.add(contest_problem)
old_save_m2m()
self.save_m2m = save_m2m
contest.save()
self.save_m2m()
return contest
class Meta:
model = Contest
fields = (
"key",
"name",
"start_time",
"end_time",
"problems",
)
widgets = {
"start_time": DateTimePickerWidget(),
"end_time": DateTimePickerWidget(),
"problems": HeavySelect2MultipleWidget(data_view="problem_select2"),
}
class EditOrganizationContestForm(ModelForm):
def __init__(self, *args, **kwargs):
self.org_id = kwargs.pop("org_id", 0)
super(EditOrganizationContestForm, self).__init__(*args, **kwargs)
for field in [
"authors",
"curators",
"testers",
"private_contestants",
"banned_users",
"view_contest_scoreboard",
]:
self.fields[field].widget.data_url = (
self.fields[field].widget.get_url() + f"?org_id={self.org_id}"
)
class Meta:
model = Contest
fields = (
"is_visible",
"key",
"name",
"start_time",
"end_time",
"format_name",
"authors",
"curators",
"testers",
"time_limit",
"freeze_after",
"use_clarifications",
"hide_problem_tags",
"public_scoreboard",
"scoreboard_visibility",
"points_precision",
"rate_limit",
"description",
"og_image",
"logo_override_image",
"summary",
"access_code",
"private_contestants",
"view_contest_scoreboard",
"banned_users",
)
widgets = {
"authors": HeavySelect2MultipleWidget(data_view="profile_select2"),
"curators": HeavySelect2MultipleWidget(data_view="profile_select2"),
"testers": HeavySelect2MultipleWidget(data_view="profile_select2"),
"private_contestants": HeavySelect2MultipleWidget(
data_view="profile_select2"
),
"banned_users": HeavySelect2MultipleWidget(data_view="profile_select2"),
"view_contest_scoreboard": HeavySelect2MultipleWidget(
data_view="profile_select2"
),
"organizations": HeavySelect2MultipleWidget(
data_view="organization_select2"
),
"tags": Select2MultipleWidget,
"description": HeavyPreviewPageDownWidget(
preview=reverse_lazy("contest_preview")
),
"start_time": DateTimePickerWidget(),
"end_time": DateTimePickerWidget(),
"format_name": Select2Widget(),
"scoreboard_visibility": Select2Widget(),
}
class AddOrganizationMemberForm(ModelForm): class AddOrganizationMemberForm(ModelForm):
new_users = CharField( new_users = CharField(
@ -369,7 +143,7 @@ class AddOrganizationMemberForm(ModelForm):
label=_("New users"), label=_("New users"),
) )
def clean_new_users(self): def clean(self):
new_users = self.cleaned_data.get("new_users") or "" new_users = self.cleaned_data.get("new_users") or ""
usernames = new_users.split() usernames = new_users.split()
invalid_usernames = [] invalid_usernames = []
@ -387,7 +161,8 @@ class AddOrganizationMemberForm(ModelForm):
usernames=str(invalid_usernames) usernames=str(invalid_usernames)
) )
) )
return valid_usernames self.cleaned_data["new_users"] = valid_usernames
return self.cleaned_data
class Meta: class Meta:
model = Organization model = Organization
@ -435,15 +210,13 @@ class NewMessageForm(ModelForm):
fields = ["title", "content"] fields = ["title", "content"]
widgets = {} widgets = {}
if PagedownWidget is not None: if PagedownWidget is not None:
widgets["content"] = PagedownWidget() widgets["content"] = MathJaxPagedownWidget()
class CustomAuthenticationForm(AuthenticationForm): class CustomAuthenticationForm(AuthenticationForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(CustomAuthenticationForm, self).__init__(*args, **kwargs) super(CustomAuthenticationForm, self).__init__(*args, **kwargs)
self.fields["username"].widget.attrs.update( self.fields["username"].widget.attrs.update({"placeholder": _("Username")})
{"placeholder": _("Username/Email")}
)
self.fields["password"].widget.attrs.update({"placeholder": _("Password")}) self.fields["password"].widget.attrs.update({"placeholder": _("Password")})
self.has_google_auth = self._has_social_auth("GOOGLE_OAUTH2") self.has_google_auth = self._has_social_auth("GOOGLE_OAUTH2")
@ -506,15 +279,6 @@ class ContestCloneForm(Form):
max_length=20, max_length=20,
validators=[RegexValidator("^[a-z0-9]+$", _("Contest id must be ^[a-z0-9]+$"))], validators=[RegexValidator("^[a-z0-9]+$", _("Contest id must be ^[a-z0-9]+$"))],
) )
organization = ChoiceField(choices=(), required=True)
def __init__(self, *args, org_choices=(), profile=None, **kwargs):
super(ContestCloneForm, self).__init__(*args, **kwargs)
self.fields["organization"].widget = Select2Widget(
attrs={"style": "width: 100%", "data-placeholder": _("Group")},
)
self.fields["organization"].choices = org_choices
self.profile = profile
def clean_key(self): def clean_key(self):
key = self.cleaned_data["key"] key = self.cleaned_data["key"]
@ -522,78 +286,8 @@ class ContestCloneForm(Form):
raise ValidationError(_("Contest with key already exists.")) raise ValidationError(_("Contest with key already exists."))
return key return key
def clean_organization(self):
organization_id = self.cleaned_data["organization"]
try:
organization = Organization.objects.get(id=organization_id)
except Exception:
raise ValidationError(_("Group doesn't exist."))
if not organization.admins.filter(id=self.profile.id).exists():
raise ValidationError(_("You don't have permission in this group."))
return organization
class ProblemPointsVoteForm(ModelForm): class ProblemPointsVoteForm(ModelForm):
class Meta: class Meta:
model = ProblemPointsVote model = ProblemPointsVote
fields = ["points"] fields = ["points"]
class ContestProblemForm(ModelForm):
class Meta:
model = ContestProblem
fields = (
"order",
"problem",
"points",
"partial",
"show_testcases",
"max_submissions",
)
widgets = {
"problem": HeavySelect2Widget(
data_view="problem_select2", attrs={"style": "width: 100%"}
),
}
class ContestProblemModelFormSet(BaseModelFormSet):
def is_valid(self):
valid = super().is_valid()
if not valid:
return valid
problems = set()
duplicates = []
for form in self.forms:
if form.cleaned_data and not form.cleaned_data.get("DELETE", False):
problem = form.cleaned_data.get("problem")
if problem in problems:
duplicates.append(problem)
else:
problems.add(problem)
if duplicates:
for form in self.forms:
problem = form.cleaned_data.get("problem")
if problem in duplicates:
form.add_error("problem", _("This problem is duplicated."))
return False
return True
class ContestProblemFormSet(
formset_factory(
ContestProblemForm, formset=ContestProblemModelFormSet, extra=6, can_delete=True
)
):
model = ContestProblem
class TestFormatterForm(ModelForm):
class Meta:
model = TestFormatterModel
fields = ["file"]

View file

@ -1,13 +1,47 @@
from django.utils.html import escape, mark_safe from django.utils.html import escape, mark_safe
from judge.markdown import markdown
__all__ = ["highlight_code"] __all__ = ["highlight_code"]
def highlight_code(code, language, linenos=True, title=None): def _make_pre_code(code):
linenos_option = 'linenums="1"' if linenos else "" return mark_safe("<pre>" + escape(code) + "</pre>")
title_option = f'title="{title}"' if title else ""
options = f"{{.{language} {linenos_option} {title_option}}}"
value = f"```{options}\n{code}\n```\n"
return mark_safe(markdown(value)) def _wrap_code(inner):
yield 0, "<code>"
for tup in inner:
yield tup
yield 0, "</code>"
try:
import pygments
import pygments.lexers
import pygments.formatters.html
import pygments.util
except ImportError:
def highlight_code(code, language, cssclass=None):
return _make_pre_code(code)
else:
class HtmlCodeFormatter(pygments.formatters.HtmlFormatter):
def wrap(self, source, outfile):
return self._wrap_div(self._wrap_pre(_wrap_code(source)))
def highlight_code(code, language, cssclass="codehilite", linenos=True):
try:
lexer = pygments.lexers.get_lexer_by_name(language)
except pygments.util.ClassNotFound:
return _make_pre_code(code)
if linenos:
return mark_safe(
pygments.highlight(
code, lexer, HtmlCodeFormatter(cssclass=cssclass, linenos="table")
)
)
return mark_safe(
pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass))
)

View file

@ -21,8 +21,8 @@ from . import (
render, render,
social, social,
spaceless, spaceless,
submission,
timedelta, timedelta,
comment,
) )
from . import registry from . import registry

View file

@ -1,12 +0,0 @@
from . import registry
from django.contrib.contenttypes.models import ContentType
from judge.models.comment import get_visible_comment_count
from judge.caching import cache_wrapper
@registry.function
def comment_count(obj):
content_type = ContentType.objects.get_for_model(obj)
return get_visible_comment_count(content_type, obj.pk)

View file

@ -23,5 +23,5 @@ registry.filter(localtime_wrapper(time))
@registry.function @registry.function
@registry.render_with("widgets/relative-time.html") @registry.render_with("widgets/relative-time.html")
def relative_time(time, format=_("N j, Y, g:i a"), rel=_("{time}"), abs=_("{time}")): def relative_time(time, format=_("N j, Y, g:i a"), rel=_("{time}"), abs=_("on {time}")):
return {"time": time, "format": format, "rel_format": rel, "abs_format": abs} return {"time": time, "format": format, "rel_format": rel, "abs_format": abs}

View file

@ -9,16 +9,14 @@ from . import registry
@registry.function @registry.function
def gravatar(profile, size=80, default=None, profile_image=None, email=None): def gravatar(email, size=80, default=None):
if profile and not profile.is_muted: if isinstance(email, Profile):
if profile_image:
return profile_image
if profile and profile.profile_image_url:
return profile.profile_image_url
if profile:
email = email or profile.email
if default is None: if default is None:
default = profile.is_muted default = email.mute
email = email.user.email
elif isinstance(email, AbstractUser):
email = email.email
gravatar_url = ( gravatar_url = (
"//www.gravatar.com/avatar/" "//www.gravatar.com/avatar/"
+ hashlib.md5(utf8bytes(email.strip().lower())).hexdigest() + hashlib.md5(utf8bytes(email.strip().lower())).hexdigest()

View file

@ -1,7 +1,172 @@
import logging
import re
from html.parser import HTMLParser
from urllib.parse import urlparse
import mistune
from django.conf import settings
from jinja2 import Markup
from lxml import html
from lxml.etree import ParserError, XMLSyntaxError
from judge.highlight_code import highlight_code
from judge.jinja2.markdown.lazy_load import lazy_load as lazy_load_processor
from judge.jinja2.markdown.math import MathInlineGrammar, MathInlineLexer, MathRenderer
from judge.jinja2.markdown.spoiler import (
SpoilerInlineGrammar,
SpoilerInlineLexer,
SpoilerRenderer,
)
from judge.utils.camo import client as camo_client
from judge.utils.texoid import TEXOID_ENABLED, TexoidRenderer
from .. import registry from .. import registry
from judge.markdown import markdown as _markdown
logger = logging.getLogger("judge.html")
NOFOLLOW_WHITELIST = settings.NOFOLLOW_EXCLUDED
class CodeSafeInlineGrammar(mistune.InlineGrammar):
double_emphasis = re.compile(r"^\*{2}([\s\S]+?)()\*{2}(?!\*)") # **word**
emphasis = re.compile(r"^\*((?:\*\*|[^\*])+?)()\*(?!\*)") # *word*
class AwesomeInlineGrammar(
MathInlineGrammar, SpoilerInlineGrammar, CodeSafeInlineGrammar
):
pass
class AwesomeInlineLexer(MathInlineLexer, SpoilerInlineLexer, mistune.InlineLexer):
grammar_class = AwesomeInlineGrammar
class AwesomeRenderer(MathRenderer, SpoilerRenderer, mistune.Renderer):
def __init__(self, *args, **kwargs):
self.nofollow = kwargs.pop("nofollow", True)
self.texoid = TexoidRenderer() if kwargs.pop("texoid", False) else None
self.parser = HTMLParser()
super(AwesomeRenderer, self).__init__(*args, **kwargs)
def _link_rel(self, href):
if href:
try:
url = urlparse(href)
except ValueError:
return ' rel="nofollow"'
else:
if url.netloc and url.netloc not in NOFOLLOW_WHITELIST:
return ' rel="nofollow"'
return ""
def autolink(self, link, is_email=False):
text = link = mistune.escape(link)
if is_email:
link = "mailto:%s" % link
return '<a href="%s"%s>%s</a>' % (link, self._link_rel(link), text)
def table(self, header, body):
return (
'<table class="table">\n<thead>%s</thead>\n'
"<tbody>\n%s</tbody>\n</table>\n"
) % (header, body)
def link(self, link, title, text):
link = mistune.escape_link(link)
if not title:
return '<a href="%s"%s>%s</a>' % (link, self._link_rel(link), text)
title = mistune.escape(title, quote=True)
return '<a href="%s" title="%s"%s>%s</a>' % (
link,
title,
self._link_rel(link),
text,
)
def block_code(self, code, lang=None):
if not lang:
return "\n<pre><code>%s</code></pre>\n" % mistune.escape(code).rstrip()
return highlight_code(code, lang)
def block_html(self, html):
if self.texoid and html.startswith("<latex"):
attr = html[6 : html.index(">")]
latex = html[html.index(">") + 1 : html.rindex("<")]
latex = self.parser.unescape(latex)
result = self.texoid.get_result(latex)
if not result:
return "<pre>%s</pre>" % mistune.escape(latex, smart_amp=False)
elif "error" not in result:
img = (
'''<img src="%(svg)s" onerror="this.src='%(png)s';this.onerror=null"'''
'width="%(width)s" height="%(height)s"%(tail)s>'
) % {
"svg": result["svg"],
"png": result["png"],
"width": result["meta"]["width"],
"height": result["meta"]["height"],
"tail": " /" if self.options.get("use_xhtml") else "",
}
style = [
"max-width: 100%",
"height: %s" % result["meta"]["height"],
"max-height: %s" % result["meta"]["height"],
"width: %s" % result["meta"]["height"],
]
if "inline" in attr:
tag = "span"
else:
tag = "div"
style += ["text-align: center"]
return '<%s style="%s">%s</%s>' % (tag, ";".join(style), img, tag)
else:
return "<pre>%s</pre>" % mistune.escape(
result["error"], smart_amp=False
)
return super(AwesomeRenderer, self).block_html(html)
def header(self, text, level, *args, **kwargs):
return super(AwesomeRenderer, self).header(text, level + 2, *args, **kwargs)
@registry.filter @registry.filter
def markdown(value, lazy_load=False): def markdown(value, style, math_engine=None, lazy_load=False):
return _markdown(value, lazy_load) styles = settings.MARKDOWN_STYLES.get(style, settings.MARKDOWN_DEFAULT_STYLE)
escape = styles.get("safe_mode", True)
nofollow = styles.get("nofollow", True)
texoid = TEXOID_ENABLED and styles.get("texoid", False)
math = hasattr(settings, "MATHOID_URL") and styles.get("math", False)
post_processors = []
if styles.get("use_camo", False) and camo_client is not None:
post_processors.append(camo_client.update_tree)
if lazy_load:
post_processors.append(lazy_load_processor)
renderer = AwesomeRenderer(
escape=escape,
nofollow=nofollow,
texoid=texoid,
math=math and math_engine is not None,
math_engine=math_engine,
)
markdown = mistune.Markdown(
renderer=renderer,
inline=AwesomeInlineLexer,
parse_block_html=1,
parse_inline_html=1,
)
result = markdown(value)
if post_processors:
try:
tree = html.fromstring(result, parser=html.HTMLParser(recover=True))
except (XMLSyntaxError, ParserError) as e:
if result and (
not isinstance(e, ParserError) or e.args[0] != "Document is empty"
):
logger.exception("Failed to parse HTML string")
tree = html.Element("div")
for processor in post_processors:
processor(tree)
result = html.tostring(tree, encoding="unicode")
return Markup(result)

View file

@ -0,0 +1,20 @@
from copy import deepcopy
from django.templatetags.static import static
from lxml import html
def lazy_load(tree):
blank = static("blank.gif")
for img in tree.xpath(".//img"):
src = img.get("src", "")
if src.startswith("data") or "-math" in img.get("class", ""):
continue
noscript = html.Element("noscript")
copy = deepcopy(img)
copy.tail = ""
noscript.append(copy)
img.addprevious(noscript)
img.set("data-src", src)
img.set("src", blank)
img.set("class", img.get("class") + " unveil" if img.get("class") else "unveil")

View file

@ -0,0 +1,69 @@
import re
import mistune
from django.conf import settings
from judge.utils.mathoid import MathoidMathParser
mistune._pre_tags.append("latex")
class MathInlineGrammar(mistune.InlineGrammar):
block_math = re.compile(r"^\$\$(.*?)\$\$|^\\\[(.*?)\\\]", re.DOTALL)
math = re.compile(r"^~(.*?)~|^\\\((.*?)\\\)", re.DOTALL)
text = re.compile(r"^[\s\S]+?(?=[\\<!\[_*`~$]|\\[\[(]|https?://| {2,}\n|$)")
class MathInlineLexer(mistune.InlineLexer):
grammar_class = MathInlineGrammar
def __init__(self, *args, **kwargs):
self.default_rules = self.default_rules[:]
self.inline_html_rules = self.default_rules
self.default_rules.insert(self.default_rules.index("strikethrough") + 1, "math")
self.default_rules.insert(
self.default_rules.index("strikethrough") + 1, "block_math"
)
super(MathInlineLexer, self).__init__(*args, **kwargs)
def output_block_math(self, m):
return self.renderer.block_math(m.group(1) or m.group(2))
def output_math(self, m):
return self.renderer.math(m.group(1) or m.group(2))
def output_inline_html(self, m):
tag = m.group(1)
text = m.group(3)
if self._parse_inline_html and text:
if tag == "a":
self._in_link = True
text = self.output(text)
self._in_link = False
else:
text = self.output(text)
extra = m.group(2) or ""
html = "<%s%s>%s</%s>" % (tag, extra, text, tag)
else:
html = m.group(0)
return self.renderer.inline_html(html)
class MathRenderer(mistune.Renderer):
def __init__(self, *args, **kwargs):
if kwargs.pop("math", False) and settings.MATHOID_URL != False:
self.mathoid = MathoidMathParser(kwargs.pop("math_engine", None) or "svg")
else:
self.mathoid = None
super(MathRenderer, self).__init__(*args, **kwargs)
def block_math(self, math):
if self.mathoid is None or not math:
return r"\[%s\]" % mistune.escape(str(math))
return self.mathoid.display_math(math)
def math(self, math):
if self.mathoid is None or not math:
return r"\(%s\)" % mistune.escape(str(math))
return self.mathoid.inline_math(math)

View file

@ -0,0 +1,30 @@
import re
import mistune
class SpoilerInlineGrammar(mistune.InlineGrammar):
spoiler = re.compile(r"^\|\|(.+?)\s+([\s\S]+?)\s*\|\|")
class SpoilerInlineLexer(mistune.InlineLexer):
grammar_class = SpoilerInlineGrammar
def __init__(self, *args, **kwargs):
self.default_rules.insert(0, "spoiler")
super(SpoilerInlineLexer, self).__init__(*args, **kwargs)
def output_spoiler(self, m):
return self.renderer.spoiler(m.group(1), m.group(2))
class SpoilerRenderer(mistune.Renderer):
def spoiler(self, summary, text):
return """<details>
<summary style="color: brown">
<span class="spoiler-summary">%s</span>
</summary>
<div class="spoiler-text">%s</div>
</details>""" % (
summary,
text,
)

View file

@ -1,3 +1,5 @@
from django.utils import six
from judge.ratings import rating_class, rating_name, rating_progress from judge.ratings import rating_class, rating_name, rating_progress
from . import registry from . import registry
@ -6,7 +8,7 @@ def _get_rating_value(func, obj):
if obj is None: if obj is None:
return None return None
if isinstance(obj, int): if isinstance(obj, six.integer_types):
return func(obj) return func(obj)
else: else:
return func(obj.rating) return func(obj.rating)

View file

@ -155,16 +155,16 @@ def item_title(item):
@registry.function @registry.function
@registry.render_with("user/link.html") @registry.render_with("user/link.html")
def link_user(user, show_image=False): def link_user(user):
if isinstance(user, Profile): if isinstance(user, Profile):
profile = user user, profile = user.user, user
elif isinstance(user, AbstractUser): elif isinstance(user, AbstractUser):
profile = user.profile profile = user.profile
elif isinstance(user, int): elif type(user).__name__ == "ContestRankingProfile":
profile = Profile(id=user) user, profile = user.user, user
else: else:
raise ValueError("Expected profile or user, got %s" % (type(user),)) raise ValueError("Expected profile or user, got %s" % (type(user),))
return {"profile": profile, "show_image": show_image} return {"user": user, "profile": profile}
@registry.function @registry.function

View file

@ -48,9 +48,5 @@ for name, template, url_func in SHARES:
@registry.function @registry.function
def recaptcha_init(language=None): def recaptcha_init(language=None):
return get_template("snowpenguin/recaptcha/recaptcha_init.html").render( return get_template("snowpenguin/recaptcha/recaptcha_init.html").render(
{ {"explicit": False, "language": language}
"explicit": False,
"language": language,
"recaptcha_host": "https://google.com",
}
) )

View file

@ -1,8 +1,7 @@
import re import re
from jinja2 import nodes from jinja2 import Markup, nodes
from jinja2.ext import Extension from jinja2.ext import Extension
from markupsafe import Markup
class SpacelessExtension(Extension): class SpacelessExtension(Extension):

View file

@ -0,0 +1,30 @@
from . import registry
@registry.function
def submission_layout(
submission, profile_id, user, editable_problem_ids, completed_problem_ids
):
problem_id = submission.problem_id
can_view = False
if problem_id in editable_problem_ids:
can_view = True
if profile_id == submission.user_id:
can_view = True
if user.has_perm("judge.change_submission"):
can_view = True
if submission.problem_id in completed_problem_ids:
can_view |= (
submission.problem.is_public or profile_id in submission.problem.tester_ids
)
if not can_view and hasattr(submission, "contest"):
contest = submission.contest.participation.contest
if contest.is_editable_by(user):
can_view = True
return can_view

View file

@ -1,12 +0,0 @@
import logging
error_log = logging.getLogger("judge.errors")
debug_log = logging.getLogger("judge.debug")
def log_exception(msg):
error_log.exception(msg)
def log_debug(category, data):
debug_log.info(f"{category}: {data}")

View file

@ -1,50 +1,44 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from judge.models import * from judge.models import *
from collections import defaultdict
import csv import csv
import os import os
from django.conf import settings from django.conf import settings
from django.db import connection
def gen_submissions(): def gen_submissions():
print("Generating submissions") headers = ["uid", "pid"]
query = """ with open(os.path.join(settings.ML_DATA_PATH, "submissions.csv"), "w") as csvfile:
SELECT user_id as uid, problem_id as pid from
(SELECT user_id, problem_id, max(date) as max_date
from judge_submission
group by user_id, problem_id) t
order by user_id, -max_date;
"""
with connection.cursor() as cursor:
cursor.execute(query)
headers = [i[0] for i in cursor.description]
with open(
os.path.join(settings.ML_DATA_PATH, "submissions.csv"), "w"
) as csvfile:
f = csv.writer(csvfile) f = csv.writer(csvfile)
f.writerow(headers) f.writerow(headers)
for row in cursor.fetchall():
f.writerow(row) last_pid = defaultdict(int)
for u in Profile.objects.all():
used = set()
print("Processing user", u.id)
for s in Submission.objects.filter(user=u).order_by("-date"):
if s.problem.id not in used:
used.add(s.problem.id)
f.writerow([u.id, s.problem.id])
def gen_users(): def gen_users():
print("Generating users")
headers = ["uid", "username", "rating", "points"] headers = ["uid", "username", "rating", "points"]
with open(os.path.join(settings.ML_DATA_PATH, "profiles.csv"), "w") as csvfile: with open(os.path.join(settings.ML_DATA_PATH, "profiles.csv"), "w") as csvfile:
f = csv.writer(csvfile) f = csv.writer(csvfile)
f.writerow(headers) f.writerow(headers)
for u in Profile.objects.all().iterator(): for u in Profile.objects.all():
f.writerow([u.id, u.username, u.rating, u.performance_points]) f.writerow([u.id, u.username, u.rating, u.performance_points])
def gen_problems(): def gen_problems():
print("Generating problems")
headers = ["pid", "code", "name", "points", "url"] headers = ["pid", "code", "name", "points", "url"]
with open(os.path.join(settings.ML_DATA_PATH, "problems.csv"), "w") as csvfile: with open(os.path.join(settings.ML_DATA_PATH, "problems.csv"), "w") as csvfile:
f = csv.writer(csvfile) f = csv.writer(csvfile)
f.writerow(headers) f.writerow(headers)
for p in Problem.objects.all().iterator():
for p in Problem.objects.all():
f.writerow( f.writerow(
[p.id, p.code, p.name, p.points, "lqdoj.edu.vn/problem/" + p.code] [p.id, p.code, p.name, p.points, "lqdoj.edu.vn/problem/" + p.code]
) )

View file

@ -89,13 +89,14 @@ class Command(BaseCommand):
if trans is None if trans is None
else trans.description, else trans.description,
"url": "", "url": "",
"math_engine": maker.math_engine,
} }
) )
.replace('"//', '"https://') .replace('"//', '"https://')
.replace("'//", "'https://") .replace("'//", "'https://")
) )
maker.title = problem_name maker.title = problem_name
for file in "style.css": for file in ("style.css", "pygment-github.css", "mathjax_config.js"):
maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file)) maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file))
maker.make(debug=True) maker.make(debug=True)
if not maker.success: if not maker.success:

View file

@ -1,149 +0,0 @@
import markdown as _markdown
import bleach
from django.utils.html import escape
from bs4 import BeautifulSoup
from pymdownx import superfences
from django.conf import settings
from urllib.parse import urlparse
from judge.markdown_extensions import YouTubeExtension, EmoticonExtension
EXTENSIONS = [
"pymdownx.arithmatex",
"pymdownx.magiclink",
"pymdownx.betterem",
"pymdownx.details",
"pymdownx.emoji",
"pymdownx.inlinehilite",
"pymdownx.superfences",
"pymdownx.highlight",
"pymdownx.tasklist",
"markdown.extensions.footnotes",
"markdown.extensions.attr_list",
"markdown.extensions.def_list",
"markdown.extensions.tables",
"markdown.extensions.admonition",
"nl2br",
"mdx_breakless_lists",
YouTubeExtension(),
EmoticonExtension(),
]
EXTENSION_CONFIGS = {
"pymdownx.arithmatex": {
"generic": True,
},
"pymdownx.superfences": {
"custom_fences": [
{
"name": "sample",
"class": "no-border",
"format": superfences.fence_code_format,
}
],
},
"pymdownx.highlight": {
"auto_title": True,
"auto_title_map": {
"Text Only": "",
},
},
}
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + [
"img",
"center",
"iframe",
"div",
"span",
"table",
"tr",
"td",
"th",
"tr",
"pre",
"code",
"p",
"hr",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"thead",
"tbody",
"sup",
"dl",
"dt",
"dd",
"br",
"details",
"summary",
]
ALLOWED_ATTRS = [
"src",
"width",
"height",
"href",
"class",
"open",
"title",
"frameborder",
"allow",
"allowfullscreen",
"loading",
]
def _wrap_img_iframe_with_lazy_load(soup):
for img in soup.findAll("img"):
if img.get("src"):
img["loading"] = "lazy"
for img in soup.findAll("iframe"):
if img.get("src"):
img["loading"] = "lazy"
return soup
def _wrap_images_with_featherlight(soup):
for img in soup.findAll("img"):
if img.get("src"):
link = soup.new_tag("a", href=img["src"], **{"data-featherlight": "image"})
img.wrap(link)
return soup
def _open_external_links_in_new_tab(soup):
domain = settings.SITE_DOMAIN.lower()
for a in soup.findAll("a", href=True):
href = a["href"]
if href.startswith("http://") or href.startswith("https://"):
link_domain = urlparse(href).netloc.lower()
if link_domain != domain:
a["target"] = "_blank"
return soup
def markdown(value, lazy_load=False):
extensions = EXTENSIONS
html = _markdown.markdown(
value, extensions=extensions, extension_configs=EXTENSION_CONFIGS
)
html = bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS)
if not html:
html = escape(value)
soup = BeautifulSoup(html, features="html.parser")
if lazy_load:
soup = _wrap_img_iframe_with_lazy_load(soup)
soup = _wrap_images_with_featherlight(soup)
soup = _open_external_links_in_new_tab(soup)
html = str(soup)
return '<div class="md-typeset content-description">%s</div>' % html

View file

@ -1,2 +0,0 @@
from .youtube import YouTubeExtension
from .emoticon import EmoticonExtension

View file

@ -1,112 +0,0 @@
import markdown
from markdown.extensions import Extension
from markdown.inlinepatterns import InlineProcessor
import xml.etree.ElementTree as etree
import re
EMOTICON_EMOJI_MAP = {
":D": "\U0001F603", # Smiling Face with Open Mouth
":)": "\U0001F642", # Slightly Smiling Face
":-)": "\U0001F642", # Slightly Smiling Face with Nose
":(": "\U0001F641", # Slightly Frowning Face
":-(": "\U0001F641", # Slightly Frowning Face with Nose
";)": "\U0001F609", # Winking Face
";-)": "\U0001F609", # Winking Face with Nose
":P": "\U0001F61B", # Face with Tongue
":-P": "\U0001F61B", # Face with Tongue and Nose
":p": "\U0001F61B", # Face with Tongue
":-p": "\U0001F61B", # Face with Tongue and Nose
";P": "\U0001F61C", # Winking Face with Tongue
";-P": "\U0001F61C", # Winking Face with Tongue and Nose
";p": "\U0001F61C", # Winking Face with Tongue
";-p": "\U0001F61C", # Winking Face with Tongue and Nose
":'(": "\U0001F622", # Crying Face
":o": "\U0001F62E", # Face with Open Mouth
":-o": "\U0001F62E", # Face with Open Mouth and Nose
":O": "\U0001F62E", # Face with Open Mouth
":-O": "\U0001F62E", # Face with Open Mouth and Nose
":-0": "\U0001F62E", # Face with Open Mouth and Nose
">:(": "\U0001F620", # Angry Face
">:-(": "\U0001F620", # Angry Face with Nose
">:)": "\U0001F608", # Smiling Face with Horns
">:-)": "\U0001F608", # Smiling Face with Horns and Nose
"XD": "\U0001F606", # Grinning Squinting Face
"xD": "\U0001F606", # Grinning Squinting Face
"B)": "\U0001F60E", # Smiling Face with Sunglasses
"B-)": "\U0001F60E", # Smiling Face with Sunglasses and Nose
"O:)": "\U0001F607", # Smiling Face with Halo
"O:-)": "\U0001F607", # Smiling Face with Halo and Nose
"0:)": "\U0001F607", # Smiling Face with Halo
"0:-)": "\U0001F607", # Smiling Face with Halo and Nose
">:P": "\U0001F92A", # Zany Face (sticking out tongue and winking)
">:-P": "\U0001F92A", # Zany Face with Nose
">:p": "\U0001F92A", # Zany Face (sticking out tongue and winking)
">:-p": "\U0001F92A", # Zany Face with Nose
":/": "\U0001F615", # Confused Face
":-/": "\U0001F615", # Confused Face with Nose
":\\": "\U0001F615", # Confused Face
":-\\": "\U0001F615", # Confused Face with Nose
"3:)": "\U0001F608", # Smiling Face with Horns
"3:-)": "\U0001F608", # Smiling Face with Horns and Nose
"<3": "\u2764\uFE0F", # Red Heart
"</3": "\U0001F494", # Broken Heart
":*": "\U0001F618", # Face Blowing a Kiss
":-*": "\U0001F618", # Face Blowing a Kiss with Nose
";P": "\U0001F61C", # Winking Face with Tongue
";-P": "\U0001F61C",
">:P": "\U0001F61D", # Face with Stuck-Out Tongue and Tightly-Closed Eyes
":-/": "\U0001F615", # Confused Face
":/": "\U0001F615",
":\\": "\U0001F615",
":-\\": "\U0001F615",
":|": "\U0001F610", # Neutral Face
":-|": "\U0001F610",
"8)": "\U0001F60E", # Smiling Face with Sunglasses
"8-)": "\U0001F60E",
"O:)": "\U0001F607", # Smiling Face with Halo
"O:-)": "\U0001F607",
":3": "\U0001F60A", # Smiling Face with Smiling Eyes
"^.^": "\U0001F60A",
"-_-": "\U0001F611", # Expressionless Face
"T_T": "\U0001F62D", # Loudly Crying Face
"T.T": "\U0001F62D",
">.<": "\U0001F623", # Persevering Face
"x_x": "\U0001F635", # Dizzy Face
"X_X": "\U0001F635",
":]": "\U0001F600", # Grinning Face
":[": "\U0001F641", # Slightly Frowning Face
"=]": "\U0001F600",
"=[": "\U0001F641",
"D:<": "\U0001F621", # Pouting Face
"D:": "\U0001F629", # Weary Face
"D=": "\U0001F6AB", # No Entry Sign (sometimes used to denote dismay or frustration)
":'D": "\U0001F602", # Face with Tears of Joy
"D':": "\U0001F625", # Disappointed but Relieved Face
"D8": "\U0001F631", # Face Screaming in Fear
"-.-": "\U0001F644", # Face with Rolling Eyes
"-_-;": "\U0001F612", # Unamused
}
class EmoticonEmojiInlineProcessor(InlineProcessor):
def handleMatch(self, m, data):
emoticon = m.group(1)
emoji = EMOTICON_EMOJI_MAP.get(emoticon, "")
if emoji:
el = etree.Element("span")
el.text = markdown.util.AtomicString(emoji)
el.set("class", "big-emoji")
return el, m.start(0), m.end(0)
else:
return None, m.start(0), m.end(0)
class EmoticonExtension(Extension):
def extendMarkdown(self, md):
emoticon_pattern = (
r"(?:(?<=\s)|^)" # Lookbehind for a whitespace character or the start of the string
r"(" + "|".join(map(re.escape, EMOTICON_EMOJI_MAP.keys())) + r")"
r"(?=\s|$)" # Lookahead for a whitespace character or the end of the string
)
emoticon_processor = EmoticonEmojiInlineProcessor(emoticon_pattern, md)
md.inlinePatterns.register(emoticon_processor, "emoticon_to_emoji", 1)

View file

@ -1,36 +0,0 @@
import markdown
from markdown.inlinepatterns import InlineProcessor
from markdown.extensions import Extension
import xml.etree.ElementTree as etree
YOUTUBE_REGEX = (
r"(https?://)?(www\.)?" "(youtube\.com/watch\?v=|youtu\.be/)" "([\w-]+)(&[\w=]*)?"
)
class YouTubeEmbedProcessor(InlineProcessor):
def handleMatch(self, m, data):
youtube_id = m.group(4)
if not youtube_id:
return None, None, None
# Create an iframe element with the YouTube embed URL
iframe = etree.Element("iframe")
iframe.set("width", "100%")
iframe.set("height", "360")
iframe.set("src", f"https://www.youtube.com/embed/{youtube_id}")
iframe.set("frameborder", "0")
iframe.set("allowfullscreen", "true")
center = etree.Element("center")
center.append(iframe)
# Return the iframe as the element to replace the match, along with the start and end indices
return center, m.start(0), m.end(0)
class YouTubeExtension(Extension):
def extendMarkdown(self, md):
# Create the YouTube link pattern
YOUTUBE_PATTERN = YouTubeEmbedProcessor(YOUTUBE_REGEX, md)
# Register the pattern to apply the YouTubeEmbedProcessor
md.inlinePatterns.register(YOUTUBE_PATTERN, "youtube", 175)

View file

@ -1,23 +1,7 @@
import time
import logging
import random
import json
from datetime import datetime
from django.conf import settings from django.conf import settings
from django.http import HttpResponseRedirect, Http404 from django.http import HttpResponseRedirect
from django.urls import Resolver404, resolve, reverse from django.urls import Resolver404, resolve, reverse
from django.utils.http import urlquote from django.utils.http import urlquote
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext as _
from judge.models import Organization
from judge.utils.views import generic_message
USED_DOMAINS = ["www"]
URL_NAMES_BYPASS_SUBDOMAIN = ["submission_source_file"]
class ShortCircuitMiddleware: class ShortCircuitMiddleware:
@ -86,95 +70,3 @@ class ContestMiddleware(object):
request.participation = None request.participation = None
request.in_contest_mode = request.in_contest and request.contest_mode request.in_contest_mode = request.in_contest and request.contest_mode
return self.get_response(request) return self.get_response(request)
class DarkModeMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if "darkmode" in request.GET:
return HttpResponseRedirect(
reverse("toggle_darkmode") + "?next=" + urlquote(request.path)
)
return self.get_response(request)
class SubdomainMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request.organization = None
if not settings.USE_SUBDOMAIN:
return self.get_response(request)
domain = request.get_host()
site = get_current_site(request).domain
subdomain = domain[: len(domain) - len(site)].lower()
if len(subdomain) <= 1:
return self.get_response(request)
subdomain = subdomain[:-1]
if (
subdomain in USED_DOMAINS
or resolve(request.path).url_name in URL_NAMES_BYPASS_SUBDOMAIN
):
return self.get_response(request)
try:
organization = Organization.objects.get(slug=subdomain)
if request.profile and organization in request.profile.organizations.all():
request.organization = organization
else:
if request.profile:
return generic_message(
request,
_("No permission"),
_("You need to join this group first"),
status=404,
)
if not request.GET.get("next", None):
return HttpResponseRedirect(
reverse("auth_login") + "?next=" + urlquote(request.path)
)
except ObjectDoesNotExist:
return generic_message(
request,
_("No such group"),
_("No such group"),
status=404,
)
return self.get_response(request)
class SlowRequestMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
logger = logging.getLogger("judge.request_time")
logger_slow = logging.getLogger("judge.slow_request")
start_time = time.time()
response = self.get_response(request)
if response.status_code == 200:
try:
response_time = time.time() - start_time
url_name = resolve(request.path).url_name
message = {
"url_name": url_name,
"response_time": response_time * 1000,
"profile": request.user.username,
"date": datetime.now().strftime("%Y/%m/%d"),
"url": request.build_absolute_uri(),
"method": request.method,
}
if response_time > 9:
logger_slow.info(json.dumps(message))
if random.random() < 0.1:
logger.info(json.dumps(message))
except Exception:
pass
return response

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import lxml.html as lh import lxml.html as lh
from django.db import migrations from django.db import migrations
from lxml_html_clean import clean_html from lxml.html.clean import clean_html
def strip_error_html(apps, schema_editor): def strip_error_html(apps, schema_editor):

View file

@ -1,54 +0,0 @@
# Generated by Django 2.2.25 on 2022-06-12 06:59
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("judge", "0125_auto_20220602_1216"),
]
operations = [
migrations.CreateModel(
name="LanguageTemplate",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"source",
models.TextField(max_length=65536, verbose_name="source code"),
),
(
"language",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="judge.Language",
verbose_name="language",
),
),
(
"problem",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="language_templates",
to="judge.Problem",
verbose_name="problem",
),
),
],
options={
"verbose_name": "language-specific template",
"verbose_name_plural": "language-specific templates",
"unique_together": {("problem", "language")},
},
),
]

View file

@ -1,29 +0,0 @@
# Generated by Django 2.2.25 on 2022-06-16 07:42
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("judge", "0126_languagetemplate"),
]
operations = [
migrations.AlterModelOptions(
name="submission",
options={
"permissions": (
("abort_any_submission", "Abort any submission"),
("rejudge_submission", "Rejudge the submission"),
("rejudge_submission_lot", "Rejudge a lot of submissions"),
("spam_submission", "Submit without limit"),
("view_all_submission", "View all submission"),
("resubmit_other", "Resubmit others' submission"),
("view_public_submission", "View public submissions"),
),
"verbose_name": "submission",
"verbose_name_plural": "submissions",
},
),
]

View file

@ -1,26 +0,0 @@
# Generated by Django 2.2.25 on 2022-06-20 15:10
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0127_auto_20220616_1442"),
]
operations = [
migrations.AlterField(
model_name="problem",
name="memory_limit",
field=models.PositiveIntegerField(
help_text="The memory limit for this problem, in kilobytes (e.g. 256mb = 262144 kilobytes).",
validators=[
django.core.validators.MinValueValidator(0),
django.core.validators.MaxValueValidator(1048576),
],
verbose_name="memory limit",
),
),
]

View file

@ -1,59 +0,0 @@
# Generated by Django 2.2.25 on 2022-06-22 07:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0128_auto_20220620_2210"),
]
operations = [
migrations.AlterField(
model_name="problemdata",
name="checker",
field=models.CharField(
blank=True,
choices=[
("standard", "Standard"),
("floats", "Floats"),
("floatsabs", "Floats (absolute)"),
("floatsrel", "Floats (relative)"),
("rstripped", "Non-trailing spaces"),
("sorted", "Unordered"),
("identical", "Byte identical"),
("linecount", "Line-by-line"),
("custom", "Custom checker (PY)"),
("customval", "Custom validator (CPP)"),
("interact", "Interactive"),
("testlib", "Testlib"),
],
max_length=10,
verbose_name="checker",
),
),
migrations.AlterField(
model_name="problemtestcase",
name="checker",
field=models.CharField(
blank=True,
choices=[
("standard", "Standard"),
("floats", "Floats"),
("floatsabs", "Floats (absolute)"),
("floatsrel", "Floats (relative)"),
("rstripped", "Non-trailing spaces"),
("sorted", "Unordered"),
("identical", "Byte identical"),
("linecount", "Line-by-line"),
("custom", "Custom checker (PY)"),
("customval", "Custom validator (CPP)"),
("interact", "Interactive"),
("testlib", "Testlib"),
],
max_length=10,
verbose_name="checker",
),
),
]

View file

@ -1,31 +0,0 @@
# Generated by Django 2.2.25 on 2022-08-31 03:48
from django.db import migrations, models
import judge.models.problem
import judge.utils.problem_data
class Migration(migrations.Migration):
dependencies = [
("judge", "0129_auto_20220622_1424"),
]
operations = [
migrations.AddField(
model_name="problem",
name="pdf_description",
field=models.FileField(
blank=True,
null=True,
storage=judge.utils.problem_data.ProblemDataStorage(),
upload_to=judge.models.problem.problem_directory_file,
verbose_name="pdf statement",
),
),
migrations.AlterField(
model_name="problem",
name="description",
field=models.TextField(blank=True, verbose_name="problem body"),
),
]

View file

@ -1,27 +0,0 @@
# Generated by Django 2.2.25 on 2022-09-04 17:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0130_auto_20220831_1048"),
]
operations = [
migrations.AlterField(
model_name="notification",
name="category",
field=models.CharField(max_length=50, verbose_name="category"),
),
migrations.AlterField(
model_name="problemtranslation",
name="language",
field=models.CharField(
choices=[("en", "English"), ("vi", "Vietnamese")],
max_length=7,
verbose_name="language",
),
),
]

View file

@ -1,28 +0,0 @@
# Generated by Django 2.2.25 on 2022-09-15 06:49
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0131_auto_20220905_0027"),
]
operations = [
migrations.AlterField(
model_name="contestproblem",
name="max_submissions",
field=models.IntegerField(
default=0,
help_text="Maximum number of submissions for this problem, or 0 for no limit.",
validators=[
django.core.validators.MinValueValidator(
0, "Why include a problem you can't submit to?"
)
],
verbose_name="max submissions",
),
),
]

View file

@ -1,46 +0,0 @@
# Generated by Django 2.2.25 on 2022-10-13 01:50
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("judge", "0132_auto_20220915_1349"),
]
operations = [
migrations.CreateModel(
name="ContestProblemClarification",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("description", models.TextField(verbose_name="clarification body")),
(
"date",
models.DateTimeField(
auto_now_add=True, verbose_name="clarification timestamp"
),
),
(
"problem",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="judge.ContestProblem",
verbose_name="clarified problem",
),
),
],
),
migrations.DeleteModel(
name="ProblemClarification",
),
]

View file

@ -1,707 +0,0 @@
# Generated by Django 2.2.28 on 2022-10-17 18:24
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("judge", "0133_auto_20221013_0850"),
]
operations = [
migrations.AlterField(
model_name="profile",
name="timezone",
field=models.CharField(
choices=[
(
"Africa",
[
("Africa/Abidjan", "Abidjan"),
("Africa/Accra", "Accra"),
("Africa/Addis_Ababa", "Addis_Ababa"),
("Africa/Algiers", "Algiers"),
("Africa/Asmara", "Asmara"),
("Africa/Asmera", "Asmera"),
("Africa/Bamako", "Bamako"),
("Africa/Bangui", "Bangui"),
("Africa/Banjul", "Banjul"),
("Africa/Bissau", "Bissau"),
("Africa/Blantyre", "Blantyre"),
("Africa/Brazzaville", "Brazzaville"),
("Africa/Bujumbura", "Bujumbura"),
("Africa/Cairo", "Cairo"),
("Africa/Casablanca", "Casablanca"),
("Africa/Ceuta", "Ceuta"),
("Africa/Conakry", "Conakry"),
("Africa/Dakar", "Dakar"),
("Africa/Dar_es_Salaam", "Dar_es_Salaam"),
("Africa/Djibouti", "Djibouti"),
("Africa/Douala", "Douala"),
("Africa/El_Aaiun", "El_Aaiun"),
("Africa/Freetown", "Freetown"),
("Africa/Gaborone", "Gaborone"),
("Africa/Harare", "Harare"),
("Africa/Johannesburg", "Johannesburg"),
("Africa/Juba", "Juba"),
("Africa/Kampala", "Kampala"),
("Africa/Khartoum", "Khartoum"),
("Africa/Kigali", "Kigali"),
("Africa/Kinshasa", "Kinshasa"),
("Africa/Lagos", "Lagos"),
("Africa/Libreville", "Libreville"),
("Africa/Lome", "Lome"),
("Africa/Luanda", "Luanda"),
("Africa/Lubumbashi", "Lubumbashi"),
("Africa/Lusaka", "Lusaka"),
("Africa/Malabo", "Malabo"),
("Africa/Maputo", "Maputo"),
("Africa/Maseru", "Maseru"),
("Africa/Mbabane", "Mbabane"),
("Africa/Mogadishu", "Mogadishu"),
("Africa/Monrovia", "Monrovia"),
("Africa/Nairobi", "Nairobi"),
("Africa/Ndjamena", "Ndjamena"),
("Africa/Niamey", "Niamey"),
("Africa/Nouakchott", "Nouakchott"),
("Africa/Ouagadougou", "Ouagadougou"),
("Africa/Porto-Novo", "Porto-Novo"),
("Africa/Sao_Tome", "Sao_Tome"),
("Africa/Timbuktu", "Timbuktu"),
("Africa/Tripoli", "Tripoli"),
("Africa/Tunis", "Tunis"),
("Africa/Windhoek", "Windhoek"),
],
),
(
"America",
[
("America/Adak", "Adak"),
("America/Anchorage", "Anchorage"),
("America/Anguilla", "Anguilla"),
("America/Antigua", "Antigua"),
("America/Araguaina", "Araguaina"),
(
"America/Argentina/Buenos_Aires",
"Argentina/Buenos_Aires",
),
("America/Argentina/Catamarca", "Argentina/Catamarca"),
(
"America/Argentina/ComodRivadavia",
"Argentina/ComodRivadavia",
),
("America/Argentina/Cordoba", "Argentina/Cordoba"),
("America/Argentina/Jujuy", "Argentina/Jujuy"),
("America/Argentina/La_Rioja", "Argentina/La_Rioja"),
("America/Argentina/Mendoza", "Argentina/Mendoza"),
(
"America/Argentina/Rio_Gallegos",
"Argentina/Rio_Gallegos",
),
("America/Argentina/Salta", "Argentina/Salta"),
("America/Argentina/San_Juan", "Argentina/San_Juan"),
("America/Argentina/San_Luis", "Argentina/San_Luis"),
("America/Argentina/Tucuman", "Argentina/Tucuman"),
("America/Argentina/Ushuaia", "Argentina/Ushuaia"),
("America/Aruba", "Aruba"),
("America/Asuncion", "Asuncion"),
("America/Atikokan", "Atikokan"),
("America/Atka", "Atka"),
("America/Bahia", "Bahia"),
("America/Bahia_Banderas", "Bahia_Banderas"),
("America/Barbados", "Barbados"),
("America/Belem", "Belem"),
("America/Belize", "Belize"),
("America/Blanc-Sablon", "Blanc-Sablon"),
("America/Boa_Vista", "Boa_Vista"),
("America/Bogota", "Bogota"),
("America/Boise", "Boise"),
("America/Buenos_Aires", "Buenos_Aires"),
("America/Cambridge_Bay", "Cambridge_Bay"),
("America/Campo_Grande", "Campo_Grande"),
("America/Cancun", "Cancun"),
("America/Caracas", "Caracas"),
("America/Catamarca", "Catamarca"),
("America/Cayenne", "Cayenne"),
("America/Cayman", "Cayman"),
("America/Chicago", "Chicago"),
("America/Chihuahua", "Chihuahua"),
("America/Coral_Harbour", "Coral_Harbour"),
("America/Cordoba", "Cordoba"),
("America/Costa_Rica", "Costa_Rica"),
("America/Creston", "Creston"),
("America/Cuiaba", "Cuiaba"),
("America/Curacao", "Curacao"),
("America/Danmarkshavn", "Danmarkshavn"),
("America/Dawson", "Dawson"),
("America/Dawson_Creek", "Dawson_Creek"),
("America/Denver", "Denver"),
("America/Detroit", "Detroit"),
("America/Dominica", "Dominica"),
("America/Edmonton", "Edmonton"),
("America/Eirunepe", "Eirunepe"),
("America/El_Salvador", "El_Salvador"),
("America/Ensenada", "Ensenada"),
("America/Fort_Nelson", "Fort_Nelson"),
("America/Fort_Wayne", "Fort_Wayne"),
("America/Fortaleza", "Fortaleza"),
("America/Glace_Bay", "Glace_Bay"),
("America/Godthab", "Godthab"),
("America/Goose_Bay", "Goose_Bay"),
("America/Grand_Turk", "Grand_Turk"),
("America/Grenada", "Grenada"),
("America/Guadeloupe", "Guadeloupe"),
("America/Guatemala", "Guatemala"),
("America/Guayaquil", "Guayaquil"),
("America/Guyana", "Guyana"),
("America/Halifax", "Halifax"),
("America/Havana", "Havana"),
("America/Hermosillo", "Hermosillo"),
("America/Indiana/Indianapolis", "Indiana/Indianapolis"),
("America/Indiana/Knox", "Indiana/Knox"),
("America/Indiana/Marengo", "Indiana/Marengo"),
("America/Indiana/Petersburg", "Indiana/Petersburg"),
("America/Indiana/Tell_City", "Indiana/Tell_City"),
("America/Indiana/Vevay", "Indiana/Vevay"),
("America/Indiana/Vincennes", "Indiana/Vincennes"),
("America/Indiana/Winamac", "Indiana/Winamac"),
("America/Indianapolis", "Indianapolis"),
("America/Inuvik", "Inuvik"),
("America/Iqaluit", "Iqaluit"),
("America/Jamaica", "Jamaica"),
("America/Jujuy", "Jujuy"),
("America/Juneau", "Juneau"),
("America/Kentucky/Louisville", "Kentucky/Louisville"),
("America/Kentucky/Monticello", "Kentucky/Monticello"),
("America/Knox_IN", "Knox_IN"),
("America/Kralendijk", "Kralendijk"),
("America/La_Paz", "La_Paz"),
("America/Lima", "Lima"),
("America/Los_Angeles", "Los_Angeles"),
("America/Louisville", "Louisville"),
("America/Lower_Princes", "Lower_Princes"),
("America/Maceio", "Maceio"),
("America/Managua", "Managua"),
("America/Manaus", "Manaus"),
("America/Marigot", "Marigot"),
("America/Martinique", "Martinique"),
("America/Matamoros", "Matamoros"),
("America/Mazatlan", "Mazatlan"),
("America/Mendoza", "Mendoza"),
("America/Menominee", "Menominee"),
("America/Merida", "Merida"),
("America/Metlakatla", "Metlakatla"),
("America/Mexico_City", "Mexico_City"),
("America/Miquelon", "Miquelon"),
("America/Moncton", "Moncton"),
("America/Monterrey", "Monterrey"),
("America/Montevideo", "Montevideo"),
("America/Montreal", "Montreal"),
("America/Montserrat", "Montserrat"),
("America/Nassau", "Nassau"),
("America/New_York", "New_York"),
("America/Nipigon", "Nipigon"),
("America/Nome", "Nome"),
("America/Noronha", "Noronha"),
("America/North_Dakota/Beulah", "North_Dakota/Beulah"),
("America/North_Dakota/Center", "North_Dakota/Center"),
(
"America/North_Dakota/New_Salem",
"North_Dakota/New_Salem",
),
("America/Nuuk", "Nuuk"),
("America/Ojinaga", "Ojinaga"),
("America/Panama", "Panama"),
("America/Pangnirtung", "Pangnirtung"),
("America/Paramaribo", "Paramaribo"),
("America/Phoenix", "Phoenix"),
("America/Port-au-Prince", "Port-au-Prince"),
("America/Port_of_Spain", "Port_of_Spain"),
("America/Porto_Acre", "Porto_Acre"),
("America/Porto_Velho", "Porto_Velho"),
("America/Puerto_Rico", "Puerto_Rico"),
("America/Punta_Arenas", "Punta_Arenas"),
("America/Rainy_River", "Rainy_River"),
("America/Rankin_Inlet", "Rankin_Inlet"),
("America/Recife", "Recife"),
("America/Regina", "Regina"),
("America/Resolute", "Resolute"),
("America/Rio_Branco", "Rio_Branco"),
("America/Rosario", "Rosario"),
("America/Santa_Isabel", "Santa_Isabel"),
("America/Santarem", "Santarem"),
("America/Santiago", "Santiago"),
("America/Santo_Domingo", "Santo_Domingo"),
("America/Sao_Paulo", "Sao_Paulo"),
("America/Scoresbysund", "Scoresbysund"),
("America/Shiprock", "Shiprock"),
("America/Sitka", "Sitka"),
("America/St_Barthelemy", "St_Barthelemy"),
("America/St_Johns", "St_Johns"),
("America/St_Kitts", "St_Kitts"),
("America/St_Lucia", "St_Lucia"),
("America/St_Thomas", "St_Thomas"),
("America/St_Vincent", "St_Vincent"),
("America/Swift_Current", "Swift_Current"),
("America/Tegucigalpa", "Tegucigalpa"),
("America/Thule", "Thule"),
("America/Thunder_Bay", "Thunder_Bay"),
("America/Tijuana", "Tijuana"),
("America/Toronto", "Toronto"),
("America/Tortola", "Tortola"),
("America/Vancouver", "Vancouver"),
("America/Virgin", "Virgin"),
("America/Whitehorse", "Whitehorse"),
("America/Winnipeg", "Winnipeg"),
("America/Yakutat", "Yakutat"),
("America/Yellowknife", "Yellowknife"),
],
),
(
"Antarctica",
[
("Antarctica/Casey", "Casey"),
("Antarctica/Davis", "Davis"),
("Antarctica/DumontDUrville", "DumontDUrville"),
("Antarctica/Macquarie", "Macquarie"),
("Antarctica/Mawson", "Mawson"),
("Antarctica/McMurdo", "McMurdo"),
("Antarctica/Palmer", "Palmer"),
("Antarctica/Rothera", "Rothera"),
("Antarctica/South_Pole", "South_Pole"),
("Antarctica/Syowa", "Syowa"),
("Antarctica/Troll", "Troll"),
("Antarctica/Vostok", "Vostok"),
],
),
("Arctic", [("Arctic/Longyearbyen", "Longyearbyen")]),
(
"Asia",
[
("Asia/Aden", "Aden"),
("Asia/Almaty", "Almaty"),
("Asia/Amman", "Amman"),
("Asia/Anadyr", "Anadyr"),
("Asia/Aqtau", "Aqtau"),
("Asia/Aqtobe", "Aqtobe"),
("Asia/Ashgabat", "Ashgabat"),
("Asia/Ashkhabad", "Ashkhabad"),
("Asia/Atyrau", "Atyrau"),
("Asia/Baghdad", "Baghdad"),
("Asia/Bahrain", "Bahrain"),
("Asia/Baku", "Baku"),
("Asia/Bangkok", "Bangkok"),
("Asia/Barnaul", "Barnaul"),
("Asia/Beirut", "Beirut"),
("Asia/Bishkek", "Bishkek"),
("Asia/Brunei", "Brunei"),
("Asia/Calcutta", "Calcutta"),
("Asia/Chita", "Chita"),
("Asia/Choibalsan", "Choibalsan"),
("Asia/Chongqing", "Chongqing"),
("Asia/Chungking", "Chungking"),
("Asia/Colombo", "Colombo"),
("Asia/Dacca", "Dacca"),
("Asia/Damascus", "Damascus"),
("Asia/Dhaka", "Dhaka"),
("Asia/Dili", "Dili"),
("Asia/Dubai", "Dubai"),
("Asia/Dushanbe", "Dushanbe"),
("Asia/Famagusta", "Famagusta"),
("Asia/Gaza", "Gaza"),
("Asia/Harbin", "Harbin"),
("Asia/Hebron", "Hebron"),
("Asia/Ho_Chi_Minh", "Ho_Chi_Minh"),
("Asia/Hong_Kong", "Hong_Kong"),
("Asia/Hovd", "Hovd"),
("Asia/Irkutsk", "Irkutsk"),
("Asia/Istanbul", "Istanbul"),
("Asia/Jakarta", "Jakarta"),
("Asia/Jayapura", "Jayapura"),
("Asia/Jerusalem", "Jerusalem"),
("Asia/Kabul", "Kabul"),
("Asia/Kamchatka", "Kamchatka"),
("Asia/Karachi", "Karachi"),
("Asia/Kashgar", "Kashgar"),
("Asia/Kathmandu", "Kathmandu"),
("Asia/Katmandu", "Katmandu"),
("Asia/Khandyga", "Khandyga"),
("Asia/Kolkata", "Kolkata"),
("Asia/Krasnoyarsk", "Krasnoyarsk"),
("Asia/Kuala_Lumpur", "Kuala_Lumpur"),
("Asia/Kuching", "Kuching"),
("Asia/Kuwait", "Kuwait"),
("Asia/Macao", "Macao"),
("Asia/Macau", "Macau"),
("Asia/Magadan", "Magadan"),
("Asia/Makassar", "Makassar"),
("Asia/Manila", "Manila"),
("Asia/Muscat", "Muscat"),
("Asia/Nicosia", "Nicosia"),
("Asia/Novokuznetsk", "Novokuznetsk"),
("Asia/Novosibirsk", "Novosibirsk"),
("Asia/Omsk", "Omsk"),
("Asia/Oral", "Oral"),
("Asia/Phnom_Penh", "Phnom_Penh"),
("Asia/Pontianak", "Pontianak"),
("Asia/Pyongyang", "Pyongyang"),
("Asia/Qatar", "Qatar"),
("Asia/Qostanay", "Qostanay"),
("Asia/Qyzylorda", "Qyzylorda"),
("Asia/Rangoon", "Rangoon"),
("Asia/Riyadh", "Riyadh"),
("Asia/Saigon", "Saigon"),
("Asia/Sakhalin", "Sakhalin"),
("Asia/Samarkand", "Samarkand"),
("Asia/Seoul", "Seoul"),
("Asia/Shanghai", "Shanghai"),
("Asia/Singapore", "Singapore"),
("Asia/Srednekolymsk", "Srednekolymsk"),
("Asia/Taipei", "Taipei"),
("Asia/Tashkent", "Tashkent"),
("Asia/Tbilisi", "Tbilisi"),
("Asia/Tehran", "Tehran"),
("Asia/Tel_Aviv", "Tel_Aviv"),
("Asia/Thimbu", "Thimbu"),
("Asia/Thimphu", "Thimphu"),
("Asia/Tokyo", "Tokyo"),
("Asia/Tomsk", "Tomsk"),
("Asia/Ujung_Pandang", "Ujung_Pandang"),
("Asia/Ulaanbaatar", "Ulaanbaatar"),
("Asia/Ulan_Bator", "Ulan_Bator"),
("Asia/Urumqi", "Urumqi"),
("Asia/Ust-Nera", "Ust-Nera"),
("Asia/Vientiane", "Vientiane"),
("Asia/Vladivostok", "Vladivostok"),
("Asia/Yakutsk", "Yakutsk"),
("Asia/Yangon", "Yangon"),
("Asia/Yekaterinburg", "Yekaterinburg"),
("Asia/Yerevan", "Yerevan"),
],
),
(
"Atlantic",
[
("Atlantic/Azores", "Azores"),
("Atlantic/Bermuda", "Bermuda"),
("Atlantic/Canary", "Canary"),
("Atlantic/Cape_Verde", "Cape_Verde"),
("Atlantic/Faeroe", "Faeroe"),
("Atlantic/Faroe", "Faroe"),
("Atlantic/Jan_Mayen", "Jan_Mayen"),
("Atlantic/Madeira", "Madeira"),
("Atlantic/Reykjavik", "Reykjavik"),
("Atlantic/South_Georgia", "South_Georgia"),
("Atlantic/St_Helena", "St_Helena"),
("Atlantic/Stanley", "Stanley"),
],
),
(
"Australia",
[
("Australia/ACT", "ACT"),
("Australia/Adelaide", "Adelaide"),
("Australia/Brisbane", "Brisbane"),
("Australia/Broken_Hill", "Broken_Hill"),
("Australia/Canberra", "Canberra"),
("Australia/Currie", "Currie"),
("Australia/Darwin", "Darwin"),
("Australia/Eucla", "Eucla"),
("Australia/Hobart", "Hobart"),
("Australia/LHI", "LHI"),
("Australia/Lindeman", "Lindeman"),
("Australia/Lord_Howe", "Lord_Howe"),
("Australia/Melbourne", "Melbourne"),
("Australia/NSW", "NSW"),
("Australia/North", "North"),
("Australia/Perth", "Perth"),
("Australia/Queensland", "Queensland"),
("Australia/South", "South"),
("Australia/Sydney", "Sydney"),
("Australia/Tasmania", "Tasmania"),
("Australia/Victoria", "Victoria"),
("Australia/West", "West"),
("Australia/Yancowinna", "Yancowinna"),
],
),
(
"Brazil",
[
("Brazil/Acre", "Acre"),
("Brazil/DeNoronha", "DeNoronha"),
("Brazil/East", "East"),
("Brazil/West", "West"),
],
),
(
"Canada",
[
("Canada/Atlantic", "Atlantic"),
("Canada/Central", "Central"),
("Canada/Eastern", "Eastern"),
("Canada/Mountain", "Mountain"),
("Canada/Newfoundland", "Newfoundland"),
("Canada/Pacific", "Pacific"),
("Canada/Saskatchewan", "Saskatchewan"),
("Canada/Yukon", "Yukon"),
],
),
(
"Chile",
[
("Chile/Continental", "Continental"),
("Chile/EasterIsland", "EasterIsland"),
],
),
(
"Etc",
[
("Etc/Greenwich", "Greenwich"),
("Etc/UCT", "UCT"),
("Etc/UTC", "UTC"),
("Etc/Universal", "Universal"),
("Etc/Zulu", "Zulu"),
],
),
(
"Europe",
[
("Europe/Amsterdam", "Amsterdam"),
("Europe/Andorra", "Andorra"),
("Europe/Astrakhan", "Astrakhan"),
("Europe/Athens", "Athens"),
("Europe/Belfast", "Belfast"),
("Europe/Belgrade", "Belgrade"),
("Europe/Berlin", "Berlin"),
("Europe/Bratislava", "Bratislava"),
("Europe/Brussels", "Brussels"),
("Europe/Bucharest", "Bucharest"),
("Europe/Budapest", "Budapest"),
("Europe/Busingen", "Busingen"),
("Europe/Chisinau", "Chisinau"),
("Europe/Copenhagen", "Copenhagen"),
("Europe/Dublin", "Dublin"),
("Europe/Gibraltar", "Gibraltar"),
("Europe/Guernsey", "Guernsey"),
("Europe/Helsinki", "Helsinki"),
("Europe/Isle_of_Man", "Isle_of_Man"),
("Europe/Istanbul", "Istanbul"),
("Europe/Jersey", "Jersey"),
("Europe/Kaliningrad", "Kaliningrad"),
("Europe/Kiev", "Kiev"),
("Europe/Kirov", "Kirov"),
("Europe/Kyiv", "Kyiv"),
("Europe/Lisbon", "Lisbon"),
("Europe/Ljubljana", "Ljubljana"),
("Europe/London", "London"),
("Europe/Luxembourg", "Luxembourg"),
("Europe/Madrid", "Madrid"),
("Europe/Malta", "Malta"),
("Europe/Mariehamn", "Mariehamn"),
("Europe/Minsk", "Minsk"),
("Europe/Monaco", "Monaco"),
("Europe/Moscow", "Moscow"),
("Europe/Nicosia", "Nicosia"),
("Europe/Oslo", "Oslo"),
("Europe/Paris", "Paris"),
("Europe/Podgorica", "Podgorica"),
("Europe/Prague", "Prague"),
("Europe/Riga", "Riga"),
("Europe/Rome", "Rome"),
("Europe/Samara", "Samara"),
("Europe/San_Marino", "San_Marino"),
("Europe/Sarajevo", "Sarajevo"),
("Europe/Saratov", "Saratov"),
("Europe/Simferopol", "Simferopol"),
("Europe/Skopje", "Skopje"),
("Europe/Sofia", "Sofia"),
("Europe/Stockholm", "Stockholm"),
("Europe/Tallinn", "Tallinn"),
("Europe/Tirane", "Tirane"),
("Europe/Tiraspol", "Tiraspol"),
("Europe/Ulyanovsk", "Ulyanovsk"),
("Europe/Uzhgorod", "Uzhgorod"),
("Europe/Vaduz", "Vaduz"),
("Europe/Vatican", "Vatican"),
("Europe/Vienna", "Vienna"),
("Europe/Vilnius", "Vilnius"),
("Europe/Volgograd", "Volgograd"),
("Europe/Warsaw", "Warsaw"),
("Europe/Zagreb", "Zagreb"),
("Europe/Zaporozhye", "Zaporozhye"),
("Europe/Zurich", "Zurich"),
],
),
(
"Indian",
[
("Indian/Antananarivo", "Antananarivo"),
("Indian/Chagos", "Chagos"),
("Indian/Christmas", "Christmas"),
("Indian/Cocos", "Cocos"),
("Indian/Comoro", "Comoro"),
("Indian/Kerguelen", "Kerguelen"),
("Indian/Mahe", "Mahe"),
("Indian/Maldives", "Maldives"),
("Indian/Mauritius", "Mauritius"),
("Indian/Mayotte", "Mayotte"),
("Indian/Reunion", "Reunion"),
],
),
(
"Mexico",
[
("Mexico/BajaNorte", "BajaNorte"),
("Mexico/BajaSur", "BajaSur"),
("Mexico/General", "General"),
],
),
(
"Other",
[
("CET", "CET"),
("CST6CDT", "CST6CDT"),
("Cuba", "Cuba"),
("EET", "EET"),
("EST", "EST"),
("EST5EDT", "EST5EDT"),
("Egypt", "Egypt"),
("Eire", "Eire"),
("GB", "GB"),
("GB-Eire", "GB-Eire"),
("Greenwich", "Greenwich"),
("HST", "HST"),
("Hongkong", "Hongkong"),
("Iceland", "Iceland"),
("Iran", "Iran"),
("Israel", "Israel"),
("Jamaica", "Jamaica"),
("Japan", "Japan"),
("Kwajalein", "Kwajalein"),
("Libya", "Libya"),
("MET", "MET"),
("MST", "MST"),
("MST7MDT", "MST7MDT"),
("NZ", "NZ"),
("NZ-CHAT", "NZ-CHAT"),
("Navajo", "Navajo"),
("PRC", "PRC"),
("PST8PDT", "PST8PDT"),
("Poland", "Poland"),
("Portugal", "Portugal"),
("ROC", "ROC"),
("ROK", "ROK"),
("Singapore", "Singapore"),
("Turkey", "Turkey"),
("UCT", "UCT"),
("UTC", "UTC"),
("Universal", "Universal"),
("W-SU", "W-SU"),
("WET", "WET"),
("Zulu", "Zulu"),
],
),
(
"Pacific",
[
("Pacific/Apia", "Apia"),
("Pacific/Auckland", "Auckland"),
("Pacific/Bougainville", "Bougainville"),
("Pacific/Chatham", "Chatham"),
("Pacific/Chuuk", "Chuuk"),
("Pacific/Easter", "Easter"),
("Pacific/Efate", "Efate"),
("Pacific/Enderbury", "Enderbury"),
("Pacific/Fakaofo", "Fakaofo"),
("Pacific/Fiji", "Fiji"),
("Pacific/Funafuti", "Funafuti"),
("Pacific/Galapagos", "Galapagos"),
("Pacific/Gambier", "Gambier"),
("Pacific/Guadalcanal", "Guadalcanal"),
("Pacific/Guam", "Guam"),
("Pacific/Honolulu", "Honolulu"),
("Pacific/Johnston", "Johnston"),
("Pacific/Kanton", "Kanton"),
("Pacific/Kiritimati", "Kiritimati"),
("Pacific/Kosrae", "Kosrae"),
("Pacific/Kwajalein", "Kwajalein"),
("Pacific/Majuro", "Majuro"),
("Pacific/Marquesas", "Marquesas"),
("Pacific/Midway", "Midway"),
("Pacific/Nauru", "Nauru"),
("Pacific/Niue", "Niue"),
("Pacific/Norfolk", "Norfolk"),
("Pacific/Noumea", "Noumea"),
("Pacific/Pago_Pago", "Pago_Pago"),
("Pacific/Palau", "Palau"),
("Pacific/Pitcairn", "Pitcairn"),
("Pacific/Pohnpei", "Pohnpei"),
("Pacific/Ponape", "Ponape"),
("Pacific/Port_Moresby", "Port_Moresby"),
("Pacific/Rarotonga", "Rarotonga"),
("Pacific/Saipan", "Saipan"),
("Pacific/Samoa", "Samoa"),
("Pacific/Tahiti", "Tahiti"),
("Pacific/Tarawa", "Tarawa"),
("Pacific/Tongatapu", "Tongatapu"),
("Pacific/Truk", "Truk"),
("Pacific/Wake", "Wake"),
("Pacific/Wallis", "Wallis"),
("Pacific/Yap", "Yap"),
],
),
(
"US",
[
("US/Alaska", "Alaska"),
("US/Aleutian", "Aleutian"),
("US/Arizona", "Arizona"),
("US/Central", "Central"),
("US/East-Indiana", "East-Indiana"),
("US/Eastern", "Eastern"),
("US/Hawaii", "Hawaii"),
("US/Indiana-Starke", "Indiana-Starke"),
("US/Michigan", "Michigan"),
("US/Mountain", "Mountain"),
("US/Pacific", "Pacific"),
("US/Samoa", "Samoa"),
],
),
],
default="Asia/Ho_Chi_Minh",
max_length=50,
verbose_name="location",
),
),
migrations.CreateModel(
name="OrganizationProfile",
fields=[
(
"last_visit",
models.AutoField(
primary_key=True, serialize=False, verbose_name="last visit"
),
),
(
"organization",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="last_vist",
to="judge.Organization",
verbose_name="organization",
),
),
(
"users",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="last_visit",
to="judge.Profile",
verbose_name="user",
),
),
],
),
]

View file

@ -1,676 +0,0 @@
# Generated by Django 2.2.25 on 2022-10-27 20:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0134_auto_20221018_0124"),
]
operations = [
migrations.AlterField(
model_name="profile",
name="timezone",
field=models.CharField(
choices=[
(
"Africa",
[
("Africa/Abidjan", "Abidjan"),
("Africa/Accra", "Accra"),
("Africa/Addis_Ababa", "Addis_Ababa"),
("Africa/Algiers", "Algiers"),
("Africa/Asmara", "Asmara"),
("Africa/Asmera", "Asmera"),
("Africa/Bamako", "Bamako"),
("Africa/Bangui", "Bangui"),
("Africa/Banjul", "Banjul"),
("Africa/Bissau", "Bissau"),
("Africa/Blantyre", "Blantyre"),
("Africa/Brazzaville", "Brazzaville"),
("Africa/Bujumbura", "Bujumbura"),
("Africa/Cairo", "Cairo"),
("Africa/Casablanca", "Casablanca"),
("Africa/Ceuta", "Ceuta"),
("Africa/Conakry", "Conakry"),
("Africa/Dakar", "Dakar"),
("Africa/Dar_es_Salaam", "Dar_es_Salaam"),
("Africa/Djibouti", "Djibouti"),
("Africa/Douala", "Douala"),
("Africa/El_Aaiun", "El_Aaiun"),
("Africa/Freetown", "Freetown"),
("Africa/Gaborone", "Gaborone"),
("Africa/Harare", "Harare"),
("Africa/Johannesburg", "Johannesburg"),
("Africa/Juba", "Juba"),
("Africa/Kampala", "Kampala"),
("Africa/Khartoum", "Khartoum"),
("Africa/Kigali", "Kigali"),
("Africa/Kinshasa", "Kinshasa"),
("Africa/Lagos", "Lagos"),
("Africa/Libreville", "Libreville"),
("Africa/Lome", "Lome"),
("Africa/Luanda", "Luanda"),
("Africa/Lubumbashi", "Lubumbashi"),
("Africa/Lusaka", "Lusaka"),
("Africa/Malabo", "Malabo"),
("Africa/Maputo", "Maputo"),
("Africa/Maseru", "Maseru"),
("Africa/Mbabane", "Mbabane"),
("Africa/Mogadishu", "Mogadishu"),
("Africa/Monrovia", "Monrovia"),
("Africa/Nairobi", "Nairobi"),
("Africa/Ndjamena", "Ndjamena"),
("Africa/Niamey", "Niamey"),
("Africa/Nouakchott", "Nouakchott"),
("Africa/Ouagadougou", "Ouagadougou"),
("Africa/Porto-Novo", "Porto-Novo"),
("Africa/Sao_Tome", "Sao_Tome"),
("Africa/Timbuktu", "Timbuktu"),
("Africa/Tripoli", "Tripoli"),
("Africa/Tunis", "Tunis"),
("Africa/Windhoek", "Windhoek"),
],
),
(
"America",
[
("America/Adak", "Adak"),
("America/Anchorage", "Anchorage"),
("America/Anguilla", "Anguilla"),
("America/Antigua", "Antigua"),
("America/Araguaina", "Araguaina"),
(
"America/Argentina/Buenos_Aires",
"Argentina/Buenos_Aires",
),
("America/Argentina/Catamarca", "Argentina/Catamarca"),
(
"America/Argentina/ComodRivadavia",
"Argentina/ComodRivadavia",
),
("America/Argentina/Cordoba", "Argentina/Cordoba"),
("America/Argentina/Jujuy", "Argentina/Jujuy"),
("America/Argentina/La_Rioja", "Argentina/La_Rioja"),
("America/Argentina/Mendoza", "Argentina/Mendoza"),
(
"America/Argentina/Rio_Gallegos",
"Argentina/Rio_Gallegos",
),
("America/Argentina/Salta", "Argentina/Salta"),
("America/Argentina/San_Juan", "Argentina/San_Juan"),
("America/Argentina/San_Luis", "Argentina/San_Luis"),
("America/Argentina/Tucuman", "Argentina/Tucuman"),
("America/Argentina/Ushuaia", "Argentina/Ushuaia"),
("America/Aruba", "Aruba"),
("America/Asuncion", "Asuncion"),
("America/Atikokan", "Atikokan"),
("America/Atka", "Atka"),
("America/Bahia", "Bahia"),
("America/Bahia_Banderas", "Bahia_Banderas"),
("America/Barbados", "Barbados"),
("America/Belem", "Belem"),
("America/Belize", "Belize"),
("America/Blanc-Sablon", "Blanc-Sablon"),
("America/Boa_Vista", "Boa_Vista"),
("America/Bogota", "Bogota"),
("America/Boise", "Boise"),
("America/Buenos_Aires", "Buenos_Aires"),
("America/Cambridge_Bay", "Cambridge_Bay"),
("America/Campo_Grande", "Campo_Grande"),
("America/Cancun", "Cancun"),
("America/Caracas", "Caracas"),
("America/Catamarca", "Catamarca"),
("America/Cayenne", "Cayenne"),
("America/Cayman", "Cayman"),
("America/Chicago", "Chicago"),
("America/Chihuahua", "Chihuahua"),
("America/Coral_Harbour", "Coral_Harbour"),
("America/Cordoba", "Cordoba"),
("America/Costa_Rica", "Costa_Rica"),
("America/Creston", "Creston"),
("America/Cuiaba", "Cuiaba"),
("America/Curacao", "Curacao"),
("America/Danmarkshavn", "Danmarkshavn"),
("America/Dawson", "Dawson"),
("America/Dawson_Creek", "Dawson_Creek"),
("America/Denver", "Denver"),
("America/Detroit", "Detroit"),
("America/Dominica", "Dominica"),
("America/Edmonton", "Edmonton"),
("America/Eirunepe", "Eirunepe"),
("America/El_Salvador", "El_Salvador"),
("America/Ensenada", "Ensenada"),
("America/Fort_Nelson", "Fort_Nelson"),
("America/Fort_Wayne", "Fort_Wayne"),
("America/Fortaleza", "Fortaleza"),
("America/Glace_Bay", "Glace_Bay"),
("America/Godthab", "Godthab"),
("America/Goose_Bay", "Goose_Bay"),
("America/Grand_Turk", "Grand_Turk"),
("America/Grenada", "Grenada"),
("America/Guadeloupe", "Guadeloupe"),
("America/Guatemala", "Guatemala"),
("America/Guayaquil", "Guayaquil"),
("America/Guyana", "Guyana"),
("America/Halifax", "Halifax"),
("America/Havana", "Havana"),
("America/Hermosillo", "Hermosillo"),
("America/Indiana/Indianapolis", "Indiana/Indianapolis"),
("America/Indiana/Knox", "Indiana/Knox"),
("America/Indiana/Marengo", "Indiana/Marengo"),
("America/Indiana/Petersburg", "Indiana/Petersburg"),
("America/Indiana/Tell_City", "Indiana/Tell_City"),
("America/Indiana/Vevay", "Indiana/Vevay"),
("America/Indiana/Vincennes", "Indiana/Vincennes"),
("America/Indiana/Winamac", "Indiana/Winamac"),
("America/Indianapolis", "Indianapolis"),
("America/Inuvik", "Inuvik"),
("America/Iqaluit", "Iqaluit"),
("America/Jamaica", "Jamaica"),
("America/Jujuy", "Jujuy"),
("America/Juneau", "Juneau"),
("America/Kentucky/Louisville", "Kentucky/Louisville"),
("America/Kentucky/Monticello", "Kentucky/Monticello"),
("America/Knox_IN", "Knox_IN"),
("America/Kralendijk", "Kralendijk"),
("America/La_Paz", "La_Paz"),
("America/Lima", "Lima"),
("America/Los_Angeles", "Los_Angeles"),
("America/Louisville", "Louisville"),
("America/Lower_Princes", "Lower_Princes"),
("America/Maceio", "Maceio"),
("America/Managua", "Managua"),
("America/Manaus", "Manaus"),
("America/Marigot", "Marigot"),
("America/Martinique", "Martinique"),
("America/Matamoros", "Matamoros"),
("America/Mazatlan", "Mazatlan"),
("America/Mendoza", "Mendoza"),
("America/Menominee", "Menominee"),
("America/Merida", "Merida"),
("America/Metlakatla", "Metlakatla"),
("America/Mexico_City", "Mexico_City"),
("America/Miquelon", "Miquelon"),
("America/Moncton", "Moncton"),
("America/Monterrey", "Monterrey"),
("America/Montevideo", "Montevideo"),
("America/Montreal", "Montreal"),
("America/Montserrat", "Montserrat"),
("America/Nassau", "Nassau"),
("America/New_York", "New_York"),
("America/Nipigon", "Nipigon"),
("America/Nome", "Nome"),
("America/Noronha", "Noronha"),
("America/North_Dakota/Beulah", "North_Dakota/Beulah"),
("America/North_Dakota/Center", "North_Dakota/Center"),
(
"America/North_Dakota/New_Salem",
"North_Dakota/New_Salem",
),
("America/Nuuk", "Nuuk"),
("America/Ojinaga", "Ojinaga"),
("America/Panama", "Panama"),
("America/Pangnirtung", "Pangnirtung"),
("America/Paramaribo", "Paramaribo"),
("America/Phoenix", "Phoenix"),
("America/Port-au-Prince", "Port-au-Prince"),
("America/Port_of_Spain", "Port_of_Spain"),
("America/Porto_Acre", "Porto_Acre"),
("America/Porto_Velho", "Porto_Velho"),
("America/Puerto_Rico", "Puerto_Rico"),
("America/Punta_Arenas", "Punta_Arenas"),
("America/Rainy_River", "Rainy_River"),
("America/Rankin_Inlet", "Rankin_Inlet"),
("America/Recife", "Recife"),
("America/Regina", "Regina"),
("America/Resolute", "Resolute"),
("America/Rio_Branco", "Rio_Branco"),
("America/Rosario", "Rosario"),
("America/Santa_Isabel", "Santa_Isabel"),
("America/Santarem", "Santarem"),
("America/Santiago", "Santiago"),
("America/Santo_Domingo", "Santo_Domingo"),
("America/Sao_Paulo", "Sao_Paulo"),
("America/Scoresbysund", "Scoresbysund"),
("America/Shiprock", "Shiprock"),
("America/Sitka", "Sitka"),
("America/St_Barthelemy", "St_Barthelemy"),
("America/St_Johns", "St_Johns"),
("America/St_Kitts", "St_Kitts"),
("America/St_Lucia", "St_Lucia"),
("America/St_Thomas", "St_Thomas"),
("America/St_Vincent", "St_Vincent"),
("America/Swift_Current", "Swift_Current"),
("America/Tegucigalpa", "Tegucigalpa"),
("America/Thule", "Thule"),
("America/Thunder_Bay", "Thunder_Bay"),
("America/Tijuana", "Tijuana"),
("America/Toronto", "Toronto"),
("America/Tortola", "Tortola"),
("America/Vancouver", "Vancouver"),
("America/Virgin", "Virgin"),
("America/Whitehorse", "Whitehorse"),
("America/Winnipeg", "Winnipeg"),
("America/Yakutat", "Yakutat"),
("America/Yellowknife", "Yellowknife"),
],
),
(
"Antarctica",
[
("Antarctica/Casey", "Casey"),
("Antarctica/Davis", "Davis"),
("Antarctica/DumontDUrville", "DumontDUrville"),
("Antarctica/Macquarie", "Macquarie"),
("Antarctica/Mawson", "Mawson"),
("Antarctica/McMurdo", "McMurdo"),
("Antarctica/Palmer", "Palmer"),
("Antarctica/Rothera", "Rothera"),
("Antarctica/South_Pole", "South_Pole"),
("Antarctica/Syowa", "Syowa"),
("Antarctica/Troll", "Troll"),
("Antarctica/Vostok", "Vostok"),
],
),
("Arctic", [("Arctic/Longyearbyen", "Longyearbyen")]),
(
"Asia",
[
("Asia/Aden", "Aden"),
("Asia/Almaty", "Almaty"),
("Asia/Amman", "Amman"),
("Asia/Anadyr", "Anadyr"),
("Asia/Aqtau", "Aqtau"),
("Asia/Aqtobe", "Aqtobe"),
("Asia/Ashgabat", "Ashgabat"),
("Asia/Ashkhabad", "Ashkhabad"),
("Asia/Atyrau", "Atyrau"),
("Asia/Baghdad", "Baghdad"),
("Asia/Bahrain", "Bahrain"),
("Asia/Baku", "Baku"),
("Asia/Bangkok", "Bangkok"),
("Asia/Barnaul", "Barnaul"),
("Asia/Beirut", "Beirut"),
("Asia/Bishkek", "Bishkek"),
("Asia/Brunei", "Brunei"),
("Asia/Calcutta", "Calcutta"),
("Asia/Chita", "Chita"),
("Asia/Choibalsan", "Choibalsan"),
("Asia/Chongqing", "Chongqing"),
("Asia/Chungking", "Chungking"),
("Asia/Colombo", "Colombo"),
("Asia/Dacca", "Dacca"),
("Asia/Damascus", "Damascus"),
("Asia/Dhaka", "Dhaka"),
("Asia/Dili", "Dili"),
("Asia/Dubai", "Dubai"),
("Asia/Dushanbe", "Dushanbe"),
("Asia/Famagusta", "Famagusta"),
("Asia/Gaza", "Gaza"),
("Asia/Harbin", "Harbin"),
("Asia/Hebron", "Hebron"),
("Asia/Ho_Chi_Minh", "Ho_Chi_Minh"),
("Asia/Hong_Kong", "Hong_Kong"),
("Asia/Hovd", "Hovd"),
("Asia/Irkutsk", "Irkutsk"),
("Asia/Istanbul", "Istanbul"),
("Asia/Jakarta", "Jakarta"),
("Asia/Jayapura", "Jayapura"),
("Asia/Jerusalem", "Jerusalem"),
("Asia/Kabul", "Kabul"),
("Asia/Kamchatka", "Kamchatka"),
("Asia/Karachi", "Karachi"),
("Asia/Kashgar", "Kashgar"),
("Asia/Kathmandu", "Kathmandu"),
("Asia/Katmandu", "Katmandu"),
("Asia/Khandyga", "Khandyga"),
("Asia/Kolkata", "Kolkata"),
("Asia/Krasnoyarsk", "Krasnoyarsk"),
("Asia/Kuala_Lumpur", "Kuala_Lumpur"),
("Asia/Kuching", "Kuching"),
("Asia/Kuwait", "Kuwait"),
("Asia/Macao", "Macao"),
("Asia/Macau", "Macau"),
("Asia/Magadan", "Magadan"),
("Asia/Makassar", "Makassar"),
("Asia/Manila", "Manila"),
("Asia/Muscat", "Muscat"),
("Asia/Nicosia", "Nicosia"),
("Asia/Novokuznetsk", "Novokuznetsk"),
("Asia/Novosibirsk", "Novosibirsk"),
("Asia/Omsk", "Omsk"),
("Asia/Oral", "Oral"),
("Asia/Phnom_Penh", "Phnom_Penh"),
("Asia/Pontianak", "Pontianak"),
("Asia/Pyongyang", "Pyongyang"),
("Asia/Qatar", "Qatar"),
("Asia/Qostanay", "Qostanay"),
("Asia/Qyzylorda", "Qyzylorda"),
("Asia/Rangoon", "Rangoon"),
("Asia/Riyadh", "Riyadh"),
("Asia/Saigon", "Saigon"),
("Asia/Sakhalin", "Sakhalin"),
("Asia/Samarkand", "Samarkand"),
("Asia/Seoul", "Seoul"),
("Asia/Shanghai", "Shanghai"),
("Asia/Singapore", "Singapore"),
("Asia/Srednekolymsk", "Srednekolymsk"),
("Asia/Taipei", "Taipei"),
("Asia/Tashkent", "Tashkent"),
("Asia/Tbilisi", "Tbilisi"),
("Asia/Tehran", "Tehran"),
("Asia/Tel_Aviv", "Tel_Aviv"),
("Asia/Thimbu", "Thimbu"),
("Asia/Thimphu", "Thimphu"),
("Asia/Tokyo", "Tokyo"),
("Asia/Tomsk", "Tomsk"),
("Asia/Ujung_Pandang", "Ujung_Pandang"),
("Asia/Ulaanbaatar", "Ulaanbaatar"),
("Asia/Ulan_Bator", "Ulan_Bator"),
("Asia/Urumqi", "Urumqi"),
("Asia/Ust-Nera", "Ust-Nera"),
("Asia/Vientiane", "Vientiane"),
("Asia/Vladivostok", "Vladivostok"),
("Asia/Yakutsk", "Yakutsk"),
("Asia/Yangon", "Yangon"),
("Asia/Yekaterinburg", "Yekaterinburg"),
("Asia/Yerevan", "Yerevan"),
],
),
(
"Atlantic",
[
("Atlantic/Azores", "Azores"),
("Atlantic/Bermuda", "Bermuda"),
("Atlantic/Canary", "Canary"),
("Atlantic/Cape_Verde", "Cape_Verde"),
("Atlantic/Faeroe", "Faeroe"),
("Atlantic/Faroe", "Faroe"),
("Atlantic/Jan_Mayen", "Jan_Mayen"),
("Atlantic/Madeira", "Madeira"),
("Atlantic/Reykjavik", "Reykjavik"),
("Atlantic/South_Georgia", "South_Georgia"),
("Atlantic/St_Helena", "St_Helena"),
("Atlantic/Stanley", "Stanley"),
],
),
(
"Australia",
[
("Australia/ACT", "ACT"),
("Australia/Adelaide", "Adelaide"),
("Australia/Brisbane", "Brisbane"),
("Australia/Broken_Hill", "Broken_Hill"),
("Australia/Canberra", "Canberra"),
("Australia/Currie", "Currie"),
("Australia/Darwin", "Darwin"),
("Australia/Eucla", "Eucla"),
("Australia/Hobart", "Hobart"),
("Australia/LHI", "LHI"),
("Australia/Lindeman", "Lindeman"),
("Australia/Lord_Howe", "Lord_Howe"),
("Australia/Melbourne", "Melbourne"),
("Australia/NSW", "NSW"),
("Australia/North", "North"),
("Australia/Perth", "Perth"),
("Australia/Queensland", "Queensland"),
("Australia/South", "South"),
("Australia/Sydney", "Sydney"),
("Australia/Tasmania", "Tasmania"),
("Australia/Victoria", "Victoria"),
("Australia/West", "West"),
("Australia/Yancowinna", "Yancowinna"),
],
),
(
"Brazil",
[
("Brazil/Acre", "Acre"),
("Brazil/DeNoronha", "DeNoronha"),
("Brazil/East", "East"),
("Brazil/West", "West"),
],
),
(
"Canada",
[
("Canada/Atlantic", "Atlantic"),
("Canada/Central", "Central"),
("Canada/Eastern", "Eastern"),
("Canada/Mountain", "Mountain"),
("Canada/Newfoundland", "Newfoundland"),
("Canada/Pacific", "Pacific"),
("Canada/Saskatchewan", "Saskatchewan"),
("Canada/Yukon", "Yukon"),
],
),
(
"Chile",
[
("Chile/Continental", "Continental"),
("Chile/EasterIsland", "EasterIsland"),
],
),
(
"Etc",
[
("Etc/Greenwich", "Greenwich"),
("Etc/UCT", "UCT"),
("Etc/UTC", "UTC"),
("Etc/Universal", "Universal"),
("Etc/Zulu", "Zulu"),
],
),
(
"Europe",
[
("Europe/Amsterdam", "Amsterdam"),
("Europe/Andorra", "Andorra"),
("Europe/Astrakhan", "Astrakhan"),
("Europe/Athens", "Athens"),
("Europe/Belfast", "Belfast"),
("Europe/Belgrade", "Belgrade"),
("Europe/Berlin", "Berlin"),
("Europe/Bratislava", "Bratislava"),
("Europe/Brussels", "Brussels"),
("Europe/Bucharest", "Bucharest"),
("Europe/Budapest", "Budapest"),
("Europe/Busingen", "Busingen"),
("Europe/Chisinau", "Chisinau"),
("Europe/Copenhagen", "Copenhagen"),
("Europe/Dublin", "Dublin"),
("Europe/Gibraltar", "Gibraltar"),
("Europe/Guernsey", "Guernsey"),
("Europe/Helsinki", "Helsinki"),
("Europe/Isle_of_Man", "Isle_of_Man"),
("Europe/Istanbul", "Istanbul"),
("Europe/Jersey", "Jersey"),
("Europe/Kaliningrad", "Kaliningrad"),
("Europe/Kiev", "Kiev"),
("Europe/Kirov", "Kirov"),
("Europe/Lisbon", "Lisbon"),
("Europe/Ljubljana", "Ljubljana"),
("Europe/London", "London"),
("Europe/Luxembourg", "Luxembourg"),
("Europe/Madrid", "Madrid"),
("Europe/Malta", "Malta"),
("Europe/Mariehamn", "Mariehamn"),
("Europe/Minsk", "Minsk"),
("Europe/Monaco", "Monaco"),
("Europe/Moscow", "Moscow"),
("Europe/Nicosia", "Nicosia"),
("Europe/Oslo", "Oslo"),
("Europe/Paris", "Paris"),
("Europe/Podgorica", "Podgorica"),
("Europe/Prague", "Prague"),
("Europe/Riga", "Riga"),
("Europe/Rome", "Rome"),
("Europe/Samara", "Samara"),
("Europe/San_Marino", "San_Marino"),
("Europe/Sarajevo", "Sarajevo"),
("Europe/Saratov", "Saratov"),
("Europe/Simferopol", "Simferopol"),
("Europe/Skopje", "Skopje"),
("Europe/Sofia", "Sofia"),
("Europe/Stockholm", "Stockholm"),
("Europe/Tallinn", "Tallinn"),
("Europe/Tirane", "Tirane"),
("Europe/Tiraspol", "Tiraspol"),
("Europe/Ulyanovsk", "Ulyanovsk"),
("Europe/Uzhgorod", "Uzhgorod"),
("Europe/Vaduz", "Vaduz"),
("Europe/Vatican", "Vatican"),
("Europe/Vienna", "Vienna"),
("Europe/Vilnius", "Vilnius"),
("Europe/Volgograd", "Volgograd"),
("Europe/Warsaw", "Warsaw"),
("Europe/Zagreb", "Zagreb"),
("Europe/Zaporozhye", "Zaporozhye"),
("Europe/Zurich", "Zurich"),
],
),
(
"Indian",
[
("Indian/Antananarivo", "Antananarivo"),
("Indian/Chagos", "Chagos"),
("Indian/Christmas", "Christmas"),
("Indian/Cocos", "Cocos"),
("Indian/Comoro", "Comoro"),
("Indian/Kerguelen", "Kerguelen"),
("Indian/Mahe", "Mahe"),
("Indian/Maldives", "Maldives"),
("Indian/Mauritius", "Mauritius"),
("Indian/Mayotte", "Mayotte"),
("Indian/Reunion", "Reunion"),
],
),
(
"Mexico",
[
("Mexico/BajaNorte", "BajaNorte"),
("Mexico/BajaSur", "BajaSur"),
("Mexico/General", "General"),
],
),
(
"Other",
[
("CET", "CET"),
("CST6CDT", "CST6CDT"),
("Cuba", "Cuba"),
("EET", "EET"),
("EST", "EST"),
("EST5EDT", "EST5EDT"),
("Egypt", "Egypt"),
("Eire", "Eire"),
("GB", "GB"),
("GB-Eire", "GB-Eire"),
("Greenwich", "Greenwich"),
("HST", "HST"),
("Hongkong", "Hongkong"),
("Iceland", "Iceland"),
("Iran", "Iran"),
("Israel", "Israel"),
("Jamaica", "Jamaica"),
("Japan", "Japan"),
("Kwajalein", "Kwajalein"),
("Libya", "Libya"),
("MET", "MET"),
("MST", "MST"),
("MST7MDT", "MST7MDT"),
("NZ", "NZ"),
("NZ-CHAT", "NZ-CHAT"),
("Navajo", "Navajo"),
("PRC", "PRC"),
("PST8PDT", "PST8PDT"),
("Poland", "Poland"),
("Portugal", "Portugal"),
("ROC", "ROC"),
("ROK", "ROK"),
("Singapore", "Singapore"),
("Turkey", "Turkey"),
("UCT", "UCT"),
("UTC", "UTC"),
("Universal", "Universal"),
("W-SU", "W-SU"),
("WET", "WET"),
("Zulu", "Zulu"),
],
),
(
"Pacific",
[
("Pacific/Apia", "Apia"),
("Pacific/Auckland", "Auckland"),
("Pacific/Bougainville", "Bougainville"),
("Pacific/Chatham", "Chatham"),
("Pacific/Chuuk", "Chuuk"),
("Pacific/Easter", "Easter"),
("Pacific/Efate", "Efate"),
("Pacific/Enderbury", "Enderbury"),
("Pacific/Fakaofo", "Fakaofo"),
("Pacific/Fiji", "Fiji"),
("Pacific/Funafuti", "Funafuti"),
("Pacific/Galapagos", "Galapagos"),
("Pacific/Gambier", "Gambier"),
("Pacific/Guadalcanal", "Guadalcanal"),
("Pacific/Guam", "Guam"),
("Pacific/Honolulu", "Honolulu"),
("Pacific/Johnston", "Johnston"),
("Pacific/Kanton", "Kanton"),
("Pacific/Kiritimati", "Kiritimati"),
("Pacific/Kosrae", "Kosrae"),
("Pacific/Kwajalein", "Kwajalein"),
("Pacific/Majuro", "Majuro"),
("Pacific/Marquesas", "Marquesas"),
("Pacific/Midway", "Midway"),
("Pacific/Nauru", "Nauru"),
("Pacific/Niue", "Niue"),
("Pacific/Norfolk", "Norfolk"),
("Pacific/Noumea", "Noumea"),
("Pacific/Pago_Pago", "Pago_Pago"),
("Pacific/Palau", "Palau"),
("Pacific/Pitcairn", "Pitcairn"),
("Pacific/Pohnpei", "Pohnpei"),
("Pacific/Ponape", "Ponape"),
("Pacific/Port_Moresby", "Port_Moresby"),
("Pacific/Rarotonga", "Rarotonga"),
("Pacific/Saipan", "Saipan"),
("Pacific/Samoa", "Samoa"),
("Pacific/Tahiti", "Tahiti"),
("Pacific/Tarawa", "Tarawa"),
("Pacific/Tongatapu", "Tongatapu"),
("Pacific/Truk", "Truk"),
("Pacific/Wake", "Wake"),
("Pacific/Wallis", "Wallis"),
("Pacific/Yap", "Yap"),
],
),
(
"US",
[
("US/Alaska", "Alaska"),
("US/Aleutian", "Aleutian"),
("US/Arizona", "Arizona"),
("US/Central", "Central"),
("US/East-Indiana", "East-Indiana"),
("US/Eastern", "Eastern"),
("US/Hawaii", "Hawaii"),
("US/Indiana-Starke", "Indiana-Starke"),
("US/Michigan", "Michigan"),
("US/Mountain", "Mountain"),
("US/Pacific", "Pacific"),
("US/Samoa", "Samoa"),
],
),
],
default="Asia/Ho_Chi_Minh",
max_length=50,
verbose_name="location",
),
),
]

View file

@ -1,677 +0,0 @@
# Generated by Django 3.2.16 on 2022-11-01 01:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0135_auto_20221028_0300"),
]
operations = [
migrations.AlterField(
model_name="profile",
name="timezone",
field=models.CharField(
choices=[
(
"Africa",
[
("Africa/Abidjan", "Abidjan"),
("Africa/Accra", "Accra"),
("Africa/Addis_Ababa", "Addis_Ababa"),
("Africa/Algiers", "Algiers"),
("Africa/Asmara", "Asmara"),
("Africa/Asmera", "Asmera"),
("Africa/Bamako", "Bamako"),
("Africa/Bangui", "Bangui"),
("Africa/Banjul", "Banjul"),
("Africa/Bissau", "Bissau"),
("Africa/Blantyre", "Blantyre"),
("Africa/Brazzaville", "Brazzaville"),
("Africa/Bujumbura", "Bujumbura"),
("Africa/Cairo", "Cairo"),
("Africa/Casablanca", "Casablanca"),
("Africa/Ceuta", "Ceuta"),
("Africa/Conakry", "Conakry"),
("Africa/Dakar", "Dakar"),
("Africa/Dar_es_Salaam", "Dar_es_Salaam"),
("Africa/Djibouti", "Djibouti"),
("Africa/Douala", "Douala"),
("Africa/El_Aaiun", "El_Aaiun"),
("Africa/Freetown", "Freetown"),
("Africa/Gaborone", "Gaborone"),
("Africa/Harare", "Harare"),
("Africa/Johannesburg", "Johannesburg"),
("Africa/Juba", "Juba"),
("Africa/Kampala", "Kampala"),
("Africa/Khartoum", "Khartoum"),
("Africa/Kigali", "Kigali"),
("Africa/Kinshasa", "Kinshasa"),
("Africa/Lagos", "Lagos"),
("Africa/Libreville", "Libreville"),
("Africa/Lome", "Lome"),
("Africa/Luanda", "Luanda"),
("Africa/Lubumbashi", "Lubumbashi"),
("Africa/Lusaka", "Lusaka"),
("Africa/Malabo", "Malabo"),
("Africa/Maputo", "Maputo"),
("Africa/Maseru", "Maseru"),
("Africa/Mbabane", "Mbabane"),
("Africa/Mogadishu", "Mogadishu"),
("Africa/Monrovia", "Monrovia"),
("Africa/Nairobi", "Nairobi"),
("Africa/Ndjamena", "Ndjamena"),
("Africa/Niamey", "Niamey"),
("Africa/Nouakchott", "Nouakchott"),
("Africa/Ouagadougou", "Ouagadougou"),
("Africa/Porto-Novo", "Porto-Novo"),
("Africa/Sao_Tome", "Sao_Tome"),
("Africa/Timbuktu", "Timbuktu"),
("Africa/Tripoli", "Tripoli"),
("Africa/Tunis", "Tunis"),
("Africa/Windhoek", "Windhoek"),
],
),
(
"America",
[
("America/Adak", "Adak"),
("America/Anchorage", "Anchorage"),
("America/Anguilla", "Anguilla"),
("America/Antigua", "Antigua"),
("America/Araguaina", "Araguaina"),
(
"America/Argentina/Buenos_Aires",
"Argentina/Buenos_Aires",
),
("America/Argentina/Catamarca", "Argentina/Catamarca"),
(
"America/Argentina/ComodRivadavia",
"Argentina/ComodRivadavia",
),
("America/Argentina/Cordoba", "Argentina/Cordoba"),
("America/Argentina/Jujuy", "Argentina/Jujuy"),
("America/Argentina/La_Rioja", "Argentina/La_Rioja"),
("America/Argentina/Mendoza", "Argentina/Mendoza"),
(
"America/Argentina/Rio_Gallegos",
"Argentina/Rio_Gallegos",
),
("America/Argentina/Salta", "Argentina/Salta"),
("America/Argentina/San_Juan", "Argentina/San_Juan"),
("America/Argentina/San_Luis", "Argentina/San_Luis"),
("America/Argentina/Tucuman", "Argentina/Tucuman"),
("America/Argentina/Ushuaia", "Argentina/Ushuaia"),
("America/Aruba", "Aruba"),
("America/Asuncion", "Asuncion"),
("America/Atikokan", "Atikokan"),
("America/Atka", "Atka"),
("America/Bahia", "Bahia"),
("America/Bahia_Banderas", "Bahia_Banderas"),
("America/Barbados", "Barbados"),
("America/Belem", "Belem"),
("America/Belize", "Belize"),
("America/Blanc-Sablon", "Blanc-Sablon"),
("America/Boa_Vista", "Boa_Vista"),
("America/Bogota", "Bogota"),
("America/Boise", "Boise"),
("America/Buenos_Aires", "Buenos_Aires"),
("America/Cambridge_Bay", "Cambridge_Bay"),
("America/Campo_Grande", "Campo_Grande"),
("America/Cancun", "Cancun"),
("America/Caracas", "Caracas"),
("America/Catamarca", "Catamarca"),
("America/Cayenne", "Cayenne"),
("America/Cayman", "Cayman"),
("America/Chicago", "Chicago"),
("America/Chihuahua", "Chihuahua"),
("America/Coral_Harbour", "Coral_Harbour"),
("America/Cordoba", "Cordoba"),
("America/Costa_Rica", "Costa_Rica"),
("America/Creston", "Creston"),
("America/Cuiaba", "Cuiaba"),
("America/Curacao", "Curacao"),
("America/Danmarkshavn", "Danmarkshavn"),
("America/Dawson", "Dawson"),
("America/Dawson_Creek", "Dawson_Creek"),
("America/Denver", "Denver"),
("America/Detroit", "Detroit"),
("America/Dominica", "Dominica"),
("America/Edmonton", "Edmonton"),
("America/Eirunepe", "Eirunepe"),
("America/El_Salvador", "El_Salvador"),
("America/Ensenada", "Ensenada"),
("America/Fort_Nelson", "Fort_Nelson"),
("America/Fort_Wayne", "Fort_Wayne"),
("America/Fortaleza", "Fortaleza"),
("America/Glace_Bay", "Glace_Bay"),
("America/Godthab", "Godthab"),
("America/Goose_Bay", "Goose_Bay"),
("America/Grand_Turk", "Grand_Turk"),
("America/Grenada", "Grenada"),
("America/Guadeloupe", "Guadeloupe"),
("America/Guatemala", "Guatemala"),
("America/Guayaquil", "Guayaquil"),
("America/Guyana", "Guyana"),
("America/Halifax", "Halifax"),
("America/Havana", "Havana"),
("America/Hermosillo", "Hermosillo"),
("America/Indiana/Indianapolis", "Indiana/Indianapolis"),
("America/Indiana/Knox", "Indiana/Knox"),
("America/Indiana/Marengo", "Indiana/Marengo"),
("America/Indiana/Petersburg", "Indiana/Petersburg"),
("America/Indiana/Tell_City", "Indiana/Tell_City"),
("America/Indiana/Vevay", "Indiana/Vevay"),
("America/Indiana/Vincennes", "Indiana/Vincennes"),
("America/Indiana/Winamac", "Indiana/Winamac"),
("America/Indianapolis", "Indianapolis"),
("America/Inuvik", "Inuvik"),
("America/Iqaluit", "Iqaluit"),
("America/Jamaica", "Jamaica"),
("America/Jujuy", "Jujuy"),
("America/Juneau", "Juneau"),
("America/Kentucky/Louisville", "Kentucky/Louisville"),
("America/Kentucky/Monticello", "Kentucky/Monticello"),
("America/Knox_IN", "Knox_IN"),
("America/Kralendijk", "Kralendijk"),
("America/La_Paz", "La_Paz"),
("America/Lima", "Lima"),
("America/Los_Angeles", "Los_Angeles"),
("America/Louisville", "Louisville"),
("America/Lower_Princes", "Lower_Princes"),
("America/Maceio", "Maceio"),
("America/Managua", "Managua"),
("America/Manaus", "Manaus"),
("America/Marigot", "Marigot"),
("America/Martinique", "Martinique"),
("America/Matamoros", "Matamoros"),
("America/Mazatlan", "Mazatlan"),
("America/Mendoza", "Mendoza"),
("America/Menominee", "Menominee"),
("America/Merida", "Merida"),
("America/Metlakatla", "Metlakatla"),
("America/Mexico_City", "Mexico_City"),
("America/Miquelon", "Miquelon"),
("America/Moncton", "Moncton"),
("America/Monterrey", "Monterrey"),
("America/Montevideo", "Montevideo"),
("America/Montreal", "Montreal"),
("America/Montserrat", "Montserrat"),
("America/Nassau", "Nassau"),
("America/New_York", "New_York"),
("America/Nipigon", "Nipigon"),
("America/Nome", "Nome"),
("America/Noronha", "Noronha"),
("America/North_Dakota/Beulah", "North_Dakota/Beulah"),
("America/North_Dakota/Center", "North_Dakota/Center"),
(
"America/North_Dakota/New_Salem",
"North_Dakota/New_Salem",
),
("America/Nuuk", "Nuuk"),
("America/Ojinaga", "Ojinaga"),
("America/Panama", "Panama"),
("America/Pangnirtung", "Pangnirtung"),
("America/Paramaribo", "Paramaribo"),
("America/Phoenix", "Phoenix"),
("America/Port-au-Prince", "Port-au-Prince"),
("America/Port_of_Spain", "Port_of_Spain"),
("America/Porto_Acre", "Porto_Acre"),
("America/Porto_Velho", "Porto_Velho"),
("America/Puerto_Rico", "Puerto_Rico"),
("America/Punta_Arenas", "Punta_Arenas"),
("America/Rainy_River", "Rainy_River"),
("America/Rankin_Inlet", "Rankin_Inlet"),
("America/Recife", "Recife"),
("America/Regina", "Regina"),
("America/Resolute", "Resolute"),
("America/Rio_Branco", "Rio_Branco"),
("America/Rosario", "Rosario"),
("America/Santa_Isabel", "Santa_Isabel"),
("America/Santarem", "Santarem"),
("America/Santiago", "Santiago"),
("America/Santo_Domingo", "Santo_Domingo"),
("America/Sao_Paulo", "Sao_Paulo"),
("America/Scoresbysund", "Scoresbysund"),
("America/Shiprock", "Shiprock"),
("America/Sitka", "Sitka"),
("America/St_Barthelemy", "St_Barthelemy"),
("America/St_Johns", "St_Johns"),
("America/St_Kitts", "St_Kitts"),
("America/St_Lucia", "St_Lucia"),
("America/St_Thomas", "St_Thomas"),
("America/St_Vincent", "St_Vincent"),
("America/Swift_Current", "Swift_Current"),
("America/Tegucigalpa", "Tegucigalpa"),
("America/Thule", "Thule"),
("America/Thunder_Bay", "Thunder_Bay"),
("America/Tijuana", "Tijuana"),
("America/Toronto", "Toronto"),
("America/Tortola", "Tortola"),
("America/Vancouver", "Vancouver"),
("America/Virgin", "Virgin"),
("America/Whitehorse", "Whitehorse"),
("America/Winnipeg", "Winnipeg"),
("America/Yakutat", "Yakutat"),
("America/Yellowknife", "Yellowknife"),
],
),
(
"Antarctica",
[
("Antarctica/Casey", "Casey"),
("Antarctica/Davis", "Davis"),
("Antarctica/DumontDUrville", "DumontDUrville"),
("Antarctica/Macquarie", "Macquarie"),
("Antarctica/Mawson", "Mawson"),
("Antarctica/McMurdo", "McMurdo"),
("Antarctica/Palmer", "Palmer"),
("Antarctica/Rothera", "Rothera"),
("Antarctica/South_Pole", "South_Pole"),
("Antarctica/Syowa", "Syowa"),
("Antarctica/Troll", "Troll"),
("Antarctica/Vostok", "Vostok"),
],
),
("Arctic", [("Arctic/Longyearbyen", "Longyearbyen")]),
(
"Asia",
[
("Asia/Aden", "Aden"),
("Asia/Almaty", "Almaty"),
("Asia/Amman", "Amman"),
("Asia/Anadyr", "Anadyr"),
("Asia/Aqtau", "Aqtau"),
("Asia/Aqtobe", "Aqtobe"),
("Asia/Ashgabat", "Ashgabat"),
("Asia/Ashkhabad", "Ashkhabad"),
("Asia/Atyrau", "Atyrau"),
("Asia/Baghdad", "Baghdad"),
("Asia/Bahrain", "Bahrain"),
("Asia/Baku", "Baku"),
("Asia/Bangkok", "Bangkok"),
("Asia/Barnaul", "Barnaul"),
("Asia/Beirut", "Beirut"),
("Asia/Bishkek", "Bishkek"),
("Asia/Brunei", "Brunei"),
("Asia/Calcutta", "Calcutta"),
("Asia/Chita", "Chita"),
("Asia/Choibalsan", "Choibalsan"),
("Asia/Chongqing", "Chongqing"),
("Asia/Chungking", "Chungking"),
("Asia/Colombo", "Colombo"),
("Asia/Dacca", "Dacca"),
("Asia/Damascus", "Damascus"),
("Asia/Dhaka", "Dhaka"),
("Asia/Dili", "Dili"),
("Asia/Dubai", "Dubai"),
("Asia/Dushanbe", "Dushanbe"),
("Asia/Famagusta", "Famagusta"),
("Asia/Gaza", "Gaza"),
("Asia/Harbin", "Harbin"),
("Asia/Hebron", "Hebron"),
("Asia/Ho_Chi_Minh", "Ho_Chi_Minh"),
("Asia/Hong_Kong", "Hong_Kong"),
("Asia/Hovd", "Hovd"),
("Asia/Irkutsk", "Irkutsk"),
("Asia/Istanbul", "Istanbul"),
("Asia/Jakarta", "Jakarta"),
("Asia/Jayapura", "Jayapura"),
("Asia/Jerusalem", "Jerusalem"),
("Asia/Kabul", "Kabul"),
("Asia/Kamchatka", "Kamchatka"),
("Asia/Karachi", "Karachi"),
("Asia/Kashgar", "Kashgar"),
("Asia/Kathmandu", "Kathmandu"),
("Asia/Katmandu", "Katmandu"),
("Asia/Khandyga", "Khandyga"),
("Asia/Kolkata", "Kolkata"),
("Asia/Krasnoyarsk", "Krasnoyarsk"),
("Asia/Kuala_Lumpur", "Kuala_Lumpur"),
("Asia/Kuching", "Kuching"),
("Asia/Kuwait", "Kuwait"),
("Asia/Macao", "Macao"),
("Asia/Macau", "Macau"),
("Asia/Magadan", "Magadan"),
("Asia/Makassar", "Makassar"),
("Asia/Manila", "Manila"),
("Asia/Muscat", "Muscat"),
("Asia/Nicosia", "Nicosia"),
("Asia/Novokuznetsk", "Novokuznetsk"),
("Asia/Novosibirsk", "Novosibirsk"),
("Asia/Omsk", "Omsk"),
("Asia/Oral", "Oral"),
("Asia/Phnom_Penh", "Phnom_Penh"),
("Asia/Pontianak", "Pontianak"),
("Asia/Pyongyang", "Pyongyang"),
("Asia/Qatar", "Qatar"),
("Asia/Qostanay", "Qostanay"),
("Asia/Qyzylorda", "Qyzylorda"),
("Asia/Rangoon", "Rangoon"),
("Asia/Riyadh", "Riyadh"),
("Asia/Saigon", "Saigon"),
("Asia/Sakhalin", "Sakhalin"),
("Asia/Samarkand", "Samarkand"),
("Asia/Seoul", "Seoul"),
("Asia/Shanghai", "Shanghai"),
("Asia/Singapore", "Singapore"),
("Asia/Srednekolymsk", "Srednekolymsk"),
("Asia/Taipei", "Taipei"),
("Asia/Tashkent", "Tashkent"),
("Asia/Tbilisi", "Tbilisi"),
("Asia/Tehran", "Tehran"),
("Asia/Tel_Aviv", "Tel_Aviv"),
("Asia/Thimbu", "Thimbu"),
("Asia/Thimphu", "Thimphu"),
("Asia/Tokyo", "Tokyo"),
("Asia/Tomsk", "Tomsk"),
("Asia/Ujung_Pandang", "Ujung_Pandang"),
("Asia/Ulaanbaatar", "Ulaanbaatar"),
("Asia/Ulan_Bator", "Ulan_Bator"),
("Asia/Urumqi", "Urumqi"),
("Asia/Ust-Nera", "Ust-Nera"),
("Asia/Vientiane", "Vientiane"),
("Asia/Vladivostok", "Vladivostok"),
("Asia/Yakutsk", "Yakutsk"),
("Asia/Yangon", "Yangon"),
("Asia/Yekaterinburg", "Yekaterinburg"),
("Asia/Yerevan", "Yerevan"),
],
),
(
"Atlantic",
[
("Atlantic/Azores", "Azores"),
("Atlantic/Bermuda", "Bermuda"),
("Atlantic/Canary", "Canary"),
("Atlantic/Cape_Verde", "Cape_Verde"),
("Atlantic/Faeroe", "Faeroe"),
("Atlantic/Faroe", "Faroe"),
("Atlantic/Jan_Mayen", "Jan_Mayen"),
("Atlantic/Madeira", "Madeira"),
("Atlantic/Reykjavik", "Reykjavik"),
("Atlantic/South_Georgia", "South_Georgia"),
("Atlantic/St_Helena", "St_Helena"),
("Atlantic/Stanley", "Stanley"),
],
),
(
"Australia",
[
("Australia/ACT", "ACT"),
("Australia/Adelaide", "Adelaide"),
("Australia/Brisbane", "Brisbane"),
("Australia/Broken_Hill", "Broken_Hill"),
("Australia/Canberra", "Canberra"),
("Australia/Currie", "Currie"),
("Australia/Darwin", "Darwin"),
("Australia/Eucla", "Eucla"),
("Australia/Hobart", "Hobart"),
("Australia/LHI", "LHI"),
("Australia/Lindeman", "Lindeman"),
("Australia/Lord_Howe", "Lord_Howe"),
("Australia/Melbourne", "Melbourne"),
("Australia/NSW", "NSW"),
("Australia/North", "North"),
("Australia/Perth", "Perth"),
("Australia/Queensland", "Queensland"),
("Australia/South", "South"),
("Australia/Sydney", "Sydney"),
("Australia/Tasmania", "Tasmania"),
("Australia/Victoria", "Victoria"),
("Australia/West", "West"),
("Australia/Yancowinna", "Yancowinna"),
],
),
(
"Brazil",
[
("Brazil/Acre", "Acre"),
("Brazil/DeNoronha", "DeNoronha"),
("Brazil/East", "East"),
("Brazil/West", "West"),
],
),
(
"Canada",
[
("Canada/Atlantic", "Atlantic"),
("Canada/Central", "Central"),
("Canada/Eastern", "Eastern"),
("Canada/Mountain", "Mountain"),
("Canada/Newfoundland", "Newfoundland"),
("Canada/Pacific", "Pacific"),
("Canada/Saskatchewan", "Saskatchewan"),
("Canada/Yukon", "Yukon"),
],
),
(
"Chile",
[
("Chile/Continental", "Continental"),
("Chile/EasterIsland", "EasterIsland"),
],
),
(
"Etc",
[
("Etc/Greenwich", "Greenwich"),
("Etc/UCT", "UCT"),
("Etc/UTC", "UTC"),
("Etc/Universal", "Universal"),
("Etc/Zulu", "Zulu"),
],
),
(
"Europe",
[
("Europe/Amsterdam", "Amsterdam"),
("Europe/Andorra", "Andorra"),
("Europe/Astrakhan", "Astrakhan"),
("Europe/Athens", "Athens"),
("Europe/Belfast", "Belfast"),
("Europe/Belgrade", "Belgrade"),
("Europe/Berlin", "Berlin"),
("Europe/Bratislava", "Bratislava"),
("Europe/Brussels", "Brussels"),
("Europe/Bucharest", "Bucharest"),
("Europe/Budapest", "Budapest"),
("Europe/Busingen", "Busingen"),
("Europe/Chisinau", "Chisinau"),
("Europe/Copenhagen", "Copenhagen"),
("Europe/Dublin", "Dublin"),
("Europe/Gibraltar", "Gibraltar"),
("Europe/Guernsey", "Guernsey"),
("Europe/Helsinki", "Helsinki"),
("Europe/Isle_of_Man", "Isle_of_Man"),
("Europe/Istanbul", "Istanbul"),
("Europe/Jersey", "Jersey"),
("Europe/Kaliningrad", "Kaliningrad"),
("Europe/Kiev", "Kiev"),
("Europe/Kirov", "Kirov"),
("Europe/Kyiv", "Kyiv"),
("Europe/Lisbon", "Lisbon"),
("Europe/Ljubljana", "Ljubljana"),
("Europe/London", "London"),
("Europe/Luxembourg", "Luxembourg"),
("Europe/Madrid", "Madrid"),
("Europe/Malta", "Malta"),
("Europe/Mariehamn", "Mariehamn"),
("Europe/Minsk", "Minsk"),
("Europe/Monaco", "Monaco"),
("Europe/Moscow", "Moscow"),
("Europe/Nicosia", "Nicosia"),
("Europe/Oslo", "Oslo"),
("Europe/Paris", "Paris"),
("Europe/Podgorica", "Podgorica"),
("Europe/Prague", "Prague"),
("Europe/Riga", "Riga"),
("Europe/Rome", "Rome"),
("Europe/Samara", "Samara"),
("Europe/San_Marino", "San_Marino"),
("Europe/Sarajevo", "Sarajevo"),
("Europe/Saratov", "Saratov"),
("Europe/Simferopol", "Simferopol"),
("Europe/Skopje", "Skopje"),
("Europe/Sofia", "Sofia"),
("Europe/Stockholm", "Stockholm"),
("Europe/Tallinn", "Tallinn"),
("Europe/Tirane", "Tirane"),
("Europe/Tiraspol", "Tiraspol"),
("Europe/Ulyanovsk", "Ulyanovsk"),
("Europe/Uzhgorod", "Uzhgorod"),
("Europe/Vaduz", "Vaduz"),
("Europe/Vatican", "Vatican"),
("Europe/Vienna", "Vienna"),
("Europe/Vilnius", "Vilnius"),
("Europe/Volgograd", "Volgograd"),
("Europe/Warsaw", "Warsaw"),
("Europe/Zagreb", "Zagreb"),
("Europe/Zaporozhye", "Zaporozhye"),
("Europe/Zurich", "Zurich"),
],
),
(
"Indian",
[
("Indian/Antananarivo", "Antananarivo"),
("Indian/Chagos", "Chagos"),
("Indian/Christmas", "Christmas"),
("Indian/Cocos", "Cocos"),
("Indian/Comoro", "Comoro"),
("Indian/Kerguelen", "Kerguelen"),
("Indian/Mahe", "Mahe"),
("Indian/Maldives", "Maldives"),
("Indian/Mauritius", "Mauritius"),
("Indian/Mayotte", "Mayotte"),
("Indian/Reunion", "Reunion"),
],
),
(
"Mexico",
[
("Mexico/BajaNorte", "BajaNorte"),
("Mexico/BajaSur", "BajaSur"),
("Mexico/General", "General"),
],
),
(
"Other",
[
("CET", "CET"),
("CST6CDT", "CST6CDT"),
("Cuba", "Cuba"),
("EET", "EET"),
("EST", "EST"),
("EST5EDT", "EST5EDT"),
("Egypt", "Egypt"),
("Eire", "Eire"),
("GB", "GB"),
("GB-Eire", "GB-Eire"),
("Greenwich", "Greenwich"),
("HST", "HST"),
("Hongkong", "Hongkong"),
("Iceland", "Iceland"),
("Iran", "Iran"),
("Israel", "Israel"),
("Jamaica", "Jamaica"),
("Japan", "Japan"),
("Kwajalein", "Kwajalein"),
("Libya", "Libya"),
("MET", "MET"),
("MST", "MST"),
("MST7MDT", "MST7MDT"),
("NZ", "NZ"),
("NZ-CHAT", "NZ-CHAT"),
("Navajo", "Navajo"),
("PRC", "PRC"),
("PST8PDT", "PST8PDT"),
("Poland", "Poland"),
("Portugal", "Portugal"),
("ROC", "ROC"),
("ROK", "ROK"),
("Singapore", "Singapore"),
("Turkey", "Turkey"),
("UCT", "UCT"),
("UTC", "UTC"),
("Universal", "Universal"),
("W-SU", "W-SU"),
("WET", "WET"),
("Zulu", "Zulu"),
],
),
(
"Pacific",
[
("Pacific/Apia", "Apia"),
("Pacific/Auckland", "Auckland"),
("Pacific/Bougainville", "Bougainville"),
("Pacific/Chatham", "Chatham"),
("Pacific/Chuuk", "Chuuk"),
("Pacific/Easter", "Easter"),
("Pacific/Efate", "Efate"),
("Pacific/Enderbury", "Enderbury"),
("Pacific/Fakaofo", "Fakaofo"),
("Pacific/Fiji", "Fiji"),
("Pacific/Funafuti", "Funafuti"),
("Pacific/Galapagos", "Galapagos"),
("Pacific/Gambier", "Gambier"),
("Pacific/Guadalcanal", "Guadalcanal"),
("Pacific/Guam", "Guam"),
("Pacific/Honolulu", "Honolulu"),
("Pacific/Johnston", "Johnston"),
("Pacific/Kanton", "Kanton"),
("Pacific/Kiritimati", "Kiritimati"),
("Pacific/Kosrae", "Kosrae"),
("Pacific/Kwajalein", "Kwajalein"),
("Pacific/Majuro", "Majuro"),
("Pacific/Marquesas", "Marquesas"),
("Pacific/Midway", "Midway"),
("Pacific/Nauru", "Nauru"),
("Pacific/Niue", "Niue"),
("Pacific/Norfolk", "Norfolk"),
("Pacific/Noumea", "Noumea"),
("Pacific/Pago_Pago", "Pago_Pago"),
("Pacific/Palau", "Palau"),
("Pacific/Pitcairn", "Pitcairn"),
("Pacific/Pohnpei", "Pohnpei"),
("Pacific/Ponape", "Ponape"),
("Pacific/Port_Moresby", "Port_Moresby"),
("Pacific/Rarotonga", "Rarotonga"),
("Pacific/Saipan", "Saipan"),
("Pacific/Samoa", "Samoa"),
("Pacific/Tahiti", "Tahiti"),
("Pacific/Tarawa", "Tarawa"),
("Pacific/Tongatapu", "Tongatapu"),
("Pacific/Truk", "Truk"),
("Pacific/Wake", "Wake"),
("Pacific/Wallis", "Wallis"),
("Pacific/Yap", "Yap"),
],
),
(
"US",
[
("US/Alaska", "Alaska"),
("US/Aleutian", "Aleutian"),
("US/Arizona", "Arizona"),
("US/Central", "Central"),
("US/East-Indiana", "East-Indiana"),
("US/Eastern", "Eastern"),
("US/Hawaii", "Hawaii"),
("US/Indiana-Starke", "Indiana-Starke"),
("US/Michigan", "Michigan"),
("US/Mountain", "Mountain"),
("US/Pacific", "Pacific"),
("US/Samoa", "Samoa"),
],
),
],
default="Asia/Ho_Chi_Minh",
max_length=50,
verbose_name="location",
),
),
]

View file

@ -1,84 +0,0 @@
# Generated by Django 3.2.16 on 2022-11-16 15:01
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("judge", "0136_alter_profile_timezone"),
]
operations = [
migrations.CreateModel(
name="PageVote",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"page",
models.CharField(
db_index=True, max_length=30, verbose_name="associated page"
),
),
("score", models.IntegerField(default=0, verbose_name="votes")),
],
options={
"verbose_name": "pagevote",
"verbose_name_plural": "pagevotes",
},
),
migrations.AlterField(
model_name="problemtranslation",
name="language",
field=models.CharField(
choices=[("vi", "Vietnamese"), ("en", "English")],
max_length=7,
verbose_name="language",
),
),
migrations.CreateModel(
name="PageVoteVoter",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("score", models.IntegerField()),
(
"pagevote",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="votes",
to="judge.pagevote",
),
),
(
"voter",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="voted_page",
to="judge.profile",
),
),
],
options={
"verbose_name": "pagevote vote",
"verbose_name_plural": "pagevote votes",
"unique_together": {("voter", "pagevote")},
},
),
]

View file

@ -1,73 +0,0 @@
# Generated by Django 3.2.16 on 2022-11-17 17:13
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("judge", "0137_auto_20221116_2201"),
]
operations = [
migrations.CreateModel(
name="BookMark",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"page",
models.CharField(
db_index=True, max_length=30, verbose_name="associated page"
),
),
],
options={
"verbose_name": "bookmark",
"verbose_name_plural": "bookmarks",
},
),
migrations.CreateModel(
name="MakeBookMark",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"bookmark",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="bookmark",
to="judge.bookmark",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="user_bookmark",
to="judge.profile",
),
),
],
options={
"verbose_name": "make bookmark",
"verbose_name_plural": "make bookmarks",
"unique_together": {("user", "bookmark")},
},
),
]

View file

@ -1,23 +0,0 @@
# Generated by Django 3.2.16 on 2022-11-18 06:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0138_bookmark_makebookmark"),
]
operations = [
migrations.AddField(
model_name="contest",
name="freeze_after",
field=models.DurationField(
blank=True,
help_text="Format hh:mm:ss. For example, if you want to freeze contest after 2 hours, enter 02:00:00",
null=True,
verbose_name="freeze after",
),
),
]

Some files were not shown because too many files have changed in this diff Show more