Cloned DMOJ
This commit is contained in:
parent
f623974b58
commit
49dc9ff10c
513 changed files with 132349 additions and 39 deletions
1
.browserslistrc
Normal file
1
.browserslistrc
Normal file
|
@ -0,0 +1 @@
|
|||
cover 97%
|
24
.flake8
Normal file
24
.flake8
Normal file
|
@ -0,0 +1,24 @@
|
|||
[flake8]
|
||||
max-line-length = 120
|
||||
application-import-names = dmoj,judge,django_ace,event_socket_server
|
||||
import-order-style = pycharm
|
||||
enable-extensions = G
|
||||
ignore =
|
||||
W504, # line break occurred after a binary operator
|
||||
# allow only generator_stop and annotations future imports
|
||||
FI10,FI11,FI12,FI13,FI14,FI15,FI16,FI17,FI18,FI55,FI58,
|
||||
C814, # missing trailing comma in Python 2 only
|
||||
per-file-ignores =
|
||||
# F401: unused imports, ignore in all __init__.py
|
||||
# F403: import *
|
||||
./*/__init__.py:F401,F403
|
||||
# F405: name comes from import *
|
||||
./event_socket_server/__init__.py:F401,F403,F405
|
||||
./judge/management/commands/runmoss.py:F403,F405
|
||||
# E501: line too long, ignore in migrations
|
||||
./judge/migrations/*.py:E501
|
||||
# E303: too many blank lines
|
||||
# PyCharm likes to have double lines between class/def in an if statement.
|
||||
./judge/widgets/pagedown.py:E303
|
||||
exclude =
|
||||
./dmoj/local_settings.py, # belongs to the user
|
17
.github/workflows/build.yml
vendored
Normal file
17
.github/workflows/build.yml
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
name: build
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Install flake8
|
||||
run: pip install flake8 flake8-import-order flake8-future-import flake8-commas flake8-logging-format
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
flake8 --version
|
||||
flake8
|
40
.github/workflows/compilemessages.yml
vendored
Normal file
40
.github/workflows/compilemessages.yml
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
name: compilemessages
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'locale/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'locale/**'
|
||||
jobs:
|
||||
compilemessages:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Checkout submodules
|
||||
run: |
|
||||
git submodule init
|
||||
git submodule update
|
||||
- name: Install requirements
|
||||
run: |
|
||||
sudo apt-get install gettext
|
||||
pip install -r requirements.txt
|
||||
pip install pymysql
|
||||
- name: Check .po file validity
|
||||
run: |
|
||||
fail=0
|
||||
while read -r file; do
|
||||
if ! msgfmt --check-format "$file"; then
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
done < <(find locale -name '*.po')
|
||||
exit "$fail"
|
||||
shell: bash
|
||||
- name: Compile messages
|
||||
run: |
|
||||
echo "STATIC_ROOT = '/tmp'" > dmoj/local_settings.py
|
||||
python manage.py compilemessages
|
53
.github/workflows/makemessages.yml
vendored
Normal file
53
.github/workflows/makemessages.yml
vendored
Normal file
|
@ -0,0 +1,53 @@
|
|||
name: makemessages
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
makemessages:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Checkout submodules
|
||||
run: |
|
||||
git submodule init
|
||||
git submodule update
|
||||
- name: Install requirements
|
||||
run: |
|
||||
sudo apt-get install gettext
|
||||
curl -O https://artifacts.crowdin.com/repo/deb/crowdin.deb
|
||||
sudo dpkg -i crowdin.deb
|
||||
pip install -r requirements.txt
|
||||
pip install pymysql
|
||||
- name: Collect localizable strings
|
||||
run: |
|
||||
echo "STATIC_ROOT = '/tmp'" > dmoj/local_settings.py
|
||||
python manage.py makemessages -l en -e py,html,txt
|
||||
python manage.py makemessages -l en -d djangojs
|
||||
- name: Upload strings to Crowdin
|
||||
env:
|
||||
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
|
||||
run: |
|
||||
cat > crowdin.yaml <<EOF
|
||||
project_identifier: dmoj
|
||||
|
||||
files:
|
||||
- source: /locale/en/LC_MESSAGES/django.po
|
||||
translation: /locale/%two_letters_code%/LC_MESSAGES/django.po
|
||||
languages_mapping:
|
||||
two_letters_code:
|
||||
zh-CN: zh_Hans
|
||||
sr-CS: sr_Latn
|
||||
- source: /locale/en/LC_MESSAGES/djangojs.po
|
||||
translation: /locale/%two_letters_code%/LC_MESSAGES/djangojs.po
|
||||
languages_mapping:
|
||||
two_letters_code:
|
||||
zh-CN: zh_Hans
|
||||
sr-CS: sr_Latn
|
||||
EOF
|
||||
echo "api_key: ${CROWDIN_API_TOKEN}" >> crowdin.yaml
|
||||
crowdin upload sources
|
67
.github/workflows/updatemessages.yml
vendored
Normal file
67
.github/workflows/updatemessages.yml
vendored
Normal file
|
@ -0,0 +1,67 @@
|
|||
name: updatemessages
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
jobs:
|
||||
updatemessages:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Checkout submodules
|
||||
run: |
|
||||
git submodule init
|
||||
git submodule update
|
||||
- name: Install requirements
|
||||
run: |
|
||||
sudo apt-get install gettext
|
||||
curl -O https://artifacts.crowdin.com/repo/deb/crowdin.deb
|
||||
sudo dpkg -i crowdin.deb
|
||||
pip install -r requirements.txt
|
||||
pip install pymysql
|
||||
- name: Download strings from Crowdin
|
||||
env:
|
||||
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
|
||||
run: |
|
||||
cat > crowdin.yaml <<EOF
|
||||
project_identifier: dmoj
|
||||
|
||||
files:
|
||||
- source: /locale/en/LC_MESSAGES/django.po
|
||||
translation: /locale/%two_letters_code%/LC_MESSAGES/django.po
|
||||
languages_mapping:
|
||||
two_letters_code:
|
||||
zh-CN: zh_Hans
|
||||
zh-TW: zh_Hant
|
||||
sr-CS: sr_Latn
|
||||
- source: /locale/en/LC_MESSAGES/djangojs.po
|
||||
translation: /locale/%two_letters_code%/LC_MESSAGES/djangojs.po
|
||||
languages_mapping:
|
||||
two_letters_code:
|
||||
zh-CN: zh_Hans
|
||||
zh-TW: zh_Hant
|
||||
sr-CS: sr_Latn
|
||||
EOF
|
||||
echo "api_key: ${CROWDIN_API_TOKEN}" >> crowdin.yaml
|
||||
crowdin download
|
||||
rm crowdin.yaml
|
||||
- name: Cleanup
|
||||
run: |
|
||||
rm -rf src/
|
||||
git add locale
|
||||
git checkout .
|
||||
git clean -fd
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@v1.4.1-multi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.REPO_SCOPED_TOKEN }}
|
||||
COMMIT_MESSAGE: 'i18n: update translations from Crowdin'
|
||||
PULL_REQUEST_TITLE: 'Update translations from Crowdin'
|
||||
PULL_REQUEST_BODY: This PR has been auto-generated to pull in latest translations from [Crowdin](https://translate.dmoj.ca).
|
||||
PULL_REQUEST_LABELS: i18n, enhancement
|
||||
PULL_REQUEST_REVIEWERS: Xyene, quantum5
|
||||
PULL_REQUEST_BRANCH: update-i18n
|
||||
BRANCH_SUFFIX: none
|
52
.gitignore
vendored
52
.gitignore
vendored
|
@ -1,39 +1,13 @@
|
|||
# History files
|
||||
.Rhistory
|
||||
.Rapp.history
|
||||
|
||||
# Session Data files
|
||||
.RData
|
||||
|
||||
# User-specific files
|
||||
.Ruserdata
|
||||
|
||||
# Example code in package build process
|
||||
*-Ex.R
|
||||
|
||||
# Output files from R CMD build
|
||||
/*.tar.gz
|
||||
|
||||
# Output files from R CMD check
|
||||
/*.Rcheck/
|
||||
|
||||
# RStudio files
|
||||
.Rproj.user/
|
||||
|
||||
# produced vignettes
|
||||
vignettes/*.html
|
||||
vignettes/*.pdf
|
||||
|
||||
# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3
|
||||
.httr-oauth
|
||||
|
||||
# knitr and R markdown default cache directories
|
||||
*_cache/
|
||||
/cache/
|
||||
|
||||
# Temporary files created by R markdown
|
||||
*.utf8.md
|
||||
*.knit.md
|
||||
|
||||
# R Environment Variables
|
||||
.Renviron
|
||||
.idea
|
||||
.vscode
|
||||
.sass-cache
|
||||
*.sqlite3
|
||||
*.py[co]
|
||||
*.mo
|
||||
*~
|
||||
dmoj/local_settings.py
|
||||
resources/style.css
|
||||
resources/content-description.css
|
||||
resources/ranks.css
|
||||
resources/table.css
|
||||
sass_processed
|
||||
|
|
8
.gitmodules
vendored
Normal file
8
.gitmodules
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
[submodule "resources/pagedown"]
|
||||
path = resources/pagedown
|
||||
url = https://github.com/DMOJ/dmoj-pagedown.git
|
||||
branch = master
|
||||
[submodule "resources/libs"]
|
||||
path = resources/libs
|
||||
url = https://github.com/DMOJ/site-assets.git
|
||||
branch = master
|
62
502.html
Normal file
62
502.html
Normal file
|
@ -0,0 +1,62 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>502 bad gateway - DMOJ</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<style type="text/css">
|
||||
hr {
|
||||
margin: 0 22em;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #f5f5f5;
|
||||
padding-top: 3em;
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 30%;
|
||||
display: table;
|
||||
text-align: center;
|
||||
margin: 0px auto;
|
||||
border: 3px solid rgb(41, 128, 185);
|
||||
border-radius: 30px;
|
||||
box-shadow: 5px 5px 10px rgb(85, 85, 85);
|
||||
}
|
||||
|
||||
.display {
|
||||
position: fixed;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo {
|
||||
top: 25%;
|
||||
width: 115px;
|
||||
text-align: center;
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.msg {
|
||||
font-size: 1.2em;
|
||||
color: rgb(85, 85, 85);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="display">
|
||||
<hr>
|
||||
<br>
|
||||
<div class="popup">
|
||||
<div>
|
||||
<img class="logo" src="/logo.png" alt="DMOJ">
|
||||
</div>
|
||||
<h1 style="width: 100%;">Oops, the DMOJ is down.</h1>
|
||||
</div>
|
||||
<br>
|
||||
<hr>
|
||||
<br>
|
||||
<h2 class="msg">But don't worry, we'll be back soon.</h2>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
17
django_2_2_pymysql_patch.py
Normal file
17
django_2_2_pymysql_patch.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
import django
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
if (2, 2) <= django.VERSION < (3,):
|
||||
# Django 2.2.x is incompatible with PyMySQL.
|
||||
# This monkey patch backports the Django 3.0+ code.
|
||||
|
||||
from django.db.backends.mysql.operations import DatabaseOperations
|
||||
|
||||
def last_executed_query(self, cursor, sql, params):
|
||||
# With MySQLdb, cursor objects have an (undocumented) "_executed"
|
||||
# attribute where the exact query sent to the database is saved.
|
||||
# See MySQLdb/cursors.py in the source distribution.
|
||||
# MySQLdb returns string, PyMySQL bytes.
|
||||
return force_text(getattr(cursor, '_executed', None), errors='replace')
|
||||
|
||||
DatabaseOperations.last_executed_query = last_executed_query
|
5
django_ace/__init__.py
Normal file
5
django_ace/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
"""
|
||||
Django-ace originally from https://github.com/bradleyayers/django-ace.
|
||||
"""
|
||||
|
||||
from .widgets import AceWidget
|
BIN
django_ace/static/django_ace/img/contract.png
Normal file
BIN
django_ace/static/django_ace/img/contract.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 304 B |
BIN
django_ace/static/django_ace/img/expand.png
Normal file
BIN
django_ace/static/django_ace/img/expand.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 285 B |
58
django_ace/static/django_ace/widget.css
Normal file
58
django_ace/static/django_ace/widget.css
Normal file
|
@ -0,0 +1,58 @@
|
|||
.django-ace-widget {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.django-ace-widget > div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.django-ace-widget.loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.django-ace-toolbar {
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
color: #555;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
border-bottom: 1px solid #d8d8d8;
|
||||
background-color: #eaeaea;
|
||||
background-image: -moz-linear-gradient(#fafafa, #eaeaea);
|
||||
background-image: -webkit-linear-gradient(#fafafa, #eaeaea);
|
||||
background-image: linear-gradient(#fafafa, #eaeaea);
|
||||
background-repeat: repeat-x;
|
||||
clear: both;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.django-ace-max_min {
|
||||
float: right;
|
||||
padding: 5px;
|
||||
|
||||
background: url(img/expand.png) no-repeat 5px 5px;
|
||||
display: block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.django-ace-editor {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.django-ace-editor-fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.django-ace-editor-fullscreen .django-ace-max_min {
|
||||
background-image: url(img/contract.png);
|
||||
}
|
182
django_ace/static/django_ace/widget.js
Normal file
182
django_ace/static/django_ace/widget.js
Normal file
|
@ -0,0 +1,182 @@
|
|||
(function () {
|
||||
function getDocHeight() {
|
||||
var D = document;
|
||||
return Math.max(
|
||||
Math.max(D.body.scrollHeight, D.documentElement.scrollHeight),
|
||||
Math.max(D.body.offsetHeight, D.documentElement.offsetHeight),
|
||||
Math.max(D.body.clientHeight, D.documentElement.clientHeight)
|
||||
);
|
||||
}
|
||||
|
||||
function getDocWidth() {
|
||||
var D = document;
|
||||
return Math.max(
|
||||
Math.max(D.body.scrollWidth, D.documentElement.scrollWidth),
|
||||
Math.max(D.body.offsetWidth, D.documentElement.offsetWidth),
|
||||
Math.max(D.body.clientWidth, D.documentElement.clientWidth)
|
||||
);
|
||||
}
|
||||
|
||||
function next(elem) {
|
||||
// Credit to John Resig for this function
|
||||
// taken from Pro JavaScript techniques
|
||||
do {
|
||||
elem = elem.nextSibling;
|
||||
} while (elem && elem.nodeType != 1);
|
||||
return elem;
|
||||
}
|
||||
|
||||
function prev(elem) {
|
||||
// Credit to John Resig for this function
|
||||
// taken from Pro JavaScript techniques
|
||||
do {
|
||||
elem = elem.previousSibling;
|
||||
} while (elem && elem.nodeType != 1);
|
||||
return elem;
|
||||
}
|
||||
|
||||
function redraw(element) {
|
||||
element = $(element);
|
||||
var n = document.createTextNode(' ');
|
||||
element.appendChild(n);
|
||||
(function () {
|
||||
n.parentNode.removeChild(n)
|
||||
}).defer();
|
||||
return element;
|
||||
}
|
||||
|
||||
function minimizeMaximize(widget, main_block, editor) {
|
||||
if (window.fullscreen == true) {
|
||||
main_block.className = 'django-ace-editor';
|
||||
|
||||
widget.style.width = window.ace_widget.width + 'px';
|
||||
widget.style.height = window.ace_widget.height + 'px';
|
||||
window.fullscreen = false;
|
||||
}
|
||||
else {
|
||||
window.ace_widget = {
|
||||
'width': widget.offsetWidth,
|
||||
'height': widget.offsetHeight
|
||||
};
|
||||
|
||||
main_block.className = 'django-ace-editor-fullscreen';
|
||||
|
||||
widget.style.height = getDocHeight() + 'px';
|
||||
widget.style.width = getDocWidth() + 'px';
|
||||
|
||||
window.scrollTo(0, 0);
|
||||
window.fullscreen = true;
|
||||
}
|
||||
editor.resize();
|
||||
}
|
||||
|
||||
function apply_widget(widget) {
|
||||
var div = widget.firstChild,
|
||||
textarea = next(widget),
|
||||
editor = ace.edit(div),
|
||||
mode = widget.getAttribute('data-mode'),
|
||||
theme = widget.getAttribute('data-theme'),
|
||||
wordwrap = widget.getAttribute('data-wordwrap'),
|
||||
toolbar = prev(widget),
|
||||
main_block = toolbar.parentNode;
|
||||
|
||||
// Toolbar maximize/minimize button
|
||||
var min_max = toolbar.getElementsByClassName('django-ace-max_min');
|
||||
min_max[0].onclick = function () {
|
||||
minimizeMaximize(widget, main_block, editor);
|
||||
return false;
|
||||
};
|
||||
|
||||
editor.getSession().setValue(textarea.value);
|
||||
|
||||
// the editor is initially absolute positioned
|
||||
textarea.style.display = "none";
|
||||
|
||||
// options
|
||||
if (mode) {
|
||||
editor.getSession().setMode('ace/mode/' + mode);
|
||||
}
|
||||
if (theme) {
|
||||
editor.setTheme("ace/theme/" + theme);
|
||||
}
|
||||
if (wordwrap == "true") {
|
||||
editor.getSession().setUseWrapMode(true);
|
||||
}
|
||||
|
||||
editor.getSession().on('change', function () {
|
||||
textarea.value = editor.getSession().getValue();
|
||||
});
|
||||
|
||||
editor.commands.addCommands([
|
||||
{
|
||||
name: 'Full screen',
|
||||
bindKey: {win: 'Ctrl-F11', mac: 'Command-F11'},
|
||||
exec: function (editor) {
|
||||
minimizeMaximize(widget, main_block, editor);
|
||||
},
|
||||
readOnly: true // false if this command should not apply in readOnly mode
|
||||
},
|
||||
{
|
||||
name: 'submit',
|
||||
bindKey: "Ctrl+Enter",
|
||||
exec: function (editor) {
|
||||
$('form#problem_submit').submit();
|
||||
},
|
||||
readOnly: true
|
||||
},
|
||||
{
|
||||
name: "showKeyboardShortcuts",
|
||||
bindKey: {win: "Ctrl-Shift-/", mac: "Command-Shift-/"},
|
||||
exec: function (editor) {
|
||||
ace.config.loadModule("ace/ext/keybinding_menu", function (module) {
|
||||
module.init(editor);
|
||||
editor.showKeyboardShortcuts();
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "increaseFontSize",
|
||||
bindKey: "Ctrl-+",
|
||||
exec: function (editor) {
|
||||
var size = parseInt(editor.getFontSize(), 10) || 12;
|
||||
editor.setFontSize(size + 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "decreaseFontSize",
|
||||
bindKey: "Ctrl+-",
|
||||
exec: function (editor) {
|
||||
var size = parseInt(editor.getFontSize(), 10) || 12;
|
||||
editor.setFontSize(Math.max(size - 1 || 1));
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "resetFontSize",
|
||||
bindKey: "Ctrl+0",
|
||||
exec: function (editor) {
|
||||
editor.setFontSize(12);
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
window[widget.id] = editor;
|
||||
$(widget).trigger('ace_load', [editor]);
|
||||
}
|
||||
|
||||
function init() {
|
||||
var widgets = document.getElementsByClassName('django-ace-widget');
|
||||
|
||||
for (var i = 0; i < widgets.length; i++) {
|
||||
var widget = widgets[i];
|
||||
widget.className = "django-ace-widget"; // remove `loading` class
|
||||
|
||||
apply_widget(widget);
|
||||
}
|
||||
}
|
||||
|
||||
if (window.addEventListener) { // W3C
|
||||
window.addEventListener('load', init);
|
||||
} else if (window.attachEvent) { // Microsoft
|
||||
window.attachEvent('onload', init);
|
||||
}
|
||||
})();
|
57
django_ace/widgets.py
Normal file
57
django_ace/widgets.py
Normal file
|
@ -0,0 +1,57 @@
|
|||
"""
|
||||
Django-ace originally from https://github.com/bradleyayers/django-ace.
|
||||
"""
|
||||
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.forms.utils import flatatt
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
||||
class AceWidget(forms.Textarea):
|
||||
def __init__(self, mode=None, theme=None, wordwrap=False, width='100%', height='300px',
|
||||
no_ace_media=False, *args, **kwargs):
|
||||
self.mode = mode
|
||||
self.theme = theme
|
||||
self.wordwrap = wordwrap
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.ace_media = not no_ace_media
|
||||
super(AceWidget, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
js = [urljoin(settings.ACE_URL, 'ace.js')] if self.ace_media else []
|
||||
js.append('django_ace/widget.js')
|
||||
css = {
|
||||
'screen': ['django_ace/widget.css'],
|
||||
}
|
||||
return forms.Media(js=js, css=css)
|
||||
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
attrs = attrs or {}
|
||||
|
||||
ace_attrs = {
|
||||
'class': 'django-ace-widget loading',
|
||||
'style': 'width:%s; height:%s' % (self.width, self.height),
|
||||
'id': 'ace_%s' % name,
|
||||
}
|
||||
if self.mode:
|
||||
ace_attrs['data-mode'] = self.mode
|
||||
if self.theme:
|
||||
ace_attrs['data-theme'] = self.theme
|
||||
if self.wordwrap:
|
||||
ace_attrs['data-wordwrap'] = 'true'
|
||||
|
||||
attrs.update(style='width: 100%; min-width: 100%; max-width: 100%; resize: none')
|
||||
textarea = super(AceWidget, self).render(name, value, attrs)
|
||||
|
||||
html = '<div%s><div></div></div>%s' % (flatatt(ace_attrs), textarea)
|
||||
|
||||
# add toolbar
|
||||
html = ('<div class="django-ace-editor"><div style="width: 100%%" class="django-ace-toolbar">'
|
||||
'<a href="./" class="django-ace-max_min"></a></div>%s</div>') % html
|
||||
|
||||
return mark_safe(html)
|
1
dmoj/__init__.py
Normal file
1
dmoj/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from dmoj.celery import app as celery_app
|
27
dmoj/celery.py
Normal file
27
dmoj/celery.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
import logging
|
||||
import socket
|
||||
|
||||
from celery import Celery
|
||||
from celery.signals import task_failure
|
||||
|
||||
app = Celery('dmoj')
|
||||
|
||||
from django.conf import settings # noqa: E402, I202, django must be imported here
|
||||
app.config_from_object(settings, namespace='CELERY')
|
||||
|
||||
if hasattr(settings, 'CELERY_BROKER_URL_SECRET'):
|
||||
app.conf.broker_url = settings.CELERY_BROKER_URL_SECRET
|
||||
if hasattr(settings, 'CELERY_RESULT_BACKEND_SECRET'):
|
||||
app.conf.result_backend = settings.CELERY_RESULT_BACKEND_SECRET
|
||||
|
||||
# Load task modules from all registered Django app configs.
|
||||
app.autodiscover_tasks()
|
||||
|
||||
# Logger to enable errors be reported.
|
||||
logger = logging.getLogger('judge.celery')
|
||||
|
||||
|
||||
@task_failure.connect()
|
||||
def celery_failure_log(sender, task_id, exception, traceback, *args, **kwargs):
|
||||
logger.error('Celery Task %s: %s on %s', sender.name, task_id, socket.gethostname(), # noqa: G201
|
||||
exc_info=(type(exception), exception, traceback))
|
497
dmoj/settings.py
Normal file
497
dmoj/settings.py
Normal file
|
@ -0,0 +1,497 @@
|
|||
"""
|
||||
Django settings for dmoj project.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.11/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/1.11/ref/settings/
|
||||
"""
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django_jinja.builtins import DEFAULT_EXTENSIONS
|
||||
from jinja2 import select_autoescape
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = '5*9f5q57mqmlz2#f$x1h76&jxy#yortjl1v+l*6hd18$d*yx#0'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
SITE_ID = 1
|
||||
SITE_NAME = 'DMOJ'
|
||||
SITE_LONG_NAME = 'DMOJ: Modern Online Judge'
|
||||
SITE_ADMIN_EMAIL = False
|
||||
|
||||
DMOJ_REQUIRE_STAFF_2FA = True
|
||||
|
||||
# Set to 1 to use HTTPS if request was made to https://
|
||||
# Set to 2 to always use HTTPS for links
|
||||
# Set to 0 to always use HTTP for links
|
||||
DMOJ_SSL = 0
|
||||
|
||||
# Refer to dmoj.ca/post/103-point-system-rework
|
||||
DMOJ_PP_STEP = 0.95
|
||||
DMOJ_PP_ENTRIES = 100
|
||||
DMOJ_PP_BONUS_FUNCTION = lambda n: 300 * (1 - 0.997 ** n) # noqa: E731
|
||||
|
||||
NODEJS = '/usr/bin/node'
|
||||
EXIFTOOL = '/usr/bin/exiftool'
|
||||
ACE_URL = '//cdnjs.cloudflare.com/ajax/libs/ace/1.1.3'
|
||||
SELECT2_JS_URL = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js'
|
||||
DEFAULT_SELECT2_CSS = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css'
|
||||
|
||||
DMOJ_CAMO_URL = None
|
||||
DMOJ_CAMO_KEY = None
|
||||
DMOJ_CAMO_HTTPS = False
|
||||
DMOJ_CAMO_EXCLUDE = ()
|
||||
DMOJ_PROBLEM_DATA_ROOT = None
|
||||
DMOJ_PROBLEM_MIN_TIME_LIMIT = 0 # seconds
|
||||
DMOJ_PROBLEM_MAX_TIME_LIMIT = 60 # seconds
|
||||
DMOJ_PROBLEM_MIN_MEMORY_LIMIT = 0 # kilobytes
|
||||
DMOJ_PROBLEM_MAX_MEMORY_LIMIT = 1048576 # kilobytes
|
||||
DMOJ_PROBLEM_MIN_PROBLEM_POINTS = 0
|
||||
DMOJ_RATING_COLORS = False
|
||||
DMOJ_EMAIL_THROTTLING = (10, 60)
|
||||
DMOJ_STATS_LANGUAGE_THRESHOLD = 10
|
||||
DMOJ_SUBMISSIONS_REJUDGE_LIMIT = 10
|
||||
# Maximum number of submissions a single user can queue without the `spam_submission` permission
|
||||
DMOJ_SUBMISSION_LIMIT = 2
|
||||
DMOJ_BLOG_NEW_PROBLEM_COUNT = 7
|
||||
DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT = 7
|
||||
DMOJ_TOTP_TOLERANCE_HALF_MINUTES = 1
|
||||
DMOJ_USER_MAX_ORGANIZATION_COUNT = 3
|
||||
DMOJ_COMMENT_VOTE_HIDE_THRESHOLD = -5
|
||||
DMOJ_PDF_PROBLEM_CACHE = ''
|
||||
DMOJ_PDF_PROBLEM_TEMP_DIR = tempfile.gettempdir()
|
||||
DMOJ_STATS_SUBMISSION_RESULT_COLORS = {
|
||||
'TLE': '#a3bcbd',
|
||||
'AC': '#00a92a',
|
||||
'WA': '#ed4420',
|
||||
'CE': '#42586d',
|
||||
'ERR': '#ffa71c',
|
||||
}
|
||||
|
||||
MARKDOWN_STYLES = {}
|
||||
MARKDOWN_DEFAULT_STYLE = {}
|
||||
|
||||
MATHOID_URL = False
|
||||
MATHOID_GZIP = False
|
||||
MATHOID_MML_CACHE = None
|
||||
MATHOID_CSS_CACHE = 'default'
|
||||
MATHOID_DEFAULT_TYPE = 'auto'
|
||||
MATHOID_MML_CACHE_TTL = 86400
|
||||
MATHOID_CACHE_ROOT = ''
|
||||
MATHOID_CACHE_URL = False
|
||||
|
||||
TEXOID_GZIP = False
|
||||
TEXOID_META_CACHE = 'default'
|
||||
TEXOID_META_CACHE_TTL = 86400
|
||||
DMOJ_NEWSLETTER_ID_ON_REGISTER = None
|
||||
|
||||
BAD_MAIL_PROVIDERS = ()
|
||||
BAD_MAIL_PROVIDER_REGEX = ()
|
||||
NOFOLLOW_EXCLUDED = set()
|
||||
|
||||
TIMEZONE_BG = None
|
||||
TIMEZONE_MAP = None
|
||||
TIMEZONE_DETECT_BACKEND = None
|
||||
|
||||
TERMS_OF_SERVICE_URL = None
|
||||
DEFAULT_USER_LANGUAGE = 'PY3'
|
||||
|
||||
PHANTOMJS = ''
|
||||
PHANTOMJS_PDF_ZOOM = 0.75
|
||||
PHANTOMJS_PDF_TIMEOUT = 5.0
|
||||
PHANTOMJS_PAPER_SIZE = 'Letter'
|
||||
|
||||
SLIMERJS = ''
|
||||
SLIMERJS_PDF_ZOOM = 0.75
|
||||
SLIMERJS_FIREFOX_PATH = ''
|
||||
SLIMERJS_PAPER_SIZE = 'Letter'
|
||||
|
||||
PUPPETEER_MODULE = '/usr/lib/node_modules/puppeteer'
|
||||
PUPPETEER_PAPER_SIZE = 'Letter'
|
||||
|
||||
PYGMENT_THEME = 'pygment-github.css'
|
||||
INLINE_JQUERY = True
|
||||
INLINE_FONTAWESOME = True
|
||||
JQUERY_JS = '//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js'
|
||||
FONTAWESOME_CSS = '//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css'
|
||||
DMOJ_CANONICAL = ''
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = ()
|
||||
|
||||
try:
|
||||
import wpadmin
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
del wpadmin
|
||||
INSTALLED_APPS += ('wpadmin',)
|
||||
|
||||
WPADMIN = {
|
||||
'admin': {
|
||||
'title': 'DMOJ Admin',
|
||||
'menu': {
|
||||
'top': 'wpadmin.menu.menus.BasicTopMenu',
|
||||
'left': 'wpadmin.menu.custom.CustomModelLeftMenuWithDashboard',
|
||||
},
|
||||
'custom_menu': [
|
||||
{
|
||||
'model': 'judge.Problem',
|
||||
'icon': 'fa-question-circle',
|
||||
'children': [
|
||||
'judge.ProblemGroup',
|
||||
'judge.ProblemType',
|
||||
],
|
||||
},
|
||||
{
|
||||
'model': 'judge.Submission',
|
||||
'icon': 'fa-check-square-o',
|
||||
'children': [
|
||||
'judge.Language',
|
||||
'judge.Judge',
|
||||
],
|
||||
},
|
||||
{
|
||||
'model': 'judge.Contest',
|
||||
'icon': 'fa-bar-chart',
|
||||
'children': [
|
||||
'judge.ContestParticipation',
|
||||
'judge.ContestTag',
|
||||
],
|
||||
},
|
||||
{
|
||||
'model': 'auth.User',
|
||||
'icon': 'fa-user',
|
||||
'children': [
|
||||
'auth.Group',
|
||||
'registration.RegistrationProfile',
|
||||
],
|
||||
},
|
||||
{
|
||||
'model': 'judge.Profile',
|
||||
'icon': 'fa-user-plus',
|
||||
'children': [
|
||||
'judge.Organization',
|
||||
'judge.OrganizationRequest',
|
||||
],
|
||||
},
|
||||
{
|
||||
'model': 'judge.NavigationBar',
|
||||
'icon': 'fa-bars',
|
||||
'children': [
|
||||
'judge.MiscConfig',
|
||||
'judge.License',
|
||||
'sites.Site',
|
||||
'redirects.Redirect',
|
||||
],
|
||||
},
|
||||
('judge.BlogPost', 'fa-rss-square'),
|
||||
('judge.Comment', 'fa-comment-o'),
|
||||
('flatpages.FlatPage', 'fa-file-text-o'),
|
||||
('judge.Solution', 'fa-pencil'),
|
||||
],
|
||||
'dashboard': {
|
||||
'breadcrumbs': True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
INSTALLED_APPS += (
|
||||
'django.contrib.admin',
|
||||
'judge',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.flatpages',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.redirects',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.sitemaps',
|
||||
'registration',
|
||||
'mptt',
|
||||
'reversion',
|
||||
'django_social_share',
|
||||
'social_django',
|
||||
'compressor',
|
||||
'django_ace',
|
||||
'pagedown',
|
||||
'sortedm2m',
|
||||
'statici18n',
|
||||
'impersonate',
|
||||
'django_jinja',
|
||||
)
|
||||
|
||||
MIDDLEWARE = (
|
||||
'judge.middleware.ShortCircuitMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'judge.middleware.DMOJLoginMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'judge.user_log.LogUserAccessMiddleware',
|
||||
'judge.timezone.TimezoneMiddleware',
|
||||
'impersonate.middleware.ImpersonateMiddleware',
|
||||
'judge.middleware.DMOJImpersonationMiddleware',
|
||||
'judge.middleware.ContestMiddleware',
|
||||
'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
|
||||
'judge.social_auth.SocialAuthExceptionMiddleware',
|
||||
'django.contrib.redirects.middleware.RedirectFallbackMiddleware',
|
||||
)
|
||||
|
||||
IMPERSONATE_REQUIRE_SUPERUSER = True
|
||||
IMPERSONATE_DISABLE_LOGGING = True
|
||||
|
||||
ACCOUNT_ACTIVATION_DAYS = 7
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'judge.utils.pwned.PwnedPasswordsValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
SILENCED_SYSTEM_CHECKS = ['urls.W002', 'fields.W342']
|
||||
|
||||
ROOT_URLCONF = 'dmoj.urls'
|
||||
LOGIN_REDIRECT_URL = '/user'
|
||||
WSGI_APPLICATION = 'dmoj.wsgi.application'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django_jinja.backend.Jinja2',
|
||||
'DIRS': [
|
||||
os.path.join(BASE_DIR, 'templates'),
|
||||
],
|
||||
'APP_DIRS': False,
|
||||
'OPTIONS': {
|
||||
'match_extension': ('.html', '.txt'),
|
||||
'match_regex': '^(?!admin/)',
|
||||
'context_processors': [
|
||||
'django.template.context_processors.media',
|
||||
'django.template.context_processors.tz',
|
||||
'django.template.context_processors.i18n',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'judge.template_context.comet_location',
|
||||
'judge.template_context.get_resource',
|
||||
'judge.template_context.general_info',
|
||||
'judge.template_context.site',
|
||||
'judge.template_context.site_name',
|
||||
'judge.template_context.misc_config',
|
||||
'judge.template_context.math_setting',
|
||||
'social_django.context_processors.backends',
|
||||
'social_django.context_processors.login_redirect',
|
||||
],
|
||||
'autoescape': select_autoescape(['html', 'xml']),
|
||||
'trim_blocks': True,
|
||||
'lstrip_blocks': True,
|
||||
'extensions': DEFAULT_EXTENSIONS + [
|
||||
'compressor.contrib.jinja2ext.CompressorExtension',
|
||||
'judge.jinja2.DMOJExtension',
|
||||
'judge.jinja2.spaceless.SpacelessExtension',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'APP_DIRS': True,
|
||||
'DIRS': [
|
||||
os.path.join(BASE_DIR, 'templates'),
|
||||
],
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.template.context_processors.media',
|
||||
'django.template.context_processors.tz',
|
||||
'django.template.context_processors.i18n',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
LOCALE_PATHS = [
|
||||
os.path.join(BASE_DIR, 'locale'),
|
||||
]
|
||||
|
||||
LANGUAGES = [
|
||||
('de', _('German')),
|
||||
('en', _('English')),
|
||||
('es', _('Spanish')),
|
||||
('fr', _('French')),
|
||||
('hr', _('Croatian')),
|
||||
('hu', _('Hungarian')),
|
||||
('ja', _('Japanese')),
|
||||
('ko', _('Korean')),
|
||||
('pt', _('Brazilian Portuguese')),
|
||||
('ro', _('Romanian')),
|
||||
('ru', _('Russian')),
|
||||
('sr-latn', _('Serbian (Latin)')),
|
||||
('tr', _('Turkish')),
|
||||
('vi', _('Vietnamese')),
|
||||
('zh-hans', _('Simplified Chinese')),
|
||||
('zh-hant', _('Traditional Chinese')),
|
||||
]
|
||||
|
||||
MARKDOWN_ADMIN_EDITABLE_STYLE = {
|
||||
'safe_mode': False,
|
||||
'use_camo': True,
|
||||
'texoid': True,
|
||||
'math': True,
|
||||
}
|
||||
|
||||
MARKDOWN_DEFAULT_STYLE = {
|
||||
'safe_mode': True,
|
||||
'nofollow': True,
|
||||
'use_camo': True,
|
||||
'math': True,
|
||||
}
|
||||
|
||||
MARKDOWN_USER_LARGE_STYLE = {
|
||||
'safe_mode': True,
|
||||
'nofollow': True,
|
||||
'use_camo': True,
|
||||
'math': True,
|
||||
}
|
||||
|
||||
MARKDOWN_STYLES = {
|
||||
'comment': MARKDOWN_DEFAULT_STYLE,
|
||||
'self-description': MARKDOWN_USER_LARGE_STYLE,
|
||||
'problem': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
||||
'contest': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
||||
'language': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
||||
'license': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
||||
'judge': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
||||
'blog': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
||||
'solution': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
||||
'contest_tag': MARKDOWN_ADMIN_EDITABLE_STYLE,
|
||||
'organization-about': MARKDOWN_USER_LARGE_STYLE,
|
||||
'ticket': MARKDOWN_USER_LARGE_STYLE,
|
||||
}
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
},
|
||||
}
|
||||
|
||||
ENABLE_FTS = False
|
||||
|
||||
# Bridged configuration
|
||||
BRIDGED_JUDGE_ADDRESS = [('localhost', 9999)]
|
||||
BRIDGED_JUDGE_PROXIES = None
|
||||
BRIDGED_DJANGO_ADDRESS = [('localhost', 9998)]
|
||||
BRIDGED_DJANGO_CONNECT = None
|
||||
|
||||
# Event Server configuration
|
||||
EVENT_DAEMON_USE = False
|
||||
EVENT_DAEMON_POST = 'ws://localhost:9997/'
|
||||
EVENT_DAEMON_GET = 'ws://localhost:9996/'
|
||||
EVENT_DAEMON_POLL = '/channels/'
|
||||
EVENT_DAEMON_KEY = None
|
||||
EVENT_DAEMON_AMQP_EXCHANGE = 'dmoj-events'
|
||||
EVENT_DAEMON_SUBMISSION_KEY = '6Sdmkx^%pk@GsifDfXcwX*Y7LRF%RGT8vmFpSxFBT$fwS7trc8raWfN#CSfQuKApx&$B#Gh2L7p%W!Ww'
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
||||
|
||||
# Whatever you do, this better be one of the entries in `LANGUAGES`.
|
||||
LANGUAGE_CODE = 'en'
|
||||
TIME_ZONE = 'UTC'
|
||||
DEFAULT_USER_TIME_ZONE = 'America/Toronto'
|
||||
USE_I18N = True
|
||||
USE_L10N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Cookies
|
||||
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
||||
|
||||
DMOJ_RESOURCES = os.path.join(BASE_DIR, 'resources')
|
||||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
)
|
||||
STATICFILES_DIRS = [
|
||||
os.path.join(BASE_DIR, 'resources'),
|
||||
]
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
# Define a cache
|
||||
CACHES = {}
|
||||
|
||||
# Authentication
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'social_core.backends.google.GoogleOAuth2',
|
||||
'social_core.backends.facebook.FacebookOAuth2',
|
||||
'judge.social_auth.GitHubSecureEmailOAuth2',
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
)
|
||||
|
||||
SOCIAL_AUTH_PIPELINE = (
|
||||
'social_core.pipeline.social_auth.social_details',
|
||||
'social_core.pipeline.social_auth.social_uid',
|
||||
'social_core.pipeline.social_auth.auth_allowed',
|
||||
'judge.social_auth.verify_email',
|
||||
'social_core.pipeline.social_auth.social_user',
|
||||
'social_core.pipeline.user.get_username',
|
||||
'social_core.pipeline.social_auth.associate_by_email',
|
||||
'judge.social_auth.choose_username',
|
||||
'social_core.pipeline.user.create_user',
|
||||
'judge.social_auth.make_profile',
|
||||
'social_core.pipeline.social_auth.associate_user',
|
||||
'social_core.pipeline.social_auth.load_extra_data',
|
||||
'social_core.pipeline.user.user_details',
|
||||
)
|
||||
|
||||
SOCIAL_AUTH_GITHUB_SECURE_SCOPE = ['user:email']
|
||||
SOCIAL_AUTH_FACEBOOK_SCOPE = ['email']
|
||||
SOCIAL_AUTH_SLUGIFY_USERNAMES = True
|
||||
SOCIAL_AUTH_SLUGIFY_FUNCTION = 'judge.social_auth.slugify_username'
|
||||
|
||||
JUDGE_AMQP_PATH = None
|
||||
|
||||
MOSS_API_KEY = None
|
||||
|
||||
CELERY_WORKER_HIJACK_ROOT_LOGGER = False
|
||||
|
||||
try:
|
||||
with open(os.path.join(os.path.dirname(__file__), 'local_settings.py')) as f:
|
||||
exec(f.read(), globals())
|
||||
except IOError:
|
||||
pass
|
29
dmoj/throttle_mail.py
Normal file
29
dmoj/throttle_mail.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
import traceback
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.utils.log import AdminEmailHandler
|
||||
|
||||
DEFAULT_THROTTLE = (10, 60)
|
||||
|
||||
|
||||
def new_email():
|
||||
cache.add('error_email_throttle', 0, settings.DMOJ_EMAIL_THROTTLING[1])
|
||||
return cache.incr('error_email_throttle')
|
||||
|
||||
|
||||
class ThrottledEmailHandler(AdminEmailHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ThrottledEmailHandler, self).__init__(*args, **kwargs)
|
||||
|
||||
self.throttle = settings.DMOJ_EMAIL_THROTTLING[0]
|
||||
|
||||
def emit(self, record):
|
||||
try:
|
||||
count = new_email()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
else:
|
||||
if count >= self.throttle:
|
||||
return
|
||||
AdminEmailHandler.emit(self, record)
|
387
dmoj/urls.py
Normal file
387
dmoj/urls.py
Normal file
|
@ -0,0 +1,387 @@
|
|||
from django.conf import settings
|
||||
from django.conf.urls import include, url
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import views as auth_views
|
||||
from django.contrib.sitemaps.views import sitemap
|
||||
from django.http import Http404, HttpResponsePermanentRedirect
|
||||
from django.templatetags.static import static
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import lazystr
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from judge.feed import AtomBlogFeed, AtomCommentFeed, AtomProblemFeed, BlogFeed, CommentFeed, ProblemFeed
|
||||
from judge.forms import CustomAuthenticationForm
|
||||
from judge.sitemap import BlogPostSitemap, ContestSitemap, HomePageSitemap, OrganizationSitemap, ProblemSitemap, \
|
||||
SolutionSitemap, UrlSitemap, UserSitemap
|
||||
from judge.views import TitledTemplateView, api, blog, comment, contests, language, license, mailgun, organization, \
|
||||
preview, problem, problem_manage, ranked_submission, register, stats, status, submission, tasks, ticket, totp, \
|
||||
user, widgets
|
||||
from judge.views.problem_data import ProblemDataView, ProblemSubmissionDiff, \
|
||||
problem_data_file, problem_init_view
|
||||
from judge.views.register import ActivationView, RegistrationView
|
||||
from judge.views.select2 import AssigneeSelect2View, CommentSelect2View, ContestSelect2View, \
|
||||
ContestUserSearchSelect2View, OrganizationSelect2View, ProblemSelect2View, TicketUserSelect2View, \
|
||||
UserSearchSelect2View, UserSelect2View
|
||||
|
||||
admin.autodiscover()
|
||||
|
||||
register_patterns = [
|
||||
url(r'^activate/complete/$',
|
||||
TitledTemplateView.as_view(template_name='registration/activation_complete.html',
|
||||
title='Activation Successful!'),
|
||||
name='registration_activation_complete'),
|
||||
# Activation keys get matched by \w+ instead of the more specific
|
||||
# [a-fA-F0-9]{40} because a bad activation key should still get to the view;
|
||||
# that way it can return a sensible "invalid key" message instead of a
|
||||
# confusing 404.
|
||||
url(r'^activate/(?P<activation_key>\w+)/$',
|
||||
ActivationView.as_view(title='Activation key invalid'),
|
||||
name='registration_activate'),
|
||||
url(r'^register/$',
|
||||
RegistrationView.as_view(title='Register'),
|
||||
name='registration_register'),
|
||||
url(r'^register/complete/$',
|
||||
TitledTemplateView.as_view(template_name='registration/registration_complete.html',
|
||||
title='Registration Completed'),
|
||||
name='registration_complete'),
|
||||
url(r'^register/closed/$',
|
||||
TitledTemplateView.as_view(template_name='registration/registration_closed.html',
|
||||
title='Registration not allowed'),
|
||||
name='registration_disallowed'),
|
||||
url(r'^login/$', auth_views.LoginView.as_view(
|
||||
template_name='registration/login.html',
|
||||
extra_context={'title': _('Login')},
|
||||
authentication_form=CustomAuthenticationForm,
|
||||
redirect_authenticated_user=True,
|
||||
), name='auth_login'),
|
||||
url(r'^logout/$', user.UserLogoutView.as_view(), name='auth_logout'),
|
||||
url(r'^password/change/$', auth_views.PasswordChangeView.as_view(
|
||||
template_name='registration/password_change_form.html',
|
||||
), name='password_change'),
|
||||
url(r'^password/change/done/$', auth_views.PasswordChangeDoneView.as_view(
|
||||
template_name='registration/password_change_done.html',
|
||||
), name='password_change_done'),
|
||||
url(r'^password/reset/$', auth_views.PasswordResetView.as_view(
|
||||
template_name='registration/password_reset.html',
|
||||
html_email_template_name='registration/password_reset_email.html',
|
||||
email_template_name='registration/password_reset_email.txt',
|
||||
), name='password_reset'),
|
||||
url(r'^password/reset/confirm/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$',
|
||||
auth_views.PasswordResetConfirmView.as_view(
|
||||
template_name='registration/password_reset_confirm.html',
|
||||
), name='password_reset_confirm'),
|
||||
url(r'^password/reset/complete/$', auth_views.PasswordResetCompleteView.as_view(
|
||||
template_name='registration/password_reset_complete.html',
|
||||
), name='password_reset_complete'),
|
||||
url(r'^password/reset/done/$', auth_views.PasswordResetDoneView.as_view(
|
||||
template_name='registration/password_reset_done.html',
|
||||
), name='password_reset_done'),
|
||||
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/enable/$', totp.TOTPEnableView.as_view(), name='enable_2fa'),
|
||||
url(r'^2fa/disable/$', totp.TOTPDisableView.as_view(), name='disable_2fa'),
|
||||
]
|
||||
|
||||
|
||||
def exception(request):
|
||||
if not request.user.is_superuser:
|
||||
raise Http404()
|
||||
raise RuntimeError('@Xyene asked me to cause this')
|
||||
|
||||
|
||||
def paged_list_view(view, name):
|
||||
return include([
|
||||
url(r'^$', view.as_view(), name=name),
|
||||
url(r'^(?P<page>\d+)$', view.as_view(), name=name),
|
||||
])
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', blog.PostList.as_view(template_name='home.html', title=_('Home')), kwargs={'page': 1}, name='home'),
|
||||
url(r'^500/$', exception),
|
||||
url(r'^admin/', admin.site.urls),
|
||||
url(r'^i18n/', include('django.conf.urls.i18n')),
|
||||
url(r'^accounts/', include(register_patterns)),
|
||||
url(r'^', include('social_django.urls')),
|
||||
|
||||
url(r'^problems/$', problem.ProblemList.as_view(), name='problem_list'),
|
||||
url(r'^problems/random/$', problem.RandomProblem.as_view(), name='problem_random'),
|
||||
|
||||
url(r'^problem/(?P<problem>[^/]+)', include([
|
||||
url(r'^$', problem.ProblemDetail.as_view(), name='problem_detail'),
|
||||
url(r'^/editorial$', problem.ProblemSolution.as_view(), name='problem_editorial'),
|
||||
url(r'^/raw$', problem.ProblemRaw.as_view(), name='problem_raw'),
|
||||
url(r'^/pdf$', problem.ProblemPdfView.as_view(), name='problem_pdf'),
|
||||
url(r'^/pdf/(?P<language>[a-z-]+)$', problem.ProblemPdfView.as_view(), name='problem_pdf'),
|
||||
url(r'^/clone', problem.ProblemClone.as_view(), name='problem_clone'),
|
||||
url(r'^/submit$', problem.problem_submit, name='problem_submit'),
|
||||
url(r'^/resubmit/(?P<submission>\d+)$', problem.problem_submit, name='problem_submit'),
|
||||
|
||||
url(r'^/rank/', paged_list_view(ranked_submission.RankedSubmissions, 'ranked_submissions')),
|
||||
url(r'^/submissions/', paged_list_view(submission.ProblemSubmissions, 'chronological_submissions')),
|
||||
url(r'^/submissions/(?P<user>\w+)/', paged_list_view(submission.UserProblemSubmissions, 'user_submissions')),
|
||||
|
||||
url(r'^/$', lambda _, problem: HttpResponsePermanentRedirect(reverse('problem_detail', args=[problem]))),
|
||||
|
||||
url(r'^/test_data$', ProblemDataView.as_view(), name='problem_data'),
|
||||
url(r'^/test_data/init$', problem_init_view, name='problem_data_init'),
|
||||
url(r'^/test_data/diff$', ProblemSubmissionDiff.as_view(), name='problem_submission_diff'),
|
||||
url(r'^/data/(?P<path>.+)$', problem_data_file, name='problem_data_file'),
|
||||
|
||||
url(r'^/tickets$', ticket.ProblemTicketListView.as_view(), name='problem_ticket_list'),
|
||||
url(r'^/tickets/new$', ticket.NewProblemTicketView.as_view(), name='new_problem_ticket'),
|
||||
|
||||
url(r'^/manage/submission', include([
|
||||
url('^$', problem_manage.ManageProblemSubmissionView.as_view(), name='problem_manage_submissions'),
|
||||
url('^/rejudge$', problem_manage.RejudgeSubmissionsView.as_view(), name='problem_submissions_rejudge'),
|
||||
url('^/rejudge/preview$', problem_manage.PreviewRejudgeSubmissionsView.as_view(),
|
||||
name='problem_submissions_rejudge_preview'),
|
||||
url('^/rejudge/success/(?P<task_id>[A-Za-z0-9-]*)$', problem_manage.rejudge_success,
|
||||
name='problem_submissions_rejudge_success'),
|
||||
url('^/rescore/all$', problem_manage.RescoreAllSubmissionsView.as_view(),
|
||||
name='problem_submissions_rescore_all'),
|
||||
url('^/rescore/success/(?P<task_id>[A-Za-z0-9-]*)$', problem_manage.rescore_success,
|
||||
name='problem_submissions_rescore_success'),
|
||||
])),
|
||||
])),
|
||||
|
||||
url(r'^submissions/', paged_list_view(submission.AllSubmissions, 'all_submissions')),
|
||||
url(r'^submissions/user/(?P<user>\w+)/', paged_list_view(submission.AllUserSubmissions, 'all_user_submissions')),
|
||||
|
||||
url(r'^src/(?P<submission>\d+)$', submission.SubmissionSource.as_view(), name='submission_source'),
|
||||
url(r'^src/(?P<submission>\d+)/raw$', submission.SubmissionSourceRaw.as_view(), name='submission_source_raw'),
|
||||
|
||||
url(r'^submission/(?P<submission>\d+)', include([
|
||||
url(r'^$', submission.SubmissionStatus.as_view(), name='submission_status'),
|
||||
url(r'^/abort$', submission.abort_submission, name='submission_abort'),
|
||||
url(r'^/html$', submission.single_submission),
|
||||
])),
|
||||
|
||||
url(r'^users/', include([
|
||||
url(r'^$', user.users, name='user_list'),
|
||||
url(r'^(?P<page>\d+)$', lambda request, page:
|
||||
HttpResponsePermanentRedirect('%s?page=%s' % (reverse('user_list'), page))),
|
||||
url(r'^find$', user.user_ranking_redirect, name='user_ranking_redirect'),
|
||||
])),
|
||||
|
||||
url(r'^user$', user.UserAboutPage.as_view(), name='user_page'),
|
||||
url(r'^edit/profile/$', user.edit_profile, name='user_edit_profile'),
|
||||
url(r'^user/(?P<user>\w+)', include([
|
||||
url(r'^$', user.UserAboutPage.as_view(), name='user_page'),
|
||||
url(r'^/solved', include([
|
||||
url(r'^$', user.UserProblemsPage.as_view(), name='user_problems'),
|
||||
url(r'/ajax$', user.UserPerformancePointsAjax.as_view(), name='user_pp_ajax'),
|
||||
])),
|
||||
url(r'^/submissions/', paged_list_view(submission.AllUserSubmissions, 'all_user_submissions_old')),
|
||||
url(r'^/submissions/', lambda _, user:
|
||||
HttpResponsePermanentRedirect(reverse('all_user_submissions', args=[user]))),
|
||||
|
||||
url(r'^/$', lambda _, user: HttpResponsePermanentRedirect(reverse('user_page', args=[user]))),
|
||||
])),
|
||||
|
||||
url(r'^comments/upvote/$', comment.upvote_comment, name='comment_upvote'),
|
||||
url(r'^comments/downvote/$', comment.downvote_comment, name='comment_downvote'),
|
||||
url(r'^comments/hide/$', comment.comment_hide, name='comment_hide'),
|
||||
url(r'^comments/(?P<id>\d+)/', include([
|
||||
url(r'^edit$', comment.CommentEdit.as_view(), name='comment_edit'),
|
||||
url(r'^history/ajax$', comment.CommentRevisionAjax.as_view(), name='comment_revision_ajax'),
|
||||
url(r'^edit/ajax$', comment.CommentEditAjax.as_view(), name='comment_edit_ajax'),
|
||||
url(r'^votes/ajax$', comment.CommentVotesAjax.as_view(), name='comment_votes_ajax'),
|
||||
url(r'^render$', comment.CommentContent.as_view(), name='comment_content'),
|
||||
])),
|
||||
|
||||
url(r'^contests/', paged_list_view(contests.ContestList, 'contest_list')),
|
||||
url(r'^contests/(?P<year>\d+)/(?P<month>\d+)/$', contests.ContestCalendar.as_view(), name='contest_calendar'),
|
||||
url(r'^contests/tag/(?P<name>[a-z-]+)', include([
|
||||
url(r'^$', contests.ContestTagDetail.as_view(), name='contest_tag'),
|
||||
url(r'^/ajax$', contests.ContestTagDetailAjax.as_view(), name='contest_tag_ajax'),
|
||||
])),
|
||||
|
||||
url(r'^contest/(?P<contest>\w+)', include([
|
||||
url(r'^$', contests.ContestDetail.as_view(), name='contest_view'),
|
||||
url(r'^/moss$', contests.ContestMossView.as_view(), name='contest_moss'),
|
||||
url(r'^/moss/delete$', contests.ContestMossDelete.as_view(), name='contest_moss_delete'),
|
||||
url(r'^/clone$', contests.ContestClone.as_view(), name='contest_clone'),
|
||||
url(r'^/ranking/$', contests.ContestRanking.as_view(), name='contest_ranking'),
|
||||
url(r'^/ranking/ajax$', contests.contest_ranking_ajax, name='contest_ranking_ajax'),
|
||||
url(r'^/join$', contests.ContestJoin.as_view(), name='contest_join'),
|
||||
url(r'^/leave$', contests.ContestLeave.as_view(), name='contest_leave'),
|
||||
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(r'^/submissions/(?P<user>\w+)/(?P<problem>\w+)/',
|
||||
paged_list_view(submission.UserContestSubmissions, 'contest_user_submissions')),
|
||||
|
||||
url(r'^/participations$', contests.ContestParticipationList.as_view(), name='contest_participation_own'),
|
||||
url(r'^/participations/(?P<user>\w+)$',
|
||||
contests.ContestParticipationList.as_view(), name='contest_participation'),
|
||||
url(r'^/participation/disqualify$', contests.ContestParticipationDisqualify.as_view(),
|
||||
name='contest_participation_disqualify'),
|
||||
|
||||
url(r'^/$', lambda _, contest: HttpResponsePermanentRedirect(reverse('contest_view', args=[contest]))),
|
||||
])),
|
||||
|
||||
url(r'^organizations/$', organization.OrganizationList.as_view(), name='organization_list'),
|
||||
url(r'^organization/(?P<pk>\d+)-(?P<slug>[\w-]*)', include([
|
||||
url(r'^$', organization.OrganizationHome.as_view(), name='organization_home'),
|
||||
url(r'^/users$', organization.OrganizationUsers.as_view(), name='organization_users'),
|
||||
url(r'^/join$', organization.JoinOrganization.as_view(), name='join_organization'),
|
||||
url(r'^/leave$', organization.LeaveOrganization.as_view(), name='leave_organization'),
|
||||
url(r'^/edit$', organization.EditOrganization.as_view(), name='edit_organization'),
|
||||
url(r'^/kick$', organization.KickUserWidgetView.as_view(), name='organization_user_kick'),
|
||||
|
||||
url(r'^/request$', organization.RequestJoinOrganization.as_view(), name='request_organization'),
|
||||
url(r'^/request/(?P<rpk>\d+)$', organization.OrganizationRequestDetail.as_view(),
|
||||
name='request_organization_detail'),
|
||||
url(r'^/requests/', include([
|
||||
url(r'^pending$', organization.OrganizationRequestView.as_view(), name='organization_requests_pending'),
|
||||
url(r'^log$', organization.OrganizationRequestLog.as_view(), name='organization_requests_log'),
|
||||
url(r'^approved$', organization.OrganizationRequestLog.as_view(states=('A',), tab='approved'),
|
||||
name='organization_requests_approved'),
|
||||
url(r'^rejected$', organization.OrganizationRequestLog.as_view(states=('R',), tab='rejected'),
|
||||
name='organization_requests_rejected'),
|
||||
])),
|
||||
|
||||
url(r'^/$', lambda _, pk, slug: HttpResponsePermanentRedirect(reverse('organization_home', args=[pk, slug]))),
|
||||
])),
|
||||
|
||||
url(r'^runtimes/$', language.LanguageList.as_view(), name='runtime_list'),
|
||||
url(r'^runtimes/matrix/$', status.version_matrix, name='version_matrix'),
|
||||
url(r'^status/$', status.status_all, name='status_all'),
|
||||
|
||||
url(r'^api/', include([
|
||||
url(r'^contest/list$', api.api_v1_contest_list),
|
||||
url(r'^contest/info/(\w+)$', api.api_v1_contest_detail),
|
||||
url(r'^problem/list$', api.api_v1_problem_list),
|
||||
url(r'^problem/info/(\w+)$', api.api_v1_problem_info),
|
||||
url(r'^user/list$', api.api_v1_user_list),
|
||||
url(r'^user/info/(\w+)$', api.api_v1_user_info),
|
||||
url(r'^user/submissions/(\w+)$', api.api_v1_user_submissions),
|
||||
])),
|
||||
|
||||
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'^license/(?P<key>[-\w.]+)$', license.LicenseDetail.as_view(), name='license'),
|
||||
|
||||
url(r'^mailgun/mail_activate/$', mailgun.MailgunActivationView.as_view(), name='mailgun_activate'),
|
||||
|
||||
url(r'^widgets/', include([
|
||||
url(r'^rejudge$', widgets.rejudge_submission, name='submission_rejudge'),
|
||||
url(r'^single_submission$', submission.single_submission_query, name='submission_single_query'),
|
||||
url(r'^submission_testcases$', submission.SubmissionTestCaseQuery.as_view(), name='submission_testcases_query'),
|
||||
url(r'^detect_timezone$', widgets.DetectTimezone.as_view(), name='detect_timezone'),
|
||||
url(r'^status-table$', status.status_table, name='status_table'),
|
||||
|
||||
url(r'^template$', problem.LanguageTemplateAjax.as_view(), name='language_template_ajax'),
|
||||
|
||||
url(r'^select2/', include([
|
||||
url(r'^user_search$', UserSearchSelect2View.as_view(), name='user_search_select2_ajax'),
|
||||
url(r'^contest_users/(?P<contest>\w+)$', ContestUserSearchSelect2View.as_view(),
|
||||
name='contest_user_search_select2_ajax'),
|
||||
url(r'^ticket_user$', TicketUserSelect2View.as_view(), name='ticket_user_select2_ajax'),
|
||||
url(r'^ticket_assignee$', AssigneeSelect2View.as_view(), name='ticket_assignee_select2_ajax'),
|
||||
])),
|
||||
|
||||
url(r'^preview/', include([
|
||||
url(r'^problem$', preview.ProblemMarkdownPreviewView.as_view(), name='problem_preview'),
|
||||
url(r'^blog$', preview.BlogMarkdownPreviewView.as_view(), name='blog_preview'),
|
||||
url(r'^contest$', preview.ContestMarkdownPreviewView.as_view(), name='contest_preview'),
|
||||
url(r'^comment$', preview.CommentMarkdownPreviewView.as_view(), name='comment_preview'),
|
||||
url(r'^profile$', preview.ProfileMarkdownPreviewView.as_view(), name='profile_preview'),
|
||||
url(r'^organization$', preview.OrganizationMarkdownPreviewView.as_view(), name='organization_preview'),
|
||||
url(r'^solution$', preview.SolutionMarkdownPreviewView.as_view(), name='solution_preview'),
|
||||
url(r'^license$', preview.LicenseMarkdownPreviewView.as_view(), name='license_preview'),
|
||||
url(r'^ticket$', preview.TicketMarkdownPreviewView.as_view(), name='ticket_preview'),
|
||||
])),
|
||||
])),
|
||||
|
||||
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(r'^stats/', include([
|
||||
url('^language/', include([
|
||||
url('^$', stats.language, name='language_stats'),
|
||||
url('^data/all/$', stats.language_data, name='language_stats_data_all'),
|
||||
url('^data/ac/$', stats.ac_language_data, name='language_stats_data_ac'),
|
||||
url('^data/status/$', stats.status_data, name='stats_data_status'),
|
||||
url('^data/ac_rate/$', stats.ac_rate, name='language_stats_data_ac_rate'),
|
||||
])),
|
||||
])),
|
||||
|
||||
url(r'^tickets/', include([
|
||||
url(r'^$', ticket.TicketList.as_view(), name='ticket_list'),
|
||||
url(r'^ajax$', ticket.TicketListDataAjax.as_view(), name='ticket_ajax'),
|
||||
])),
|
||||
|
||||
url(r'^ticket/(?P<pk>\d+)', include([
|
||||
url(r'^$', ticket.TicketView.as_view(), name='ticket'),
|
||||
url(r'^/ajax$', ticket.TicketMessageDataAjax.as_view(), name='ticket_message_ajax'),
|
||||
url(r'^/open$', ticket.TicketStatusChangeView.as_view(open=True), name='ticket_open'),
|
||||
url(r'^/close$', ticket.TicketStatusChangeView.as_view(open=False), name='ticket_close'),
|
||||
url(r'^/notes$', ticket.TicketNotesEditView.as_view(), name='ticket_notes'),
|
||||
])),
|
||||
|
||||
url(r'^sitemap\.xml$', sitemap, {'sitemaps': {
|
||||
'problem': ProblemSitemap,
|
||||
'user': UserSitemap,
|
||||
'home': HomePageSitemap,
|
||||
'contest': ContestSitemap,
|
||||
'organization': OrganizationSitemap,
|
||||
'blog': BlogPostSitemap,
|
||||
'solutions': SolutionSitemap,
|
||||
'pages': UrlSitemap([
|
||||
{'location': '/about/', 'priority': 0.9},
|
||||
]),
|
||||
}}),
|
||||
|
||||
url(r'^judge-select2/', include([
|
||||
url(r'^profile/$', UserSelect2View.as_view(), name='profile_select2'),
|
||||
url(r'^organization/$', OrganizationSelect2View.as_view(), name='organization_select2'),
|
||||
url(r'^problem/$', ProblemSelect2View.as_view(), name='problem_select2'),
|
||||
url(r'^contest/$', ContestSelect2View.as_view(), name='contest_select2'),
|
||||
url(r'^comment/$', CommentSelect2View.as_view(), name='comment_select2'),
|
||||
])),
|
||||
|
||||
url(r'^tasks/', include([
|
||||
url(r'^status/(?P<task_id>[A-Za-z0-9-]*)$', tasks.task_status, name='task_status'),
|
||||
url(r'^ajax_status$', tasks.task_status_ajax, name='task_status_ajax'),
|
||||
url(r'^success$', tasks.demo_success),
|
||||
url(r'^failure$', tasks.demo_failure),
|
||||
url(r'^progress$', tasks.demo_progress),
|
||||
])),
|
||||
]
|
||||
|
||||
favicon_paths = ['apple-touch-icon-180x180.png', 'apple-touch-icon-114x114.png', 'android-chrome-72x72.png',
|
||||
'apple-touch-icon-57x57.png', 'apple-touch-icon-72x72.png', 'apple-touch-icon.png', 'mstile-70x70.png',
|
||||
'android-chrome-36x36.png', 'apple-touch-icon-precomposed.png', 'apple-touch-icon-76x76.png',
|
||||
'apple-touch-icon-60x60.png', 'android-chrome-96x96.png', 'mstile-144x144.png', 'mstile-150x150.png',
|
||||
'safari-pinned-tab.svg', 'android-chrome-144x144.png', 'apple-touch-icon-152x152.png',
|
||||
'favicon-96x96.png',
|
||||
'favicon-32x32.png', 'favicon-16x16.png', 'android-chrome-192x192.png', 'android-chrome-48x48.png',
|
||||
'mstile-310x150.png', 'apple-touch-icon-144x144.png', 'browserconfig.xml', 'manifest.json',
|
||||
'apple-touch-icon-120x120.png', 'mstile-310x310.png']
|
||||
|
||||
for favicon in favicon_paths:
|
||||
urlpatterns.append(url(r'^%s$' % favicon, RedirectView.as_view(
|
||||
url=lazystr(lambda: static('icons/' + favicon)),
|
||||
)))
|
||||
|
||||
handler404 = 'judge.views.error.error404'
|
||||
handler403 = 'judge.views.error.error403'
|
||||
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:
|
||||
urlpatterns.append(url(r'^impersonate/', include('impersonate.urls')))
|
12
dmoj/wsgi.py
Normal file
12
dmoj/wsgi.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
import os
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings')
|
||||
|
||||
try:
|
||||
import MySQLdb # noqa: F401, imported for side effect
|
||||
except ImportError:
|
||||
import pymysql
|
||||
|
||||
pymysql.install_as_MySQLdb()
|
||||
|
||||
from django.core.wsgi import get_wsgi_application # noqa: E402, django must be imported here
|
||||
application = get_wsgi_application()
|
14
dmoj/wsgi_async.py
Normal file
14
dmoj/wsgi_async.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
import os
|
||||
|
||||
import gevent.monkey # noqa: I100, gevent must be imported here
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings')
|
||||
gevent.monkey.patch_all()
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
import dmoj_install_pymysql # noqa: F401, I100, I202, imported for side effect
|
||||
|
||||
from django.core.wsgi import get_wsgi_application # noqa: E402, I100, I202, django must be imported here
|
||||
# noinspection PyUnresolvedReferences
|
||||
import django_2_2_pymysql_patch # noqa: I100, F401, I202, imported for side effect
|
||||
application = get_wsgi_application()
|
15
dmoj_celery.py
Normal file
15
dmoj_celery.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
import os
|
||||
|
||||
try:
|
||||
import MySQLdb # noqa: F401, imported for side effect
|
||||
except ImportError:
|
||||
import dmoj_install_pymysql # noqa: F401, imported for side effect
|
||||
|
||||
# set the default Django settings module for the 'celery' program.
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings')
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
import django_2_2_pymysql_patch # noqa: I100, F401, I202, imported for side effect
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
from dmoj.celery import app # noqa: F401, imported for side effect
|
4
dmoj_install_pymysql.py
Normal file
4
dmoj_install_pymysql.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
import pymysql
|
||||
|
||||
pymysql.install_as_MySQLdb()
|
||||
pymysql.version_info = (1, 3, 13, "final", 0)
|
11
event_socket_server/__init__.py
Normal file
11
event_socket_server/__init__.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from .base_server import BaseServer
|
||||
from .engines import *
|
||||
from .handler import Handler
|
||||
from .helpers import ProxyProtocolMixin, SizedPacketHandler, ZlibPacketHandler
|
||||
|
||||
|
||||
def get_preferred_engine(choices=('epoll', 'poll', 'select')):
|
||||
for choice in choices:
|
||||
if choice in engines:
|
||||
return engines[choice]
|
||||
return engines['select']
|
169
event_socket_server/base_server.py
Normal file
169
event_socket_server/base_server.py
Normal file
|
@ -0,0 +1,169 @@
|
|||
import logging
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from collections import defaultdict, deque
|
||||
from functools import total_ordering
|
||||
from heapq import heappop, heappush
|
||||
|
||||
logger = logging.getLogger('event_socket_server')
|
||||
|
||||
|
||||
class SendMessage(object):
|
||||
__slots__ = ('data', 'callback')
|
||||
|
||||
def __init__(self, data, callback):
|
||||
self.data = data
|
||||
self.callback = callback
|
||||
|
||||
|
||||
@total_ordering
|
||||
class ScheduledJob(object):
|
||||
__slots__ = ('time', 'func', 'args', 'kwargs', 'cancel', 'dispatched')
|
||||
|
||||
def __init__(self, time, func, args, kwargs):
|
||||
self.time = time
|
||||
self.func = func
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.cancel = False
|
||||
self.dispatched = False
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.time == other.time
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.time < other.time
|
||||
|
||||
|
||||
class BaseServer(object):
|
||||
def __init__(self, addresses, client):
|
||||
self._servers = set()
|
||||
for address, port in addresses:
|
||||
info = socket.getaddrinfo(address, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
|
||||
for af, socktype, proto, canonname, sa in info:
|
||||
sock = socket.socket(af, socktype, proto)
|
||||
sock.setblocking(0)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(sa)
|
||||
self._servers.add(sock)
|
||||
|
||||
self._stop = threading.Event()
|
||||
self._clients = set()
|
||||
self._ClientClass = client
|
||||
self._send_queue = defaultdict(deque)
|
||||
self._job_queue = []
|
||||
self._job_queue_lock = threading.Lock()
|
||||
|
||||
def _serve(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _accept(self, sock):
|
||||
conn, address = sock.accept()
|
||||
conn.setblocking(0)
|
||||
client = self._ClientClass(self, conn)
|
||||
self._clients.add(client)
|
||||
return client
|
||||
|
||||
def schedule(self, delay, func, *args, **kwargs):
|
||||
with self._job_queue_lock:
|
||||
job = ScheduledJob(time.time() + delay, func, args, kwargs)
|
||||
heappush(self._job_queue, job)
|
||||
return job
|
||||
|
||||
def unschedule(self, job):
|
||||
with self._job_queue_lock:
|
||||
if job.dispatched or job.cancel:
|
||||
return False
|
||||
job.cancel = True
|
||||
return True
|
||||
|
||||
def _register_write(self, client):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _register_read(self, client):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _clean_up_client(self, client, finalize=False):
|
||||
try:
|
||||
del self._send_queue[client.fileno()]
|
||||
except KeyError:
|
||||
pass
|
||||
client.on_close()
|
||||
client._socket.close()
|
||||
if not finalize:
|
||||
self._clients.remove(client)
|
||||
|
||||
def _dispatch_event(self):
|
||||
t = time.time()
|
||||
tasks = []
|
||||
with self._job_queue_lock:
|
||||
while True:
|
||||
dt = self._job_queue[0].time - t if self._job_queue else 1
|
||||
if dt > 0:
|
||||
break
|
||||
task = heappop(self._job_queue)
|
||||
task.dispatched = True
|
||||
if not task.cancel:
|
||||
tasks.append(task)
|
||||
for task in tasks:
|
||||
logger.debug('Dispatching event: %r(*%r, **%r)', task.func, task.args, task.kwargs)
|
||||
task.func(*task.args, **task.kwargs)
|
||||
if not self._job_queue or dt > 1:
|
||||
dt = 1
|
||||
return dt
|
||||
|
||||
def _nonblock_read(self, client):
|
||||
try:
|
||||
data = client._socket.recv(1024)
|
||||
except socket.error:
|
||||
self._clean_up_client(client)
|
||||
else:
|
||||
logger.debug('Read from %s: %d bytes', client.client_address, len(data))
|
||||
if not data:
|
||||
self._clean_up_client(client)
|
||||
else:
|
||||
try:
|
||||
client._recv_data(data)
|
||||
except Exception:
|
||||
logger.exception('Client recv_data failure')
|
||||
self._clean_up_client(client)
|
||||
|
||||
def _nonblock_write(self, client):
|
||||
fd = client.fileno()
|
||||
queue = self._send_queue[fd]
|
||||
try:
|
||||
top = queue[0]
|
||||
cb = client._socket.send(top.data)
|
||||
top.data = top.data[cb:]
|
||||
logger.debug('Send to %s: %d bytes', client.client_address, cb)
|
||||
if not top.data:
|
||||
logger.debug('Finished sending: %s', client.client_address)
|
||||
if top.callback is not None:
|
||||
logger.debug('Calling callback: %s: %r', client.client_address, top.callback)
|
||||
try:
|
||||
top.callback()
|
||||
except Exception:
|
||||
logger.exception('Client write callback failure')
|
||||
self._clean_up_client(client)
|
||||
return
|
||||
queue.popleft()
|
||||
if not queue:
|
||||
self._register_read(client)
|
||||
del self._send_queue[fd]
|
||||
except socket.error:
|
||||
self._clean_up_client(client)
|
||||
|
||||
def send(self, client, data, callback=None):
|
||||
logger.debug('Writing %d bytes to client %s, callback: %s', len(data), client.client_address, callback)
|
||||
self._send_queue[client.fileno()].append(SendMessage(data, callback))
|
||||
self._register_write(client)
|
||||
|
||||
def stop(self):
|
||||
self._stop.set()
|
||||
|
||||
def serve_forever(self):
|
||||
self._serve()
|
||||
|
||||
def on_shutdown(self):
|
||||
pass
|
17
event_socket_server/engines/__init__.py
Normal file
17
event_socket_server/engines/__init__.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
import select
|
||||
|
||||
__author__ = 'Quantum'
|
||||
engines = {}
|
||||
|
||||
from .select_server import SelectServer # noqa: E402, import not at top for consistency
|
||||
engines['select'] = SelectServer
|
||||
|
||||
if hasattr(select, 'poll'):
|
||||
from .poll_server import PollServer
|
||||
engines['poll'] = PollServer
|
||||
|
||||
if hasattr(select, 'epoll'):
|
||||
from .epoll_server import EpollServer
|
||||
engines['epoll'] = EpollServer
|
||||
|
||||
del select
|
17
event_socket_server/engines/epoll_server.py
Normal file
17
event_socket_server/engines/epoll_server.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
import select
|
||||
__author__ = 'Quantum'
|
||||
|
||||
if not hasattr(select, 'epoll'):
|
||||
raise ImportError('System does not support epoll')
|
||||
|
||||
from .poll_server import PollServer # noqa: E402, must be imported here
|
||||
|
||||
|
||||
class EpollServer(PollServer):
|
||||
poll = select.epoll
|
||||
WRITE = select.EPOLLIN | select.EPOLLOUT | select.EPOLLERR | select.EPOLLHUP
|
||||
READ = select.EPOLLIN | select.EPOLLERR | select.EPOLLHUP
|
||||
POLLIN = select.EPOLLIN
|
||||
POLLOUT = select.EPOLLOUT
|
||||
POLL_CLOSE = select.EPOLLHUP | select.EPOLLERR
|
||||
NEED_CLOSE = True
|
97
event_socket_server/engines/poll_server.py
Normal file
97
event_socket_server/engines/poll_server.py
Normal file
|
@ -0,0 +1,97 @@
|
|||
import errno
|
||||
import logging
|
||||
import select
|
||||
import threading
|
||||
|
||||
from ..base_server import BaseServer
|
||||
|
||||
logger = logging.getLogger('event_socket_server')
|
||||
|
||||
if not hasattr(select, 'poll'):
|
||||
raise ImportError('System does not support poll')
|
||||
|
||||
|
||||
class PollServer(BaseServer):
|
||||
poll = select.poll
|
||||
WRITE = select.POLLIN | select.POLLOUT | select.POLLERR | select.POLLHUP
|
||||
READ = select.POLLIN | select.POLLERR | select.POLLHUP
|
||||
POLLIN = select.POLLIN
|
||||
POLLOUT = select.POLLOUT
|
||||
POLL_CLOSE = select.POLLERR | select.POLLHUP
|
||||
NEED_CLOSE = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PollServer, self).__init__(*args, **kwargs)
|
||||
self._poll = self.poll()
|
||||
self._fdmap = {}
|
||||
self._server_fds = {sock.fileno(): sock for sock in self._servers}
|
||||
self._close_lock = threading.RLock()
|
||||
|
||||
def _register_write(self, client):
|
||||
logger.debug('On write mode: %s', client.client_address)
|
||||
self._poll.modify(client.fileno(), self.WRITE)
|
||||
|
||||
def _register_read(self, client):
|
||||
logger.debug('On read mode: %s', client.client_address)
|
||||
self._poll.modify(client.fileno(), self.READ)
|
||||
|
||||
def _clean_up_client(self, client, finalize=False):
|
||||
logger.debug('Taking close lock: cleanup')
|
||||
with self._close_lock:
|
||||
logger.debug('Cleaning up client: %s, finalize: %d', client.client_address, finalize)
|
||||
fd = client.fileno()
|
||||
try:
|
||||
self._poll.unregister(fd)
|
||||
except IOError as e:
|
||||
if e.errno != errno.ENOENT:
|
||||
raise
|
||||
except KeyError:
|
||||
pass
|
||||
del self._fdmap[fd]
|
||||
super(PollServer, self)._clean_up_client(client, finalize)
|
||||
|
||||
def _serve(self):
|
||||
for fd, sock in self._server_fds.items():
|
||||
self._poll.register(fd, self.POLLIN)
|
||||
sock.listen(16)
|
||||
try:
|
||||
while not self._stop.is_set():
|
||||
for fd, event in self._poll.poll(self._dispatch_event()):
|
||||
if fd in self._server_fds:
|
||||
client = self._accept(self._server_fds[fd])
|
||||
logger.debug('Accepting: %s', client.client_address)
|
||||
fd = client.fileno()
|
||||
self._poll.register(fd, self.READ)
|
||||
self._fdmap[fd] = client
|
||||
elif event & self.POLL_CLOSE:
|
||||
logger.debug('Client closed: %s', self._fdmap[fd].client_address)
|
||||
self._clean_up_client(self._fdmap[fd])
|
||||
else:
|
||||
logger.debug('Taking close lock: event loop')
|
||||
with self._close_lock:
|
||||
try:
|
||||
client = self._fdmap[fd]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
logger.debug('Client active: %s, read: %d, write: %d',
|
||||
client.client_address,
|
||||
event & self.POLLIN,
|
||||
event & self.POLLOUT)
|
||||
if event & self.POLLIN:
|
||||
logger.debug('Non-blocking read on client: %s', client.client_address)
|
||||
self._nonblock_read(client)
|
||||
# Might be closed in the read handler.
|
||||
if event & self.POLLOUT and fd in self._fdmap:
|
||||
logger.debug('Non-blocking write on client: %s', client.client_address)
|
||||
self._nonblock_write(client)
|
||||
finally:
|
||||
logger.info('Shutting down server')
|
||||
self.on_shutdown()
|
||||
for client in self._clients:
|
||||
self._clean_up_client(client, True)
|
||||
for fd, sock in self._server_fds.items():
|
||||
self._poll.unregister(fd)
|
||||
sock.close()
|
||||
if self.NEED_CLOSE:
|
||||
self._poll.close()
|
49
event_socket_server/engines/select_server.py
Normal file
49
event_socket_server/engines/select_server.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
import select
|
||||
|
||||
from ..base_server import BaseServer
|
||||
|
||||
|
||||
class SelectServer(BaseServer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SelectServer, self).__init__(*args, **kwargs)
|
||||
self._reads = set(self._servers)
|
||||
self._writes = set()
|
||||
|
||||
def _register_write(self, client):
|
||||
self._writes.add(client)
|
||||
|
||||
def _register_read(self, client):
|
||||
self._writes.remove(client)
|
||||
|
||||
def _clean_up_client(self, client, finalize=False):
|
||||
self._writes.discard(client)
|
||||
self._reads.remove(client)
|
||||
super(SelectServer, self)._clean_up_client(client, finalize)
|
||||
|
||||
def _serve(self, select=select.select):
|
||||
for server in self._servers:
|
||||
server.listen(16)
|
||||
try:
|
||||
while not self._stop.is_set():
|
||||
r, w, x = select(self._reads, self._writes, self._reads, self._dispatch_event())
|
||||
for s in r:
|
||||
if s in self._servers:
|
||||
self._reads.add(self._accept(s))
|
||||
else:
|
||||
self._nonblock_read(s)
|
||||
|
||||
for client in w:
|
||||
self._nonblock_write(client)
|
||||
|
||||
for s in x:
|
||||
s.close()
|
||||
if s in self._servers:
|
||||
raise RuntimeError('Server is in exceptional condition')
|
||||
else:
|
||||
self._clean_up_client(s)
|
||||
finally:
|
||||
self.on_shutdown()
|
||||
for client in self._clients:
|
||||
self._clean_up_client(client, True)
|
||||
for server in self._servers:
|
||||
server.close()
|
27
event_socket_server/handler.py
Normal file
27
event_socket_server/handler.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
__author__ = 'Quantum'
|
||||
|
||||
|
||||
class Handler(object):
|
||||
def __init__(self, server, socket):
|
||||
self._socket = socket
|
||||
self.server = server
|
||||
self.client_address = socket.getpeername()
|
||||
|
||||
def fileno(self):
|
||||
return self._socket.fileno()
|
||||
|
||||
def _recv_data(self, data):
|
||||
raise NotImplementedError
|
||||
|
||||
def _send(self, data, callback=None):
|
||||
return self.server.send(self, data, callback)
|
||||
|
||||
def close(self):
|
||||
self.server._clean_up_client(self)
|
||||
|
||||
def on_close(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def socket(self):
|
||||
return self._socket
|
125
event_socket_server/helpers.py
Normal file
125
event_socket_server/helpers.py
Normal file
|
@ -0,0 +1,125 @@
|
|||
import struct
|
||||
import zlib
|
||||
|
||||
from judge.utils.unicode import utf8text
|
||||
from .handler import Handler
|
||||
|
||||
size_pack = struct.Struct('!I')
|
||||
|
||||
|
||||
class SizedPacketHandler(Handler):
|
||||
def __init__(self, server, socket):
|
||||
super(SizedPacketHandler, self).__init__(server, socket)
|
||||
self._buffer = b''
|
||||
self._packetlen = 0
|
||||
|
||||
def _packet(self, data):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _format_send(self, data):
|
||||
return data
|
||||
|
||||
def _recv_data(self, data):
|
||||
self._buffer += data
|
||||
while len(self._buffer) >= self._packetlen if self._packetlen else len(self._buffer) >= size_pack.size:
|
||||
if self._packetlen:
|
||||
data = self._buffer[:self._packetlen]
|
||||
self._buffer = self._buffer[self._packetlen:]
|
||||
self._packetlen = 0
|
||||
self._packet(data)
|
||||
else:
|
||||
data = self._buffer[:size_pack.size]
|
||||
self._buffer = self._buffer[size_pack.size:]
|
||||
self._packetlen = size_pack.unpack(data)[0]
|
||||
|
||||
def send(self, data, callback=None):
|
||||
data = self._format_send(data)
|
||||
self._send(size_pack.pack(len(data)) + data, callback)
|
||||
|
||||
|
||||
class ZlibPacketHandler(SizedPacketHandler):
|
||||
def _format_send(self, data):
|
||||
return zlib.compress(data.encode('utf-8'))
|
||||
|
||||
def packet(self, data):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _packet(self, data):
|
||||
try:
|
||||
self.packet(zlib.decompress(data).decode('utf-8'))
|
||||
except zlib.error as e:
|
||||
self.malformed_packet(e)
|
||||
|
||||
def malformed_packet(self, exception):
|
||||
self.close()
|
||||
|
||||
|
||||
class ProxyProtocolMixin(object):
|
||||
__UNKNOWN_TYPE = 0
|
||||
__PROXY1 = 1
|
||||
__PROXY2 = 2
|
||||
__DATA = 3
|
||||
|
||||
__HEADER2 = b'\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A'
|
||||
__HEADER2_LEN = len(__HEADER2)
|
||||
|
||||
_REAL_IP_SET = None
|
||||
|
||||
@classmethod
|
||||
def with_proxy_set(cls, ranges):
|
||||
from netaddr import IPSet, IPGlob
|
||||
from itertools import chain
|
||||
|
||||
globs = []
|
||||
addrs = []
|
||||
for item in ranges:
|
||||
if '*' in item or '-' in item:
|
||||
globs.append(IPGlob(item))
|
||||
else:
|
||||
addrs.append(item)
|
||||
ipset = IPSet(chain(chain.from_iterable(globs), addrs))
|
||||
return type(cls.__name__, (cls,), {'_REAL_IP_SET': ipset})
|
||||
|
||||
def __init__(self, server, socket):
|
||||
super(ProxyProtocolMixin, self).__init__(server, socket)
|
||||
self.__buffer = b''
|
||||
self.__type = (self.__UNKNOWN_TYPE if self._REAL_IP_SET and
|
||||
self.client_address[0] in self._REAL_IP_SET else self.__DATA)
|
||||
|
||||
def __parse_proxy1(self, data):
|
||||
self.__buffer += data
|
||||
index = self.__buffer.find(b'\r\n')
|
||||
if 0 <= index < 106:
|
||||
proxy = data[:index].split()
|
||||
if len(proxy) < 2:
|
||||
return self.close()
|
||||
if proxy[1] == b'TCP4':
|
||||
if len(proxy) != 6:
|
||||
return self.close()
|
||||
self.client_address = (utf8text(proxy[2]), utf8text(proxy[4]))
|
||||
self.server_address = (utf8text(proxy[3]), utf8text(proxy[5]))
|
||||
elif proxy[1] == b'TCP6':
|
||||
self.client_address = (utf8text(proxy[2]), utf8text(proxy[4]), 0, 0)
|
||||
self.server_address = (utf8text(proxy[3]), utf8text(proxy[5]), 0, 0)
|
||||
elif proxy[1] != b'UNKNOWN':
|
||||
return self.close()
|
||||
|
||||
self.__type = self.__DATA
|
||||
super(ProxyProtocolMixin, self)._recv_data(data[index + 2:])
|
||||
elif len(self.__buffer) > 107 or index > 105:
|
||||
self.close()
|
||||
|
||||
def _recv_data(self, data):
|
||||
if self.__type == self.__DATA:
|
||||
super(ProxyProtocolMixin, self)._recv_data(data)
|
||||
elif self.__type == self.__UNKNOWN_TYPE:
|
||||
if len(data) >= 16 and data[:self.__HEADER2_LEN] == self.__HEADER2:
|
||||
self.close()
|
||||
elif len(data) >= 8 and data[:5] == b'PROXY':
|
||||
self.__type = self.__PROXY1
|
||||
self.__parse_proxy1(data)
|
||||
else:
|
||||
self.__type = self.__DATA
|
||||
super(ProxyProtocolMixin, self)._recv_data(data)
|
||||
else:
|
||||
self.__parse_proxy1(data)
|
94
event_socket_server/test_client.py
Normal file
94
event_socket_server/test_client.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
import ctypes
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
import zlib
|
||||
|
||||
size_pack = struct.Struct('!I')
|
||||
try:
|
||||
RtlGenRandom = ctypes.windll.advapi32.SystemFunction036
|
||||
except AttributeError:
|
||||
RtlGenRandom = None
|
||||
|
||||
|
||||
def open_connection():
|
||||
sock = socket.create_connection((host, port))
|
||||
return sock
|
||||
|
||||
|
||||
def zlibify(data):
|
||||
data = zlib.compress(data.encode('utf-8'))
|
||||
return size_pack.pack(len(data)) + data
|
||||
|
||||
|
||||
def dezlibify(data, skip_head=True):
|
||||
if skip_head:
|
||||
data = data[size_pack.size:]
|
||||
return zlib.decompress(data).decode('utf-8')
|
||||
|
||||
|
||||
def random(length):
|
||||
if RtlGenRandom is None:
|
||||
with open('/dev/urandom') as f:
|
||||
return f.read(length)
|
||||
buf = ctypes.create_string_buffer(length)
|
||||
RtlGenRandom(buf, length)
|
||||
return buf.raw
|
||||
|
||||
|
||||
def main():
|
||||
global host, port
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-l', '--host', default='localhost')
|
||||
parser.add_argument('-p', '--port', default=9999, type=int)
|
||||
args = parser.parse_args()
|
||||
host, port = args.host, args.port
|
||||
|
||||
print('Opening idle connection:', end=' ')
|
||||
s1 = open_connection()
|
||||
print('Success')
|
||||
print('Opening hello world connection:', end=' ')
|
||||
s2 = open_connection()
|
||||
print('Success')
|
||||
print('Sending Hello, World!', end=' ')
|
||||
s2.sendall(zlibify('Hello, World!'))
|
||||
print('Success')
|
||||
print('Testing blank connection:', end=' ')
|
||||
s3 = open_connection()
|
||||
s3.close()
|
||||
print('Success')
|
||||
result = dezlibify(s2.recv(1024))
|
||||
assert result == 'Hello, World!'
|
||||
print(result)
|
||||
s2.close()
|
||||
print('Large random data test:', end=' ')
|
||||
s4 = open_connection()
|
||||
data = random(1000000)
|
||||
print('Generated', end=' ')
|
||||
s4.sendall(zlibify(data))
|
||||
print('Sent', end=' ')
|
||||
result = ''
|
||||
while len(result) < size_pack.size:
|
||||
result += s4.recv(1024)
|
||||
size = size_pack.unpack(result[:size_pack.size])[0]
|
||||
result = result[size_pack.size:]
|
||||
while len(result) < size:
|
||||
result += s4.recv(1024)
|
||||
print('Received', end=' ')
|
||||
assert dezlibify(result, False) == data
|
||||
print('Success')
|
||||
s4.close()
|
||||
print('Test malformed connection:', end=' ')
|
||||
s5 = open_connection()
|
||||
s5.sendall(data[:100000])
|
||||
s5.close()
|
||||
print('Success')
|
||||
print('Waiting for timeout to close idle connection:', end=' ')
|
||||
time.sleep(6)
|
||||
print('Done')
|
||||
s1.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
54
event_socket_server/test_server.py
Normal file
54
event_socket_server/test_server.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
from .engines import engines
|
||||
from .helpers import ProxyProtocolMixin, ZlibPacketHandler
|
||||
|
||||
|
||||
class EchoPacketHandler(ProxyProtocolMixin, ZlibPacketHandler):
|
||||
def __init__(self, server, socket):
|
||||
super(EchoPacketHandler, self).__init__(server, socket)
|
||||
self._gotdata = False
|
||||
self.server.schedule(5, self._kill_if_no_data)
|
||||
|
||||
def _kill_if_no_data(self):
|
||||
if not self._gotdata:
|
||||
print('Inactive client:', self._socket.getpeername())
|
||||
self.close()
|
||||
|
||||
def packet(self, data):
|
||||
self._gotdata = True
|
||||
print('Data from %s: %r' % (self._socket.getpeername(), data[:30] if len(data) > 30 else data))
|
||||
self.send(data)
|
||||
|
||||
def on_close(self):
|
||||
self._gotdata = True
|
||||
print('Closed client:', self._socket.getpeername())
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-l', '--host', action='append')
|
||||
parser.add_argument('-p', '--port', type=int, action='append')
|
||||
parser.add_argument('-e', '--engine', default='select', choices=sorted(engines.keys()))
|
||||
try:
|
||||
import netaddr
|
||||
except ImportError:
|
||||
netaddr = None
|
||||
else:
|
||||
parser.add_argument('-P', '--proxy', action='append')
|
||||
args = parser.parse_args()
|
||||
|
||||
class TestServer(engines[args.engine]):
|
||||
def _accept(self, sock):
|
||||
client = super(TestServer, self)._accept(sock)
|
||||
print('New connection:', client.socket.getpeername())
|
||||
return client
|
||||
|
||||
handler = EchoPacketHandler
|
||||
if netaddr is not None and args.proxy:
|
||||
handler = handler.with_proxy_set(args.proxy)
|
||||
server = TestServer(list(zip(args.host, args.port)), handler)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
1
judge/__init__.py
Normal file
1
judge/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
default_app_config = 'judge.apps.JudgeAppConfig'
|
37
judge/admin/__init__.py
Normal file
37
judge/admin/__init__.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.admin.models import LogEntry
|
||||
|
||||
from judge.admin.comments import CommentAdmin
|
||||
from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin
|
||||
from judge.admin.interface import BlogPostAdmin, LicenseAdmin, LogEntryAdmin, NavigationBarAdmin
|
||||
from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin
|
||||
from judge.admin.problem import ProblemAdmin
|
||||
from judge.admin.profile import ProfileAdmin
|
||||
from judge.admin.runtime import JudgeAdmin, LanguageAdmin
|
||||
from judge.admin.submission import SubmissionAdmin
|
||||
from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin
|
||||
from judge.admin.ticket import TicketAdmin
|
||||
from judge.models import BlogPost, Comment, CommentLock, Contest, ContestParticipation, \
|
||||
ContestTag, Judge, Language, License, MiscConfig, NavigationBar, Organization, \
|
||||
OrganizationRequest, Problem, ProblemGroup, ProblemType, Profile, Submission, Ticket
|
||||
|
||||
admin.site.register(BlogPost, BlogPostAdmin)
|
||||
admin.site.register(Comment, CommentAdmin)
|
||||
admin.site.register(CommentLock)
|
||||
admin.site.register(Contest, ContestAdmin)
|
||||
admin.site.register(ContestParticipation, ContestParticipationAdmin)
|
||||
admin.site.register(ContestTag, ContestTagAdmin)
|
||||
admin.site.register(Judge, JudgeAdmin)
|
||||
admin.site.register(Language, LanguageAdmin)
|
||||
admin.site.register(License, LicenseAdmin)
|
||||
admin.site.register(LogEntry, LogEntryAdmin)
|
||||
admin.site.register(MiscConfig)
|
||||
admin.site.register(NavigationBar, NavigationBarAdmin)
|
||||
admin.site.register(Organization, OrganizationAdmin)
|
||||
admin.site.register(OrganizationRequest, OrganizationRequestAdmin)
|
||||
admin.site.register(Problem, ProblemAdmin)
|
||||
admin.site.register(ProblemGroup, ProblemGroupAdmin)
|
||||
admin.site.register(ProblemType, ProblemTypeAdmin)
|
||||
admin.site.register(Profile, ProfileAdmin)
|
||||
admin.site.register(Submission, SubmissionAdmin)
|
||||
admin.site.register(Ticket, TicketAdmin)
|
64
judge/admin/comments.py
Normal file
64
judge/admin/comments.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
from django.forms import ModelForm
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _, ungettext
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from judge.models import Comment
|
||||
from judge.widgets import AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
|
||||
|
||||
|
||||
class CommentForm(ModelForm):
|
||||
class Meta:
|
||||
widgets = {
|
||||
'author': AdminHeavySelect2Widget(data_view='profile_select2'),
|
||||
'parent': AdminHeavySelect2Widget(data_view='comment_select2'),
|
||||
}
|
||||
if HeavyPreviewAdminPageDownWidget is not None:
|
||||
widgets['body'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('comment_preview'))
|
||||
|
||||
|
||||
class CommentAdmin(VersionAdmin):
|
||||
fieldsets = (
|
||||
(None, {'fields': ('author', 'page', 'parent', 'score', 'hidden')}),
|
||||
('Content', {'fields': ('body',)}),
|
||||
)
|
||||
list_display = ['author', 'linked_page', 'time']
|
||||
search_fields = ['author__user__username', 'page', 'body']
|
||||
actions = ['hide_comment', 'unhide_comment']
|
||||
list_filter = ['hidden']
|
||||
actions_on_top = True
|
||||
actions_on_bottom = True
|
||||
form = CommentForm
|
||||
date_hierarchy = 'time'
|
||||
|
||||
def get_queryset(self, request):
|
||||
return Comment.objects.order_by('-time')
|
||||
|
||||
def hide_comment(self, request, queryset):
|
||||
count = queryset.update(hidden=True)
|
||||
self.message_user(request, ungettext('%d comment successfully hidden.',
|
||||
'%d comments successfully hidden.',
|
||||
count) % count)
|
||||
hide_comment.short_description = _('Hide comments')
|
||||
|
||||
def unhide_comment(self, request, queryset):
|
||||
count = queryset.update(hidden=False)
|
||||
self.message_user(request, ungettext('%d comment successfully unhidden.',
|
||||
'%d comments successfully unhidden.',
|
||||
count) % count)
|
||||
unhide_comment.short_description = _('Unhide comments')
|
||||
|
||||
def linked_page(self, obj):
|
||||
link = obj.link
|
||||
if link is not None:
|
||||
return format_html('<a href="{0}">{1}</a>', link, obj.page)
|
||||
else:
|
||||
return format_html('{0}', obj.page)
|
||||
linked_page.short_description = _('Associated page')
|
||||
linked_page.admin_order_field = 'page'
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
super(CommentAdmin, self).save_model(request, obj, form, change)
|
||||
if obj.hidden:
|
||||
obj.get_descendants().update(hidden=obj.hidden)
|
269
judge/admin/contest.py
Normal file
269
judge/admin/contest.py
Normal file
|
@ -0,0 +1,269 @@
|
|||
from django.conf.urls import url
|
||||
from django.contrib import admin
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import connection, transaction
|
||||
from django.db.models import Q, TextField
|
||||
from django.forms import ModelForm, ModelMultipleChoiceField
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _, ungettext
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from judge.models import Contest, ContestProblem, ContestSubmission, Profile, Rating
|
||||
from judge.ratings import rate_contest
|
||||
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, AdminPagedownWidget, \
|
||||
AdminSelect2MultipleWidget, AdminSelect2Widget, HeavyPreviewAdminPageDownWidget
|
||||
|
||||
|
||||
class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
|
||||
@property
|
||||
def is_hidden(self):
|
||||
return False
|
||||
|
||||
|
||||
class ContestTagForm(ModelForm):
|
||||
contests = ModelMultipleChoiceField(
|
||||
label=_('Included contests'),
|
||||
queryset=Contest.objects.all(),
|
||||
required=False,
|
||||
widget=AdminHeavySelect2MultipleWidget(data_view='contest_select2'))
|
||||
|
||||
|
||||
class ContestTagAdmin(admin.ModelAdmin):
|
||||
fields = ('name', 'color', 'description', 'contests')
|
||||
list_display = ('name', 'color')
|
||||
actions_on_top = True
|
||||
actions_on_bottom = True
|
||||
form = ContestTagForm
|
||||
|
||||
if AdminPagedownWidget is not None:
|
||||
formfield_overrides = {
|
||||
TextField: {'widget': AdminPagedownWidget},
|
||||
}
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
super(ContestTagAdmin, self).save_model(request, obj, form, change)
|
||||
obj.contests.set(form.cleaned_data['contests'])
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
form = super(ContestTagAdmin, self).get_form(request, obj, **kwargs)
|
||||
if obj is not None:
|
||||
form.base_fields['contests'].initial = obj.contests.all()
|
||||
return form
|
||||
|
||||
|
||||
class ContestProblemInlineForm(ModelForm):
|
||||
class Meta:
|
||||
widgets = {'problem': AdminHeavySelect2Widget(data_view='problem_select2')}
|
||||
|
||||
|
||||
class ContestProblemInline(admin.TabularInline):
|
||||
model = ContestProblem
|
||||
verbose_name = _('Problem')
|
||||
verbose_name_plural = 'Problems'
|
||||
fields = ('problem', 'points', 'partial', 'is_pretested', 'max_submissions', 'output_prefix_override', 'order',
|
||||
'rejudge_column')
|
||||
readonly_fields = ('rejudge_column',)
|
||||
form = ContestProblemInlineForm
|
||||
|
||||
def rejudge_column(self, obj):
|
||||
if obj.id is None:
|
||||
return ''
|
||||
return format_html('<a class="button rejudge-link" href="{}">Rejudge</a>',
|
||||
reverse('admin:judge_contest_rejudge', args=(obj.contest.id, obj.id)))
|
||||
rejudge_column.short_description = ''
|
||||
|
||||
|
||||
class ContestForm(ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ContestForm, self).__init__(*args, **kwargs)
|
||||
if 'rate_exclude' in self.fields:
|
||||
if self.instance and self.instance.id:
|
||||
self.fields['rate_exclude'].queryset = \
|
||||
Profile.objects.filter(contest_history__contest=self.instance).distinct()
|
||||
else:
|
||||
self.fields['rate_exclude'].queryset = Profile.objects.none()
|
||||
self.fields['banned_users'].widget.can_add_related = False
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(ContestForm, self).clean()
|
||||
cleaned_data['banned_users'].filter(current_contest__contest=self.instance).update(current_contest=None)
|
||||
|
||||
class Meta:
|
||||
widgets = {
|
||||
'organizers': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
|
||||
'private_contestants': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
|
||||
attrs={'style': 'width: 100%'}),
|
||||
'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2'),
|
||||
'tags': AdminSelect2MultipleWidget,
|
||||
'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
|
||||
attrs={'style': 'width: 100%'}),
|
||||
}
|
||||
|
||||
if HeavyPreviewAdminPageDownWidget is not None:
|
||||
widgets['description'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('contest_preview'))
|
||||
|
||||
|
||||
class ContestAdmin(VersionAdmin):
|
||||
fieldsets = (
|
||||
(None, {'fields': ('key', 'name', 'organizers')}),
|
||||
(_('Settings'), {'fields': ('is_visible', 'use_clarifications', 'hide_problem_tags', 'hide_scoreboard',
|
||||
'run_pretests_only')}),
|
||||
(_('Scheduling'), {'fields': ('start_time', 'end_time', 'time_limit')}),
|
||||
(_('Details'), {'fields': ('description', 'og_image', 'logo_override_image', 'tags', 'summary')}),
|
||||
(_('Format'), {'fields': ('format_name', 'format_config')}),
|
||||
(_('Rating'), {'fields': ('is_rated', 'rate_all', 'rating_floor', 'rating_ceiling', 'rate_exclude')}),
|
||||
(_('Access'), {'fields': ('access_code', 'is_private', 'private_contestants', 'is_organization_private',
|
||||
'organizations')}),
|
||||
(_('Justice'), {'fields': ('banned_users',)}),
|
||||
)
|
||||
list_display = ('key', 'name', 'is_visible', 'is_rated', 'start_time', 'end_time', 'time_limit', 'user_count')
|
||||
actions = ['make_visible', 'make_hidden']
|
||||
inlines = [ContestProblemInline]
|
||||
actions_on_top = True
|
||||
actions_on_bottom = True
|
||||
form = ContestForm
|
||||
change_list_template = 'admin/judge/contest/change_list.html'
|
||||
filter_horizontal = ['rate_exclude']
|
||||
date_hierarchy = 'start_time'
|
||||
|
||||
def get_queryset(self, request):
|
||||
queryset = Contest.objects.all()
|
||||
if request.user.has_perm('judge.edit_all_contest'):
|
||||
return queryset
|
||||
else:
|
||||
return queryset.filter(organizers__id=request.profile.id)
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
readonly = []
|
||||
if not request.user.has_perm('judge.contest_rating'):
|
||||
readonly += ['is_rated', 'rate_all', 'rate_exclude']
|
||||
if not request.user.has_perm('judge.contest_access_code'):
|
||||
readonly += ['access_code']
|
||||
if not request.user.has_perm('judge.create_private_contest'):
|
||||
readonly += ['is_private', 'private_contestants', 'is_organization_private', 'organizations']
|
||||
return readonly
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
if not request.user.has_perm('judge.edit_own_contest'):
|
||||
return False
|
||||
if request.user.has_perm('judge.edit_all_contest') or obj is None:
|
||||
return True
|
||||
return obj.organizers.filter(id=request.profile.id).exists()
|
||||
|
||||
def make_visible(self, request, queryset):
|
||||
count = queryset.update(is_visible=True)
|
||||
self.message_user(request, ungettext('%d contest successfully marked as visible.',
|
||||
'%d contests successfully marked as visible.',
|
||||
count) % count)
|
||||
make_visible.short_description = _('Mark contests as visible')
|
||||
|
||||
def make_hidden(self, request, queryset):
|
||||
count = queryset.update(is_visible=False)
|
||||
self.message_user(request, ungettext('%d contest successfully marked as hidden.',
|
||||
'%d contests successfully marked as hidden.',
|
||||
count) % count)
|
||||
make_hidden.short_description = _('Mark contests as hidden')
|
||||
|
||||
def get_urls(self):
|
||||
return [
|
||||
url(r'^rate/all/$', self.rate_all_view, name='judge_contest_rate_all'),
|
||||
url(r'^(\d+)/rate/$', self.rate_view, name='judge_contest_rate'),
|
||||
url(r'^(\d+)/judge/(\d+)/$', self.rejudge_view, name='judge_contest_rejudge'),
|
||||
] + super(ContestAdmin, self).get_urls()
|
||||
|
||||
def rejudge_view(self, request, contest_id, problem_id):
|
||||
queryset = ContestSubmission.objects.filter(problem_id=problem_id).select_related('submission')
|
||||
for model in queryset:
|
||||
model.submission.judge(rejudge=True)
|
||||
|
||||
self.message_user(request, ungettext('%d submission was successfully scheduled for rejudging.',
|
||||
'%d submissions were successfully scheduled for rejudging.',
|
||||
len(queryset)) % len(queryset))
|
||||
return HttpResponseRedirect(reverse('admin:judge_contest_change', args=(contest_id,)))
|
||||
|
||||
def rate_all_view(self, request):
|
||||
if not request.user.has_perm('judge.contest_rating'):
|
||||
raise PermissionDenied()
|
||||
with transaction.atomic():
|
||||
if connection.vendor == 'sqlite':
|
||||
Rating.objects.all().delete()
|
||||
else:
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('TRUNCATE TABLE `%s`' % Rating._meta.db_table)
|
||||
cursor.close()
|
||||
Profile.objects.update(rating=None)
|
||||
for contest in Contest.objects.filter(is_rated=True).order_by('end_time'):
|
||||
rate_contest(contest)
|
||||
return HttpResponseRedirect(reverse('admin:judge_contest_changelist'))
|
||||
|
||||
def rate_view(self, request, id):
|
||||
if not request.user.has_perm('judge.contest_rating'):
|
||||
raise PermissionDenied()
|
||||
contest = get_object_or_404(Contest, id=id)
|
||||
if not contest.is_rated:
|
||||
raise Http404()
|
||||
with transaction.atomic():
|
||||
contest.rate()
|
||||
return HttpResponseRedirect(request.META.get('HTTP_REFERER', reverse('admin:judge_contest_changelist')))
|
||||
|
||||
def get_form(self, *args, **kwargs):
|
||||
form = super(ContestAdmin, self).get_form(*args, **kwargs)
|
||||
perms = ('edit_own_contest', 'edit_all_contest')
|
||||
form.base_fields['organizers'].queryset = Profile.objects.filter(
|
||||
Q(user__is_superuser=True) |
|
||||
Q(user__groups__permissions__codename__in=perms) |
|
||||
Q(user__user_permissions__codename__in=perms),
|
||||
).distinct()
|
||||
return form
|
||||
|
||||
|
||||
class ContestParticipationForm(ModelForm):
|
||||
class Meta:
|
||||
widgets = {
|
||||
'contest': AdminSelect2Widget(),
|
||||
'user': AdminHeavySelect2Widget(data_view='profile_select2'),
|
||||
}
|
||||
|
||||
|
||||
class ContestParticipationAdmin(admin.ModelAdmin):
|
||||
fields = ('contest', 'user', 'real_start', 'virtual', 'is_disqualified')
|
||||
list_display = ('contest', 'username', 'show_virtual', 'real_start', 'score', 'cumtime')
|
||||
actions = ['recalculate_results']
|
||||
actions_on_bottom = actions_on_top = True
|
||||
search_fields = ('contest__key', 'contest__name', 'user__user__username')
|
||||
form = ContestParticipationForm
|
||||
date_hierarchy = 'real_start'
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super(ContestParticipationAdmin, self).get_queryset(request).only(
|
||||
'contest__name', 'contest__format_name', 'contest__format_config',
|
||||
'user__user__username', 'real_start', 'score', 'cumtime', 'virtual',
|
||||
)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
super().save_model(request, obj, form, change)
|
||||
if form.changed_data and 'is_disqualified' in form.changed_data:
|
||||
obj.set_disqualified(obj.is_disqualified)
|
||||
|
||||
def recalculate_results(self, request, queryset):
|
||||
count = 0
|
||||
for participation in queryset:
|
||||
participation.recompute_results()
|
||||
count += 1
|
||||
self.message_user(request, ungettext('%d participation recalculated.',
|
||||
'%d participations recalculated.',
|
||||
count) % count)
|
||||
recalculate_results.short_description = _('Recalculate results')
|
||||
|
||||
def username(self, obj):
|
||||
return obj.user.username
|
||||
username.short_description = _('username')
|
||||
username.admin_order_field = 'user__user__username'
|
||||
|
||||
def show_virtual(self, obj):
|
||||
return obj.virtual or '-'
|
||||
show_virtual.short_description = _('virtual')
|
||||
show_virtual.admin_order_field = 'virtual'
|
151
judge/admin/interface.py
Normal file
151
judge/admin/interface.py
Normal file
|
@ -0,0 +1,151 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.auth.models import User
|
||||
from django.forms import ModelForm
|
||||
from django.urls import NoReverseMatch, reverse, reverse_lazy
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from mptt.admin import DraggableMPTTAdmin
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from judge.dblock import LockModel
|
||||
from judge.models import NavigationBar
|
||||
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
|
||||
|
||||
|
||||
class NavigationBarAdmin(DraggableMPTTAdmin):
|
||||
list_display = DraggableMPTTAdmin.list_display + ('key', 'linked_path')
|
||||
fields = ('key', 'label', 'path', 'order', 'regex', 'parent')
|
||||
list_editable = () # Bug in SortableModelAdmin: 500 without list_editable being set
|
||||
mptt_level_indent = 20
|
||||
sortable = 'order'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NavigationBarAdmin, self).__init__(*args, **kwargs)
|
||||
self.__save_model_calls = 0
|
||||
|
||||
def linked_path(self, obj):
|
||||
return format_html(u'<a href="{0}" target="_blank">{0}</a>', obj.path)
|
||||
linked_path.short_description = _('link path')
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
self.__save_model_calls += 1
|
||||
return super(NavigationBarAdmin, self).save_model(request, obj, form, change)
|
||||
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
self.__save_model_calls = 0
|
||||
with NavigationBar.objects.disable_mptt_updates():
|
||||
result = super(NavigationBarAdmin, self).changelist_view(request, extra_context)
|
||||
if self.__save_model_calls:
|
||||
with LockModel(write=(NavigationBar,)):
|
||||
NavigationBar.objects.rebuild()
|
||||
return result
|
||||
|
||||
|
||||
class BlogPostForm(ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BlogPostForm, self).__init__(*args, **kwargs)
|
||||
self.fields['authors'].widget.can_add_related = False
|
||||
|
||||
class Meta:
|
||||
widgets = {
|
||||
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||
}
|
||||
|
||||
if HeavyPreviewAdminPageDownWidget is not None:
|
||||
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('blog_preview'))
|
||||
widgets['summary'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('blog_preview'))
|
||||
|
||||
|
||||
class BlogPostAdmin(VersionAdmin):
|
||||
fieldsets = (
|
||||
(None, {'fields': ('title', 'slug', 'authors', 'visible', 'sticky', 'publish_on')}),
|
||||
(_('Content'), {'fields': ('content', 'og_image')}),
|
||||
(_('Summary'), {'classes': ('collapse',), 'fields': ('summary',)}),
|
||||
)
|
||||
prepopulated_fields = {'slug': ('title',)}
|
||||
list_display = ('id', 'title', 'visible', 'sticky', 'publish_on')
|
||||
list_display_links = ('id', 'title')
|
||||
ordering = ('-publish_on',)
|
||||
form = BlogPostForm
|
||||
date_hierarchy = 'publish_on'
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return (request.user.has_perm('judge.edit_all_post') or
|
||||
request.user.has_perm('judge.change_blogpost') and (
|
||||
obj is None or
|
||||
obj.authors.filter(id=request.profile.id).exists()))
|
||||
|
||||
|
||||
class SolutionForm(ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SolutionForm, self).__init__(*args, **kwargs)
|
||||
self.fields['authors'].widget.can_add_related = False
|
||||
|
||||
class Meta:
|
||||
widgets = {
|
||||
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||
'problem': AdminHeavySelect2Widget(data_view='problem_select2', attrs={'style': 'width: 250px'}),
|
||||
}
|
||||
|
||||
if HeavyPreviewAdminPageDownWidget is not None:
|
||||
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('solution_preview'))
|
||||
|
||||
|
||||
class LicenseForm(ModelForm):
|
||||
class Meta:
|
||||
if HeavyPreviewAdminPageDownWidget is not None:
|
||||
widgets = {'text': HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('license_preview'))}
|
||||
|
||||
|
||||
class LicenseAdmin(admin.ModelAdmin):
|
||||
fields = ('key', 'link', 'name', 'display', 'icon', 'text')
|
||||
list_display = ('name', 'key')
|
||||
form = LicenseForm
|
||||
|
||||
|
||||
class UserListFilter(admin.SimpleListFilter):
|
||||
title = _('user')
|
||||
parameter_name = 'user'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return User.objects.filter(is_staff=True).values_list('id', 'username')
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value():
|
||||
return queryset.filter(user_id=self.value(), user__is_staff=True)
|
||||
return queryset
|
||||
|
||||
|
||||
class LogEntryAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ('user', 'content_type', 'object_id', 'object_repr', 'action_flag', 'change_message')
|
||||
list_display = ('__str__', 'action_time', 'user', 'content_type', 'object_link')
|
||||
search_fields = ('object_repr', 'change_message')
|
||||
list_filter = (UserListFilter, 'content_type')
|
||||
list_display_links = None
|
||||
actions = None
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return obj is None and request.user.is_superuser
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def object_link(self, obj):
|
||||
if obj.is_deletion():
|
||||
link = obj.object_repr
|
||||
else:
|
||||
ct = obj.content_type
|
||||
try:
|
||||
link = format_html('<a href="{1}">{0}</a>', obj.object_repr,
|
||||
reverse('admin:%s_%s_change' % (ct.app_label, ct.model), args=(obj.object_id,)))
|
||||
except NoReverseMatch:
|
||||
link = obj.object_repr
|
||||
return link
|
||||
object_link.admin_order_field = 'object_repr'
|
||||
object_link.short_description = _('object')
|
||||
|
||||
def queryset(self, request):
|
||||
return super().queryset(request).prefetch_related('content_type')
|
66
judge/admin/organization.py
Normal file
66
judge/admin/organization.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
from django.contrib import admin
|
||||
from django.forms import ModelForm
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from judge.models import Organization
|
||||
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
|
||||
|
||||
|
||||
class OrganizationForm(ModelForm):
|
||||
class Meta:
|
||||
widgets = {
|
||||
'admins': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
|
||||
'registrant': AdminHeavySelect2Widget(data_view='profile_select2'),
|
||||
}
|
||||
if HeavyPreviewAdminPageDownWidget is not None:
|
||||
widgets['about'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('organization_preview'))
|
||||
|
||||
|
||||
class OrganizationAdmin(VersionAdmin):
|
||||
readonly_fields = ('creation_date',)
|
||||
fields = ('name', 'slug', 'short_name', 'is_open', 'about', 'logo_override_image', 'slots', 'registrant',
|
||||
'creation_date', 'admins')
|
||||
list_display = ('name', 'short_name', 'is_open', 'slots', 'registrant', 'show_public')
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
actions_on_top = True
|
||||
actions_on_bottom = True
|
||||
form = OrganizationForm
|
||||
|
||||
def show_public(self, obj):
|
||||
return format_html('<a href="{0}" style="white-space:nowrap;">{1}</a>',
|
||||
obj.get_absolute_url(), gettext('View on site'))
|
||||
|
||||
show_public.short_description = ''
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
fields = self.readonly_fields
|
||||
if not request.user.has_perm('judge.organization_admin'):
|
||||
return fields + ('registrant', 'admins', 'is_open', 'slots')
|
||||
return fields
|
||||
|
||||
def get_queryset(self, request):
|
||||
queryset = Organization.objects.all()
|
||||
if request.user.has_perm('judge.edit_all_organization'):
|
||||
return queryset
|
||||
else:
|
||||
return queryset.filter(admins=request.profile.id)
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
if not request.user.has_perm('judge.change_organization'):
|
||||
return False
|
||||
if request.user.has_perm('judge.edit_all_organization') or obj is None:
|
||||
return True
|
||||
return obj.admins.filter(id=request.profile.id).exists()
|
||||
|
||||
|
||||
class OrganizationRequestAdmin(admin.ModelAdmin):
|
||||
list_display = ('username', 'organization', 'state', 'time')
|
||||
readonly_fields = ('user', 'organization')
|
||||
|
||||
def username(self, obj):
|
||||
return obj.user.user.username
|
||||
username.short_description = _('username')
|
||||
username.admin_order_field = 'user__user__username'
|
238
judge/admin/problem.py
Normal file
238
judge/admin/problem.py
Normal file
|
@ -0,0 +1,238 @@
|
|||
from operator import attrgetter
|
||||
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.forms import ModelForm
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext, gettext_lazy as _, ungettext
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from judge.models import LanguageLimit, Problem, ProblemClarification, ProblemTranslation, Profile, Solution
|
||||
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminSelect2MultipleWidget, AdminSelect2Widget, \
|
||||
CheckboxSelectMultipleWithSelectAll, HeavyPreviewAdminPageDownWidget, HeavyPreviewPageDownWidget
|
||||
|
||||
|
||||
class ProblemForm(ModelForm):
|
||||
change_message = forms.CharField(max_length=256, label='Edit reason', required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ProblemForm, self).__init__(*args, **kwargs)
|
||||
self.fields['authors'].widget.can_add_related = False
|
||||
self.fields['curators'].widget.can_add_related = False
|
||||
self.fields['testers'].widget.can_add_related = False
|
||||
self.fields['banned_users'].widget.can_add_related = False
|
||||
self.fields['change_message'].widget.attrs.update({
|
||||
'placeholder': gettext('Describe the changes you made (optional)'),
|
||||
})
|
||||
|
||||
class Meta:
|
||||
widgets = {
|
||||
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||
'curators': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||
'testers': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||
'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
|
||||
attrs={'style': 'width: 100%'}),
|
||||
'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2',
|
||||
attrs={'style': 'width: 100%'}),
|
||||
'types': AdminSelect2MultipleWidget,
|
||||
'group': AdminSelect2Widget,
|
||||
}
|
||||
if HeavyPreviewAdminPageDownWidget is not None:
|
||||
widgets['description'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('problem_preview'))
|
||||
|
||||
|
||||
class ProblemCreatorListFilter(admin.SimpleListFilter):
|
||||
title = parameter_name = 'creator'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
queryset = Profile.objects.exclude(authored_problems=None).values_list('user__username', flat=True)
|
||||
return [(name, name) for name in queryset]
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() is None:
|
||||
return queryset
|
||||
return queryset.filter(authors__user__username=self.value())
|
||||
|
||||
|
||||
class LanguageLimitInlineForm(ModelForm):
|
||||
class Meta:
|
||||
widgets = {'language': AdminSelect2Widget}
|
||||
|
||||
|
||||
class LanguageLimitInline(admin.TabularInline):
|
||||
model = LanguageLimit
|
||||
fields = ('language', 'time_limit', 'memory_limit')
|
||||
form = LanguageLimitInlineForm
|
||||
|
||||
|
||||
class ProblemClarificationForm(ModelForm):
|
||||
class Meta:
|
||||
if HeavyPreviewPageDownWidget is not None:
|
||||
widgets = {'description': HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'))}
|
||||
|
||||
|
||||
class ProblemClarificationInline(admin.StackedInline):
|
||||
model = ProblemClarification
|
||||
fields = ('description',)
|
||||
form = ProblemClarificationForm
|
||||
extra = 0
|
||||
|
||||
|
||||
class ProblemSolutionForm(ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ProblemSolutionForm, self).__init__(*args, **kwargs)
|
||||
self.fields['authors'].widget.can_add_related = False
|
||||
|
||||
class Meta:
|
||||
widgets = {
|
||||
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||
}
|
||||
|
||||
if HeavyPreviewAdminPageDownWidget is not None:
|
||||
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('solution_preview'))
|
||||
|
||||
|
||||
class ProblemSolutionInline(admin.StackedInline):
|
||||
model = Solution
|
||||
fields = ('is_public', 'publish_on', 'authors', 'content')
|
||||
form = ProblemSolutionForm
|
||||
extra = 0
|
||||
|
||||
|
||||
class ProblemTranslationForm(ModelForm):
|
||||
class Meta:
|
||||
if HeavyPreviewAdminPageDownWidget is not None:
|
||||
widgets = {'description': HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('problem_preview'))}
|
||||
|
||||
|
||||
class ProblemTranslationInline(admin.StackedInline):
|
||||
model = ProblemTranslation
|
||||
fields = ('language', 'name', 'description')
|
||||
form = ProblemTranslationForm
|
||||
extra = 0
|
||||
|
||||
|
||||
class ProblemAdmin(VersionAdmin):
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': (
|
||||
'code', 'name', 'is_public', 'is_manually_managed', 'date', 'authors', 'curators', 'testers',
|
||||
'is_organization_private', 'organizations', 'description', 'license',
|
||||
),
|
||||
}),
|
||||
(_('Social Media'), {'classes': ('collapse',), 'fields': ('og_image', 'summary')}),
|
||||
(_('Taxonomy'), {'fields': ('types', 'group')}),
|
||||
(_('Points'), {'fields': (('points', 'partial'), 'short_circuit')}),
|
||||
(_('Limits'), {'fields': ('time_limit', 'memory_limit')}),
|
||||
(_('Language'), {'fields': ('allowed_languages',)}),
|
||||
(_('Justice'), {'fields': ('banned_users',)}),
|
||||
(_('History'), {'fields': ('change_message',)}),
|
||||
)
|
||||
list_display = ['code', 'name', 'show_authors', 'points', 'is_public', 'show_public']
|
||||
ordering = ['code']
|
||||
search_fields = ('code', 'name', 'authors__user__username', 'curators__user__username')
|
||||
inlines = [LanguageLimitInline, ProblemClarificationInline, ProblemSolutionInline, ProblemTranslationInline]
|
||||
list_max_show_all = 1000
|
||||
actions_on_top = True
|
||||
actions_on_bottom = True
|
||||
list_filter = ('is_public', ProblemCreatorListFilter)
|
||||
form = ProblemForm
|
||||
date_hierarchy = 'date'
|
||||
|
||||
def get_actions(self, request):
|
||||
actions = super(ProblemAdmin, self).get_actions(request)
|
||||
|
||||
if request.user.has_perm('judge.change_public_visibility'):
|
||||
func, name, desc = self.get_action('make_public')
|
||||
actions[name] = (func, name, desc)
|
||||
|
||||
func, name, desc = self.get_action('make_private')
|
||||
actions[name] = (func, name, desc)
|
||||
|
||||
return actions
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
fields = self.readonly_fields
|
||||
if not request.user.has_perm('judge.change_public_visibility'):
|
||||
fields += ('is_public',)
|
||||
if not request.user.has_perm('judge.change_manually_managed'):
|
||||
fields += ('is_manually_managed',)
|
||||
return fields
|
||||
|
||||
def show_authors(self, obj):
|
||||
return ', '.join(map(attrgetter('user.username'), obj.authors.all()))
|
||||
|
||||
show_authors.short_description = _('Authors')
|
||||
|
||||
def show_public(self, obj):
|
||||
return format_html('<a href="{1}">{0}</a>', gettext('View on site'), obj.get_absolute_url())
|
||||
|
||||
show_public.short_description = ''
|
||||
|
||||
def _rescore(self, request, problem_id):
|
||||
from judge.tasks import rescore_problem
|
||||
transaction.on_commit(rescore_problem.s(problem_id).delay)
|
||||
|
||||
def make_public(self, request, queryset):
|
||||
count = queryset.update(is_public=True)
|
||||
for problem_id in queryset.values_list('id', flat=True):
|
||||
self._rescore(request, problem_id)
|
||||
self.message_user(request, ungettext('%d problem successfully marked as public.',
|
||||
'%d problems successfully marked as public.',
|
||||
count) % count)
|
||||
|
||||
make_public.short_description = _('Mark problems as public')
|
||||
|
||||
def make_private(self, request, queryset):
|
||||
count = queryset.update(is_public=False)
|
||||
for problem_id in queryset.values_list('id', flat=True):
|
||||
self._rescore(request, problem_id)
|
||||
self.message_user(request, ungettext('%d problem successfully marked as private.',
|
||||
'%d problems successfully marked as private.',
|
||||
count) % count)
|
||||
|
||||
make_private.short_description = _('Mark problems as private')
|
||||
|
||||
def get_queryset(self, request):
|
||||
queryset = Problem.objects.prefetch_related('authors__user')
|
||||
if request.user.has_perm('judge.edit_all_problem'):
|
||||
return queryset
|
||||
|
||||
access = Q()
|
||||
if request.user.has_perm('judge.edit_public_problem'):
|
||||
access |= Q(is_public=True)
|
||||
if request.user.has_perm('judge.edit_own_problem'):
|
||||
access |= Q(authors__id=request.profile.id) | Q(curators__id=request.profile.id)
|
||||
return queryset.filter(access).distinct() if access else queryset.none()
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
if request.user.has_perm('judge.edit_all_problem') or obj is None:
|
||||
return True
|
||||
if request.user.has_perm('judge.edit_public_problem') and obj.is_public:
|
||||
return True
|
||||
if not request.user.has_perm('judge.edit_own_problem'):
|
||||
return False
|
||||
return obj.is_editor(request.profile)
|
||||
|
||||
def formfield_for_manytomany(self, db_field, request=None, **kwargs):
|
||||
if db_field.name == 'allowed_languages':
|
||||
kwargs['widget'] = CheckboxSelectMultipleWithSelectAll()
|
||||
return super(ProblemAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)
|
||||
|
||||
def get_form(self, *args, **kwargs):
|
||||
form = super(ProblemAdmin, self).get_form(*args, **kwargs)
|
||||
form.base_fields['authors'].queryset = Profile.objects.all()
|
||||
return form
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
super(ProblemAdmin, self).save_model(request, obj, form, change)
|
||||
if form.changed_data and any(f in form.changed_data for f in ('is_public', 'points', 'partial')):
|
||||
self._rescore(request, obj.id)
|
||||
|
||||
def construct_change_message(self, request, form, *args, **kwargs):
|
||||
if form.cleaned_data.get('change_message'):
|
||||
return form.cleaned_data['change_message']
|
||||
return super(ProblemAdmin, self).construct_change_message(request, form, *args, **kwargs)
|
118
judge/admin/profile.py
Normal file
118
judge/admin/profile.py
Normal file
|
@ -0,0 +1,118 @@
|
|||
from django.contrib import admin
|
||||
from django.forms import ModelForm
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext, gettext_lazy as _, ungettext
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from django_ace import AceWidget
|
||||
from judge.models import Profile
|
||||
from judge.widgets import AdminPagedownWidget, AdminSelect2Widget
|
||||
|
||||
|
||||
class ProfileForm(ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ProfileForm, self).__init__(*args, **kwargs)
|
||||
if 'current_contest' in self.base_fields:
|
||||
# form.fields['current_contest'] does not exist when the user has only view permission on the model.
|
||||
self.fields['current_contest'].queryset = self.instance.contest_history.select_related('contest') \
|
||||
.only('contest__name', 'user_id', 'virtual')
|
||||
self.fields['current_contest'].label_from_instance = \
|
||||
lambda obj: '%s v%d' % (obj.contest.name, obj.virtual) if obj.virtual else obj.contest.name
|
||||
|
||||
class Meta:
|
||||
widgets = {
|
||||
'timezone': AdminSelect2Widget,
|
||||
'language': AdminSelect2Widget,
|
||||
'ace_theme': AdminSelect2Widget,
|
||||
'current_contest': AdminSelect2Widget,
|
||||
}
|
||||
if AdminPagedownWidget is not None:
|
||||
widgets['about'] = AdminPagedownWidget
|
||||
|
||||
|
||||
class TimezoneFilter(admin.SimpleListFilter):
|
||||
title = _('timezone')
|
||||
parameter_name = 'timezone'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return Profile.objects.values_list('timezone', 'timezone').distinct().order_by('timezone')
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() is None:
|
||||
return queryset
|
||||
return queryset.filter(timezone=self.value())
|
||||
|
||||
|
||||
class ProfileAdmin(VersionAdmin):
|
||||
fields = ('user', 'display_rank', 'about', 'organizations', 'timezone', 'language', 'ace_theme',
|
||||
'math_engine', 'last_access', 'ip', 'mute', 'is_unlisted', 'notes', 'is_totp_enabled', 'user_script',
|
||||
'current_contest')
|
||||
readonly_fields = ('user',)
|
||||
list_display = ('admin_user_admin', 'email', 'is_totp_enabled', 'timezone_full',
|
||||
'date_joined', 'last_access', 'ip', 'show_public')
|
||||
ordering = ('user__username',)
|
||||
search_fields = ('user__username', 'ip', 'user__email')
|
||||
list_filter = ('language', TimezoneFilter)
|
||||
actions = ('recalculate_points',)
|
||||
actions_on_top = True
|
||||
actions_on_bottom = True
|
||||
form = ProfileForm
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super(ProfileAdmin, self).get_queryset(request).select_related('user')
|
||||
|
||||
def get_fields(self, request, obj=None):
|
||||
if request.user.has_perm('judge.totp'):
|
||||
fields = list(self.fields)
|
||||
fields.insert(fields.index('is_totp_enabled') + 1, 'totp_key')
|
||||
return tuple(fields)
|
||||
else:
|
||||
return self.fields
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
fields = self.readonly_fields
|
||||
if not request.user.has_perm('judge.totp'):
|
||||
fields += ('is_totp_enabled',)
|
||||
return fields
|
||||
|
||||
def show_public(self, obj):
|
||||
return format_html('<a href="{0}" style="white-space:nowrap;">{1}</a>',
|
||||
obj.get_absolute_url(), gettext('View on site'))
|
||||
show_public.short_description = ''
|
||||
|
||||
def admin_user_admin(self, obj):
|
||||
return obj.username
|
||||
admin_user_admin.admin_order_field = 'user__username'
|
||||
admin_user_admin.short_description = _('User')
|
||||
|
||||
def email(self, obj):
|
||||
return obj.user.email
|
||||
email.admin_order_field = 'user__email'
|
||||
email.short_description = _('Email')
|
||||
|
||||
def timezone_full(self, obj):
|
||||
return obj.timezone
|
||||
timezone_full.admin_order_field = 'timezone'
|
||||
timezone_full.short_description = _('Timezone')
|
||||
|
||||
def date_joined(self, obj):
|
||||
return obj.user.date_joined
|
||||
date_joined.admin_order_field = 'user__date_joined'
|
||||
date_joined.short_description = _('date joined')
|
||||
|
||||
def recalculate_points(self, request, queryset):
|
||||
count = 0
|
||||
for profile in queryset:
|
||||
profile.calculate_points()
|
||||
count += 1
|
||||
self.message_user(request, ungettext('%d user have scores recalculated.',
|
||||
'%d users have scores recalculated.',
|
||||
count) % count)
|
||||
recalculate_points.short_description = _('Recalculate scores')
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
form = super(ProfileAdmin, self).get_form(request, obj, **kwargs)
|
||||
if 'user_script' in form.base_fields:
|
||||
# form.base_fields['user_script'] does not exist when the user has only view permission on the model.
|
||||
form.base_fields['user_script'].widget = AceWidget('javascript', request.profile.ace_theme)
|
||||
return form
|
120
judge/admin/runtime.py
Normal file
120
judge/admin/runtime.py
Normal file
|
@ -0,0 +1,120 @@
|
|||
from django.conf.urls import url
|
||||
from django.db.models import TextField
|
||||
from django.forms import ModelForm, ModelMultipleChoiceField, TextInput
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from django_ace import AceWidget
|
||||
from judge.models import Judge, Problem
|
||||
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminPagedownWidget
|
||||
|
||||
|
||||
class LanguageForm(ModelForm):
|
||||
problems = ModelMultipleChoiceField(
|
||||
label=_('Disallowed problems'),
|
||||
queryset=Problem.objects.all(),
|
||||
required=False,
|
||||
help_text=_('These problems are NOT allowed to be submitted in this language'),
|
||||
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'))
|
||||
|
||||
class Meta:
|
||||
if AdminPagedownWidget is not None:
|
||||
widgets = {'description': AdminPagedownWidget}
|
||||
|
||||
|
||||
class LanguageAdmin(VersionAdmin):
|
||||
fields = ('key', 'name', 'short_name', 'common_name', 'ace', 'pygments', 'info', 'description',
|
||||
'template', 'problems')
|
||||
list_display = ('key', 'name', 'common_name', 'info')
|
||||
form = LanguageForm
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
super(LanguageAdmin, self).save_model(request, obj, form, change)
|
||||
obj.problem_set.set(Problem.objects.exclude(id__in=form.cleaned_data['problems'].values('id')))
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
self.form.base_fields['problems'].initial = \
|
||||
Problem.objects.exclude(id__in=obj.problem_set.values('id')).values_list('pk', flat=True) if obj else []
|
||||
form = super(LanguageAdmin, self).get_form(request, obj, **kwargs)
|
||||
if obj is not None:
|
||||
form.base_fields['template'].widget = AceWidget(obj.ace, request.profile.ace_theme)
|
||||
return form
|
||||
|
||||
|
||||
class GenerateKeyTextInput(TextInput):
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
text = super(TextInput, self).render(name, value, attrs)
|
||||
return mark_safe(text + format_html(
|
||||
'''\
|
||||
<a href="#" onclick="return false;" class="button" id="id_{0}_regen">Regenerate</a>
|
||||
<script type="text/javascript">
|
||||
django.jQuery(document).ready(function ($) {{
|
||||
$('#id_{0}_regen').click(function () {{
|
||||
var length = 100,
|
||||
charset = "abcdefghijklnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`~!@#$%^&*()_+-=|[]{{}};:,<>./?",
|
||||
key = "";
|
||||
for (var i = 0, n = charset.length; i < length; ++i) {{
|
||||
key += charset.charAt(Math.floor(Math.random() * n));
|
||||
}}
|
||||
$('#id_{0}').val(key);
|
||||
}});
|
||||
}});
|
||||
</script>
|
||||
''', name))
|
||||
|
||||
|
||||
class JudgeAdminForm(ModelForm):
|
||||
class Meta:
|
||||
widgets = {'auth_key': GenerateKeyTextInput}
|
||||
if AdminPagedownWidget is not None:
|
||||
widgets['description'] = AdminPagedownWidget
|
||||
|
||||
|
||||
class JudgeAdmin(VersionAdmin):
|
||||
form = JudgeAdminForm
|
||||
readonly_fields = ('created', 'online', 'start_time', 'ping', 'load', 'last_ip', 'runtimes', 'problems')
|
||||
fieldsets = (
|
||||
(None, {'fields': ('name', 'auth_key', 'is_blocked')}),
|
||||
(_('Description'), {'fields': ('description',)}),
|
||||
(_('Information'), {'fields': ('created', 'online', 'last_ip', 'start_time', 'ping', 'load')}),
|
||||
(_('Capabilities'), {'fields': ('runtimes', 'problems')}),
|
||||
)
|
||||
list_display = ('name', 'online', 'start_time', 'ping', 'load', 'last_ip')
|
||||
ordering = ['-online', 'name']
|
||||
|
||||
def get_urls(self):
|
||||
return ([url(r'^(\d+)/disconnect/$', self.disconnect_view, name='judge_judge_disconnect'),
|
||||
url(r'^(\d+)/terminate/$', self.terminate_view, name='judge_judge_terminate')] +
|
||||
super(JudgeAdmin, self).get_urls())
|
||||
|
||||
def disconnect_judge(self, id, force=False):
|
||||
judge = get_object_or_404(Judge, id=id)
|
||||
judge.disconnect(force=force)
|
||||
return HttpResponseRedirect(reverse('admin:judge_judge_changelist'))
|
||||
|
||||
def disconnect_view(self, request, id):
|
||||
return self.disconnect_judge(id)
|
||||
|
||||
def terminate_view(self, request, id):
|
||||
return self.disconnect_judge(id, force=True)
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
if obj is not None and obj.online:
|
||||
return self.readonly_fields + ('name',)
|
||||
return self.readonly_fields
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
result = super(JudgeAdmin, self).has_delete_permission(request, obj)
|
||||
if result and obj is not None:
|
||||
return not obj.online
|
||||
return result
|
||||
|
||||
if AdminPagedownWidget is not None:
|
||||
formfield_overrides = {
|
||||
TextField: {'widget': AdminPagedownWidget},
|
||||
}
|
251
judge/admin/submission.py
Normal file
251
judge/admin/submission.py
Normal file
|
@ -0,0 +1,251 @@
|
|||
from functools import partial
|
||||
from operator import itemgetter
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
from django.contrib import admin, messages
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext, gettext_lazy as _, pgettext, ungettext
|
||||
|
||||
from django_ace import AceWidget
|
||||
from judge.models import ContestParticipation, ContestProblem, ContestSubmission, Profile, Submission, \
|
||||
SubmissionSource, SubmissionTestCase
|
||||
from judge.utils.raw_sql import use_straight_join
|
||||
|
||||
|
||||
class SubmissionStatusFilter(admin.SimpleListFilter):
|
||||
parameter_name = title = 'status'
|
||||
__lookups = (('None', _('None')), ('NotDone', _('Not done')), ('EX', _('Exceptional'))) + Submission.STATUS
|
||||
__handles = set(map(itemgetter(0), Submission.STATUS))
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return self.__lookups
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() == 'None':
|
||||
return queryset.filter(status=None)
|
||||
elif self.value() == 'NotDone':
|
||||
return queryset.exclude(status__in=['D', 'IE', 'CE', 'AB'])
|
||||
elif self.value() == 'EX':
|
||||
return queryset.exclude(status__in=['D', 'CE', 'G', 'AB'])
|
||||
elif self.value() in self.__handles:
|
||||
return queryset.filter(status=self.value())
|
||||
|
||||
|
||||
class SubmissionResultFilter(admin.SimpleListFilter):
|
||||
parameter_name = title = 'result'
|
||||
__lookups = (('None', _('None')), ('BAD', _('Unaccepted'))) + Submission.RESULT
|
||||
__handles = set(map(itemgetter(0), Submission.RESULT))
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return self.__lookups
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() == 'None':
|
||||
return queryset.filter(result=None)
|
||||
elif self.value() == 'BAD':
|
||||
return queryset.exclude(result='AC')
|
||||
elif self.value() in self.__handles:
|
||||
return queryset.filter(result=self.value())
|
||||
|
||||
|
||||
class SubmissionTestCaseInline(admin.TabularInline):
|
||||
fields = ('case', 'batch', 'status', 'time', 'memory', 'points', 'total')
|
||||
readonly_fields = ('case', 'batch', 'total')
|
||||
model = SubmissionTestCase
|
||||
can_delete = False
|
||||
max_num = 0
|
||||
|
||||
|
||||
class ContestSubmissionInline(admin.StackedInline):
|
||||
fields = ('problem', 'participation', 'points')
|
||||
model = ContestSubmission
|
||||
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
kwargs['formfield_callback'] = partial(self.formfield_for_dbfield, request=request, obj=obj)
|
||||
return super(ContestSubmissionInline, self).get_formset(request, obj, **kwargs)
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
submission = kwargs.pop('obj', None)
|
||||
label = None
|
||||
if submission:
|
||||
if db_field.name == 'participation':
|
||||
kwargs['queryset'] = ContestParticipation.objects.filter(user=submission.user,
|
||||
contest__problems=submission.problem) \
|
||||
.only('id', 'contest__name')
|
||||
|
||||
def label(obj):
|
||||
return obj.contest.name
|
||||
elif db_field.name == 'problem':
|
||||
kwargs['queryset'] = ContestProblem.objects.filter(problem=submission.problem) \
|
||||
.only('id', 'problem__name', 'contest__name')
|
||||
|
||||
def label(obj):
|
||||
return pgettext('contest problem', '%(problem)s in %(contest)s') % {
|
||||
'problem': obj.problem.name, 'contest': obj.contest.name,
|
||||
}
|
||||
field = super(ContestSubmissionInline, self).formfield_for_dbfield(db_field, **kwargs)
|
||||
if label is not None:
|
||||
field.label_from_instance = label
|
||||
return field
|
||||
|
||||
|
||||
class SubmissionSourceInline(admin.StackedInline):
|
||||
fields = ('source',)
|
||||
model = SubmissionSource
|
||||
can_delete = False
|
||||
extra = 0
|
||||
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
kwargs.setdefault('widgets', {})['source'] = AceWidget(mode=obj and obj.language.ace,
|
||||
theme=request.profile.ace_theme)
|
||||
return super().get_formset(request, obj, **kwargs)
|
||||
|
||||
|
||||
class SubmissionAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ('user', 'problem', 'date')
|
||||
fields = ('user', 'problem', 'date', 'time', 'memory', 'points', 'language', 'status', 'result',
|
||||
'case_points', 'case_total', 'judged_on', 'error')
|
||||
actions = ('judge', 'recalculate_score')
|
||||
list_display = ('id', 'problem_code', 'problem_name', 'user_column', 'execution_time', 'pretty_memory',
|
||||
'points', 'language_column', 'status', 'result', 'judge_column')
|
||||
list_filter = ('language', SubmissionStatusFilter, SubmissionResultFilter)
|
||||
search_fields = ('problem__code', 'problem__name', 'user__user__username')
|
||||
actions_on_top = True
|
||||
actions_on_bottom = True
|
||||
inlines = [SubmissionSourceInline, SubmissionTestCaseInline, ContestSubmissionInline]
|
||||
|
||||
def get_queryset(self, request):
|
||||
queryset = Submission.objects.select_related('problem', 'user__user', 'language').only(
|
||||
'problem__code', 'problem__name', 'user__user__username', 'language__name',
|
||||
'time', 'memory', 'points', 'status', 'result',
|
||||
)
|
||||
use_straight_join(queryset)
|
||||
if not request.user.has_perm('judge.edit_all_problem'):
|
||||
id = request.profile.id
|
||||
queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id)).distinct()
|
||||
return queryset
|
||||
|
||||
def has_add_permission(self, request):
|
||||
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):
|
||||
return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in ('problem__code',)
|
||||
|
||||
def judge(self, request, queryset):
|
||||
if not request.user.has_perm('judge.rejudge_submission') or not request.user.has_perm('judge.edit_own_problem'):
|
||||
self.message_user(request, gettext('You do not have the permission to rejudge submissions.'),
|
||||
level=messages.ERROR)
|
||||
return
|
||||
queryset = queryset.order_by('id')
|
||||
if not request.user.has_perm('judge.rejudge_submission_lot') and \
|
||||
queryset.count() > settings.DMOJ_SUBMISSIONS_REJUDGE_LIMIT:
|
||||
self.message_user(request, gettext('You do not have the permission to rejudge THAT many submissions.'),
|
||||
level=messages.ERROR)
|
||||
return
|
||||
if not request.user.has_perm('judge.edit_all_problem'):
|
||||
id = request.profile.id
|
||||
queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id))
|
||||
judged = len(queryset)
|
||||
for model in queryset:
|
||||
model.judge(rejudge=True, batch_rejudge=True)
|
||||
self.message_user(request, ungettext('%d submission was successfully scheduled for rejudging.',
|
||||
'%d submissions were successfully scheduled for rejudging.',
|
||||
judged) % judged)
|
||||
judge.short_description = _('Rejudge the selected submissions')
|
||||
|
||||
def recalculate_score(self, request, queryset):
|
||||
if not request.user.has_perm('judge.rejudge_submission'):
|
||||
self.message_user(request, gettext('You do not have the permission to rejudge submissions.'),
|
||||
level=messages.ERROR)
|
||||
return
|
||||
submissions = list(queryset.defer(None).select_related(None).select_related('problem')
|
||||
.only('points', 'case_points', 'case_total', 'problem__partial', 'problem__points'))
|
||||
for submission in submissions:
|
||||
submission.points = round(submission.case_points / submission.case_total * submission.problem.points
|
||||
if submission.case_total else 0, 1)
|
||||
if not submission.problem.partial and submission.points < submission.problem.points:
|
||||
submission.points = 0
|
||||
submission.save()
|
||||
submission.update_contest()
|
||||
|
||||
for profile in Profile.objects.filter(id__in=queryset.values_list('user_id', flat=True).distinct()):
|
||||
profile.calculate_points()
|
||||
cache.delete('user_complete:%d' % profile.id)
|
||||
cache.delete('user_attempted:%d' % profile.id)
|
||||
|
||||
for participation in ContestParticipation.objects.filter(
|
||||
id__in=queryset.values_list('contest__participation_id')).prefetch_related('contest'):
|
||||
participation.recompute_results()
|
||||
|
||||
self.message_user(request, ungettext('%d submission were successfully rescored.',
|
||||
'%d submissions were successfully rescored.',
|
||||
len(submissions)) % len(submissions))
|
||||
recalculate_score.short_description = _('Rescore the selected submissions')
|
||||
|
||||
def problem_code(self, obj):
|
||||
return obj.problem.code
|
||||
problem_code.short_description = _('Problem code')
|
||||
problem_code.admin_order_field = 'problem__code'
|
||||
|
||||
def problem_name(self, obj):
|
||||
return obj.problem.name
|
||||
problem_name.short_description = _('Problem name')
|
||||
problem_name.admin_order_field = 'problem__name'
|
||||
|
||||
def user_column(self, obj):
|
||||
return obj.user.user.username
|
||||
user_column.admin_order_field = 'user__user__username'
|
||||
user_column.short_description = _('User')
|
||||
|
||||
def execution_time(self, obj):
|
||||
return round(obj.time, 2) if obj.time is not None else 'None'
|
||||
execution_time.short_description = _('Time')
|
||||
execution_time.admin_order_field = 'time'
|
||||
|
||||
def pretty_memory(self, obj):
|
||||
memory = obj.memory
|
||||
if memory is None:
|
||||
return gettext('None')
|
||||
if memory < 1000:
|
||||
return gettext('%d KB') % memory
|
||||
else:
|
||||
return gettext('%.2f MB') % (memory / 1024)
|
||||
pretty_memory.admin_order_field = 'memory'
|
||||
pretty_memory.short_description = _('Memory')
|
||||
|
||||
def language_column(self, obj):
|
||||
return obj.language.name
|
||||
language_column.admin_order_field = 'language__name'
|
||||
language_column.short_description = _('Language')
|
||||
|
||||
def judge_column(self, obj):
|
||||
return format_html('<input type="button" value="Rejudge" onclick="location.href=\'{}/judge/\'" />', obj.id)
|
||||
judge_column.short_description = ''
|
||||
|
||||
def get_urls(self):
|
||||
return [
|
||||
url(r'^(\d+)/judge/$', self.judge_view, name='judge_submission_rejudge'),
|
||||
] + super(SubmissionAdmin, self).get_urls()
|
||||
|
||||
def judge_view(self, request, id):
|
||||
if not request.user.has_perm('judge.rejudge_submission') or not request.user.has_perm('judge.edit_own_problem'):
|
||||
raise PermissionDenied()
|
||||
submission = get_object_or_404(Submission, id=id)
|
||||
if not request.user.has_perm('judge.edit_all_problem') and \
|
||||
not submission.problem.is_editor(request.profile):
|
||||
raise PermissionDenied()
|
||||
submission.judge(rejudge=True)
|
||||
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
|
52
judge/admin/taxon.py
Normal file
52
judge/admin/taxon.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
from django.contrib import admin
|
||||
from django.forms import ModelForm, ModelMultipleChoiceField
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from judge.models import Problem
|
||||
from judge.widgets import AdminHeavySelect2MultipleWidget
|
||||
|
||||
|
||||
class ProblemGroupForm(ModelForm):
|
||||
problems = ModelMultipleChoiceField(
|
||||
label=_('Included problems'),
|
||||
queryset=Problem.objects.all(),
|
||||
required=False,
|
||||
help_text=_('These problems are included in this group of problems'),
|
||||
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'))
|
||||
|
||||
|
||||
class ProblemGroupAdmin(admin.ModelAdmin):
|
||||
fields = ('name', 'full_name', 'problems')
|
||||
form = ProblemGroupForm
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
super(ProblemGroupAdmin, self).save_model(request, obj, form, change)
|
||||
obj.problem_set.set(form.cleaned_data['problems'])
|
||||
obj.save()
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
self.form.base_fields['problems'].initial = [o.pk for o in obj.problem_set.all()] if obj else []
|
||||
return super(ProblemGroupAdmin, self).get_form(request, obj, **kwargs)
|
||||
|
||||
|
||||
class ProblemTypeForm(ModelForm):
|
||||
problems = ModelMultipleChoiceField(
|
||||
label=_('Included problems'),
|
||||
queryset=Problem.objects.all(),
|
||||
required=False,
|
||||
help_text=_('These problems are included in this type of problems'),
|
||||
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'))
|
||||
|
||||
|
||||
class ProblemTypeAdmin(admin.ModelAdmin):
|
||||
fields = ('name', 'full_name', 'problems')
|
||||
form = ProblemTypeForm
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
super(ProblemTypeAdmin, self).save_model(request, obj, form, change)
|
||||
obj.problem_set.set(form.cleaned_data['problems'])
|
||||
obj.save()
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
self.form.base_fields['problems'].initial = [o.pk for o in obj.problem_set.all()] if obj else []
|
||||
return super(ProblemTypeAdmin, self).get_form(request, obj, **kwargs)
|
39
judge/admin/ticket.py
Normal file
39
judge/admin/ticket.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
from django.contrib.admin import ModelAdmin
|
||||
from django.contrib.admin.options import StackedInline
|
||||
from django.forms import ModelForm
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from judge.models import TicketMessage
|
||||
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
|
||||
|
||||
|
||||
class TicketMessageForm(ModelForm):
|
||||
class Meta:
|
||||
widgets = {
|
||||
'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||
}
|
||||
if HeavyPreviewAdminPageDownWidget is not None:
|
||||
widgets['body'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('ticket_preview'))
|
||||
|
||||
|
||||
class TicketMessageInline(StackedInline):
|
||||
model = TicketMessage
|
||||
form = TicketMessageForm
|
||||
fields = ('user', 'body')
|
||||
|
||||
|
||||
class TicketForm(ModelForm):
|
||||
class Meta:
|
||||
widgets = {
|
||||
'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||
'assignees': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||
}
|
||||
|
||||
|
||||
class TicketAdmin(ModelAdmin):
|
||||
fields = ('title', 'time', 'user', 'assignees', 'content_type', 'object_id', 'notes')
|
||||
readonly_fields = ('time',)
|
||||
list_display = ('title', 'user', 'time', 'linked_item')
|
||||
inlines = [TicketMessageInline]
|
||||
form = TicketForm
|
||||
date_hierarchy = 'time'
|
39
judge/apps.py
Normal file
39
judge/apps.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
from django.apps import AppConfig
|
||||
from django.db import DatabaseError
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
|
||||
class JudgeAppConfig(AppConfig):
|
||||
name = 'judge'
|
||||
verbose_name = gettext_lazy('Online Judge')
|
||||
|
||||
def ready(self):
|
||||
# WARNING: AS THIS IS NOT A FUNCTIONAL PROGRAMMING LANGUAGE,
|
||||
# OPERATIONS MAY HAVE SIDE EFFECTS.
|
||||
# DO NOT REMOVE THINKING THE IMPORT IS UNUSED.
|
||||
# noinspection PyUnresolvedReferences
|
||||
from . import signals, jinja2 # noqa: F401, imported for side effects
|
||||
|
||||
from django.contrib.flatpages.models import FlatPage
|
||||
from django.contrib.flatpages.admin import FlatPageAdmin
|
||||
from django.contrib import admin
|
||||
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
class FlatPageVersionAdmin(VersionAdmin, FlatPageAdmin):
|
||||
pass
|
||||
|
||||
admin.site.unregister(FlatPage)
|
||||
admin.site.register(FlatPage, FlatPageVersionAdmin)
|
||||
|
||||
from judge.models import Language, Profile
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
try:
|
||||
lang = Language.get_python3()
|
||||
for user in User.objects.filter(profile=None):
|
||||
# These poor profileless users
|
||||
profile = Profile(user=user, language=lang)
|
||||
profile.save()
|
||||
except DatabaseError:
|
||||
pass
|
6
judge/bridge/__init__.py
Normal file
6
judge/bridge/__init__.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from .djangohandler import DjangoHandler
|
||||
from .djangoserver import DjangoServer
|
||||
from .judgecallback import DjangoJudgeHandler
|
||||
from .judgehandler import JudgeHandler
|
||||
from .judgelist import JudgeList
|
||||
from .judgeserver import JudgeServer
|
67
judge/bridge/djangohandler.py
Normal file
67
judge/bridge/djangohandler.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
import json
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from event_socket_server import ZlibPacketHandler
|
||||
|
||||
logger = logging.getLogger('judge.bridge')
|
||||
size_pack = struct.Struct('!I')
|
||||
|
||||
|
||||
class DjangoHandler(ZlibPacketHandler):
|
||||
def __init__(self, server, socket):
|
||||
super(DjangoHandler, self).__init__(server, socket)
|
||||
|
||||
self.handlers = {
|
||||
'submission-request': self.on_submission,
|
||||
'terminate-submission': self.on_termination,
|
||||
'disconnect-judge': self.on_disconnect,
|
||||
}
|
||||
self._to_kill = True
|
||||
# self.server.schedule(5, self._kill_if_no_request)
|
||||
|
||||
def _kill_if_no_request(self):
|
||||
if self._to_kill:
|
||||
logger.info('Killed inactive connection: %s', self._socket.getpeername())
|
||||
self.close()
|
||||
|
||||
def _format_send(self, data):
|
||||
return super(DjangoHandler, self)._format_send(json.dumps(data, separators=(',', ':')))
|
||||
|
||||
def packet(self, packet):
|
||||
self._to_kill = False
|
||||
packet = json.loads(packet)
|
||||
try:
|
||||
result = self.handlers.get(packet.get('name', None), self.on_malformed)(packet)
|
||||
except Exception:
|
||||
logger.exception('Error in packet handling (Django-facing)')
|
||||
result = {'name': 'bad-request'}
|
||||
self.send(result, self._schedule_close)
|
||||
|
||||
def _schedule_close(self):
|
||||
self.server.schedule(0, self.close)
|
||||
|
||||
def on_submission(self, data):
|
||||
id = data['submission-id']
|
||||
problem = data['problem-id']
|
||||
language = data['language']
|
||||
source = data['source']
|
||||
priority = data['priority']
|
||||
if not self.server.judges.check_priority(priority):
|
||||
return {'name': 'bad-request'}
|
||||
self.server.judges.judge(id, problem, language, source, priority)
|
||||
return {'name': 'submission-received', 'submission-id': id}
|
||||
|
||||
def on_termination(self, data):
|
||||
return {'name': 'submission-received', 'judge-aborted': self.server.judges.abort(data['submission-id'])}
|
||||
|
||||
def on_disconnect(self, data):
|
||||
judge_id = data['judge-id']
|
||||
force = data['force']
|
||||
self.server.judges.disconnect(judge_id, force=force)
|
||||
|
||||
def on_malformed(self, packet):
|
||||
logger.error('Malformed packet: %s', packet)
|
||||
|
||||
def on_close(self):
|
||||
self._to_kill = False
|
7
judge/bridge/djangoserver.py
Normal file
7
judge/bridge/djangoserver.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from event_socket_server import get_preferred_engine
|
||||
|
||||
|
||||
class DjangoServer(get_preferred_engine()):
|
||||
def __init__(self, judges, *args, **kwargs):
|
||||
super(DjangoServer, self).__init__(*args, **kwargs)
|
||||
self.judges = judges
|
411
judge/bridge/judgecallback.py
Normal file
411
judge/bridge/judgecallback.py
Normal file
|
@ -0,0 +1,411 @@
|
|||
import json
|
||||
import logging
|
||||
import time
|
||||
from operator import itemgetter
|
||||
|
||||
from django import db
|
||||
from django.utils import timezone
|
||||
|
||||
from judge import event_poster as event
|
||||
from judge.caching import finished_submission
|
||||
from judge.models import Judge, Language, LanguageLimit, Problem, RuntimeVersion, Submission, SubmissionTestCase
|
||||
from .judgehandler import JudgeHandler, SubmissionData
|
||||
|
||||
logger = logging.getLogger('judge.bridge')
|
||||
json_log = logging.getLogger('judge.json.bridge')
|
||||
|
||||
UPDATE_RATE_LIMIT = 5
|
||||
UPDATE_RATE_TIME = 0.5
|
||||
|
||||
|
||||
def _ensure_connection():
|
||||
try:
|
||||
db.connection.cursor().execute('SELECT 1').fetchall()
|
||||
except Exception:
|
||||
db.connection.close()
|
||||
|
||||
|
||||
class DjangoJudgeHandler(JudgeHandler):
|
||||
def __init__(self, server, socket):
|
||||
super(DjangoJudgeHandler, self).__init__(server, socket)
|
||||
|
||||
# each value is (updates, last reset)
|
||||
self.update_counter = {}
|
||||
self.judge = None
|
||||
self.judge_address = None
|
||||
|
||||
self._submission_cache_id = None
|
||||
self._submission_cache = {}
|
||||
|
||||
json_log.info(self._make_json_log(action='connect'))
|
||||
|
||||
def on_close(self):
|
||||
super(DjangoJudgeHandler, self).on_close()
|
||||
json_log.info(self._make_json_log(action='disconnect', info='judge disconnected'))
|
||||
if self._working:
|
||||
Submission.objects.filter(id=self._working).update(status='IE', result='IE')
|
||||
json_log.error(self._make_json_log(sub=self._working, action='close', info='IE due to shutdown on grading'))
|
||||
|
||||
def on_malformed(self, packet):
|
||||
super(DjangoJudgeHandler, self).on_malformed(packet)
|
||||
json_log.exception(self._make_json_log(sub=self._working, info='malformed zlib packet'))
|
||||
|
||||
def _packet_exception(self):
|
||||
json_log.exception(self._make_json_log(sub=self._working, info='packet processing exception'))
|
||||
|
||||
def get_related_submission_data(self, submission):
|
||||
_ensure_connection() # We are called from the django-facing daemon thread. Guess what happens.
|
||||
|
||||
try:
|
||||
pid, time, memory, short_circuit, lid, is_pretested, sub_date, uid, part_virtual, part_id = (
|
||||
Submission.objects.filter(id=submission)
|
||||
.values_list('problem__id', 'problem__time_limit', 'problem__memory_limit',
|
||||
'problem__short_circuit', 'language__id', 'is_pretested', 'date', 'user__id',
|
||||
'contest__participation__virtual', 'contest__participation__id')).get()
|
||||
except Submission.DoesNotExist:
|
||||
logger.error('Submission vanished: %s', submission)
|
||||
json_log.error(self._make_json_log(
|
||||
sub=self._working, action='request',
|
||||
info='submission vanished when fetching info',
|
||||
))
|
||||
return
|
||||
|
||||
attempt_no = Submission.objects.filter(problem__id=pid, contest__participation__id=part_id, user__id=uid,
|
||||
date__lt=sub_date).exclude(status__in=('CE', 'IE')).count() + 1
|
||||
|
||||
try:
|
||||
time, memory = (LanguageLimit.objects.filter(problem__id=pid, language__id=lid)
|
||||
.values_list('time_limit', 'memory_limit').get())
|
||||
except LanguageLimit.DoesNotExist:
|
||||
pass
|
||||
|
||||
return SubmissionData(
|
||||
time=time,
|
||||
memory=memory,
|
||||
short_circuit=short_circuit,
|
||||
pretests_only=is_pretested,
|
||||
contest_no=part_virtual,
|
||||
attempt_no=attempt_no,
|
||||
user_id=uid,
|
||||
)
|
||||
|
||||
def _authenticate(self, id, key):
|
||||
result = Judge.objects.filter(name=id, auth_key=key, is_blocked=False).exists()
|
||||
if not result:
|
||||
json_log.warning(self._make_json_log(action='auth', judge=id, info='judge failed authentication'))
|
||||
return result
|
||||
|
||||
def _connected(self):
|
||||
judge = self.judge = Judge.objects.get(name=self.name)
|
||||
judge.start_time = timezone.now()
|
||||
judge.online = True
|
||||
judge.problems.set(Problem.objects.filter(code__in=list(self.problems.keys())))
|
||||
judge.runtimes.set(Language.objects.filter(key__in=list(self.executors.keys())))
|
||||
|
||||
# Delete now in case we somehow crashed and left some over from the last connection
|
||||
RuntimeVersion.objects.filter(judge=judge).delete()
|
||||
versions = []
|
||||
for lang in judge.runtimes.all():
|
||||
versions += [
|
||||
RuntimeVersion(language=lang, name=name, version='.'.join(map(str, version)), priority=idx, judge=judge)
|
||||
for idx, (name, version) in enumerate(self.executors[lang.key])
|
||||
]
|
||||
RuntimeVersion.objects.bulk_create(versions)
|
||||
judge.last_ip = self.client_address[0]
|
||||
judge.save()
|
||||
self.judge_address = '[%s]:%s' % (self.client_address[0], self.client_address[1])
|
||||
json_log.info(self._make_json_log(action='auth', info='judge successfully authenticated',
|
||||
executors=list(self.executors.keys())))
|
||||
|
||||
def _disconnected(self):
|
||||
Judge.objects.filter(id=self.judge.id).update(online=False)
|
||||
RuntimeVersion.objects.filter(judge=self.judge).delete()
|
||||
|
||||
def _update_ping(self):
|
||||
try:
|
||||
Judge.objects.filter(name=self.name).update(ping=self.latency, load=self.load)
|
||||
except Exception as e:
|
||||
# What can I do? I don't want to tie this to MySQL.
|
||||
if e.__class__.__name__ == 'OperationalError' and e.__module__ == '_mysql_exceptions' and e.args[0] == 2006:
|
||||
db.connection.close()
|
||||
|
||||
def _post_update_submission(self, id, state, done=False):
|
||||
if self._submission_cache_id == id:
|
||||
data = self._submission_cache
|
||||
else:
|
||||
self._submission_cache = data = Submission.objects.filter(id=id).values(
|
||||
'problem__is_public', 'contest__participation__contest__key',
|
||||
'user_id', 'problem_id', 'status', 'language__key',
|
||||
).get()
|
||||
self._submission_cache_id = id
|
||||
|
||||
if data['problem__is_public']:
|
||||
event.post('submissions', {
|
||||
'type': 'done-submission' if done else 'update-submission',
|
||||
'state': state, 'id': id,
|
||||
'contest': data['contest__participation__contest__key'],
|
||||
'user': data['user_id'], 'problem': data['problem_id'],
|
||||
'status': data['status'], 'language': data['language__key'],
|
||||
})
|
||||
|
||||
def on_submission_processing(self, packet):
|
||||
id = packet['submission-id']
|
||||
if Submission.objects.filter(id=id).update(status='P', judged_on=self.judge):
|
||||
event.post('sub_%s' % Submission.get_id_secret(id), {'type': 'processing'})
|
||||
self._post_update_submission(id, 'processing')
|
||||
json_log.info(self._make_json_log(packet, action='processing'))
|
||||
else:
|
||||
logger.warning('Unknown submission: %s', id)
|
||||
json_log.error(self._make_json_log(packet, action='processing', info='unknown submission'))
|
||||
|
||||
def on_submission_wrong_acknowledge(self, packet, expected, got):
|
||||
json_log.error(self._make_json_log(packet, action='processing', info='wrong-acknowledge', expected=expected))
|
||||
|
||||
def on_grading_begin(self, packet):
|
||||
super(DjangoJudgeHandler, self).on_grading_begin(packet)
|
||||
if Submission.objects.filter(id=packet['submission-id']).update(
|
||||
status='G', is_pretested=packet['pretested'],
|
||||
current_testcase=1, batch=False):
|
||||
SubmissionTestCase.objects.filter(submission_id=packet['submission-id']).delete()
|
||||
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'grading-begin'})
|
||||
self._post_update_submission(packet['submission-id'], 'grading-begin')
|
||||
json_log.info(self._make_json_log(packet, action='grading-begin'))
|
||||
else:
|
||||
logger.warning('Unknown submission: %s', packet['submission-id'])
|
||||
json_log.error(self._make_json_log(packet, action='grading-begin', info='unknown submission'))
|
||||
|
||||
def _submission_is_batch(self, id):
|
||||
if not Submission.objects.filter(id=id).update(batch=True):
|
||||
logger.warning('Unknown submission: %s', id)
|
||||
|
||||
def on_grading_end(self, packet):
|
||||
super(DjangoJudgeHandler, self).on_grading_end(packet)
|
||||
|
||||
try:
|
||||
submission = Submission.objects.get(id=packet['submission-id'])
|
||||
except Submission.DoesNotExist:
|
||||
logger.warning('Unknown submission: %s', packet['submission-id'])
|
||||
json_log.error(self._make_json_log(packet, action='grading-end', info='unknown submission'))
|
||||
return
|
||||
|
||||
time = 0
|
||||
memory = 0
|
||||
points = 0.0
|
||||
total = 0
|
||||
status = 0
|
||||
status_codes = ['SC', 'AC', 'WA', 'MLE', 'TLE', 'IR', 'RTE', 'OLE']
|
||||
batches = {} # batch number: (points, total)
|
||||
|
||||
for case in SubmissionTestCase.objects.filter(submission=submission):
|
||||
time += case.time
|
||||
if not case.batch:
|
||||
points += case.points
|
||||
total += case.total
|
||||
else:
|
||||
if case.batch in batches:
|
||||
batches[case.batch][0] = min(batches[case.batch][0], case.points)
|
||||
batches[case.batch][1] = max(batches[case.batch][1], case.total)
|
||||
else:
|
||||
batches[case.batch] = [case.points, case.total]
|
||||
memory = max(memory, case.memory)
|
||||
i = status_codes.index(case.status)
|
||||
if i > status:
|
||||
status = i
|
||||
|
||||
for i in batches:
|
||||
points += batches[i][0]
|
||||
total += batches[i][1]
|
||||
|
||||
points = round(points, 1)
|
||||
total = round(total, 1)
|
||||
submission.case_points = points
|
||||
submission.case_total = total
|
||||
|
||||
problem = submission.problem
|
||||
sub_points = round(points / total * problem.points if total > 0 else 0, 3)
|
||||
if not problem.partial and sub_points != problem.points:
|
||||
sub_points = 0
|
||||
|
||||
submission.status = 'D'
|
||||
submission.time = time
|
||||
submission.memory = memory
|
||||
submission.points = sub_points
|
||||
submission.result = status_codes[status]
|
||||
submission.save()
|
||||
|
||||
json_log.info(self._make_json_log(
|
||||
packet, action='grading-end', time=time, memory=memory,
|
||||
points=sub_points, total=problem.points, result=submission.result,
|
||||
case_points=points, case_total=total, user=submission.user_id,
|
||||
problem=problem.code, finish=True,
|
||||
))
|
||||
|
||||
submission.user._updating_stats_only = True
|
||||
submission.user.calculate_points()
|
||||
problem._updating_stats_only = True
|
||||
problem.update_stats()
|
||||
submission.update_contest()
|
||||
|
||||
finished_submission(submission)
|
||||
|
||||
event.post('sub_%s' % submission.id_secret, {
|
||||
'type': 'grading-end',
|
||||
'time': time,
|
||||
'memory': memory,
|
||||
'points': float(points),
|
||||
'total': float(problem.points),
|
||||
'result': submission.result,
|
||||
})
|
||||
if hasattr(submission, 'contest'):
|
||||
participation = submission.contest.participation
|
||||
event.post('contest_%d' % participation.contest_id, {'type': 'update'})
|
||||
self._post_update_submission(submission.id, 'grading-end', done=True)
|
||||
|
||||
def on_compile_error(self, packet):
|
||||
super(DjangoJudgeHandler, self).on_compile_error(packet)
|
||||
|
||||
if Submission.objects.filter(id=packet['submission-id']).update(status='CE', result='CE', error=packet['log']):
|
||||
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {
|
||||
'type': 'compile-error',
|
||||
'log': packet['log'],
|
||||
})
|
||||
self._post_update_submission(packet['submission-id'], 'compile-error', done=True)
|
||||
json_log.info(self._make_json_log(packet, action='compile-error', log=packet['log'],
|
||||
finish=True, result='CE'))
|
||||
else:
|
||||
logger.warning('Unknown submission: %s', packet['submission-id'])
|
||||
json_log.error(self._make_json_log(packet, action='compile-error', info='unknown submission',
|
||||
log=packet['log'], finish=True, result='CE'))
|
||||
|
||||
def on_compile_message(self, packet):
|
||||
super(DjangoJudgeHandler, self).on_compile_message(packet)
|
||||
|
||||
if Submission.objects.filter(id=packet['submission-id']).update(error=packet['log']):
|
||||
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'compile-message'})
|
||||
json_log.info(self._make_json_log(packet, action='compile-message', log=packet['log']))
|
||||
else:
|
||||
logger.warning('Unknown submission: %s', packet['submission-id'])
|
||||
json_log.error(self._make_json_log(packet, action='compile-message', info='unknown submission',
|
||||
log=packet['log']))
|
||||
|
||||
def on_internal_error(self, packet):
|
||||
super(DjangoJudgeHandler, self).on_internal_error(packet)
|
||||
|
||||
id = packet['submission-id']
|
||||
if Submission.objects.filter(id=id).update(status='IE', result='IE', error=packet['message']):
|
||||
event.post('sub_%s' % Submission.get_id_secret(id), {'type': 'internal-error'})
|
||||
self._post_update_submission(id, 'internal-error', done=True)
|
||||
json_log.info(self._make_json_log(packet, action='internal-error', message=packet['message'],
|
||||
finish=True, result='IE'))
|
||||
else:
|
||||
logger.warning('Unknown submission: %s', id)
|
||||
json_log.error(self._make_json_log(packet, action='internal-error', info='unknown submission',
|
||||
message=packet['message'], finish=True, result='IE'))
|
||||
|
||||
def on_submission_terminated(self, packet):
|
||||
super(DjangoJudgeHandler, self).on_submission_terminated(packet)
|
||||
|
||||
if Submission.objects.filter(id=packet['submission-id']).update(status='AB', result='AB'):
|
||||
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'aborted-submission'})
|
||||
self._post_update_submission(packet['submission-id'], 'terminated', done=True)
|
||||
json_log.info(self._make_json_log(packet, action='aborted', finish=True, result='AB'))
|
||||
else:
|
||||
logger.warning('Unknown submission: %s', packet['submission-id'])
|
||||
json_log.error(self._make_json_log(packet, action='aborted', info='unknown submission',
|
||||
finish=True, result='AB'))
|
||||
|
||||
def on_batch_begin(self, packet):
|
||||
super(DjangoJudgeHandler, self).on_batch_begin(packet)
|
||||
json_log.info(self._make_json_log(packet, action='batch-begin', batch=self.batch_id))
|
||||
|
||||
def on_batch_end(self, packet):
|
||||
super(DjangoJudgeHandler, self).on_batch_end(packet)
|
||||
json_log.info(self._make_json_log(packet, action='batch-end', batch=self.batch_id))
|
||||
|
||||
def on_test_case(self, packet, max_feedback=SubmissionTestCase._meta.get_field('feedback').max_length):
|
||||
super(DjangoJudgeHandler, self).on_test_case(packet)
|
||||
id = packet['submission-id']
|
||||
updates = packet['cases']
|
||||
max_position = max(map(itemgetter('position'), updates))
|
||||
|
||||
if not Submission.objects.filter(id=id).update(current_testcase=max_position + 1):
|
||||
logger.warning('Unknown submission: %s', id)
|
||||
json_log.error(self._make_json_log(packet, action='test-case', info='unknown submission'))
|
||||
return
|
||||
|
||||
bulk_test_case_updates = []
|
||||
for result in updates:
|
||||
test_case = SubmissionTestCase(submission_id=id, case=result['position'])
|
||||
status = result['status']
|
||||
if status & 4:
|
||||
test_case.status = 'TLE'
|
||||
elif status & 8:
|
||||
test_case.status = 'MLE'
|
||||
elif status & 64:
|
||||
test_case.status = 'OLE'
|
||||
elif status & 2:
|
||||
test_case.status = 'RTE'
|
||||
elif status & 16:
|
||||
test_case.status = 'IR'
|
||||
elif status & 1:
|
||||
test_case.status = 'WA'
|
||||
elif status & 32:
|
||||
test_case.status = 'SC'
|
||||
else:
|
||||
test_case.status = 'AC'
|
||||
test_case.time = result['time']
|
||||
test_case.memory = result['memory']
|
||||
test_case.points = result['points']
|
||||
test_case.total = result['total-points']
|
||||
test_case.batch = self.batch_id if self.in_batch else None
|
||||
test_case.feedback = (result.get('feedback') or '')[:max_feedback]
|
||||
test_case.extended_feedback = result.get('extended-feedback') or ''
|
||||
test_case.output = result['output']
|
||||
bulk_test_case_updates.append(test_case)
|
||||
|
||||
json_log.info(self._make_json_log(
|
||||
packet, action='test-case', case=test_case.case, batch=test_case.batch,
|
||||
time=test_case.time, memory=test_case.memory, feedback=test_case.feedback,
|
||||
extended_feedback=test_case.extended_feedback, output=test_case.output,
|
||||
points=test_case.points, total=test_case.total, status=test_case.status,
|
||||
))
|
||||
|
||||
do_post = True
|
||||
|
||||
if id in self.update_counter:
|
||||
cnt, reset = self.update_counter[id]
|
||||
cnt += 1
|
||||
if time.monotonic() - reset > UPDATE_RATE_TIME:
|
||||
del self.update_counter[id]
|
||||
else:
|
||||
self.update_counter[id] = (cnt, reset)
|
||||
if cnt > UPDATE_RATE_LIMIT:
|
||||
do_post = False
|
||||
if id not in self.update_counter:
|
||||
self.update_counter[id] = (1, time.monotonic())
|
||||
|
||||
if do_post:
|
||||
event.post('sub_%s' % Submission.get_id_secret(id), {
|
||||
'type': 'test-case',
|
||||
'id': max_position,
|
||||
})
|
||||
self._post_update_submission(id, state='test-case')
|
||||
|
||||
SubmissionTestCase.objects.bulk_create(bulk_test_case_updates)
|
||||
|
||||
def on_supported_problems(self, packet):
|
||||
super(DjangoJudgeHandler, self).on_supported_problems(packet)
|
||||
self.judge.problems.set(Problem.objects.filter(code__in=list(self.problems.keys())))
|
||||
json_log.info(self._make_json_log(action='update-problems', count=len(self.problems)))
|
||||
|
||||
def _make_json_log(self, packet=None, sub=None, **kwargs):
|
||||
data = {
|
||||
'judge': self.name,
|
||||
'address': self.judge_address,
|
||||
}
|
||||
if sub is None and packet is not None:
|
||||
sub = packet.get('submission-id')
|
||||
if sub is not None:
|
||||
data['submission'] = sub
|
||||
data.update(kwargs)
|
||||
return json.dumps(data)
|
268
judge/bridge/judgehandler.py
Normal file
268
judge/bridge/judgehandler.py
Normal file
|
@ -0,0 +1,268 @@
|
|||
import json
|
||||
import logging
|
||||
import time
|
||||
from collections import deque, namedtuple
|
||||
|
||||
from event_socket_server import ProxyProtocolMixin, ZlibPacketHandler
|
||||
|
||||
logger = logging.getLogger('judge.bridge')
|
||||
|
||||
SubmissionData = namedtuple('SubmissionData', 'time memory short_circuit pretests_only contest_no attempt_no user_id')
|
||||
|
||||
|
||||
class JudgeHandler(ProxyProtocolMixin, ZlibPacketHandler):
|
||||
def __init__(self, server, socket):
|
||||
super(JudgeHandler, self).__init__(server, socket)
|
||||
|
||||
self.handlers = {
|
||||
'grading-begin': self.on_grading_begin,
|
||||
'grading-end': self.on_grading_end,
|
||||
'compile-error': self.on_compile_error,
|
||||
'compile-message': self.on_compile_message,
|
||||
'batch-begin': self.on_batch_begin,
|
||||
'batch-end': self.on_batch_end,
|
||||
'test-case-status': self.on_test_case,
|
||||
'internal-error': self.on_internal_error,
|
||||
'submission-terminated': self.on_submission_terminated,
|
||||
'submission-acknowledged': self.on_submission_acknowledged,
|
||||
'ping-response': self.on_ping_response,
|
||||
'supported-problems': self.on_supported_problems,
|
||||
'handshake': self.on_handshake,
|
||||
}
|
||||
self._to_kill = True
|
||||
self._working = False
|
||||
self._no_response_job = None
|
||||
self._problems = []
|
||||
self.executors = []
|
||||
self.problems = {}
|
||||
self.latency = None
|
||||
self.time_delta = None
|
||||
self.load = 1e100
|
||||
self.name = None
|
||||
self.batch_id = None
|
||||
self.in_batch = False
|
||||
self._ping_average = deque(maxlen=6) # 1 minute average, just like load
|
||||
self._time_delta = deque(maxlen=6)
|
||||
|
||||
self.server.schedule(15, self._kill_if_no_auth)
|
||||
logger.info('Judge connected from: %s', self.client_address)
|
||||
|
||||
def _kill_if_no_auth(self):
|
||||
if self._to_kill:
|
||||
logger.info('Judge not authenticated: %s', self.client_address)
|
||||
self.close()
|
||||
|
||||
def on_close(self):
|
||||
self._to_kill = False
|
||||
if self._no_response_job:
|
||||
self.server.unschedule(self._no_response_job)
|
||||
self.server.judges.remove(self)
|
||||
if self.name is not None:
|
||||
self._disconnected()
|
||||
logger.info('Judge disconnected from: %s', self.client_address)
|
||||
|
||||
def _authenticate(self, id, key):
|
||||
return False
|
||||
|
||||
def _connected(self):
|
||||
pass
|
||||
|
||||
def _disconnected(self):
|
||||
pass
|
||||
|
||||
def _update_ping(self):
|
||||
pass
|
||||
|
||||
def _format_send(self, data):
|
||||
return super(JudgeHandler, self)._format_send(json.dumps(data, separators=(',', ':')))
|
||||
|
||||
def on_handshake(self, packet):
|
||||
if 'id' not in packet or 'key' not in packet:
|
||||
logger.warning('Malformed handshake: %s', self.client_address)
|
||||
self.close()
|
||||
return
|
||||
|
||||
if not self._authenticate(packet['id'], packet['key']):
|
||||
logger.warning('Authentication failure: %s', self.client_address)
|
||||
self.close()
|
||||
return
|
||||
|
||||
self._to_kill = False
|
||||
self._problems = packet['problems']
|
||||
self.problems = dict(self._problems)
|
||||
self.executors = packet['executors']
|
||||
self.name = packet['id']
|
||||
|
||||
self.send({'name': 'handshake-success'})
|
||||
logger.info('Judge authenticated: %s (%s)', self.client_address, packet['id'])
|
||||
self.server.judges.register(self)
|
||||
self._connected()
|
||||
|
||||
def can_judge(self, problem, executor):
|
||||
return problem in self.problems and executor in self.executors
|
||||
|
||||
@property
|
||||
def working(self):
|
||||
return bool(self._working)
|
||||
|
||||
def get_related_submission_data(self, submission):
|
||||
return SubmissionData(
|
||||
time=2,
|
||||
memory=16384,
|
||||
short_circuit=False,
|
||||
pretests_only=False,
|
||||
contest_no=None,
|
||||
attempt_no=1,
|
||||
user_id=None,
|
||||
)
|
||||
|
||||
def disconnect(self, force=False):
|
||||
if force:
|
||||
# Yank the power out.
|
||||
self.close()
|
||||
else:
|
||||
self.send({'name': 'disconnect'})
|
||||
|
||||
def submit(self, id, problem, language, source):
|
||||
data = self.get_related_submission_data(id)
|
||||
self._working = id
|
||||
self._no_response_job = self.server.schedule(20, self._kill_if_no_response)
|
||||
self.send({
|
||||
'name': 'submission-request',
|
||||
'submission-id': id,
|
||||
'problem-id': problem,
|
||||
'language': language,
|
||||
'source': source,
|
||||
'time-limit': data.time,
|
||||
'memory-limit': data.memory,
|
||||
'short-circuit': data.short_circuit,
|
||||
'meta': {
|
||||
'pretests-only': data.pretests_only,
|
||||
'in-contest': data.contest_no,
|
||||
'attempt-no': data.attempt_no,
|
||||
'user': data.user_id,
|
||||
},
|
||||
})
|
||||
|
||||
def _kill_if_no_response(self):
|
||||
logger.error('Judge seems dead: %s: %s', self.name, self._working)
|
||||
self.close()
|
||||
|
||||
def malformed_packet(self, exception):
|
||||
logger.exception('Judge sent malformed packet: %s', self.name)
|
||||
super(JudgeHandler, self).malformed_packet(exception)
|
||||
|
||||
def on_submission_processing(self, packet):
|
||||
pass
|
||||
|
||||
def on_submission_wrong_acknowledge(self, packet, expected, got):
|
||||
pass
|
||||
|
||||
def on_submission_acknowledged(self, packet):
|
||||
if not packet.get('submission-id', None) == self._working:
|
||||
logger.error('Wrong acknowledgement: %s: %s, expected: %s', self.name, packet.get('submission-id', None),
|
||||
self._working)
|
||||
self.on_submission_wrong_acknowledge(packet, self._working, packet.get('submission-id', None))
|
||||
self.close()
|
||||
logger.info('Submission acknowledged: %d', self._working)
|
||||
if self._no_response_job:
|
||||
self.server.unschedule(self._no_response_job)
|
||||
self._no_response_job = None
|
||||
self.on_submission_processing(packet)
|
||||
|
||||
def abort(self):
|
||||
self.send({'name': 'terminate-submission'})
|
||||
|
||||
def get_current_submission(self):
|
||||
return self._working or None
|
||||
|
||||
def ping(self):
|
||||
self.send({'name': 'ping', 'when': time.time()})
|
||||
|
||||
def packet(self, data):
|
||||
try:
|
||||
try:
|
||||
data = json.loads(data)
|
||||
if 'name' not in data:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
self.on_malformed(data)
|
||||
else:
|
||||
handler = self.handlers.get(data['name'], self.on_malformed)
|
||||
handler(data)
|
||||
except Exception:
|
||||
logger.exception('Error in packet handling (Judge-side): %s', self.name)
|
||||
self._packet_exception()
|
||||
# You can't crash here because you aren't so sure about the judges
|
||||
# not being malicious or simply malforms. THIS IS A SERVER!
|
||||
|
||||
def _packet_exception(self):
|
||||
pass
|
||||
|
||||
def _submission_is_batch(self, id):
|
||||
pass
|
||||
|
||||
def on_supported_problems(self, packet):
|
||||
logger.info('%s: Updated problem list', self.name)
|
||||
self._problems = packet['problems']
|
||||
self.problems = dict(self._problems)
|
||||
if not self.working:
|
||||
self.server.judges.update_problems(self)
|
||||
|
||||
def on_grading_begin(self, packet):
|
||||
logger.info('%s: Grading has begun on: %s', self.name, packet['submission-id'])
|
||||
self.batch_id = None
|
||||
|
||||
def on_grading_end(self, packet):
|
||||
logger.info('%s: Grading has ended on: %s', self.name, packet['submission-id'])
|
||||
self._free_self(packet)
|
||||
self.batch_id = None
|
||||
|
||||
def on_compile_error(self, packet):
|
||||
logger.info('%s: Submission failed to compile: %s', self.name, packet['submission-id'])
|
||||
self._free_self(packet)
|
||||
|
||||
def on_compile_message(self, packet):
|
||||
logger.info('%s: Submission generated compiler messages: %s', self.name, packet['submission-id'])
|
||||
|
||||
def on_internal_error(self, packet):
|
||||
try:
|
||||
raise ValueError('\n\n' + packet['message'])
|
||||
except ValueError:
|
||||
logger.exception('Judge %s failed while handling submission %s', self.name, packet['submission-id'])
|
||||
self._free_self(packet)
|
||||
|
||||
def on_submission_terminated(self, packet):
|
||||
logger.info('%s: Submission aborted: %s', self.name, packet['submission-id'])
|
||||
self._free_self(packet)
|
||||
|
||||
def on_batch_begin(self, packet):
|
||||
logger.info('%s: Batch began on: %s', self.name, packet['submission-id'])
|
||||
self.in_batch = True
|
||||
if self.batch_id is None:
|
||||
self.batch_id = 0
|
||||
self._submission_is_batch(packet['submission-id'])
|
||||
self.batch_id += 1
|
||||
|
||||
def on_batch_end(self, packet):
|
||||
self.in_batch = False
|
||||
logger.info('%s: Batch ended on: %s', self.name, packet['submission-id'])
|
||||
|
||||
def on_test_case(self, packet):
|
||||
logger.info('%s: %d test case(s) completed on: %s', self.name, len(packet['cases']), packet['submission-id'])
|
||||
|
||||
def on_malformed(self, packet):
|
||||
logger.error('%s: Malformed packet: %s', self.name, packet)
|
||||
|
||||
def on_ping_response(self, packet):
|
||||
end = time.time()
|
||||
self._ping_average.append(end - packet['when'])
|
||||
self._time_delta.append((end + packet['when']) / 2 - packet['time'])
|
||||
self.latency = sum(self._ping_average) / len(self._ping_average)
|
||||
self.time_delta = sum(self._time_delta) / len(self._time_delta)
|
||||
self.load = packet['load']
|
||||
self._update_ping()
|
||||
|
||||
def _free_self(self, packet):
|
||||
self._working = False
|
||||
self.server.judges.on_judge_free(self, packet['submission-id'])
|
123
judge/bridge/judgelist.py
Normal file
123
judge/bridge/judgelist.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
import logging
|
||||
from collections import namedtuple
|
||||
from operator import attrgetter
|
||||
from threading import RLock
|
||||
|
||||
try:
|
||||
from llist import dllist
|
||||
except ImportError:
|
||||
from pyllist import dllist
|
||||
|
||||
logger = logging.getLogger('judge.bridge')
|
||||
|
||||
PriorityMarker = namedtuple('PriorityMarker', 'priority')
|
||||
|
||||
|
||||
class JudgeList(object):
|
||||
priorities = 4
|
||||
|
||||
def __init__(self):
|
||||
self.queue = dllist()
|
||||
self.priority = [self.queue.append(PriorityMarker(i)) for i in range(self.priorities)]
|
||||
self.judges = set()
|
||||
self.node_map = {}
|
||||
self.submission_map = {}
|
||||
self.lock = RLock()
|
||||
|
||||
def _handle_free_judge(self, judge):
|
||||
with self.lock:
|
||||
node = self.queue.first
|
||||
while node:
|
||||
if not isinstance(node.value, PriorityMarker):
|
||||
id, problem, language, source = node.value
|
||||
if judge.can_judge(problem, language):
|
||||
self.submission_map[id] = judge
|
||||
logger.info('Dispatched queued submission %d: %s', id, judge.name)
|
||||
try:
|
||||
judge.submit(id, problem, language, source)
|
||||
except Exception:
|
||||
logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name)
|
||||
self.judges.remove(judge)
|
||||
return
|
||||
self.queue.remove(node)
|
||||
del self.node_map[id]
|
||||
break
|
||||
node = node.next
|
||||
|
||||
def register(self, judge):
|
||||
with self.lock:
|
||||
# Disconnect all judges with the same name, see <https://github.com/DMOJ/online-judge/issues/828>
|
||||
self.disconnect(judge, force=True)
|
||||
self.judges.add(judge)
|
||||
self._handle_free_judge(judge)
|
||||
|
||||
def disconnect(self, judge_id, force=False):
|
||||
for judge in self.judges:
|
||||
if judge.name == judge_id:
|
||||
judge.disconnect(force=force)
|
||||
|
||||
def update_problems(self, judge):
|
||||
with self.lock:
|
||||
self._handle_free_judge(judge)
|
||||
|
||||
def remove(self, judge):
|
||||
with self.lock:
|
||||
sub = judge.get_current_submission()
|
||||
if sub is not None:
|
||||
try:
|
||||
del self.submission_map[sub]
|
||||
except KeyError:
|
||||
pass
|
||||
self.judges.discard(judge)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.judges)
|
||||
|
||||
def on_judge_free(self, judge, submission):
|
||||
with self.lock:
|
||||
logger.info('Judge available after grading %d: %s', submission, judge.name)
|
||||
del self.submission_map[submission]
|
||||
self._handle_free_judge(judge)
|
||||
|
||||
def abort(self, submission):
|
||||
with self.lock:
|
||||
logger.info('Abort request: %d', submission)
|
||||
try:
|
||||
self.submission_map[submission].abort()
|
||||
return True
|
||||
except KeyError:
|
||||
try:
|
||||
node = self.node_map[submission]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self.queue.remove(node)
|
||||
del self.node_map[submission]
|
||||
return False
|
||||
|
||||
def check_priority(self, priority):
|
||||
return 0 <= priority < self.priorities
|
||||
|
||||
def judge(self, id, problem, language, source, priority):
|
||||
with self.lock:
|
||||
if id in self.submission_map or id in self.node_map:
|
||||
# Already judging, don't queue again. This can happen during batch rejudges, rejudges should be
|
||||
# idempotent.
|
||||
return
|
||||
|
||||
candidates = [judge for judge in self.judges if not judge.working and judge.can_judge(problem, language)]
|
||||
logger.info('Free judges: %d', len(candidates))
|
||||
if candidates:
|
||||
# Schedule the submission on the judge reporting least load.
|
||||
judge = min(candidates, key=attrgetter('load'))
|
||||
logger.info('Dispatched submission %d to: %s', id, judge.name)
|
||||
self.submission_map[id] = judge
|
||||
try:
|
||||
judge.submit(id, problem, language, source)
|
||||
except Exception:
|
||||
logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name)
|
||||
self.judges.discard(judge)
|
||||
return self.judge(id, problem, language, source, priority)
|
||||
else:
|
||||
self.node_map[id] = self.queue.insert((id, problem, language, source), self.priority[priority])
|
||||
logger.info('Queued submission: %d', id)
|
68
judge/bridge/judgeserver.py
Normal file
68
judge/bridge/judgeserver.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
from event_socket_server import get_preferred_engine
|
||||
from judge.models import Judge
|
||||
from .judgelist import JudgeList
|
||||
|
||||
logger = logging.getLogger('judge.bridge')
|
||||
|
||||
|
||||
def reset_judges():
|
||||
Judge.objects.update(online=False, ping=None, load=None)
|
||||
|
||||
|
||||
class JudgeServer(get_preferred_engine()):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(JudgeServer, self).__init__(*args, **kwargs)
|
||||
reset_judges()
|
||||
self.judges = JudgeList()
|
||||
self.ping_judge_thread = threading.Thread(target=self.ping_judge, args=())
|
||||
self.ping_judge_thread.daemon = True
|
||||
self.ping_judge_thread.start()
|
||||
|
||||
def on_shutdown(self):
|
||||
super(JudgeServer, self).on_shutdown()
|
||||
reset_judges()
|
||||
|
||||
def ping_judge(self):
|
||||
try:
|
||||
while True:
|
||||
for judge in self.judges:
|
||||
judge.ping()
|
||||
time.sleep(10)
|
||||
except Exception:
|
||||
logger.exception('Ping error')
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
import logging
|
||||
from .judgehandler import JudgeHandler
|
||||
|
||||
format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s'
|
||||
logging.basicConfig(format=format)
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
handler = logging.FileHandler(os.path.join(os.path.dirname(__file__), 'judgeserver.log'), encoding='utf-8')
|
||||
handler.setFormatter(logging.Formatter(format))
|
||||
handler.setLevel(logging.INFO)
|
||||
logging.getLogger().addHandler(handler)
|
||||
|
||||
parser = argparse.ArgumentParser(description='''
|
||||
Runs the bridge between DMOJ website and judges.
|
||||
''')
|
||||
parser.add_argument('judge_host', nargs='+', action='append',
|
||||
help='host to listen for the judge')
|
||||
parser.add_argument('-p', '--judge-port', type=int, action='append',
|
||||
help='port to listen for the judge')
|
||||
|
||||
args = parser.parse_args()
|
||||
server = JudgeServer(list(zip(args.judge_host, args.judge_port)), JudgeHandler)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
10
judge/caching.py
Normal file
10
judge/caching.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from django.core.cache import cache
|
||||
|
||||
|
||||
def finished_submission(sub):
|
||||
keys = ['user_complete:%d' % sub.user_id, 'user_attempted:%s' % sub.user_id]
|
||||
if hasattr(sub, 'contest'):
|
||||
participation = sub.contest.participation
|
||||
keys += ['contest_complete:%d' % participation.id]
|
||||
keys += ['contest_attempted:%d' % participation.id]
|
||||
cache.delete_many(keys)
|
122
judge/comments.py
Normal file
122
judge/comments.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Count
|
||||
from django.db.models.expressions import F, Value
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.forms import ModelForm
|
||||
from django.http import HttpResponseForbidden, HttpResponseNotFound, HttpResponseRedirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import View
|
||||
from django.views.generic.base import TemplateResponseMixin
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from reversion import revisions
|
||||
from reversion.models import Revision, Version
|
||||
|
||||
from judge.dblock import LockModel
|
||||
from judge.models import Comment, CommentLock, CommentVote
|
||||
from judge.utils.raw_sql import RawSQLColumn, unique_together_left_join
|
||||
from judge.widgets import HeavyPreviewPageDownWidget
|
||||
|
||||
|
||||
class CommentForm(ModelForm):
|
||||
class Meta:
|
||||
model = Comment
|
||||
fields = ['body', 'parent']
|
||||
widgets = {
|
||||
'parent': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
if HeavyPreviewPageDownWidget is not None:
|
||||
widgets['body'] = HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'),
|
||||
preview_timeout=1000, hide_preview_button=True)
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
self.request = request
|
||||
super(CommentForm, self).__init__(*args, **kwargs)
|
||||
self.fields['body'].widget.attrs.update({'placeholder': _('Comment body')})
|
||||
|
||||
def clean(self):
|
||||
if self.request is not None and self.request.user.is_authenticated:
|
||||
profile = self.request.profile
|
||||
if profile.mute:
|
||||
raise ValidationError(_('Your part is silent, little toad.'))
|
||||
elif (not self.request.user.is_staff and
|
||||
not profile.submission_set.filter(points=F('problem__points')).exists()):
|
||||
raise ValidationError(_('You need to have solved at least one problem '
|
||||
'before your voice can be heard.'))
|
||||
return super(CommentForm, self).clean()
|
||||
|
||||
|
||||
class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
|
||||
comment_page = None
|
||||
|
||||
def get_comment_page(self):
|
||||
if self.comment_page is None:
|
||||
raise NotImplementedError()
|
||||
return self.comment_page
|
||||
|
||||
def is_comment_locked(self):
|
||||
return (CommentLock.objects.filter(page=self.get_comment_page()).exists() and
|
||||
not self.request.user.has_perm('judge.override_comment_lock'))
|
||||
|
||||
@method_decorator(login_required)
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
page = self.get_comment_page()
|
||||
|
||||
if self.is_comment_locked():
|
||||
return HttpResponseForbidden()
|
||||
|
||||
parent = request.POST.get('parent')
|
||||
if parent:
|
||||
try:
|
||||
parent = int(parent)
|
||||
except ValueError:
|
||||
return HttpResponseNotFound()
|
||||
else:
|
||||
if not Comment.objects.filter(hidden=False, id=parent, page=page).exists():
|
||||
return HttpResponseNotFound()
|
||||
|
||||
form = CommentForm(request, request.POST)
|
||||
if form.is_valid():
|
||||
comment = form.save(commit=False)
|
||||
comment.author = request.profile
|
||||
comment.page = page
|
||||
with LockModel(write=(Comment, Revision, Version), read=(ContentType,)), revisions.create_revision():
|
||||
revisions.set_user(request.user)
|
||||
revisions.set_comment(_('Posted comment'))
|
||||
comment.save()
|
||||
return HttpResponseRedirect(request.path)
|
||||
|
||||
context = self.get_context_data(object=self.object, comment_form=form)
|
||||
return self.render_to_response(context)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
return self.render_to_response(self.get_context_data(
|
||||
object=self.object,
|
||||
comment_form=CommentForm(request, initial={'page': self.get_comment_page(), 'parent': None}),
|
||||
))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(CommentedDetailView, self).get_context_data(**kwargs)
|
||||
queryset = Comment.objects.filter(hidden=False, page=self.get_comment_page())
|
||||
context['has_comments'] = queryset.exists()
|
||||
context['comment_lock'] = self.is_comment_locked()
|
||||
queryset = queryset.select_related('author__user').defer('author__about').annotate(revisions=Count('versions'))
|
||||
|
||||
if self.request.user.is_authenticated:
|
||||
queryset = queryset.annotate(vote_score=Coalesce(RawSQLColumn(CommentVote, 'score'), Value(0)))
|
||||
profile = self.request.profile
|
||||
unique_together_left_join(queryset, CommentVote, 'comment', 'voter', profile.id)
|
||||
context['is_new_user'] = (not self.request.user.is_staff and
|
||||
not profile.submission_set.filter(points=F('problem__points')).exists())
|
||||
context['comment_list'] = queryset
|
||||
context['vote_hide_threshold'] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD
|
||||
|
||||
return context
|
5
judge/contest_format/__init__.py
Normal file
5
judge/contest_format/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from judge.contest_format.atcoder import AtCoderContestFormat
|
||||
from judge.contest_format.default import DefaultContestFormat
|
||||
from judge.contest_format.ecoo import ECOOContestFormat
|
||||
from judge.contest_format.ioi import IOIContestFormat
|
||||
from judge.contest_format.registry import choices, formats
|
113
judge/contest_format/atcoder.py
Normal file
113
judge/contest_format/atcoder.py
Normal file
|
@ -0,0 +1,113 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import connection
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from judge.contest_format.default import DefaultContestFormat
|
||||
from judge.contest_format.registry import register_contest_format
|
||||
from judge.timezone import from_database_time
|
||||
from judge.utils.timedelta import nice_repr
|
||||
|
||||
|
||||
@register_contest_format('atcoder')
|
||||
class AtCoderContestFormat(DefaultContestFormat):
|
||||
name = gettext_lazy('AtCoder')
|
||||
config_defaults = {'penalty': 5}
|
||||
config_validators = {'penalty': lambda x: x >= 0}
|
||||
'''
|
||||
penalty: Number of penalty minutes each incorrect submission adds. Defaults to 5.
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def validate(cls, config):
|
||||
if config is None:
|
||||
return
|
||||
|
||||
if not isinstance(config, dict):
|
||||
raise ValidationError('AtCoder-styled contest expects no config or dict as config')
|
||||
|
||||
for key, value in config.items():
|
||||
if key not in cls.config_defaults:
|
||||
raise ValidationError('unknown config key "%s"' % key)
|
||||
if not isinstance(value, type(cls.config_defaults[key])):
|
||||
raise ValidationError('invalid type for config key "%s"' % key)
|
||||
if not cls.config_validators[key](value):
|
||||
raise ValidationError('invalid value "%s" for config key "%s"' % (value, key))
|
||||
|
||||
def __init__(self, contest, config):
|
||||
self.config = self.config_defaults.copy()
|
||||
self.config.update(config or {})
|
||||
self.contest = contest
|
||||
|
||||
def update_participation(self, participation):
|
||||
cumtime = 0
|
||||
penalty = 0
|
||||
points = 0
|
||||
format_data = {}
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('''
|
||||
SELECT MAX(cs.points) as `score`, (
|
||||
SELECT MIN(csub.date)
|
||||
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
||||
judge_submission csub ON (csub.id = ccs.submission_id)
|
||||
WHERE ccs.problem_id = cp.id AND ccs.participation_id = %s AND ccs.points = MAX(cs.points)
|
||||
) AS `time`, cp.id AS `prob`
|
||||
FROM judge_contestproblem cp INNER JOIN
|
||||
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
||||
judge_submission sub ON (sub.id = cs.submission_id)
|
||||
GROUP BY cp.id
|
||||
''', (participation.id, participation.id))
|
||||
|
||||
for score, time, prob in cursor.fetchall():
|
||||
time = from_database_time(time)
|
||||
dt = (time - participation.start).total_seconds()
|
||||
|
||||
# Compute penalty
|
||||
if self.config['penalty']:
|
||||
# An IE can have a submission result of `None`
|
||||
subs = participation.submissions.exclude(submission__result__isnull=True) \
|
||||
.exclude(submission__result__in=['IE', 'CE']) \
|
||||
.filter(problem_id=prob)
|
||||
if score:
|
||||
prev = subs.filter(submission__date__lte=time).count() - 1
|
||||
penalty += prev * self.config['penalty'] * 60
|
||||
else:
|
||||
# We should always display the penalty, even if the user has a score of 0
|
||||
prev = subs.count()
|
||||
else:
|
||||
prev = 0
|
||||
|
||||
if score:
|
||||
cumtime = max(cumtime, dt)
|
||||
|
||||
format_data[str(prob)] = {'time': dt, 'points': score, 'penalty': prev}
|
||||
points += score
|
||||
|
||||
participation.cumtime = cumtime + penalty
|
||||
participation.score = points
|
||||
participation.format_data = format_data
|
||||
participation.save()
|
||||
|
||||
def display_user_problem(self, participation, contest_problem):
|
||||
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
||||
if format_data:
|
||||
penalty = format_html('<small style="color:red"> ({penalty})</small>',
|
||||
penalty=floatformat(format_data['penalty'])) if format_data['penalty'] else ''
|
||||
return format_html(
|
||||
'<td class="{state}"><a href="{url}">{points}{penalty}<div class="solving-time">{time}</div></a></td>',
|
||||
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
|
||||
self.best_solution_state(format_data['points'], contest_problem.points)),
|
||||
url=reverse('contest_user_submissions',
|
||||
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
|
||||
points=floatformat(format_data['points']),
|
||||
penalty=penalty,
|
||||
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'),
|
||||
)
|
||||
else:
|
||||
return mark_safe('<td></td>')
|
91
judge/contest_format/base.py
Normal file
91
judge/contest_format/base.py
Normal file
|
@ -0,0 +1,91 @@
|
|||
from abc import ABCMeta, abstractmethod, abstractproperty
|
||||
|
||||
from django.utils import six
|
||||
|
||||
|
||||
class abstractclassmethod(classmethod):
|
||||
__isabstractmethod__ = True
|
||||
|
||||
def __init__(self, callable):
|
||||
callable.__isabstractmethod__ = True
|
||||
super(abstractclassmethod, self).__init__(callable)
|
||||
|
||||
|
||||
class BaseContestFormat(six.with_metaclass(ABCMeta)):
|
||||
@abstractmethod
|
||||
def __init__(self, contest, config):
|
||||
self.config = config
|
||||
self.contest = contest
|
||||
|
||||
@abstractproperty
|
||||
def name(self):
|
||||
"""
|
||||
Name of this contest format. Should be invoked with gettext_lazy.
|
||||
|
||||
:return: str
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractclassmethod
|
||||
def validate(cls, config):
|
||||
"""
|
||||
Validates the contest format configuration.
|
||||
|
||||
:param config: A dictionary containing the configuration for this contest format.
|
||||
:return: None
|
||||
:raises: ValidationError
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def update_participation(self, participation):
|
||||
"""
|
||||
Updates a ContestParticipation object's score, cumtime, and format_data fields based on this contest format.
|
||||
Implementations should call ContestParticipation.save().
|
||||
|
||||
:param participation: A ContestParticipation object.
|
||||
:return: None
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def display_user_problem(self, participation, contest_problem):
|
||||
"""
|
||||
Returns the HTML fragment to show a user's performance on an individual problem. This is expected to use
|
||||
information from the format_data field instead of computing it from scratch.
|
||||
|
||||
:param participation: The ContestParticipation object linking the user to the contest.
|
||||
:param contest_problem: The ContestProblem object representing the problem in question.
|
||||
:return: An HTML fragment, marked as safe for Jinja2.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def display_participation_result(self, participation):
|
||||
"""
|
||||
Returns the HTML fragment to show a user's performance on the whole contest. This is expected to use
|
||||
information from the format_data field instead of computing it from scratch.
|
||||
|
||||
:param participation: The ContestParticipation object.
|
||||
:return: An HTML fragment, marked as safe for Jinja2.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def get_problem_breakdown(self, participation, contest_problems):
|
||||
"""
|
||||
Returns a machine-readable breakdown for the user's performance on every problem.
|
||||
|
||||
:param participation: The ContestParticipation object.
|
||||
:param contest_problems: The list of ContestProblem objects to display performance for.
|
||||
:return: A list of dictionaries, whose content is to be determined by the contest system.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def best_solution_state(cls, points, total):
|
||||
if not points:
|
||||
return 'failed-score'
|
||||
if points == total:
|
||||
return 'full-score'
|
||||
return 'partial-score'
|
70
judge/contest_format/default.py
Normal file
70
judge/contest_format/default.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Max
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from judge.contest_format.base import BaseContestFormat
|
||||
from judge.contest_format.registry import register_contest_format
|
||||
from judge.utils.timedelta import nice_repr
|
||||
|
||||
|
||||
@register_contest_format('default')
|
||||
class DefaultContestFormat(BaseContestFormat):
|
||||
name = gettext_lazy('Default')
|
||||
|
||||
@classmethod
|
||||
def validate(cls, config):
|
||||
if config is not None and (not isinstance(config, dict) or config):
|
||||
raise ValidationError('default contest expects no config or empty dict as config')
|
||||
|
||||
def __init__(self, contest, config):
|
||||
super(DefaultContestFormat, self).__init__(contest, config)
|
||||
|
||||
def update_participation(self, participation):
|
||||
cumtime = 0
|
||||
points = 0
|
||||
format_data = {}
|
||||
|
||||
for result in participation.submissions.values('problem_id').annotate(
|
||||
time=Max('submission__date'), points=Max('points'),
|
||||
):
|
||||
dt = (result['time'] - participation.start).total_seconds()
|
||||
if result['points']:
|
||||
cumtime += dt
|
||||
format_data[str(result['problem_id'])] = {'time': dt, 'points': result['points']}
|
||||
points += result['points']
|
||||
|
||||
participation.cumtime = max(cumtime, 0)
|
||||
participation.score = points
|
||||
participation.format_data = format_data
|
||||
participation.save()
|
||||
|
||||
def display_user_problem(self, participation, contest_problem):
|
||||
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
||||
if format_data:
|
||||
return format_html(
|
||||
u'<td class="{state}"><a href="{url}">{points}<div class="solving-time">{time}</div></a></td>',
|
||||
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
|
||||
self.best_solution_state(format_data['points'], contest_problem.points)),
|
||||
url=reverse('contest_user_submissions',
|
||||
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
|
||||
points=floatformat(format_data['points']),
|
||||
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'),
|
||||
)
|
||||
else:
|
||||
return mark_safe('<td></td>')
|
||||
|
||||
def display_participation_result(self, participation):
|
||||
return format_html(
|
||||
u'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
|
||||
points=floatformat(participation.score),
|
||||
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday'),
|
||||
)
|
||||
|
||||
def get_problem_breakdown(self, participation, contest_problems):
|
||||
return [(participation.format_data or {}).get(str(contest_problem.id)) for contest_problem in contest_problems]
|
122
judge/contest_format/ecoo.py
Normal file
122
judge/contest_format/ecoo.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import connection
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from judge.contest_format.default import DefaultContestFormat
|
||||
from judge.contest_format.registry import register_contest_format
|
||||
from judge.timezone import from_database_time
|
||||
from judge.utils.timedelta import nice_repr
|
||||
|
||||
|
||||
@register_contest_format('ecoo')
|
||||
class ECOOContestFormat(DefaultContestFormat):
|
||||
name = gettext_lazy('ECOO')
|
||||
config_defaults = {'cumtime': False, 'first_ac_bonus': 10, 'time_bonus': 5}
|
||||
config_validators = {'cumtime': lambda x: True, 'first_ac_bonus': lambda x: x >= 0, 'time_bonus': lambda x: x >= 0}
|
||||
'''
|
||||
cumtime: Specify True if cumulative time is to be used in breaking ties. Defaults to False.
|
||||
first_ac_bonus: The number of points to award if a solution gets AC on its first non-IE/CE run. Defaults to 10.
|
||||
time_bonus: Number of minutes to award an extra point for submitting before the contest end.
|
||||
Specify 0 to disable. Defaults to 5.
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def validate(cls, config):
|
||||
if config is None:
|
||||
return
|
||||
|
||||
if not isinstance(config, dict):
|
||||
raise ValidationError('ECOO-styled contest expects no config or dict as config')
|
||||
|
||||
for key, value in config.items():
|
||||
if key not in cls.config_defaults:
|
||||
raise ValidationError('unknown config key "%s"' % key)
|
||||
if not isinstance(value, type(cls.config_defaults[key])):
|
||||
raise ValidationError('invalid type for config key "%s"' % key)
|
||||
if not cls.config_validators[key](value):
|
||||
raise ValidationError('invalid value "%s" for config key "%s"' % (value, key))
|
||||
|
||||
def __init__(self, contest, config):
|
||||
self.config = self.config_defaults.copy()
|
||||
self.config.update(config or {})
|
||||
self.contest = contest
|
||||
|
||||
def update_participation(self, participation):
|
||||
cumtime = 0
|
||||
points = 0
|
||||
format_data = {}
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('''
|
||||
SELECT (
|
||||
SELECT MAX(ccs.points)
|
||||
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 csub.date = MAX(sub.date)
|
||||
) AS `score`, MAX(sub.date) AS `time`, cp.id AS `prob`, (
|
||||
SELECT COUNT(ccs.id)
|
||||
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 csub.result NOT IN ('IE', 'CE')
|
||||
) AS `subs`, cp.points AS `max_score`
|
||||
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, participation.id))
|
||||
|
||||
for score, time, prob, subs, max_score in cursor.fetchall():
|
||||
time = from_database_time(time)
|
||||
dt = (time - participation.start).total_seconds()
|
||||
if self.config['cumtime']:
|
||||
cumtime += dt
|
||||
|
||||
bonus = 0
|
||||
if score > 0:
|
||||
# First AC bonus
|
||||
if subs == 1 and score == max_score:
|
||||
bonus += self.config['first_ac_bonus']
|
||||
# Time bonus
|
||||
if self.config['time_bonus']:
|
||||
bonus += (participation.end_time - time).total_seconds() // 60 // self.config['time_bonus']
|
||||
points += bonus
|
||||
|
||||
format_data[str(prob)] = {'time': dt, 'points': score, 'bonus': bonus}
|
||||
points += score
|
||||
|
||||
participation.cumtime = cumtime
|
||||
participation.score = points
|
||||
participation.format_data = format_data
|
||||
participation.save()
|
||||
|
||||
def display_user_problem(self, participation, contest_problem):
|
||||
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
||||
if format_data:
|
||||
bonus = format_html('<small> +{bonus}</small>',
|
||||
bonus=floatformat(format_data['bonus'])) if format_data['bonus'] else ''
|
||||
|
||||
return format_html(
|
||||
'<td class="{state}"><a href="{url}">{points}{bonus}<div class="solving-time">{time}</div></a></td>',
|
||||
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
|
||||
self.best_solution_state(format_data['points'], contest_problem.points)),
|
||||
url=reverse('contest_user_submissions',
|
||||
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
|
||||
points=floatformat(format_data['points']),
|
||||
bonus=bonus,
|
||||
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'),
|
||||
)
|
||||
else:
|
||||
return mark_safe('<td></td>')
|
||||
|
||||
def display_participation_result(self, participation):
|
||||
return format_html(
|
||||
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
|
||||
points=floatformat(participation.score),
|
||||
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') if self.config['cumtime'] else '',
|
||||
)
|
99
judge/contest_format/ioi.py
Normal file
99
judge/contest_format/ioi.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import connection
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from judge.contest_format.default import DefaultContestFormat
|
||||
from judge.contest_format.registry import register_contest_format
|
||||
from judge.timezone import from_database_time
|
||||
from judge.utils.timedelta import nice_repr
|
||||
|
||||
|
||||
@register_contest_format('ioi')
|
||||
class IOIContestFormat(DefaultContestFormat):
|
||||
name = gettext_lazy('IOI')
|
||||
config_defaults = {'cumtime': False}
|
||||
'''
|
||||
cumtime: Specify True if time penalties are to be computed. Defaults to False.
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def validate(cls, config):
|
||||
if config is None:
|
||||
return
|
||||
|
||||
if not isinstance(config, dict):
|
||||
raise ValidationError('IOI-styled contest expects no config or dict as config')
|
||||
|
||||
for key, value in config.items():
|
||||
if key not in cls.config_defaults:
|
||||
raise ValidationError('unknown config key "%s"' % key)
|
||||
if not isinstance(value, type(cls.config_defaults[key])):
|
||||
raise ValidationError('invalid type for config key "%s"' % key)
|
||||
|
||||
def __init__(self, contest, config):
|
||||
self.config = self.config_defaults.copy()
|
||||
self.config.update(config or {})
|
||||
self.contest = contest
|
||||
|
||||
def update_participation(self, participation):
|
||||
cumtime = 0
|
||||
points = 0
|
||||
format_data = {}
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('''
|
||||
SELECT MAX(cs.points) as `score`, (
|
||||
SELECT MIN(csub.date)
|
||||
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
||||
judge_submission csub ON (csub.id = ccs.submission_id)
|
||||
WHERE ccs.problem_id = cp.id AND ccs.participation_id = %s AND ccs.points = MAX(cs.points)
|
||||
) AS `time`, cp.id AS `prob`
|
||||
FROM judge_contestproblem cp INNER JOIN
|
||||
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
||||
judge_submission sub ON (sub.id = cs.submission_id)
|
||||
GROUP BY cp.id
|
||||
''', (participation.id, participation.id))
|
||||
|
||||
for score, time, prob in cursor.fetchall():
|
||||
if self.config['cumtime']:
|
||||
dt = (from_database_time(time) - participation.start).total_seconds()
|
||||
if score:
|
||||
cumtime += dt
|
||||
else:
|
||||
dt = 0
|
||||
|
||||
format_data[str(prob)] = {'time': dt, 'points': score}
|
||||
points += score
|
||||
|
||||
participation.cumtime = max(cumtime, 0)
|
||||
participation.score = points
|
||||
participation.format_data = format_data
|
||||
participation.save()
|
||||
|
||||
def display_user_problem(self, participation, contest_problem):
|
||||
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
||||
if format_data:
|
||||
return format_html(
|
||||
'<td class="{state}"><a href="{url}">{points}<div class="solving-time">{time}</div></a></td>',
|
||||
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
|
||||
self.best_solution_state(format_data['points'], contest_problem.points)),
|
||||
url=reverse('contest_user_submissions',
|
||||
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
|
||||
points=floatformat(format_data['points']),
|
||||
time=nice_repr(timedelta(seconds=format_data['time']), 'noday') if self.config['cumtime'] else '',
|
||||
)
|
||||
else:
|
||||
return mark_safe('<td></td>')
|
||||
|
||||
def display_participation_result(self, participation):
|
||||
return format_html(
|
||||
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
|
||||
points=floatformat(participation.score),
|
||||
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') if self.config['cumtime'] else '',
|
||||
)
|
16
judge/contest_format/registry.py
Normal file
16
judge/contest_format/registry.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from django.utils import six
|
||||
|
||||
formats = {}
|
||||
|
||||
|
||||
def register_contest_format(name):
|
||||
def register_class(contest_format_class):
|
||||
assert name not in formats
|
||||
formats[name] = contest_format_class
|
||||
return contest_format_class
|
||||
|
||||
return register_class
|
||||
|
||||
|
||||
def choices():
|
||||
return [(key, value.name) for key, value in sorted(six.iteritems(formats))]
|
23
judge/dblock.py
Normal file
23
judge/dblock.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
from itertools import chain
|
||||
|
||||
from django.db import connection, transaction
|
||||
|
||||
|
||||
class LockModel(object):
|
||||
def __init__(self, write, read=()):
|
||||
self.tables = ', '.join(chain(
|
||||
('`%s` WRITE' % model._meta.db_table for model in write),
|
||||
('`%s` READ' % model._meta.db_table for model in read),
|
||||
))
|
||||
self.cursor = connection.cursor()
|
||||
|
||||
def __enter__(self):
|
||||
self.cursor.execute('LOCK TABLES ' + self.tables)
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if exc_type is None:
|
||||
transaction.commit()
|
||||
else:
|
||||
transaction.rollback()
|
||||
self.cursor.execute('UNLOCK TABLES')
|
||||
self.cursor.close()
|
18
judge/event_poster.py
Normal file
18
judge/event_poster.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from django.conf import settings
|
||||
|
||||
__all__ = ['last', 'post']
|
||||
|
||||
if not settings.EVENT_DAEMON_USE:
|
||||
real = False
|
||||
|
||||
def post(channel, message):
|
||||
return 0
|
||||
|
||||
def last():
|
||||
return 0
|
||||
elif hasattr(settings, 'EVENT_DAEMON_AMQP'):
|
||||
from .event_poster_amqp import last, post
|
||||
real = True
|
||||
else:
|
||||
from .event_poster_ws import last, post
|
||||
real = True
|
55
judge/event_poster_amqp.py
Normal file
55
judge/event_poster_amqp.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
import json
|
||||
import threading
|
||||
from time import time
|
||||
|
||||
import pika
|
||||
from django.conf import settings
|
||||
from pika.exceptions import AMQPError
|
||||
|
||||
__all__ = ['EventPoster', 'post', 'last']
|
||||
|
||||
|
||||
class EventPoster(object):
|
||||
def __init__(self):
|
||||
self._connect()
|
||||
self._exchange = settings.EVENT_DAEMON_AMQP_EXCHANGE
|
||||
|
||||
def _connect(self):
|
||||
self._conn = pika.BlockingConnection(pika.URLParameters(settings.EVENT_DAEMON_AMQP))
|
||||
self._chan = self._conn.channel()
|
||||
|
||||
def post(self, channel, message, tries=0):
|
||||
try:
|
||||
id = int(time() * 1000000)
|
||||
self._chan.basic_publish(self._exchange, '',
|
||||
json.dumps({'id': id, 'channel': channel, 'message': message}))
|
||||
return id
|
||||
except AMQPError:
|
||||
if tries > 10:
|
||||
raise
|
||||
self._connect()
|
||||
return self.post(channel, message, tries + 1)
|
||||
|
||||
|
||||
_local = threading.local()
|
||||
|
||||
|
||||
def _get_poster():
|
||||
if 'poster' not in _local.__dict__:
|
||||
_local.poster = EventPoster()
|
||||
return _local.poster
|
||||
|
||||
|
||||
def post(channel, message):
|
||||
try:
|
||||
return _get_poster().post(channel, message)
|
||||
except AMQPError:
|
||||
try:
|
||||
del _local.poster
|
||||
except AttributeError:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
def last():
|
||||
return int(time() * 1000000)
|
82
judge/event_poster_ws.py
Normal file
82
judge/event_poster_ws.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
import json
|
||||
import socket
|
||||
import threading
|
||||
|
||||
from django.conf import settings
|
||||
from websocket import WebSocketException, create_connection
|
||||
|
||||
__all__ = ['EventPostingError', 'EventPoster', 'post', 'last']
|
||||
_local = threading.local()
|
||||
|
||||
|
||||
class EventPostingError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class EventPoster(object):
|
||||
def __init__(self):
|
||||
self._connect()
|
||||
|
||||
def _connect(self):
|
||||
self._conn = create_connection(settings.EVENT_DAEMON_POST)
|
||||
if settings.EVENT_DAEMON_KEY is not None:
|
||||
self._conn.send(json.dumps({'command': 'auth', 'key': settings.EVENT_DAEMON_KEY}))
|
||||
resp = json.loads(self._conn.recv())
|
||||
if resp['status'] == 'error':
|
||||
raise EventPostingError(resp['code'])
|
||||
|
||||
def post(self, channel, message, tries=0):
|
||||
try:
|
||||
self._conn.send(json.dumps({'command': 'post', 'channel': channel, 'message': message}))
|
||||
resp = json.loads(self._conn.recv())
|
||||
if resp['status'] == 'error':
|
||||
raise EventPostingError(resp['code'])
|
||||
else:
|
||||
return resp['id']
|
||||
except WebSocketException:
|
||||
if tries > 10:
|
||||
raise
|
||||
self._connect()
|
||||
return self.post(channel, message, tries + 1)
|
||||
|
||||
def last(self, tries=0):
|
||||
try:
|
||||
self._conn.send('{"command": "last-msg"}')
|
||||
resp = json.loads(self._conn.recv())
|
||||
if resp['status'] == 'error':
|
||||
raise EventPostingError(resp['code'])
|
||||
else:
|
||||
return resp['id']
|
||||
except WebSocketException:
|
||||
if tries > 10:
|
||||
raise
|
||||
self._connect()
|
||||
return self.last(tries + 1)
|
||||
|
||||
|
||||
def _get_poster():
|
||||
if 'poster' not in _local.__dict__:
|
||||
_local.poster = EventPoster()
|
||||
return _local.poster
|
||||
|
||||
|
||||
def post(channel, message):
|
||||
try:
|
||||
return _get_poster().post(channel, message)
|
||||
except (WebSocketException, socket.error):
|
||||
try:
|
||||
del _local.poster
|
||||
except AttributeError:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
def last():
|
||||
try:
|
||||
return _get_poster().last()
|
||||
except (WebSocketException, socket.error):
|
||||
try:
|
||||
del _local.poster
|
||||
except AttributeError:
|
||||
pass
|
||||
return 0
|
99
judge/feed.py
Normal file
99
judge/feed.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
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
|
||||
|
||||
|
||||
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).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] + '...'
|
||||
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'))
|
||||
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'))
|
||||
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
|
173
judge/fixtures/demo.json
Normal file
173
judge/fixtures/demo.json
Normal file
|
@ -0,0 +1,173 @@
|
|||
[
|
||||
{
|
||||
"fields": {
|
||||
"about": "",
|
||||
"ace_theme": "github",
|
||||
"current_contest": null,
|
||||
"display_rank": "admin",
|
||||
"ip": "10.0.2.2",
|
||||
"language": 1,
|
||||
"last_access": "2017-12-02T08:57:10.093Z",
|
||||
"math_engine": "auto",
|
||||
"mute": false,
|
||||
"organizations": [
|
||||
1
|
||||
],
|
||||
"performance_points": 0.0,
|
||||
"points": 0.0,
|
||||
"problem_count": 0,
|
||||
"rating": null,
|
||||
"timezone": "America/Toronto",
|
||||
"user": 1,
|
||||
"user_script": ""
|
||||
},
|
||||
"model": "judge.profile",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"date_joined": "2017-12-02T08:34:17.408Z",
|
||||
"email": "",
|
||||
"first_name": "",
|
||||
"groups": [
|
||||
|
||||
],
|
||||
"is_active": true,
|
||||
"is_staff": true,
|
||||
"is_superuser": true,
|
||||
"last_login": "2017-12-02T08:34:31.840Z",
|
||||
"last_name": "",
|
||||
"password": "pbkdf2_sha256$36000$eFRRZq4DgktS$md1gk0bBJb7PH/+3YEXkcCW8K+KmiI+y/amqR32G2DY=",
|
||||
"user_permissions": [
|
||||
|
||||
],
|
||||
"username": "admin"
|
||||
},
|
||||
"model": "auth.user",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"about": "This is a sample organization. You can use organizations to split up your user base, host private contests, and more.",
|
||||
"access_code": null,
|
||||
"admins": [
|
||||
1
|
||||
],
|
||||
"creation_date": "2017-12-02T08:50:25.199Z",
|
||||
"is_open": true,
|
||||
"slug": "dmoj",
|
||||
"name": "DMOJ: Modern Online Judge",
|
||||
"registrant": 1,
|
||||
"short_name": "DMOJ",
|
||||
"slots": null
|
||||
},
|
||||
"model": "judge.organization",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"full_name": "Simple Math",
|
||||
"name": "Simple Math"
|
||||
},
|
||||
"model": "judge.problemtype",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"full_name": "Uncategorized",
|
||||
"name": "Uncategorized"
|
||||
},
|
||||
"model": "judge.problemgroup",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"ac_rate": 0.0,
|
||||
"allowed_languages": [
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
2,
|
||||
7,
|
||||
1,
|
||||
8
|
||||
],
|
||||
"authors": [
|
||||
1
|
||||
],
|
||||
"banned_users": [
|
||||
|
||||
],
|
||||
"code": "aplusb",
|
||||
"curators": [
|
||||
|
||||
],
|
||||
"date": "2017-12-02T05:00:00Z",
|
||||
"description": "Tudor is sitting in math class, on his laptop. Clearly, he is not paying attention in this situation. However, he gets called on by his math teacher to do some problems. Since his math teacher did not expect much from Tudor, he only needs to do some simple addition problems. However, simple for you and I may not be simple for Tudor , so please help him!\n\n## Input Specification\n\nThe first line will contain an integer ~N~ (~1 \\le N \\le 100\\,000~), the number of addition problems Tudor needs to do. The next ~N~ lines will each contain two space-separated integers whose absolute value is less than ~1\\,000\\,000\\,000~, the two integers Tudor needs to add.\n\n## Output Specification\n\nOutput ~N~ lines of one integer each, the solutions to the addition problems in order.\n\n## Sample Input\n\n 2\n 1 1\n -1 0\n\n## Sample Output\n\n 2\n -1",
|
||||
"group": 1,
|
||||
"is_manually_managed": false,
|
||||
"is_public": true,
|
||||
"license": null,
|
||||
"memory_limit": 65536,
|
||||
"name": "A Plus B",
|
||||
"og_image": "",
|
||||
"partial": true,
|
||||
"points": 5.0,
|
||||
"short_circuit": false,
|
||||
"summary": "",
|
||||
"testers": [
|
||||
|
||||
],
|
||||
"time_limit": 2.0,
|
||||
"types": [
|
||||
1
|
||||
],
|
||||
"user_count": 0
|
||||
},
|
||||
"model": "judge.problem",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"authors": [
|
||||
1
|
||||
],
|
||||
"content": "Welcome to DMOJ!\n\n```python\nprint \"Hello, World!\"\n```\n\nYou can get started by checking out [this problem we've added for you](/problem/aplusb).",
|
||||
"og_image": "",
|
||||
"publish_on": "2017-12-02T05:00:00Z",
|
||||
"slug": "first-post",
|
||||
"sticky": true,
|
||||
"summary": "",
|
||||
"title": "First Post",
|
||||
"visible": true
|
||||
},
|
||||
"model": "judge.blogpost",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"author": 1,
|
||||
"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",
|
||||
"pk": 1
|
||||
}
|
||||
]
|
122
judge/fixtures/language_small.json
Normal file
122
judge/fixtures/language_small.json
Normal file
|
@ -0,0 +1,122 @@
|
|||
[
|
||||
{
|
||||
"fields": {
|
||||
"ace": "python",
|
||||
"common_name": "Python",
|
||||
"description": "",
|
||||
"extension": "",
|
||||
"info": "python 2.7.9",
|
||||
"key": "PY2",
|
||||
"name": "Python 2",
|
||||
"pygments": "python",
|
||||
"short_name": ""
|
||||
},
|
||||
"model": "judge.language",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"ace": "assembly_x86",
|
||||
"common_name": "Assembly",
|
||||
"description": "",
|
||||
"extension": "",
|
||||
"info": "binutils 2.25",
|
||||
"key": "GAS64",
|
||||
"name": "Assembly (x64)",
|
||||
"pygments": "gas",
|
||||
"short_name": ""
|
||||
},
|
||||
"model": "judge.language",
|
||||
"pk": 2
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"ace": "AWK",
|
||||
"common_name": "Awk",
|
||||
"description": "",
|
||||
"extension": "",
|
||||
"info": "mawk 1.3.3",
|
||||
"key": "AWK",
|
||||
"name": "AWK",
|
||||
"pygments": "awk",
|
||||
"short_name": "AWK"
|
||||
},
|
||||
"model": "judge.language",
|
||||
"pk": 3
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"ace": "c_cpp",
|
||||
"common_name": "C",
|
||||
"description": "Compile options: `gcc -std=c99 -Wall -O2 -lm -march=native -s`\r\n",
|
||||
"extension": "",
|
||||
"info": "gcc 4.9.2",
|
||||
"key": "C",
|
||||
"name": "C",
|
||||
"pygments": "c",
|
||||
"short_name": ""
|
||||
},
|
||||
"model": "judge.language",
|
||||
"pk": 4
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"ace": "c_cpp",
|
||||
"common_name": "C++",
|
||||
"description": "Compile options: `g++ -Wall -O2 -lm -march=native -s`\r\n",
|
||||
"extension": "",
|
||||
"info": "g++ 4.9.2",
|
||||
"key": "CPP03",
|
||||
"name": "C++03",
|
||||
"pygments": "cpp",
|
||||
"short_name": "C++03"
|
||||
},
|
||||
"model": "judge.language",
|
||||
"pk": 5
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"ace": "c_cpp",
|
||||
"common_name": "C++",
|
||||
"description": "Compile options: `g++ -std=c++11 -Wall -O2 -lm -march=native -s`\r\n",
|
||||
"extension": "",
|
||||
"info": "g++-4.9.2 -std=c++11",
|
||||
"key": "CPP11",
|
||||
"name": "C++11",
|
||||
"pygments": "cpp",
|
||||
"short_name": "C++11"
|
||||
},
|
||||
"model": "judge.language",
|
||||
"pk": 6
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"ace": "perl",
|
||||
"common_name": "Perl",
|
||||
"description": "",
|
||||
"extension": "",
|
||||
"info": "perl 5.10.1",
|
||||
"key": "PERL",
|
||||
"name": "Perl",
|
||||
"pygments": "perl",
|
||||
"short_name": ""
|
||||
},
|
||||
"model": "judge.language",
|
||||
"pk": 7
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"ace": "python",
|
||||
"common_name": "Python",
|
||||
"description": "",
|
||||
"extension": "",
|
||||
"info": "python 3.4.2",
|
||||
"key": "PY3",
|
||||
"name": "Python 3",
|
||||
"pygments": "python3",
|
||||
"short_name": ""
|
||||
},
|
||||
"model": "judge.language",
|
||||
"pk": 8
|
||||
}
|
||||
]
|
98
judge/fixtures/navbar.json
Normal file
98
judge/fixtures/navbar.json
Normal file
|
@ -0,0 +1,98 @@
|
|||
[
|
||||
{
|
||||
"fields": {
|
||||
"key": "problems",
|
||||
"label": "Problems",
|
||||
"level": 0,
|
||||
"lft": 1,
|
||||
"order": 1,
|
||||
"parent": null,
|
||||
"path": "/problems/",
|
||||
"regex": "^/problem",
|
||||
"rght": 2,
|
||||
"tree_id": 1
|
||||
},
|
||||
"model": "judge.navigationbar",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"key": "submit",
|
||||
"label": "Submissions",
|
||||
"level": 0,
|
||||
"lft": 1,
|
||||
"order": 2,
|
||||
"parent": null,
|
||||
"path": "/submissions/",
|
||||
"regex": "^/submi|^/src/",
|
||||
"rght": 2,
|
||||
"tree_id": 2
|
||||
},
|
||||
"model": "judge.navigationbar",
|
||||
"pk": 2
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"key": "user",
|
||||
"label": "Users",
|
||||
"level": 0,
|
||||
"lft": 1,
|
||||
"order": 3,
|
||||
"parent": null,
|
||||
"path": "/users/",
|
||||
"regex": "^/user",
|
||||
"rght": 2,
|
||||
"tree_id": 3
|
||||
},
|
||||
"model": "judge.navigationbar",
|
||||
"pk": 3
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"key": "contest",
|
||||
"label": "Contests",
|
||||
"level": 0,
|
||||
"lft": 1,
|
||||
"order": 5,
|
||||
"parent": null,
|
||||
"path": "/contests/",
|
||||
"regex": "^/contest",
|
||||
"rght": 2,
|
||||
"tree_id": 4
|
||||
},
|
||||
"model": "judge.navigationbar",
|
||||
"pk": 5
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"key": "about",
|
||||
"label": "About",
|
||||
"level": 0,
|
||||
"lft": 1,
|
||||
"order": 6,
|
||||
"parent": null,
|
||||
"path": "/about/",
|
||||
"regex": "^/about/$",
|
||||
"rght": 4,
|
||||
"tree_id": 5
|
||||
},
|
||||
"model": "judge.navigationbar",
|
||||
"pk": 6
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"key": "status",
|
||||
"label": "Status",
|
||||
"level": 1,
|
||||
"lft": 2,
|
||||
"order": 7,
|
||||
"parent": 6,
|
||||
"path": "/status/",
|
||||
"regex": "^/status/$|^/judge/",
|
||||
"rght": 3,
|
||||
"tree_id": 5
|
||||
},
|
||||
"model": "judge.navigationbar",
|
||||
"pk": 7
|
||||
}
|
||||
]
|
159
judge/forms.py
Normal file
159
judge/forms.py
Normal file
|
@ -0,0 +1,159 @@
|
|||
from operator import attrgetter
|
||||
|
||||
import pyotp
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db.models import Q
|
||||
from django.forms import CharField, Form, ModelForm
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django_ace import AceWidget
|
||||
from judge.models import Contest, Language, Organization, PrivateMessage, Problem, Profile, Submission
|
||||
from judge.utils.subscription import newsletter_id
|
||||
from judge.widgets import HeavyPreviewPageDownWidget, MathJaxPagedownWidget, PagedownWidget, Select2MultipleWidget, \
|
||||
Select2Widget
|
||||
|
||||
|
||||
def fix_unicode(string, unsafe=tuple('\u202a\u202b\u202d\u202e')):
|
||||
return string + (sum(k in unsafe for k in string) - string.count('\u202c')) * '\u202c'
|
||||
|
||||
|
||||
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:
|
||||
model = Profile
|
||||
fields = ['about', 'organizations', 'timezone', 'language', 'ace_theme', 'user_script']
|
||||
widgets = {
|
||||
'user_script': AceWidget(theme='github'),
|
||||
'timezone': Select2Widget(attrs={'style': 'width:200px'}),
|
||||
'language': Select2Widget(attrs={'style': 'width:200px'}),
|
||||
'ace_theme': Select2Widget(attrs={'style': 'width:200px'}),
|
||||
}
|
||||
|
||||
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:
|
||||
widgets['about'] = HeavyPreviewPageDownWidget(
|
||||
preview=reverse_lazy('profile_preview'),
|
||||
attrs={'style': 'max-width:700px;min-width:700px;width:700px'},
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
organizations = self.cleaned_data.get('organizations') or []
|
||||
max_orgs = settings.DMOJ_USER_MAX_ORGANIZATION_COUNT
|
||||
|
||||
if sum(org.is_open for org in organizations) > max_orgs:
|
||||
raise ValidationError(
|
||||
_('You may not be part of more than {count} public organizations.').format(count=max_orgs))
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop('user', None)
|
||||
super(ProfileForm, self).__init__(*args, **kwargs)
|
||||
if not user.has_perm('judge.edit_all_organization'):
|
||||
self.fields['organizations'].queryset = Organization.objects.filter(
|
||||
Q(is_open=True) | Q(id__in=user.profile.organizations.all()),
|
||||
)
|
||||
|
||||
|
||||
class ProblemSubmitForm(ModelForm):
|
||||
source = CharField(max_length=65536, widget=AceWidget(theme='twilight', no_ace_media=True))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ProblemSubmitForm, self).__init__(*args, **kwargs)
|
||||
self.fields['problem'].empty_label = None
|
||||
self.fields['problem'].widget = forms.HiddenInput()
|
||||
self.fields['language'].empty_label = None
|
||||
self.fields['language'].label_from_instance = attrgetter('display_name')
|
||||
self.fields['language'].queryset = Language.objects.filter(judges__online=True).distinct()
|
||||
|
||||
class Meta:
|
||||
model = Submission
|
||||
fields = ['problem', 'language']
|
||||
|
||||
|
||||
class EditOrganizationForm(ModelForm):
|
||||
class Meta:
|
||||
model = Organization
|
||||
fields = ['about', 'logo_override_image', 'admins']
|
||||
widgets = {'admins': Select2MultipleWidget()}
|
||||
if HeavyPreviewPageDownWidget is not None:
|
||||
widgets['about'] = HeavyPreviewPageDownWidget(preview=reverse_lazy('organization_preview'))
|
||||
|
||||
|
||||
class NewMessageForm(ModelForm):
|
||||
class Meta:
|
||||
model = PrivateMessage
|
||||
fields = ['title', 'content']
|
||||
widgets = {}
|
||||
if PagedownWidget is not None:
|
||||
widgets['content'] = MathJaxPagedownWidget()
|
||||
|
||||
|
||||
class CustomAuthenticationForm(AuthenticationForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CustomAuthenticationForm, self).__init__(*args, **kwargs)
|
||||
self.fields['username'].widget.attrs.update({'placeholder': _('Username')})
|
||||
self.fields['password'].widget.attrs.update({'placeholder': _('Password')})
|
||||
|
||||
self.has_google_auth = self._has_social_auth('GOOGLE_OAUTH2')
|
||||
self.has_facebook_auth = self._has_social_auth('FACEBOOK')
|
||||
self.has_github_auth = self._has_social_auth('GITHUB_SECURE')
|
||||
|
||||
def _has_social_auth(self, key):
|
||||
return (getattr(settings, 'SOCIAL_AUTH_%s_KEY' % key, None) and
|
||||
getattr(settings, 'SOCIAL_AUTH_%s_SECRET' % key, None))
|
||||
|
||||
|
||||
class NoAutoCompleteCharField(forms.CharField):
|
||||
def widget_attrs(self, widget):
|
||||
attrs = super(NoAutoCompleteCharField, self).widget_attrs(widget)
|
||||
attrs['autocomplete'] = 'off'
|
||||
return attrs
|
||||
|
||||
|
||||
class TOTPForm(Form):
|
||||
TOLERANCE = settings.DMOJ_TOTP_TOLERANCE_HALF_MINUTES
|
||||
|
||||
totp_token = NoAutoCompleteCharField(validators=[
|
||||
RegexValidator('^[0-9]{6}$', _('Two Factor Authentication tokens must be 6 decimal digits.')),
|
||||
])
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.totp_key = kwargs.pop('totp_key')
|
||||
super(TOTPForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def clean_totp_token(self):
|
||||
if not pyotp.TOTP(self.totp_key).verify(self.cleaned_data['totp_token'], valid_window=self.TOLERANCE):
|
||||
raise ValidationError(_('Invalid Two Factor Authentication token.'))
|
||||
|
||||
|
||||
class ProblemCloneForm(Form):
|
||||
code = CharField(max_length=20, validators=[RegexValidator('^[a-z0-9]+$', _('Problem code must be ^[a-z0-9]+$'))])
|
||||
|
||||
def clean_code(self):
|
||||
code = self.cleaned_data['code']
|
||||
if Problem.objects.filter(code=code).exists():
|
||||
raise ValidationError(_('Problem with code already exists.'))
|
||||
return code
|
||||
|
||||
|
||||
class ContestCloneForm(Form):
|
||||
key = CharField(max_length=20, validators=[RegexValidator('^[a-z0-9]+$', _('Contest id must be ^[a-z0-9]+$'))])
|
||||
|
||||
def clean_key(self):
|
||||
key = self.cleaned_data['key']
|
||||
if Contest.objects.filter(key=key).exists():
|
||||
raise ValidationError(_('Contest with key already exists.'))
|
||||
return key
|
55
judge/fulltext.py
Normal file
55
judge/fulltext.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
# From: http://www.mercurytide.co.uk/news/article/django-full-text-search/
|
||||
|
||||
from django.db import connection, models
|
||||
from django.db.models.query import QuerySet
|
||||
|
||||
|
||||
class SearchQuerySet(QuerySet):
|
||||
DEFAULT = ''
|
||||
BOOLEAN = ' IN BOOLEAN MODE'
|
||||
NATURAL_LANGUAGE = ' IN NATURAL LANGUAGE MODE'
|
||||
QUERY_EXPANSION = ' WITH QUERY EXPANSION'
|
||||
|
||||
def __init__(self, fields=None, **kwargs):
|
||||
super(SearchQuerySet, self).__init__(**kwargs)
|
||||
self._search_fields = fields
|
||||
|
||||
def _clone(self, *args, **kwargs):
|
||||
queryset = super(SearchQuerySet, self)._clone(*args, **kwargs)
|
||||
queryset._search_fields = self._search_fields
|
||||
return queryset
|
||||
|
||||
def search(self, query, mode=DEFAULT):
|
||||
meta = self.model._meta
|
||||
|
||||
# Get the table name and column names from the model
|
||||
# in `table_name`.`column_name` style
|
||||
columns = [meta.get_field(name).column for name in self._search_fields]
|
||||
full_names = ['%s.%s' %
|
||||
(connection.ops.quote_name(meta.db_table),
|
||||
connection.ops.quote_name(column))
|
||||
for column in columns]
|
||||
|
||||
# Create the MATCH...AGAINST expressions
|
||||
fulltext_columns = ', '.join(full_names)
|
||||
match_expr = ('MATCH(%s) AGAINST (%%s%s)' % (fulltext_columns, mode))
|
||||
|
||||
# Add the extra SELECT and WHERE options
|
||||
return self.extra(select={'relevance': match_expr},
|
||||
select_params=[query],
|
||||
where=[match_expr],
|
||||
params=[query])
|
||||
|
||||
|
||||
class SearchManager(models.Manager):
|
||||
def __init__(self, fields=None):
|
||||
super(SearchManager, self).__init__()
|
||||
self._search_fields = fields
|
||||
|
||||
def get_queryset(self):
|
||||
if self._search_fields is not None:
|
||||
return SearchQuerySet(model=self.model, fields=self._search_fields)
|
||||
return super(SearchManager, self).get_queryset()
|
||||
|
||||
def search(self, *args, **kwargs):
|
||||
return self.get_queryset().search(*args, **kwargs)
|
37
judge/highlight_code.py
Normal file
37
judge/highlight_code.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
from django.utils.html import escape, mark_safe
|
||||
|
||||
__all__ = ['highlight_code']
|
||||
|
||||
|
||||
def _make_pre_code(code):
|
||||
return mark_safe('<pre>' + escape(code) + '</pre>')
|
||||
|
||||
|
||||
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'):
|
||||
try:
|
||||
lexer = pygments.lexers.get_lexer_by_name(language)
|
||||
except pygments.util.ClassNotFound:
|
||||
return _make_pre_code(code)
|
||||
|
||||
# return mark_safe(pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass, linenos='table')))
|
||||
return mark_safe(pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass)))
|
36
judge/jinja2/__init__.py
Normal file
36
judge/jinja2/__init__.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
import itertools
|
||||
import json
|
||||
|
||||
from django.utils.http import urlquote
|
||||
from jinja2.ext import Extension
|
||||
from mptt.utils import get_cached_trees
|
||||
from statici18n.templatetags.statici18n import inlinei18n
|
||||
|
||||
from judge.highlight_code import highlight_code
|
||||
from judge.user_translations import gettext
|
||||
from . import (camo, datetime, filesize, gravatar, language, markdown, rating, reference, render, social,
|
||||
spaceless, submission, timedelta)
|
||||
from . import registry
|
||||
|
||||
registry.function('str', str)
|
||||
registry.filter('str', str)
|
||||
registry.filter('json', json.dumps)
|
||||
registry.filter('highlight', highlight_code)
|
||||
registry.filter('urlquote', urlquote)
|
||||
registry.filter('roundfloat', round)
|
||||
registry.function('inlinei18n', inlinei18n)
|
||||
registry.function('mptt_tree', get_cached_trees)
|
||||
registry.function('user_trans', gettext)
|
||||
|
||||
|
||||
@registry.function
|
||||
def counter(start=1):
|
||||
return itertools.count(start).__next__
|
||||
|
||||
|
||||
class DMOJExtension(Extension):
|
||||
def __init__(self, env):
|
||||
super(DMOJExtension, self).__init__(env)
|
||||
env.globals.update(registry.globals)
|
||||
env.filters.update(registry.filters)
|
||||
env.tests.update(registry.tests)
|
9
judge/jinja2/camo.py
Normal file
9
judge/jinja2/camo.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from judge.utils.camo import client as camo_client
|
||||
from . import registry
|
||||
|
||||
|
||||
@registry.filter
|
||||
def camo(url):
|
||||
if camo_client is None:
|
||||
return url
|
||||
return camo_client.rewrite_url(url)
|
27
judge/jinja2/datetime.py
Normal file
27
judge/jinja2/datetime.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
import functools
|
||||
|
||||
from django.template.defaultfilters import date, time
|
||||
from django.templatetags.tz import localtime
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from . import registry
|
||||
|
||||
|
||||
def localtime_wrapper(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(datetime, *args, **kwargs):
|
||||
if getattr(datetime, 'convert_to_local_time', True):
|
||||
datetime = localtime(datetime)
|
||||
return func(datetime, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
registry.filter(localtime_wrapper(date))
|
||||
registry.filter(localtime_wrapper(time))
|
||||
|
||||
|
||||
@registry.function
|
||||
@registry.render_with('widgets/relative-time.html')
|
||||
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}
|
36
judge/jinja2/filesize.py
Normal file
36
judge/jinja2/filesize.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
from django.utils.html import avoid_wrapping
|
||||
|
||||
from . import registry
|
||||
|
||||
|
||||
def _format_size(bytes, callback):
|
||||
bytes = float(bytes)
|
||||
|
||||
KB = 1 << 10
|
||||
MB = 1 << 20
|
||||
GB = 1 << 30
|
||||
TB = 1 << 40
|
||||
PB = 1 << 50
|
||||
|
||||
if bytes < KB:
|
||||
return callback('', bytes)
|
||||
elif bytes < MB:
|
||||
return callback('K', bytes / KB)
|
||||
elif bytes < GB:
|
||||
return callback('M', bytes / MB)
|
||||
elif bytes < TB:
|
||||
return callback('G', bytes / GB)
|
||||
elif bytes < PB:
|
||||
return callback('T', bytes / TB)
|
||||
else:
|
||||
return callback('P', bytes / PB)
|
||||
|
||||
|
||||
@registry.filter
|
||||
def kbdetailformat(bytes):
|
||||
return avoid_wrapping(_format_size(bytes * 1024, lambda x, y: ['%d %sB', '%.2f %sB'][bool(x)] % (y, x)))
|
||||
|
||||
|
||||
@registry.filter
|
||||
def kbsimpleformat(kb):
|
||||
return _format_size(kb * 1024, lambda x, y: '%.0f%s' % (y, x or 'B'))
|
25
judge/jinja2/gravatar.py
Normal file
25
judge/jinja2/gravatar.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
import hashlib
|
||||
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.utils.http import urlencode
|
||||
|
||||
from judge.models import Profile
|
||||
from judge.utils.unicode import utf8bytes
|
||||
from . import registry
|
||||
|
||||
|
||||
@registry.function
|
||||
def gravatar(email, size=80, default=None):
|
||||
if isinstance(email, Profile):
|
||||
if default is None:
|
||||
default = email.mute
|
||||
email = email.user.email
|
||||
elif isinstance(email, AbstractUser):
|
||||
email = email.email
|
||||
|
||||
gravatar_url = '//www.gravatar.com/avatar/' + hashlib.md5(utf8bytes(email.strip().lower())).hexdigest() + '?'
|
||||
args = {'d': 'identicon', 's': str(size)}
|
||||
if default:
|
||||
args['f'] = 'y'
|
||||
gravatar_url += urlencode(args)
|
||||
return gravatar_url
|
18
judge/jinja2/language.py
Normal file
18
judge/jinja2/language.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from django.utils import translation
|
||||
|
||||
from . import registry
|
||||
|
||||
|
||||
@registry.function('language_info')
|
||||
def get_language_info(language):
|
||||
# ``language`` is either a language code string or a sequence
|
||||
# with the language code as its first item
|
||||
if len(language[0]) > 1:
|
||||
return translation.get_language_info(language[0])
|
||||
else:
|
||||
return translation.get_language_info(str(language))
|
||||
|
||||
|
||||
@registry.function('language_info_list')
|
||||
def get_language_info_list(langs):
|
||||
return [get_language_info(lang) for lang in langs]
|
142
judge/jinja2/markdown/__init__.py
Normal file
142
judge/jinja2/markdown/__init__.py
Normal file
|
@ -0,0 +1,142 @@
|
|||
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.utils.camo import client as camo_client
|
||||
from judge.utils.texoid import TEXOID_ENABLED, TexoidRenderer
|
||||
from .. import registry
|
||||
|
||||
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, CodeSafeInlineGrammar):
|
||||
pass
|
||||
|
||||
|
||||
class AwesomeInlineLexer(MathInlineLexer, mistune.InlineLexer):
|
||||
grammar_class = AwesomeInlineGrammar
|
||||
|
||||
|
||||
class AwesomeRenderer(MathRenderer, 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
|
||||
def markdown(value, style, math_engine=None, lazy_load=False):
|
||||
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)
|
20
judge/jinja2/markdown/lazy_load.py
Normal file
20
judge/jinja2/markdown/lazy_load.py
Normal 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')
|
65
judge/jinja2/markdown/math.py
Normal file
65
judge/jinja2/markdown/math.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
import re
|
||||
|
||||
import mistune
|
||||
|
||||
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):
|
||||
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)
|
35
judge/jinja2/rating.py
Normal file
35
judge/jinja2/rating.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
from django.utils import six
|
||||
|
||||
from judge.ratings import rating_class, rating_name, rating_progress
|
||||
from . import registry
|
||||
|
||||
|
||||
def _get_rating_value(func, obj):
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
if isinstance(obj, six.integer_types):
|
||||
return func(obj)
|
||||
else:
|
||||
return func(obj.rating)
|
||||
|
||||
|
||||
@registry.function('rating_class')
|
||||
def get_rating_class(obj):
|
||||
return _get_rating_value(rating_class, obj) or 'rate-none'
|
||||
|
||||
|
||||
@registry.function(name='rating_name')
|
||||
def get_name(obj):
|
||||
return _get_rating_value(rating_name, obj) or 'Unrated'
|
||||
|
||||
|
||||
@registry.function(name='rating_progress')
|
||||
def get_progress(obj):
|
||||
return _get_rating_value(rating_progress, obj) or 0.0
|
||||
|
||||
|
||||
@registry.function
|
||||
@registry.render_with('user/rating.html')
|
||||
def rating_number(obj):
|
||||
return {'rating': obj}
|
187
judge/jinja2/reference.py
Normal file
187
judge/jinja2/reference.py
Normal file
|
@ -0,0 +1,187 @@
|
|||
import re
|
||||
from collections import defaultdict
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from ansi2html import Ansi2HTMLConverter
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from lxml.html import Element
|
||||
|
||||
from judge import lxml_tree
|
||||
from judge.models import Contest, Problem, Profile
|
||||
from judge.ratings import rating_class, rating_progress
|
||||
from . import registry
|
||||
|
||||
rereference = re.compile(r'\[(r?user):(\w+)\]')
|
||||
|
||||
|
||||
def get_user(username, data):
|
||||
if not data:
|
||||
element = Element('span')
|
||||
element.text = username
|
||||
return element
|
||||
|
||||
element = Element('span', {'class': Profile.get_user_css_class(*data)})
|
||||
link = Element('a', {'href': reverse('user_page', args=[username])})
|
||||
link.text = username
|
||||
element.append(link)
|
||||
return element
|
||||
|
||||
|
||||
def get_user_rating(username, data):
|
||||
if not data:
|
||||
element = Element('span')
|
||||
element.text = username
|
||||
return element
|
||||
|
||||
rating = data[1]
|
||||
element = Element('a', {'class': 'rate-group', 'href': reverse('user_page', args=[username])})
|
||||
if rating:
|
||||
rating_css = rating_class(rating)
|
||||
rate_box = Element('span', {'class': 'rate-box ' + rating_css})
|
||||
rate_box.append(Element('span', {'style': 'height: %3.fem' % rating_progress(rating)}))
|
||||
user = Element('span', {'class': 'rating ' + rating_css})
|
||||
user.text = username
|
||||
element.append(rate_box)
|
||||
element.append(user)
|
||||
else:
|
||||
element.text = username
|
||||
return element
|
||||
|
||||
|
||||
def get_user_info(usernames):
|
||||
return {name: (rank, rating) for name, rank, rating in
|
||||
Profile.objects.filter(user__username__in=usernames)
|
||||
.values_list('user__username', 'display_rank', 'rating')}
|
||||
|
||||
|
||||
reference_map = {
|
||||
'user': (get_user, get_user_info),
|
||||
'ruser': (get_user_rating, get_user_info),
|
||||
}
|
||||
|
||||
|
||||
def process_reference(text):
|
||||
# text/tail -> text/tail + elements
|
||||
last = 0
|
||||
tail = text
|
||||
prev = None
|
||||
elements = []
|
||||
for piece in rereference.finditer(text):
|
||||
if prev is None:
|
||||
tail = text[last:piece.start()]
|
||||
else:
|
||||
prev.append(text[last:piece.start()])
|
||||
prev = list(piece.groups())
|
||||
elements.append(prev)
|
||||
last = piece.end()
|
||||
if prev is not None:
|
||||
prev.append(text[last:])
|
||||
return tail, elements
|
||||
|
||||
|
||||
def populate_list(queries, list, element, tail, children):
|
||||
if children:
|
||||
for elem in children:
|
||||
queries[elem[0]].append(elem[1])
|
||||
list.append((element, tail, children))
|
||||
|
||||
|
||||
def update_tree(list, results, is_tail=False):
|
||||
for element, text, children in list:
|
||||
after = []
|
||||
for type, name, tail in children:
|
||||
child = reference_map[type][0](name, results[type].get(name))
|
||||
child.tail = tail
|
||||
after.append(child)
|
||||
|
||||
after = iter(reversed(after))
|
||||
if is_tail:
|
||||
element.tail = text
|
||||
link = element.getnext()
|
||||
if link is None:
|
||||
link = next(after)
|
||||
element.getparent().append(link)
|
||||
else:
|
||||
element.text = text
|
||||
link = next(after)
|
||||
element.insert(0, link)
|
||||
for child in after:
|
||||
link.addprevious(child)
|
||||
link = child
|
||||
|
||||
|
||||
@registry.filter
|
||||
def reference(text):
|
||||
tree = lxml_tree.fromstring(text)
|
||||
texts = []
|
||||
tails = []
|
||||
queries = defaultdict(list)
|
||||
for element in tree.iter():
|
||||
if element.text:
|
||||
populate_list(queries, texts, element, *process_reference(element.text))
|
||||
if element.tail:
|
||||
populate_list(queries, tails, element, *process_reference(element.tail))
|
||||
|
||||
results = {type: reference_map[type][1](values) for type, values in queries.items()}
|
||||
update_tree(texts, results, is_tail=False)
|
||||
update_tree(tails, results, is_tail=True)
|
||||
return tree
|
||||
|
||||
|
||||
@registry.filter
|
||||
def item_title(item):
|
||||
if isinstance(item, Problem):
|
||||
return item.name
|
||||
elif isinstance(item, Contest):
|
||||
return item.name
|
||||
return '<Unknown>'
|
||||
|
||||
|
||||
@registry.function
|
||||
@registry.render_with('user/link.html')
|
||||
def link_user(user):
|
||||
if isinstance(user, Profile):
|
||||
user, profile = user.user, user
|
||||
elif isinstance(user, AbstractUser):
|
||||
profile = user.profile
|
||||
elif type(user).__name__ == 'ContestRankingProfile':
|
||||
user, profile = user.user, user
|
||||
else:
|
||||
raise ValueError('Expected profile or user, got %s' % (type(user),))
|
||||
return {'user': user, 'profile': profile}
|
||||
|
||||
|
||||
@registry.function
|
||||
@registry.render_with('user/link-list.html')
|
||||
def link_users(users):
|
||||
return {'users': users}
|
||||
|
||||
|
||||
@registry.function
|
||||
@registry.render_with('runtime-version-fragment.html')
|
||||
def runtime_versions(versions):
|
||||
return {'runtime_versions': versions}
|
||||
|
||||
|
||||
@registry.filter(name='absolutify')
|
||||
def absolute_links(text, url):
|
||||
tree = lxml_tree.fromstring(text)
|
||||
for anchor in tree.xpath('.//a'):
|
||||
href = anchor.get('href')
|
||||
if href:
|
||||
anchor.set('href', urljoin(url, href))
|
||||
return tree
|
||||
|
||||
|
||||
@registry.function(name='urljoin')
|
||||
def join(first, second, *rest):
|
||||
if not rest:
|
||||
return urljoin(first, second)
|
||||
return urljoin(urljoin(first, second), *rest)
|
||||
|
||||
|
||||
@registry.filter(name='ansi2html')
|
||||
def ansi2html(s):
|
||||
return mark_safe(Ansi2HTMLConverter(inline=True).convert(s, full=False))
|
53
judge/jinja2/registry.py
Normal file
53
judge/jinja2/registry.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
from django_jinja.library import render_with
|
||||
|
||||
globals = {}
|
||||
tests = {}
|
||||
filters = {}
|
||||
extensions = []
|
||||
|
||||
__all__ = ['render_with', 'function', 'filter', 'test', 'extension']
|
||||
|
||||
|
||||
def _store_function(store, func, name=None):
|
||||
if name is None:
|
||||
name = func.__name__
|
||||
store[name] = func
|
||||
|
||||
|
||||
def _register_function(store, name, func):
|
||||
if name is None and func is None:
|
||||
def decorator(func):
|
||||
_store_function(store, func)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
elif name is not None and func is None:
|
||||
if callable(name):
|
||||
_store_function(store, name)
|
||||
return name
|
||||
else:
|
||||
def decorator(func):
|
||||
_store_function(store, func, name)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
else:
|
||||
_store_function(store, func, name)
|
||||
return func
|
||||
|
||||
|
||||
def filter(name=None, func=None):
|
||||
return _register_function(filters, name, func)
|
||||
|
||||
|
||||
def function(name=None, func=None):
|
||||
return _register_function(globals, name, func)
|
||||
|
||||
|
||||
def test(name=None, func=None):
|
||||
return _register_function(tests, name, func)
|
||||
|
||||
|
||||
def extension(cls):
|
||||
extensions.append(cls)
|
||||
return cls
|
27
judge/jinja2/render.py
Normal file
27
judge/jinja2/render.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
from django.template import (Context, Template as DjangoTemplate, TemplateSyntaxError as DjangoTemplateSyntaxError,
|
||||
VariableDoesNotExist)
|
||||
|
||||
from . import registry
|
||||
|
||||
MAX_CACHE = 100
|
||||
django_cache = {}
|
||||
|
||||
|
||||
def compile_template(code):
|
||||
if code in django_cache:
|
||||
return django_cache[code]
|
||||
|
||||
# If this works for re.compile, it works for us too.
|
||||
if len(django_cache) > MAX_CACHE:
|
||||
django_cache.clear()
|
||||
|
||||
t = django_cache[code] = DjangoTemplate(code)
|
||||
return t
|
||||
|
||||
|
||||
@registry.function
|
||||
def render_django(template, **context):
|
||||
try:
|
||||
return compile_template(template).render(Context(context))
|
||||
except (VariableDoesNotExist, DjangoTemplateSyntaxError):
|
||||
return 'Error rendering: %r' % template
|
34
judge/jinja2/social.py
Normal file
34
judge/jinja2/social.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
from django.template.loader import get_template
|
||||
from django.utils.safestring import mark_safe
|
||||
from django_social_share.templatetags.social_share import post_to_facebook_url, post_to_gplus_url, post_to_twitter_url
|
||||
|
||||
from . import registry
|
||||
|
||||
SHARES = [
|
||||
('post_to_twitter', 'django_social_share/templatetags/post_to_twitter.html', post_to_twitter_url),
|
||||
('post_to_facebook', 'django_social_share/templatetags/post_to_facebook.html', post_to_facebook_url),
|
||||
('post_to_gplus', 'django_social_share/templatetags/post_to_gplus.html', post_to_gplus_url),
|
||||
# For future versions:
|
||||
# ('post_to_linkedin', 'django_social_share/templatetags/post_to_linkedin.html', post_to_linkedin_url),
|
||||
# ('post_to_reddit', 'django_social_share/templatetags/post_to_reddit.html', post_to_reddit_url),
|
||||
]
|
||||
|
||||
|
||||
def make_func(name, template, url_func):
|
||||
def func(request, *args):
|
||||
link_text = args[-1]
|
||||
context = {'request': request, 'link_text': mark_safe(link_text)}
|
||||
context = url_func(context, *args[:-1])
|
||||
return mark_safe(get_template(template).render(context))
|
||||
|
||||
func.__name__ = name
|
||||
registry.function(name, func)
|
||||
|
||||
|
||||
for name, template, url_func in SHARES:
|
||||
make_func(name, template, url_func)
|
||||
|
||||
|
||||
@registry.function
|
||||
def recaptcha_init(language=None):
|
||||
return get_template('snowpenguin/recaptcha/recaptcha_init.html').render({'explicit': False, 'language': language})
|
29
judge/jinja2/spaceless.py
Normal file
29
judge/jinja2/spaceless.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
import re
|
||||
|
||||
from jinja2 import Markup, nodes
|
||||
from jinja2.ext import Extension
|
||||
|
||||
|
||||
class SpacelessExtension(Extension):
|
||||
"""
|
||||
Removes whitespace between HTML tags at compile time, including tab and newline characters.
|
||||
It does not remove whitespace between jinja2 tags or variables. Neither does it remove whitespace between tags
|
||||
and their text content.
|
||||
Adapted from coffin:
|
||||
https://github.com/coffin/coffin/blob/master/coffin/template/defaulttags.py
|
||||
Adapted from StackOverflow:
|
||||
https://stackoverflow.com/a/23741298/1090657
|
||||
"""
|
||||
|
||||
tags = {'spaceless'}
|
||||
|
||||
def parse(self, parser):
|
||||
lineno = next(parser.stream).lineno
|
||||
body = parser.parse_statements(['name:endspaceless'], drop_needle=True)
|
||||
return nodes.CallBlock(
|
||||
self.call_method('_strip_spaces', [], [], None, None),
|
||||
[], [], body,
|
||||
).set_lineno(lineno)
|
||||
|
||||
def _strip_spaces(self, caller=None):
|
||||
return Markup(re.sub(r'>\s+<', '><', caller().unescape().strip()))
|
21
judge/jinja2/submission.py
Normal file
21
judge/jinja2/submission.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
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
|
||||
|
||||
return can_view
|
28
judge/jinja2/timedelta.py
Normal file
28
judge/jinja2/timedelta.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
import datetime
|
||||
|
||||
from judge.utils.timedelta import nice_repr
|
||||
from . import registry
|
||||
|
||||
|
||||
@registry.filter
|
||||
def timedelta(value, display='long'):
|
||||
if value is None:
|
||||
return value
|
||||
return nice_repr(value, display)
|
||||
|
||||
|
||||
@registry.filter
|
||||
def timestampdelta(value, display='long'):
|
||||
value = datetime.timedelta(seconds=value)
|
||||
return timedelta(value, display)
|
||||
|
||||
|
||||
@registry.filter
|
||||
def seconds(timedelta):
|
||||
return timedelta.total_seconds()
|
||||
|
||||
|
||||
@registry.filter
|
||||
@registry.render_with('time-remaining-fragment.html')
|
||||
def as_countdown(timedelta):
|
||||
return {'countdown': timedelta}
|
117
judge/judgeapi.py
Normal file
117
judge/judgeapi.py
Normal file
|
@ -0,0 +1,117 @@
|
|||
import json
|
||||
import logging
|
||||
import socket
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from judge import event_poster as event
|
||||
|
||||
logger = logging.getLogger('judge.judgeapi')
|
||||
size_pack = struct.Struct('!I')
|
||||
|
||||
|
||||
def _post_update_submission(submission, done=False):
|
||||
if submission.problem.is_public:
|
||||
event.post('submissions', {'type': 'done-submission' if done else 'update-submission',
|
||||
'id': submission.id,
|
||||
'contest': submission.contest_key,
|
||||
'user': submission.user_id, 'problem': submission.problem_id,
|
||||
'status': submission.status, 'language': submission.language.key})
|
||||
|
||||
|
||||
def judge_request(packet, reply=True):
|
||||
sock = socket.create_connection(settings.BRIDGED_DJANGO_CONNECT or
|
||||
settings.BRIDGED_DJANGO_ADDRESS[0])
|
||||
|
||||
output = json.dumps(packet, separators=(',', ':'))
|
||||
output = zlib.compress(output.encode('utf-8'))
|
||||
writer = sock.makefile('wb')
|
||||
writer.write(size_pack.pack(len(output)))
|
||||
writer.write(output)
|
||||
writer.close()
|
||||
|
||||
if reply:
|
||||
reader = sock.makefile('rb', -1)
|
||||
input = reader.read(size_pack.size)
|
||||
if not input:
|
||||
raise ValueError('Judge did not respond')
|
||||
length = size_pack.unpack(input)[0]
|
||||
input = reader.read(length)
|
||||
if not input:
|
||||
raise ValueError('Judge did not respond')
|
||||
reader.close()
|
||||
sock.close()
|
||||
|
||||
result = json.loads(zlib.decompress(input).decode('utf-8'))
|
||||
return result
|
||||
|
||||
|
||||
def judge_submission(submission, rejudge, batch_rejudge=False):
|
||||
from .models import ContestSubmission, Submission, SubmissionTestCase
|
||||
|
||||
CONTEST_SUBMISSION_PRIORITY = 0
|
||||
DEFAULT_PRIORITY = 1
|
||||
REJUDGE_PRIORITY = 2
|
||||
BATCH_REJUDGE_PRIORITY = 3
|
||||
|
||||
updates = {'time': None, 'memory': None, 'points': None, 'result': None, 'error': None,
|
||||
'was_rejudged': rejudge, 'status': 'QU'}
|
||||
try:
|
||||
# This is set proactively; it might get unset in judgecallback's on_grading_begin if the problem doesn't
|
||||
# actually have pretests stored on the judge.
|
||||
updates['is_pretested'] = ContestSubmission.objects.filter(submission=submission) \
|
||||
.values_list('problem__contest__run_pretests_only', flat=True)[0]
|
||||
except IndexError:
|
||||
priority = DEFAULT_PRIORITY
|
||||
else:
|
||||
priority = CONTEST_SUBMISSION_PRIORITY
|
||||
|
||||
# This should prevent double rejudge issues by permitting only the judging of
|
||||
# QU (which is the initial state) and D (which is the final state).
|
||||
# Even though the bridge will not queue a submission already being judged,
|
||||
# we will destroy the current state by deleting all SubmissionTestCase objects.
|
||||
# However, we can't drop the old state immediately before a submission is set for judging,
|
||||
# as that would prevent people from knowing a submission is being scheduled for rejudging.
|
||||
# It is worth noting that this mechanism does not prevent a new rejudge from being scheduled
|
||||
# while already queued, but that does not lead to data corruption.
|
||||
if not Submission.objects.filter(id=submission.id).exclude(status__in=('P', 'G')).update(**updates):
|
||||
return False
|
||||
|
||||
SubmissionTestCase.objects.filter(submission_id=submission.id).delete()
|
||||
|
||||
try:
|
||||
response = judge_request({
|
||||
'name': 'submission-request',
|
||||
'submission-id': submission.id,
|
||||
'problem-id': submission.problem.code,
|
||||
'language': submission.language.key,
|
||||
'source': submission.source.source,
|
||||
'priority': BATCH_REJUDGE_PRIORITY if batch_rejudge else REJUDGE_PRIORITY if rejudge else priority,
|
||||
})
|
||||
except BaseException:
|
||||
logger.exception('Failed to send request to judge')
|
||||
Submission.objects.filter(id=submission.id).update(status='IE')
|
||||
success = False
|
||||
else:
|
||||
if response['name'] != 'submission-received' or response['submission-id'] != submission.id:
|
||||
Submission.objects.filter(id=submission.id).update(status='IE')
|
||||
_post_update_submission(submission)
|
||||
success = True
|
||||
return success
|
||||
|
||||
|
||||
def disconnect_judge(judge, force=False):
|
||||
judge_request({'name': 'disconnect-judge', 'judge-id': judge.name, 'force': force}, reply=False)
|
||||
|
||||
|
||||
def abort_submission(submission):
|
||||
from .models import Submission
|
||||
response = judge_request({'name': 'terminate-submission', 'submission-id': submission.id})
|
||||
# This defaults to true, so that in the case the judgelist fails to remove the submission from the queue,
|
||||
# and returns a bad-request, the submission is not falsely shown as "Aborted" when it will still be judged.
|
||||
if not response.get('judge-aborted', True):
|
||||
Submission.objects.filter(id=submission.id).update(status='AB', result='AB')
|
||||
event.post('sub_%s' % Submission.get_id_secret(submission.id), {'type': 'aborted-submission'})
|
||||
_post_update_submission(submission, done=True)
|
59
judge/lxml_tree.py
Normal file
59
judge/lxml_tree.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
import logging
|
||||
|
||||
from django.utils.safestring import SafeData, mark_safe
|
||||
from lxml import html
|
||||
from lxml.etree import ParserError, XMLSyntaxError
|
||||
|
||||
logger = logging.getLogger('judge.html')
|
||||
|
||||
|
||||
class HTMLTreeString(SafeData):
|
||||
def __init__(self, str):
|
||||
try:
|
||||
self._tree = html.fromstring(str, parser=html.HTMLParser(recover=True))
|
||||
except (XMLSyntaxError, ParserError) as e:
|
||||
if str and (not isinstance(e, ParserError) or e.args[0] != 'Document is empty'):
|
||||
logger.exception('Failed to parse HTML string')
|
||||
self._tree = html.Element('div')
|
||||
|
||||
def __getattr__(self, attr):
|
||||
try:
|
||||
return getattr(self._tree, attr)
|
||||
except AttributeError:
|
||||
return getattr(str(self), attr)
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if key[0] == '_':
|
||||
super(HTMLTreeString, self).__setattr__(key, value)
|
||||
setattr(self._tree, key, value)
|
||||
|
||||
def __repr__(self):
|
||||
return '<HTMLTreeString %r>' % str(self)
|
||||
|
||||
def __str__(self):
|
||||
return mark_safe(html.tostring(self._tree, encoding='unicode'))
|
||||
|
||||
def __radd__(self, other):
|
||||
return other + str(self)
|
||||
|
||||
def __add__(self, other):
|
||||
return str(self) + other
|
||||
|
||||
def __getitem__(self, item):
|
||||
return str(self)[item]
|
||||
|
||||
def __getstate__(self):
|
||||
return str(self)
|
||||
|
||||
def __setstate__(self, state):
|
||||
self._tree = html.fromstring(state)
|
||||
|
||||
@property
|
||||
def tree(self):
|
||||
return self._tree
|
||||
|
||||
|
||||
def fromstring(str):
|
||||
if isinstance(str, HTMLTreeString):
|
||||
return str
|
||||
return HTMLTreeString(str)
|
0
judge/management/__init__.py
Normal file
0
judge/management/__init__.py
Normal file
0
judge/management/commands/__init__.py
Normal file
0
judge/management/commands/__init__.py
Normal file
17
judge/management/commands/addjudge.py
Normal file
17
judge/management/commands/addjudge.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
|
||||
from judge.models import Judge
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'create a judge'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('name', help='the name of the judge')
|
||||
parser.add_argument('auth_key', help='authentication key for the judge')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
judge = Judge()
|
||||
judge.name = options['name']
|
||||
judge.auth_key = options['auth_key']
|
||||
judge.save()
|
32
judge/management/commands/adduser.py
Normal file
32
judge/management/commands/adduser.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from judge.models import Language, Profile
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'creates a user'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('name', help='username')
|
||||
parser.add_argument('email', help='email, not necessary to be resolvable')
|
||||
parser.add_argument('password', help='password for the user')
|
||||
parser.add_argument('language', nargs='?', default=settings.DEFAULT_USER_LANGUAGE,
|
||||
help='default language ID for user')
|
||||
|
||||
parser.add_argument('--superuser', action='store_true', default=False,
|
||||
help="if specified, creates user with superuser privileges")
|
||||
parser.add_argument('--staff', action='store_true', default=False,
|
||||
help="if specified, creates user with staff privileges")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
usr = User(username=options['name'], email=options['email'], is_active=True)
|
||||
usr.set_password(options['password'])
|
||||
usr.is_superuser = options['superuser']
|
||||
usr.is_staff = options['staff']
|
||||
usr.save()
|
||||
|
||||
profile = Profile(user=usr)
|
||||
profile.language = Language.objects.get(key=options['language'])
|
||||
profile.save()
|
16
judge/management/commands/camo.py
Normal file
16
judge/management/commands/camo.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from judge.utils.camo import client as camo_client
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'obtains the camo url for the specified url'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('url', help='url to use camo on')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if camo_client is None:
|
||||
raise CommandError('Camo not available')
|
||||
|
||||
print(camo_client.image_url(options['url']))
|
27
judge/management/commands/copy_language.py
Normal file
27
judge/management/commands/copy_language.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from judge.models import Language, LanguageLimit
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'allows the problems that allow <source> to be submitted in <target>'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('source', help='language to copy from')
|
||||
parser.add_argument('target', help='language to copy to')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
try:
|
||||
source = Language.objects.get(key=options['source'])
|
||||
except Language.DoesNotExist:
|
||||
raise CommandError('Invalid source language: %s' % options['source'])
|
||||
|
||||
try:
|
||||
target = Language.objects.get(key=options['target'])
|
||||
except Language.DoesNotExist:
|
||||
raise CommandError('Invalid target language: %s' % options['target'])
|
||||
|
||||
target.problem_set.set(source.problem_set.all())
|
||||
LanguageLimit.objects.bulk_create(LanguageLimit(problem=ll.problem, language=target, time_limit=ll.time_limit,
|
||||
memory_limit=ll.memory_limit)
|
||||
for ll in LanguageLimit.objects.filter(language=source))
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue