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

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