Cloned DMOJ

This commit is contained in:
thanhluong 2020-01-21 15:35:58 +09:00
parent f623974b58
commit 49dc9ff10c
513 changed files with 132349 additions and 39 deletions

1
.browserslistrc Normal file
View file

@ -0,0 +1 @@
cover 97%

24
.flake8 Normal file
View 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
View file

@ -0,0 +1,17 @@
name: build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Install flake8
run: pip install flake8 flake8-import-order flake8-future-import flake8-commas flake8-logging-format
- name: Lint with flake8
run: |
flake8 --version
flake8

40
.github/workflows/compilemessages.yml vendored Normal file
View file

@ -0,0 +1,40 @@
name: compilemessages
on:
push:
paths:
- 'locale/**'
pull_request:
paths:
- 'locale/**'
jobs:
compilemessages:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Checkout submodules
run: |
git submodule init
git submodule update
- name: Install requirements
run: |
sudo apt-get install gettext
pip install -r requirements.txt
pip install pymysql
- name: Check .po file validity
run: |
fail=0
while read -r file; do
if ! msgfmt --check-format "$file"; then
fail=$((fail + 1))
fi
done < <(find locale -name '*.po')
exit "$fail"
shell: bash
- name: Compile messages
run: |
echo "STATIC_ROOT = '/tmp'" > dmoj/local_settings.py
python manage.py compilemessages

53
.github/workflows/makemessages.yml vendored Normal file
View file

@ -0,0 +1,53 @@
name: makemessages
on:
push:
branches:
- master
jobs:
makemessages:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Checkout submodules
run: |
git submodule init
git submodule update
- name: Install requirements
run: |
sudo apt-get install gettext
curl -O https://artifacts.crowdin.com/repo/deb/crowdin.deb
sudo dpkg -i crowdin.deb
pip install -r requirements.txt
pip install pymysql
- name: Collect localizable strings
run: |
echo "STATIC_ROOT = '/tmp'" > dmoj/local_settings.py
python manage.py makemessages -l en -e py,html,txt
python manage.py makemessages -l en -d djangojs
- name: Upload strings to Crowdin
env:
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
run: |
cat > crowdin.yaml <<EOF
project_identifier: dmoj
files:
- source: /locale/en/LC_MESSAGES/django.po
translation: /locale/%two_letters_code%/LC_MESSAGES/django.po
languages_mapping:
two_letters_code:
zh-CN: zh_Hans
sr-CS: sr_Latn
- source: /locale/en/LC_MESSAGES/djangojs.po
translation: /locale/%two_letters_code%/LC_MESSAGES/djangojs.po
languages_mapping:
two_letters_code:
zh-CN: zh_Hans
sr-CS: sr_Latn
EOF
echo "api_key: ${CROWDIN_API_TOKEN}" >> crowdin.yaml
crowdin upload sources

67
.github/workflows/updatemessages.yml vendored Normal file
View file

@ -0,0 +1,67 @@
name: updatemessages
on:
schedule:
- cron: '0 * * * *'
jobs:
updatemessages:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Checkout submodules
run: |
git submodule init
git submodule update
- name: Install requirements
run: |
sudo apt-get install gettext
curl -O https://artifacts.crowdin.com/repo/deb/crowdin.deb
sudo dpkg -i crowdin.deb
pip install -r requirements.txt
pip install pymysql
- name: Download strings from Crowdin
env:
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
run: |
cat > crowdin.yaml <<EOF
project_identifier: dmoj
files:
- source: /locale/en/LC_MESSAGES/django.po
translation: /locale/%two_letters_code%/LC_MESSAGES/django.po
languages_mapping:
two_letters_code:
zh-CN: zh_Hans
zh-TW: zh_Hant
sr-CS: sr_Latn
- source: /locale/en/LC_MESSAGES/djangojs.po
translation: /locale/%two_letters_code%/LC_MESSAGES/djangojs.po
languages_mapping:
two_letters_code:
zh-CN: zh_Hans
zh-TW: zh_Hant
sr-CS: sr_Latn
EOF
echo "api_key: ${CROWDIN_API_TOKEN}" >> crowdin.yaml
crowdin download
rm crowdin.yaml
- name: Cleanup
run: |
rm -rf src/
git add locale
git checkout .
git clean -fd
- name: Create pull request
uses: peter-evans/create-pull-request@v1.4.1-multi
env:
GITHUB_TOKEN: ${{ secrets.REPO_SCOPED_TOKEN }}
COMMIT_MESSAGE: 'i18n: update translations from Crowdin'
PULL_REQUEST_TITLE: 'Update translations from Crowdin'
PULL_REQUEST_BODY: This PR has been auto-generated to pull in latest translations from [Crowdin](https://translate.dmoj.ca).
PULL_REQUEST_LABELS: i18n, enhancement
PULL_REQUEST_REVIEWERS: Xyene, quantum5
PULL_REQUEST_BRANCH: update-i18n
BRANCH_SUFFIX: none

52
.gitignore vendored
View file

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

@ -0,0 +1,8 @@
[submodule "resources/pagedown"]
path = resources/pagedown
url = https://github.com/DMOJ/dmoj-pagedown.git
branch = master
[submodule "resources/libs"]
path = resources/libs
url = https://github.com/DMOJ/site-assets.git
branch = master

62
502.html Normal file
View 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>

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

@ -0,0 +1,5 @@
"""
Django-ace originally from https://github.com/bradleyayers/django-ace.
"""
from .widgets import AceWidget

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 B

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

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

@ -0,0 +1 @@
from dmoj.celery import app as celery_app

27
dmoj/celery.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

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

View file

@ -0,0 +1,11 @@
from .base_server import BaseServer
from .engines import *
from .handler import Handler
from .helpers import ProxyProtocolMixin, SizedPacketHandler, ZlibPacketHandler
def get_preferred_engine(choices=('epoll', 'poll', 'select')):
for choice in choices:
if choice in engines:
return engines[choice]
return engines['select']

View file

@ -0,0 +1,169 @@
import logging
import socket
import threading
import time
from collections import defaultdict, deque
from functools import total_ordering
from heapq import heappop, heappush
logger = logging.getLogger('event_socket_server')
class SendMessage(object):
__slots__ = ('data', 'callback')
def __init__(self, data, callback):
self.data = data
self.callback = callback
@total_ordering
class ScheduledJob(object):
__slots__ = ('time', 'func', 'args', 'kwargs', 'cancel', 'dispatched')
def __init__(self, time, func, args, kwargs):
self.time = time
self.func = func
self.args = args
self.kwargs = kwargs
self.cancel = False
self.dispatched = False
def __eq__(self, other):
return self.time == other.time
def __lt__(self, other):
return self.time < other.time
class BaseServer(object):
def __init__(self, addresses, client):
self._servers = set()
for address, port in addresses:
info = socket.getaddrinfo(address, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
for af, socktype, proto, canonname, sa in info:
sock = socket.socket(af, socktype, proto)
sock.setblocking(0)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(sa)
self._servers.add(sock)
self._stop = threading.Event()
self._clients = set()
self._ClientClass = client
self._send_queue = defaultdict(deque)
self._job_queue = []
self._job_queue_lock = threading.Lock()
def _serve(self):
raise NotImplementedError()
def _accept(self, sock):
conn, address = sock.accept()
conn.setblocking(0)
client = self._ClientClass(self, conn)
self._clients.add(client)
return client
def schedule(self, delay, func, *args, **kwargs):
with self._job_queue_lock:
job = ScheduledJob(time.time() + delay, func, args, kwargs)
heappush(self._job_queue, job)
return job
def unschedule(self, job):
with self._job_queue_lock:
if job.dispatched or job.cancel:
return False
job.cancel = True
return True
def _register_write(self, client):
raise NotImplementedError()
def _register_read(self, client):
raise NotImplementedError()
def _clean_up_client(self, client, finalize=False):
try:
del self._send_queue[client.fileno()]
except KeyError:
pass
client.on_close()
client._socket.close()
if not finalize:
self._clients.remove(client)
def _dispatch_event(self):
t = time.time()
tasks = []
with self._job_queue_lock:
while True:
dt = self._job_queue[0].time - t if self._job_queue else 1
if dt > 0:
break
task = heappop(self._job_queue)
task.dispatched = True
if not task.cancel:
tasks.append(task)
for task in tasks:
logger.debug('Dispatching event: %r(*%r, **%r)', task.func, task.args, task.kwargs)
task.func(*task.args, **task.kwargs)
if not self._job_queue or dt > 1:
dt = 1
return dt
def _nonblock_read(self, client):
try:
data = client._socket.recv(1024)
except socket.error:
self._clean_up_client(client)
else:
logger.debug('Read from %s: %d bytes', client.client_address, len(data))
if not data:
self._clean_up_client(client)
else:
try:
client._recv_data(data)
except Exception:
logger.exception('Client recv_data failure')
self._clean_up_client(client)
def _nonblock_write(self, client):
fd = client.fileno()
queue = self._send_queue[fd]
try:
top = queue[0]
cb = client._socket.send(top.data)
top.data = top.data[cb:]
logger.debug('Send to %s: %d bytes', client.client_address, cb)
if not top.data:
logger.debug('Finished sending: %s', client.client_address)
if top.callback is not None:
logger.debug('Calling callback: %s: %r', client.client_address, top.callback)
try:
top.callback()
except Exception:
logger.exception('Client write callback failure')
self._clean_up_client(client)
return
queue.popleft()
if not queue:
self._register_read(client)
del self._send_queue[fd]
except socket.error:
self._clean_up_client(client)
def send(self, client, data, callback=None):
logger.debug('Writing %d bytes to client %s, callback: %s', len(data), client.client_address, callback)
self._send_queue[client.fileno()].append(SendMessage(data, callback))
self._register_write(client)
def stop(self):
self._stop.set()
def serve_forever(self):
self._serve()
def on_shutdown(self):
pass

View file

@ -0,0 +1,17 @@
import select
__author__ = 'Quantum'
engines = {}
from .select_server import SelectServer # noqa: E402, import not at top for consistency
engines['select'] = SelectServer
if hasattr(select, 'poll'):
from .poll_server import PollServer
engines['poll'] = PollServer
if hasattr(select, 'epoll'):
from .epoll_server import EpollServer
engines['epoll'] = EpollServer
del select

View file

@ -0,0 +1,17 @@
import select
__author__ = 'Quantum'
if not hasattr(select, 'epoll'):
raise ImportError('System does not support epoll')
from .poll_server import PollServer # noqa: E402, must be imported here
class EpollServer(PollServer):
poll = select.epoll
WRITE = select.EPOLLIN | select.EPOLLOUT | select.EPOLLERR | select.EPOLLHUP
READ = select.EPOLLIN | select.EPOLLERR | select.EPOLLHUP
POLLIN = select.EPOLLIN
POLLOUT = select.EPOLLOUT
POLL_CLOSE = select.EPOLLHUP | select.EPOLLERR
NEED_CLOSE = True

View file

@ -0,0 +1,97 @@
import errno
import logging
import select
import threading
from ..base_server import BaseServer
logger = logging.getLogger('event_socket_server')
if not hasattr(select, 'poll'):
raise ImportError('System does not support poll')
class PollServer(BaseServer):
poll = select.poll
WRITE = select.POLLIN | select.POLLOUT | select.POLLERR | select.POLLHUP
READ = select.POLLIN | select.POLLERR | select.POLLHUP
POLLIN = select.POLLIN
POLLOUT = select.POLLOUT
POLL_CLOSE = select.POLLERR | select.POLLHUP
NEED_CLOSE = False
def __init__(self, *args, **kwargs):
super(PollServer, self).__init__(*args, **kwargs)
self._poll = self.poll()
self._fdmap = {}
self._server_fds = {sock.fileno(): sock for sock in self._servers}
self._close_lock = threading.RLock()
def _register_write(self, client):
logger.debug('On write mode: %s', client.client_address)
self._poll.modify(client.fileno(), self.WRITE)
def _register_read(self, client):
logger.debug('On read mode: %s', client.client_address)
self._poll.modify(client.fileno(), self.READ)
def _clean_up_client(self, client, finalize=False):
logger.debug('Taking close lock: cleanup')
with self._close_lock:
logger.debug('Cleaning up client: %s, finalize: %d', client.client_address, finalize)
fd = client.fileno()
try:
self._poll.unregister(fd)
except IOError as e:
if e.errno != errno.ENOENT:
raise
except KeyError:
pass
del self._fdmap[fd]
super(PollServer, self)._clean_up_client(client, finalize)
def _serve(self):
for fd, sock in self._server_fds.items():
self._poll.register(fd, self.POLLIN)
sock.listen(16)
try:
while not self._stop.is_set():
for fd, event in self._poll.poll(self._dispatch_event()):
if fd in self._server_fds:
client = self._accept(self._server_fds[fd])
logger.debug('Accepting: %s', client.client_address)
fd = client.fileno()
self._poll.register(fd, self.READ)
self._fdmap[fd] = client
elif event & self.POLL_CLOSE:
logger.debug('Client closed: %s', self._fdmap[fd].client_address)
self._clean_up_client(self._fdmap[fd])
else:
logger.debug('Taking close lock: event loop')
with self._close_lock:
try:
client = self._fdmap[fd]
except KeyError:
pass
else:
logger.debug('Client active: %s, read: %d, write: %d',
client.client_address,
event & self.POLLIN,
event & self.POLLOUT)
if event & self.POLLIN:
logger.debug('Non-blocking read on client: %s', client.client_address)
self._nonblock_read(client)
# Might be closed in the read handler.
if event & self.POLLOUT and fd in self._fdmap:
logger.debug('Non-blocking write on client: %s', client.client_address)
self._nonblock_write(client)
finally:
logger.info('Shutting down server')
self.on_shutdown()
for client in self._clients:
self._clean_up_client(client, True)
for fd, sock in self._server_fds.items():
self._poll.unregister(fd)
sock.close()
if self.NEED_CLOSE:
self._poll.close()

View file

@ -0,0 +1,49 @@
import select
from ..base_server import BaseServer
class SelectServer(BaseServer):
def __init__(self, *args, **kwargs):
super(SelectServer, self).__init__(*args, **kwargs)
self._reads = set(self._servers)
self._writes = set()
def _register_write(self, client):
self._writes.add(client)
def _register_read(self, client):
self._writes.remove(client)
def _clean_up_client(self, client, finalize=False):
self._writes.discard(client)
self._reads.remove(client)
super(SelectServer, self)._clean_up_client(client, finalize)
def _serve(self, select=select.select):
for server in self._servers:
server.listen(16)
try:
while not self._stop.is_set():
r, w, x = select(self._reads, self._writes, self._reads, self._dispatch_event())
for s in r:
if s in self._servers:
self._reads.add(self._accept(s))
else:
self._nonblock_read(s)
for client in w:
self._nonblock_write(client)
for s in x:
s.close()
if s in self._servers:
raise RuntimeError('Server is in exceptional condition')
else:
self._clean_up_client(s)
finally:
self.on_shutdown()
for client in self._clients:
self._clean_up_client(client, True)
for server in self._servers:
server.close()

View file

@ -0,0 +1,27 @@
__author__ = 'Quantum'
class Handler(object):
def __init__(self, server, socket):
self._socket = socket
self.server = server
self.client_address = socket.getpeername()
def fileno(self):
return self._socket.fileno()
def _recv_data(self, data):
raise NotImplementedError
def _send(self, data, callback=None):
return self.server.send(self, data, callback)
def close(self):
self.server._clean_up_client(self)
def on_close(self):
pass
@property
def socket(self):
return self._socket

View file

@ -0,0 +1,125 @@
import struct
import zlib
from judge.utils.unicode import utf8text
from .handler import Handler
size_pack = struct.Struct('!I')
class SizedPacketHandler(Handler):
def __init__(self, server, socket):
super(SizedPacketHandler, self).__init__(server, socket)
self._buffer = b''
self._packetlen = 0
def _packet(self, data):
raise NotImplementedError()
def _format_send(self, data):
return data
def _recv_data(self, data):
self._buffer += data
while len(self._buffer) >= self._packetlen if self._packetlen else len(self._buffer) >= size_pack.size:
if self._packetlen:
data = self._buffer[:self._packetlen]
self._buffer = self._buffer[self._packetlen:]
self._packetlen = 0
self._packet(data)
else:
data = self._buffer[:size_pack.size]
self._buffer = self._buffer[size_pack.size:]
self._packetlen = size_pack.unpack(data)[0]
def send(self, data, callback=None):
data = self._format_send(data)
self._send(size_pack.pack(len(data)) + data, callback)
class ZlibPacketHandler(SizedPacketHandler):
def _format_send(self, data):
return zlib.compress(data.encode('utf-8'))
def packet(self, data):
raise NotImplementedError()
def _packet(self, data):
try:
self.packet(zlib.decompress(data).decode('utf-8'))
except zlib.error as e:
self.malformed_packet(e)
def malformed_packet(self, exception):
self.close()
class ProxyProtocolMixin(object):
__UNKNOWN_TYPE = 0
__PROXY1 = 1
__PROXY2 = 2
__DATA = 3
__HEADER2 = b'\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A'
__HEADER2_LEN = len(__HEADER2)
_REAL_IP_SET = None
@classmethod
def with_proxy_set(cls, ranges):
from netaddr import IPSet, IPGlob
from itertools import chain
globs = []
addrs = []
for item in ranges:
if '*' in item or '-' in item:
globs.append(IPGlob(item))
else:
addrs.append(item)
ipset = IPSet(chain(chain.from_iterable(globs), addrs))
return type(cls.__name__, (cls,), {'_REAL_IP_SET': ipset})
def __init__(self, server, socket):
super(ProxyProtocolMixin, self).__init__(server, socket)
self.__buffer = b''
self.__type = (self.__UNKNOWN_TYPE if self._REAL_IP_SET and
self.client_address[0] in self._REAL_IP_SET else self.__DATA)
def __parse_proxy1(self, data):
self.__buffer += data
index = self.__buffer.find(b'\r\n')
if 0 <= index < 106:
proxy = data[:index].split()
if len(proxy) < 2:
return self.close()
if proxy[1] == b'TCP4':
if len(proxy) != 6:
return self.close()
self.client_address = (utf8text(proxy[2]), utf8text(proxy[4]))
self.server_address = (utf8text(proxy[3]), utf8text(proxy[5]))
elif proxy[1] == b'TCP6':
self.client_address = (utf8text(proxy[2]), utf8text(proxy[4]), 0, 0)
self.server_address = (utf8text(proxy[3]), utf8text(proxy[5]), 0, 0)
elif proxy[1] != b'UNKNOWN':
return self.close()
self.__type = self.__DATA
super(ProxyProtocolMixin, self)._recv_data(data[index + 2:])
elif len(self.__buffer) > 107 or index > 105:
self.close()
def _recv_data(self, data):
if self.__type == self.__DATA:
super(ProxyProtocolMixin, self)._recv_data(data)
elif self.__type == self.__UNKNOWN_TYPE:
if len(data) >= 16 and data[:self.__HEADER2_LEN] == self.__HEADER2:
self.close()
elif len(data) >= 8 and data[:5] == b'PROXY':
self.__type = self.__PROXY1
self.__parse_proxy1(data)
else:
self.__type = self.__DATA
super(ProxyProtocolMixin, self)._recv_data(data)
else:
self.__parse_proxy1(data)

View file

@ -0,0 +1,94 @@
import ctypes
import socket
import struct
import time
import zlib
size_pack = struct.Struct('!I')
try:
RtlGenRandom = ctypes.windll.advapi32.SystemFunction036
except AttributeError:
RtlGenRandom = None
def open_connection():
sock = socket.create_connection((host, port))
return sock
def zlibify(data):
data = zlib.compress(data.encode('utf-8'))
return size_pack.pack(len(data)) + data
def dezlibify(data, skip_head=True):
if skip_head:
data = data[size_pack.size:]
return zlib.decompress(data).decode('utf-8')
def random(length):
if RtlGenRandom is None:
with open('/dev/urandom') as f:
return f.read(length)
buf = ctypes.create_string_buffer(length)
RtlGenRandom(buf, length)
return buf.raw
def main():
global host, port
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-l', '--host', default='localhost')
parser.add_argument('-p', '--port', default=9999, type=int)
args = parser.parse_args()
host, port = args.host, args.port
print('Opening idle connection:', end=' ')
s1 = open_connection()
print('Success')
print('Opening hello world connection:', end=' ')
s2 = open_connection()
print('Success')
print('Sending Hello, World!', end=' ')
s2.sendall(zlibify('Hello, World!'))
print('Success')
print('Testing blank connection:', end=' ')
s3 = open_connection()
s3.close()
print('Success')
result = dezlibify(s2.recv(1024))
assert result == 'Hello, World!'
print(result)
s2.close()
print('Large random data test:', end=' ')
s4 = open_connection()
data = random(1000000)
print('Generated', end=' ')
s4.sendall(zlibify(data))
print('Sent', end=' ')
result = ''
while len(result) < size_pack.size:
result += s4.recv(1024)
size = size_pack.unpack(result[:size_pack.size])[0]
result = result[size_pack.size:]
while len(result) < size:
result += s4.recv(1024)
print('Received', end=' ')
assert dezlibify(result, False) == data
print('Success')
s4.close()
print('Test malformed connection:', end=' ')
s5 = open_connection()
s5.sendall(data[:100000])
s5.close()
print('Success')
print('Waiting for timeout to close idle connection:', end=' ')
time.sleep(6)
print('Done')
s1.close()
if __name__ == '__main__':
main()

View file

@ -0,0 +1,54 @@
from .engines import engines
from .helpers import ProxyProtocolMixin, ZlibPacketHandler
class EchoPacketHandler(ProxyProtocolMixin, ZlibPacketHandler):
def __init__(self, server, socket):
super(EchoPacketHandler, self).__init__(server, socket)
self._gotdata = False
self.server.schedule(5, self._kill_if_no_data)
def _kill_if_no_data(self):
if not self._gotdata:
print('Inactive client:', self._socket.getpeername())
self.close()
def packet(self, data):
self._gotdata = True
print('Data from %s: %r' % (self._socket.getpeername(), data[:30] if len(data) > 30 else data))
self.send(data)
def on_close(self):
self._gotdata = True
print('Closed client:', self._socket.getpeername())
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-l', '--host', action='append')
parser.add_argument('-p', '--port', type=int, action='append')
parser.add_argument('-e', '--engine', default='select', choices=sorted(engines.keys()))
try:
import netaddr
except ImportError:
netaddr = None
else:
parser.add_argument('-P', '--proxy', action='append')
args = parser.parse_args()
class TestServer(engines[args.engine]):
def _accept(self, sock):
client = super(TestServer, self)._accept(sock)
print('New connection:', client.socket.getpeername())
return client
handler = EchoPacketHandler
if netaddr is not None and args.proxy:
handler = handler.with_proxy_set(args.proxy)
server = TestServer(list(zip(args.host, args.port)), handler)
server.serve_forever()
if __name__ == '__main__':
main()

1
judge/__init__.py Normal file
View file

@ -0,0 +1 @@
default_app_config = 'judge.apps.JudgeAppConfig'

37
judge/admin/__init__.py Normal file
View 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
View 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
View 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
View 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')

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

@ -0,0 +1,6 @@
from .djangohandler import DjangoHandler
from .djangoserver import DjangoServer
from .judgecallback import DjangoJudgeHandler
from .judgehandler import JudgeHandler
from .judgelist import JudgeList
from .judgeserver import JudgeServer

View file

@ -0,0 +1,67 @@
import json
import logging
import struct
from event_socket_server import ZlibPacketHandler
logger = logging.getLogger('judge.bridge')
size_pack = struct.Struct('!I')
class DjangoHandler(ZlibPacketHandler):
def __init__(self, server, socket):
super(DjangoHandler, self).__init__(server, socket)
self.handlers = {
'submission-request': self.on_submission,
'terminate-submission': self.on_termination,
'disconnect-judge': self.on_disconnect,
}
self._to_kill = True
# self.server.schedule(5, self._kill_if_no_request)
def _kill_if_no_request(self):
if self._to_kill:
logger.info('Killed inactive connection: %s', self._socket.getpeername())
self.close()
def _format_send(self, data):
return super(DjangoHandler, self)._format_send(json.dumps(data, separators=(',', ':')))
def packet(self, packet):
self._to_kill = False
packet = json.loads(packet)
try:
result = self.handlers.get(packet.get('name', None), self.on_malformed)(packet)
except Exception:
logger.exception('Error in packet handling (Django-facing)')
result = {'name': 'bad-request'}
self.send(result, self._schedule_close)
def _schedule_close(self):
self.server.schedule(0, self.close)
def on_submission(self, data):
id = data['submission-id']
problem = data['problem-id']
language = data['language']
source = data['source']
priority = data['priority']
if not self.server.judges.check_priority(priority):
return {'name': 'bad-request'}
self.server.judges.judge(id, problem, language, source, priority)
return {'name': 'submission-received', 'submission-id': id}
def on_termination(self, data):
return {'name': 'submission-received', 'judge-aborted': self.server.judges.abort(data['submission-id'])}
def on_disconnect(self, data):
judge_id = data['judge-id']
force = data['force']
self.server.judges.disconnect(judge_id, force=force)
def on_malformed(self, packet):
logger.error('Malformed packet: %s', packet)
def on_close(self):
self._to_kill = False

View file

@ -0,0 +1,7 @@
from event_socket_server import get_preferred_engine
class DjangoServer(get_preferred_engine()):
def __init__(self, judges, *args, **kwargs):
super(DjangoServer, self).__init__(*args, **kwargs)
self.judges = judges

View file

@ -0,0 +1,411 @@
import json
import logging
import time
from operator import itemgetter
from django import db
from django.utils import timezone
from judge import event_poster as event
from judge.caching import finished_submission
from judge.models import Judge, Language, LanguageLimit, Problem, RuntimeVersion, Submission, SubmissionTestCase
from .judgehandler import JudgeHandler, SubmissionData
logger = logging.getLogger('judge.bridge')
json_log = logging.getLogger('judge.json.bridge')
UPDATE_RATE_LIMIT = 5
UPDATE_RATE_TIME = 0.5
def _ensure_connection():
try:
db.connection.cursor().execute('SELECT 1').fetchall()
except Exception:
db.connection.close()
class DjangoJudgeHandler(JudgeHandler):
def __init__(self, server, socket):
super(DjangoJudgeHandler, self).__init__(server, socket)
# each value is (updates, last reset)
self.update_counter = {}
self.judge = None
self.judge_address = None
self._submission_cache_id = None
self._submission_cache = {}
json_log.info(self._make_json_log(action='connect'))
def on_close(self):
super(DjangoJudgeHandler, self).on_close()
json_log.info(self._make_json_log(action='disconnect', info='judge disconnected'))
if self._working:
Submission.objects.filter(id=self._working).update(status='IE', result='IE')
json_log.error(self._make_json_log(sub=self._working, action='close', info='IE due to shutdown on grading'))
def on_malformed(self, packet):
super(DjangoJudgeHandler, self).on_malformed(packet)
json_log.exception(self._make_json_log(sub=self._working, info='malformed zlib packet'))
def _packet_exception(self):
json_log.exception(self._make_json_log(sub=self._working, info='packet processing exception'))
def get_related_submission_data(self, submission):
_ensure_connection() # We are called from the django-facing daemon thread. Guess what happens.
try:
pid, time, memory, short_circuit, lid, is_pretested, sub_date, uid, part_virtual, part_id = (
Submission.objects.filter(id=submission)
.values_list('problem__id', 'problem__time_limit', 'problem__memory_limit',
'problem__short_circuit', 'language__id', 'is_pretested', 'date', 'user__id',
'contest__participation__virtual', 'contest__participation__id')).get()
except Submission.DoesNotExist:
logger.error('Submission vanished: %s', submission)
json_log.error(self._make_json_log(
sub=self._working, action='request',
info='submission vanished when fetching info',
))
return
attempt_no = Submission.objects.filter(problem__id=pid, contest__participation__id=part_id, user__id=uid,
date__lt=sub_date).exclude(status__in=('CE', 'IE')).count() + 1
try:
time, memory = (LanguageLimit.objects.filter(problem__id=pid, language__id=lid)
.values_list('time_limit', 'memory_limit').get())
except LanguageLimit.DoesNotExist:
pass
return SubmissionData(
time=time,
memory=memory,
short_circuit=short_circuit,
pretests_only=is_pretested,
contest_no=part_virtual,
attempt_no=attempt_no,
user_id=uid,
)
def _authenticate(self, id, key):
result = Judge.objects.filter(name=id, auth_key=key, is_blocked=False).exists()
if not result:
json_log.warning(self._make_json_log(action='auth', judge=id, info='judge failed authentication'))
return result
def _connected(self):
judge = self.judge = Judge.objects.get(name=self.name)
judge.start_time = timezone.now()
judge.online = True
judge.problems.set(Problem.objects.filter(code__in=list(self.problems.keys())))
judge.runtimes.set(Language.objects.filter(key__in=list(self.executors.keys())))
# Delete now in case we somehow crashed and left some over from the last connection
RuntimeVersion.objects.filter(judge=judge).delete()
versions = []
for lang in judge.runtimes.all():
versions += [
RuntimeVersion(language=lang, name=name, version='.'.join(map(str, version)), priority=idx, judge=judge)
for idx, (name, version) in enumerate(self.executors[lang.key])
]
RuntimeVersion.objects.bulk_create(versions)
judge.last_ip = self.client_address[0]
judge.save()
self.judge_address = '[%s]:%s' % (self.client_address[0], self.client_address[1])
json_log.info(self._make_json_log(action='auth', info='judge successfully authenticated',
executors=list(self.executors.keys())))
def _disconnected(self):
Judge.objects.filter(id=self.judge.id).update(online=False)
RuntimeVersion.objects.filter(judge=self.judge).delete()
def _update_ping(self):
try:
Judge.objects.filter(name=self.name).update(ping=self.latency, load=self.load)
except Exception as e:
# What can I do? I don't want to tie this to MySQL.
if e.__class__.__name__ == 'OperationalError' and e.__module__ == '_mysql_exceptions' and e.args[0] == 2006:
db.connection.close()
def _post_update_submission(self, id, state, done=False):
if self._submission_cache_id == id:
data = self._submission_cache
else:
self._submission_cache = data = Submission.objects.filter(id=id).values(
'problem__is_public', 'contest__participation__contest__key',
'user_id', 'problem_id', 'status', 'language__key',
).get()
self._submission_cache_id = id
if data['problem__is_public']:
event.post('submissions', {
'type': 'done-submission' if done else 'update-submission',
'state': state, 'id': id,
'contest': data['contest__participation__contest__key'],
'user': data['user_id'], 'problem': data['problem_id'],
'status': data['status'], 'language': data['language__key'],
})
def on_submission_processing(self, packet):
id = packet['submission-id']
if Submission.objects.filter(id=id).update(status='P', judged_on=self.judge):
event.post('sub_%s' % Submission.get_id_secret(id), {'type': 'processing'})
self._post_update_submission(id, 'processing')
json_log.info(self._make_json_log(packet, action='processing'))
else:
logger.warning('Unknown submission: %s', id)
json_log.error(self._make_json_log(packet, action='processing', info='unknown submission'))
def on_submission_wrong_acknowledge(self, packet, expected, got):
json_log.error(self._make_json_log(packet, action='processing', info='wrong-acknowledge', expected=expected))
def on_grading_begin(self, packet):
super(DjangoJudgeHandler, self).on_grading_begin(packet)
if Submission.objects.filter(id=packet['submission-id']).update(
status='G', is_pretested=packet['pretested'],
current_testcase=1, batch=False):
SubmissionTestCase.objects.filter(submission_id=packet['submission-id']).delete()
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'grading-begin'})
self._post_update_submission(packet['submission-id'], 'grading-begin')
json_log.info(self._make_json_log(packet, action='grading-begin'))
else:
logger.warning('Unknown submission: %s', packet['submission-id'])
json_log.error(self._make_json_log(packet, action='grading-begin', info='unknown submission'))
def _submission_is_batch(self, id):
if not Submission.objects.filter(id=id).update(batch=True):
logger.warning('Unknown submission: %s', id)
def on_grading_end(self, packet):
super(DjangoJudgeHandler, self).on_grading_end(packet)
try:
submission = Submission.objects.get(id=packet['submission-id'])
except Submission.DoesNotExist:
logger.warning('Unknown submission: %s', packet['submission-id'])
json_log.error(self._make_json_log(packet, action='grading-end', info='unknown submission'))
return
time = 0
memory = 0
points = 0.0
total = 0
status = 0
status_codes = ['SC', 'AC', 'WA', 'MLE', 'TLE', 'IR', 'RTE', 'OLE']
batches = {} # batch number: (points, total)
for case in SubmissionTestCase.objects.filter(submission=submission):
time += case.time
if not case.batch:
points += case.points
total += case.total
else:
if case.batch in batches:
batches[case.batch][0] = min(batches[case.batch][0], case.points)
batches[case.batch][1] = max(batches[case.batch][1], case.total)
else:
batches[case.batch] = [case.points, case.total]
memory = max(memory, case.memory)
i = status_codes.index(case.status)
if i > status:
status = i
for i in batches:
points += batches[i][0]
total += batches[i][1]
points = round(points, 1)
total = round(total, 1)
submission.case_points = points
submission.case_total = total
problem = submission.problem
sub_points = round(points / total * problem.points if total > 0 else 0, 3)
if not problem.partial and sub_points != problem.points:
sub_points = 0
submission.status = 'D'
submission.time = time
submission.memory = memory
submission.points = sub_points
submission.result = status_codes[status]
submission.save()
json_log.info(self._make_json_log(
packet, action='grading-end', time=time, memory=memory,
points=sub_points, total=problem.points, result=submission.result,
case_points=points, case_total=total, user=submission.user_id,
problem=problem.code, finish=True,
))
submission.user._updating_stats_only = True
submission.user.calculate_points()
problem._updating_stats_only = True
problem.update_stats()
submission.update_contest()
finished_submission(submission)
event.post('sub_%s' % submission.id_secret, {
'type': 'grading-end',
'time': time,
'memory': memory,
'points': float(points),
'total': float(problem.points),
'result': submission.result,
})
if hasattr(submission, 'contest'):
participation = submission.contest.participation
event.post('contest_%d' % participation.contest_id, {'type': 'update'})
self._post_update_submission(submission.id, 'grading-end', done=True)
def on_compile_error(self, packet):
super(DjangoJudgeHandler, self).on_compile_error(packet)
if Submission.objects.filter(id=packet['submission-id']).update(status='CE', result='CE', error=packet['log']):
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {
'type': 'compile-error',
'log': packet['log'],
})
self._post_update_submission(packet['submission-id'], 'compile-error', done=True)
json_log.info(self._make_json_log(packet, action='compile-error', log=packet['log'],
finish=True, result='CE'))
else:
logger.warning('Unknown submission: %s', packet['submission-id'])
json_log.error(self._make_json_log(packet, action='compile-error', info='unknown submission',
log=packet['log'], finish=True, result='CE'))
def on_compile_message(self, packet):
super(DjangoJudgeHandler, self).on_compile_message(packet)
if Submission.objects.filter(id=packet['submission-id']).update(error=packet['log']):
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'compile-message'})
json_log.info(self._make_json_log(packet, action='compile-message', log=packet['log']))
else:
logger.warning('Unknown submission: %s', packet['submission-id'])
json_log.error(self._make_json_log(packet, action='compile-message', info='unknown submission',
log=packet['log']))
def on_internal_error(self, packet):
super(DjangoJudgeHandler, self).on_internal_error(packet)
id = packet['submission-id']
if Submission.objects.filter(id=id).update(status='IE', result='IE', error=packet['message']):
event.post('sub_%s' % Submission.get_id_secret(id), {'type': 'internal-error'})
self._post_update_submission(id, 'internal-error', done=True)
json_log.info(self._make_json_log(packet, action='internal-error', message=packet['message'],
finish=True, result='IE'))
else:
logger.warning('Unknown submission: %s', id)
json_log.error(self._make_json_log(packet, action='internal-error', info='unknown submission',
message=packet['message'], finish=True, result='IE'))
def on_submission_terminated(self, packet):
super(DjangoJudgeHandler, self).on_submission_terminated(packet)
if Submission.objects.filter(id=packet['submission-id']).update(status='AB', result='AB'):
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'aborted-submission'})
self._post_update_submission(packet['submission-id'], 'terminated', done=True)
json_log.info(self._make_json_log(packet, action='aborted', finish=True, result='AB'))
else:
logger.warning('Unknown submission: %s', packet['submission-id'])
json_log.error(self._make_json_log(packet, action='aborted', info='unknown submission',
finish=True, result='AB'))
def on_batch_begin(self, packet):
super(DjangoJudgeHandler, self).on_batch_begin(packet)
json_log.info(self._make_json_log(packet, action='batch-begin', batch=self.batch_id))
def on_batch_end(self, packet):
super(DjangoJudgeHandler, self).on_batch_end(packet)
json_log.info(self._make_json_log(packet, action='batch-end', batch=self.batch_id))
def on_test_case(self, packet, max_feedback=SubmissionTestCase._meta.get_field('feedback').max_length):
super(DjangoJudgeHandler, self).on_test_case(packet)
id = packet['submission-id']
updates = packet['cases']
max_position = max(map(itemgetter('position'), updates))
if not Submission.objects.filter(id=id).update(current_testcase=max_position + 1):
logger.warning('Unknown submission: %s', id)
json_log.error(self._make_json_log(packet, action='test-case', info='unknown submission'))
return
bulk_test_case_updates = []
for result in updates:
test_case = SubmissionTestCase(submission_id=id, case=result['position'])
status = result['status']
if status & 4:
test_case.status = 'TLE'
elif status & 8:
test_case.status = 'MLE'
elif status & 64:
test_case.status = 'OLE'
elif status & 2:
test_case.status = 'RTE'
elif status & 16:
test_case.status = 'IR'
elif status & 1:
test_case.status = 'WA'
elif status & 32:
test_case.status = 'SC'
else:
test_case.status = 'AC'
test_case.time = result['time']
test_case.memory = result['memory']
test_case.points = result['points']
test_case.total = result['total-points']
test_case.batch = self.batch_id if self.in_batch else None
test_case.feedback = (result.get('feedback') or '')[:max_feedback]
test_case.extended_feedback = result.get('extended-feedback') or ''
test_case.output = result['output']
bulk_test_case_updates.append(test_case)
json_log.info(self._make_json_log(
packet, action='test-case', case=test_case.case, batch=test_case.batch,
time=test_case.time, memory=test_case.memory, feedback=test_case.feedback,
extended_feedback=test_case.extended_feedback, output=test_case.output,
points=test_case.points, total=test_case.total, status=test_case.status,
))
do_post = True
if id in self.update_counter:
cnt, reset = self.update_counter[id]
cnt += 1
if time.monotonic() - reset > UPDATE_RATE_TIME:
del self.update_counter[id]
else:
self.update_counter[id] = (cnt, reset)
if cnt > UPDATE_RATE_LIMIT:
do_post = False
if id not in self.update_counter:
self.update_counter[id] = (1, time.monotonic())
if do_post:
event.post('sub_%s' % Submission.get_id_secret(id), {
'type': 'test-case',
'id': max_position,
})
self._post_update_submission(id, state='test-case')
SubmissionTestCase.objects.bulk_create(bulk_test_case_updates)
def on_supported_problems(self, packet):
super(DjangoJudgeHandler, self).on_supported_problems(packet)
self.judge.problems.set(Problem.objects.filter(code__in=list(self.problems.keys())))
json_log.info(self._make_json_log(action='update-problems', count=len(self.problems)))
def _make_json_log(self, packet=None, sub=None, **kwargs):
data = {
'judge': self.name,
'address': self.judge_address,
}
if sub is None and packet is not None:
sub = packet.get('submission-id')
if sub is not None:
data['submission'] = sub
data.update(kwargs)
return json.dumps(data)

View file

@ -0,0 +1,268 @@
import json
import logging
import time
from collections import deque, namedtuple
from event_socket_server import ProxyProtocolMixin, ZlibPacketHandler
logger = logging.getLogger('judge.bridge')
SubmissionData = namedtuple('SubmissionData', 'time memory short_circuit pretests_only contest_no attempt_no user_id')
class JudgeHandler(ProxyProtocolMixin, ZlibPacketHandler):
def __init__(self, server, socket):
super(JudgeHandler, self).__init__(server, socket)
self.handlers = {
'grading-begin': self.on_grading_begin,
'grading-end': self.on_grading_end,
'compile-error': self.on_compile_error,
'compile-message': self.on_compile_message,
'batch-begin': self.on_batch_begin,
'batch-end': self.on_batch_end,
'test-case-status': self.on_test_case,
'internal-error': self.on_internal_error,
'submission-terminated': self.on_submission_terminated,
'submission-acknowledged': self.on_submission_acknowledged,
'ping-response': self.on_ping_response,
'supported-problems': self.on_supported_problems,
'handshake': self.on_handshake,
}
self._to_kill = True
self._working = False
self._no_response_job = None
self._problems = []
self.executors = []
self.problems = {}
self.latency = None
self.time_delta = None
self.load = 1e100
self.name = None
self.batch_id = None
self.in_batch = False
self._ping_average = deque(maxlen=6) # 1 minute average, just like load
self._time_delta = deque(maxlen=6)
self.server.schedule(15, self._kill_if_no_auth)
logger.info('Judge connected from: %s', self.client_address)
def _kill_if_no_auth(self):
if self._to_kill:
logger.info('Judge not authenticated: %s', self.client_address)
self.close()
def on_close(self):
self._to_kill = False
if self._no_response_job:
self.server.unschedule(self._no_response_job)
self.server.judges.remove(self)
if self.name is not None:
self._disconnected()
logger.info('Judge disconnected from: %s', self.client_address)
def _authenticate(self, id, key):
return False
def _connected(self):
pass
def _disconnected(self):
pass
def _update_ping(self):
pass
def _format_send(self, data):
return super(JudgeHandler, self)._format_send(json.dumps(data, separators=(',', ':')))
def on_handshake(self, packet):
if 'id' not in packet or 'key' not in packet:
logger.warning('Malformed handshake: %s', self.client_address)
self.close()
return
if not self._authenticate(packet['id'], packet['key']):
logger.warning('Authentication failure: %s', self.client_address)
self.close()
return
self._to_kill = False
self._problems = packet['problems']
self.problems = dict(self._problems)
self.executors = packet['executors']
self.name = packet['id']
self.send({'name': 'handshake-success'})
logger.info('Judge authenticated: %s (%s)', self.client_address, packet['id'])
self.server.judges.register(self)
self._connected()
def can_judge(self, problem, executor):
return problem in self.problems and executor in self.executors
@property
def working(self):
return bool(self._working)
def get_related_submission_data(self, submission):
return SubmissionData(
time=2,
memory=16384,
short_circuit=False,
pretests_only=False,
contest_no=None,
attempt_no=1,
user_id=None,
)
def disconnect(self, force=False):
if force:
# Yank the power out.
self.close()
else:
self.send({'name': 'disconnect'})
def submit(self, id, problem, language, source):
data = self.get_related_submission_data(id)
self._working = id
self._no_response_job = self.server.schedule(20, self._kill_if_no_response)
self.send({
'name': 'submission-request',
'submission-id': id,
'problem-id': problem,
'language': language,
'source': source,
'time-limit': data.time,
'memory-limit': data.memory,
'short-circuit': data.short_circuit,
'meta': {
'pretests-only': data.pretests_only,
'in-contest': data.contest_no,
'attempt-no': data.attempt_no,
'user': data.user_id,
},
})
def _kill_if_no_response(self):
logger.error('Judge seems dead: %s: %s', self.name, self._working)
self.close()
def malformed_packet(self, exception):
logger.exception('Judge sent malformed packet: %s', self.name)
super(JudgeHandler, self).malformed_packet(exception)
def on_submission_processing(self, packet):
pass
def on_submission_wrong_acknowledge(self, packet, expected, got):
pass
def on_submission_acknowledged(self, packet):
if not packet.get('submission-id', None) == self._working:
logger.error('Wrong acknowledgement: %s: %s, expected: %s', self.name, packet.get('submission-id', None),
self._working)
self.on_submission_wrong_acknowledge(packet, self._working, packet.get('submission-id', None))
self.close()
logger.info('Submission acknowledged: %d', self._working)
if self._no_response_job:
self.server.unschedule(self._no_response_job)
self._no_response_job = None
self.on_submission_processing(packet)
def abort(self):
self.send({'name': 'terminate-submission'})
def get_current_submission(self):
return self._working or None
def ping(self):
self.send({'name': 'ping', 'when': time.time()})
def packet(self, data):
try:
try:
data = json.loads(data)
if 'name' not in data:
raise ValueError
except ValueError:
self.on_malformed(data)
else:
handler = self.handlers.get(data['name'], self.on_malformed)
handler(data)
except Exception:
logger.exception('Error in packet handling (Judge-side): %s', self.name)
self._packet_exception()
# You can't crash here because you aren't so sure about the judges
# not being malicious or simply malforms. THIS IS A SERVER!
def _packet_exception(self):
pass
def _submission_is_batch(self, id):
pass
def on_supported_problems(self, packet):
logger.info('%s: Updated problem list', self.name)
self._problems = packet['problems']
self.problems = dict(self._problems)
if not self.working:
self.server.judges.update_problems(self)
def on_grading_begin(self, packet):
logger.info('%s: Grading has begun on: %s', self.name, packet['submission-id'])
self.batch_id = None
def on_grading_end(self, packet):
logger.info('%s: Grading has ended on: %s', self.name, packet['submission-id'])
self._free_self(packet)
self.batch_id = None
def on_compile_error(self, packet):
logger.info('%s: Submission failed to compile: %s', self.name, packet['submission-id'])
self._free_self(packet)
def on_compile_message(self, packet):
logger.info('%s: Submission generated compiler messages: %s', self.name, packet['submission-id'])
def on_internal_error(self, packet):
try:
raise ValueError('\n\n' + packet['message'])
except ValueError:
logger.exception('Judge %s failed while handling submission %s', self.name, packet['submission-id'])
self._free_self(packet)
def on_submission_terminated(self, packet):
logger.info('%s: Submission aborted: %s', self.name, packet['submission-id'])
self._free_self(packet)
def on_batch_begin(self, packet):
logger.info('%s: Batch began on: %s', self.name, packet['submission-id'])
self.in_batch = True
if self.batch_id is None:
self.batch_id = 0
self._submission_is_batch(packet['submission-id'])
self.batch_id += 1
def on_batch_end(self, packet):
self.in_batch = False
logger.info('%s: Batch ended on: %s', self.name, packet['submission-id'])
def on_test_case(self, packet):
logger.info('%s: %d test case(s) completed on: %s', self.name, len(packet['cases']), packet['submission-id'])
def on_malformed(self, packet):
logger.error('%s: Malformed packet: %s', self.name, packet)
def on_ping_response(self, packet):
end = time.time()
self._ping_average.append(end - packet['when'])
self._time_delta.append((end + packet['when']) / 2 - packet['time'])
self.latency = sum(self._ping_average) / len(self._ping_average)
self.time_delta = sum(self._time_delta) / len(self._time_delta)
self.load = packet['load']
self._update_ping()
def _free_self(self, packet):
self._working = False
self.server.judges.on_judge_free(self, packet['submission-id'])

123
judge/bridge/judgelist.py Normal file
View 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)

View file

@ -0,0 +1,68 @@
import logging
import os
import threading
import time
from event_socket_server import get_preferred_engine
from judge.models import Judge
from .judgelist import JudgeList
logger = logging.getLogger('judge.bridge')
def reset_judges():
Judge.objects.update(online=False, ping=None, load=None)
class JudgeServer(get_preferred_engine()):
def __init__(self, *args, **kwargs):
super(JudgeServer, self).__init__(*args, **kwargs)
reset_judges()
self.judges = JudgeList()
self.ping_judge_thread = threading.Thread(target=self.ping_judge, args=())
self.ping_judge_thread.daemon = True
self.ping_judge_thread.start()
def on_shutdown(self):
super(JudgeServer, self).on_shutdown()
reset_judges()
def ping_judge(self):
try:
while True:
for judge in self.judges:
judge.ping()
time.sleep(10)
except Exception:
logger.exception('Ping error')
raise
def main():
import argparse
import logging
from .judgehandler import JudgeHandler
format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s'
logging.basicConfig(format=format)
logging.getLogger().setLevel(logging.INFO)
handler = logging.FileHandler(os.path.join(os.path.dirname(__file__), 'judgeserver.log'), encoding='utf-8')
handler.setFormatter(logging.Formatter(format))
handler.setLevel(logging.INFO)
logging.getLogger().addHandler(handler)
parser = argparse.ArgumentParser(description='''
Runs the bridge between DMOJ website and judges.
''')
parser.add_argument('judge_host', nargs='+', action='append',
help='host to listen for the judge')
parser.add_argument('-p', '--judge-port', type=int, action='append',
help='port to listen for the judge')
args = parser.parse_args()
server = JudgeServer(list(zip(args.judge_host, args.judge_port)), JudgeHandler)
server.serve_forever()
if __name__ == '__main__':
main()

10
judge/caching.py Normal file
View 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
View file

@ -0,0 +1,122 @@
from django import forms
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db.models import Count
from django.db.models.expressions import F, Value
from django.db.models.functions import Coalesce
from django.forms import ModelForm
from django.http import HttpResponseForbidden, HttpResponseNotFound, HttpResponseRedirect
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
from django.views.generic import View
from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin
from reversion import revisions
from reversion.models import Revision, Version
from judge.dblock import LockModel
from judge.models import Comment, CommentLock, CommentVote
from judge.utils.raw_sql import RawSQLColumn, unique_together_left_join
from judge.widgets import HeavyPreviewPageDownWidget
class CommentForm(ModelForm):
class Meta:
model = Comment
fields = ['body', 'parent']
widgets = {
'parent': forms.HiddenInput(),
}
if HeavyPreviewPageDownWidget is not None:
widgets['body'] = HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'),
preview_timeout=1000, hide_preview_button=True)
def __init__(self, request, *args, **kwargs):
self.request = request
super(CommentForm, self).__init__(*args, **kwargs)
self.fields['body'].widget.attrs.update({'placeholder': _('Comment body')})
def clean(self):
if self.request is not None and self.request.user.is_authenticated:
profile = self.request.profile
if profile.mute:
raise ValidationError(_('Your part is silent, little toad.'))
elif (not self.request.user.is_staff and
not profile.submission_set.filter(points=F('problem__points')).exists()):
raise ValidationError(_('You need to have solved at least one problem '
'before your voice can be heard.'))
return super(CommentForm, self).clean()
class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
comment_page = None
def get_comment_page(self):
if self.comment_page is None:
raise NotImplementedError()
return self.comment_page
def is_comment_locked(self):
return (CommentLock.objects.filter(page=self.get_comment_page()).exists() and
not self.request.user.has_perm('judge.override_comment_lock'))
@method_decorator(login_required)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
page = self.get_comment_page()
if self.is_comment_locked():
return HttpResponseForbidden()
parent = request.POST.get('parent')
if parent:
try:
parent = int(parent)
except ValueError:
return HttpResponseNotFound()
else:
if not Comment.objects.filter(hidden=False, id=parent, page=page).exists():
return HttpResponseNotFound()
form = CommentForm(request, request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.author = request.profile
comment.page = page
with LockModel(write=(Comment, Revision, Version), read=(ContentType,)), revisions.create_revision():
revisions.set_user(request.user)
revisions.set_comment(_('Posted comment'))
comment.save()
return HttpResponseRedirect(request.path)
context = self.get_context_data(object=self.object, comment_form=form)
return self.render_to_response(context)
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return self.render_to_response(self.get_context_data(
object=self.object,
comment_form=CommentForm(request, initial={'page': self.get_comment_page(), 'parent': None}),
))
def get_context_data(self, **kwargs):
context = super(CommentedDetailView, self).get_context_data(**kwargs)
queryset = Comment.objects.filter(hidden=False, page=self.get_comment_page())
context['has_comments'] = queryset.exists()
context['comment_lock'] = self.is_comment_locked()
queryset = queryset.select_related('author__user').defer('author__about').annotate(revisions=Count('versions'))
if self.request.user.is_authenticated:
queryset = queryset.annotate(vote_score=Coalesce(RawSQLColumn(CommentVote, 'score'), Value(0)))
profile = self.request.profile
unique_together_left_join(queryset, CommentVote, 'comment', 'voter', profile.id)
context['is_new_user'] = (not self.request.user.is_staff and
not profile.submission_set.filter(points=F('problem__points')).exists())
context['comment_list'] = queryset
context['vote_hide_threshold'] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD
return context

View file

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

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

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

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

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

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

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

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

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

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

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

View file

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

View file

@ -0,0 +1,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
View 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
View 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
View 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
View 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
View 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
View 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()))

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

View file

View file

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

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

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

View 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