Add contest tester/curator (DMOJ)
This commit is contained in:
parent
06318a97e5
commit
297b8a2a36
25 changed files with 611 additions and 230 deletions
|
@ -7,10 +7,12 @@ from django.forms import ModelForm, ModelMultipleChoiceField
|
|||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _, ungettext
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from django_ace import AceWidget
|
||||
from judge.models import Contest, ContestProblem, ContestSubmission, Profile, Rating
|
||||
from judge.ratings import rate_contest
|
||||
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, AdminPagedownWidget, \
|
||||
|
@ -94,7 +96,9 @@ class ContestForm(ModelForm):
|
|||
|
||||
class Meta:
|
||||
widgets = {
|
||||
'organizers': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
|
||||
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
|
||||
'curators': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
|
||||
'testers': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
|
||||
'private_contestants': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
|
||||
attrs={'style': 'width: 100%'}),
|
||||
'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2'),
|
||||
|
@ -111,18 +115,19 @@ class ContestForm(ModelForm):
|
|||
|
||||
class ContestAdmin(VersionAdmin):
|
||||
fieldsets = (
|
||||
(None, {'fields': ('key', 'name', 'organizers')}),
|
||||
(_('Settings'), {'fields': ('is_visible', 'use_clarifications', 'hide_problem_tags', 'hide_scoreboard',
|
||||
(None, {'fields': ('key', 'name', 'authors', 'curators', 'testers')}),
|
||||
(_('Settings'), {'fields': ('is_visible', 'use_clarifications', 'hide_problem_tags', 'scoreboard_visibility',
|
||||
'run_pretests_only', 'points_precision')}),
|
||||
(_('Scheduling'), {'fields': ('start_time', 'end_time', 'time_limit')}),
|
||||
(_('Details'), {'fields': ('description', 'og_image', 'logo_override_image', 'tags', 'summary')}),
|
||||
(_('Format'), {'fields': ('format_name', 'format_config')}),
|
||||
(_('Format'), {'fields': ('format_name', 'format_config', 'problem_label_script')}),
|
||||
(_('Rating'), {'fields': ('is_rated', 'rate_all', 'rating_floor', 'rating_ceiling', 'rate_exclude')}),
|
||||
(_('Access'), {'fields': ('access_code', 'is_private', 'private_contestants', 'is_organization_private',
|
||||
'organizations', 'view_contest_scoreboard')}),
|
||||
(_('Justice'), {'fields': ('banned_users',)}),
|
||||
)
|
||||
list_display = ('key', 'name', 'is_visible', 'is_rated', 'start_time', 'end_time', 'time_limit', 'user_count')
|
||||
search_fields = ('key', 'name')
|
||||
inlines = [ContestProblemInline]
|
||||
actions_on_top = True
|
||||
actions_on_bottom = True
|
||||
|
@ -146,7 +151,7 @@ class ContestAdmin(VersionAdmin):
|
|||
if request.user.has_perm('judge.edit_all_contest'):
|
||||
return queryset
|
||||
else:
|
||||
return queryset.filter(organizers__id=request.profile.id)
|
||||
return queryset.filter(Q(authors=request.profile) | Q(curators=request.profile)).distinct()
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
readonly = []
|
||||
|
@ -158,6 +163,8 @@ class ContestAdmin(VersionAdmin):
|
|||
readonly += ['is_private', 'private_contestants', 'is_organization_private', 'organizations']
|
||||
if not request.user.has_perm('judge.change_contest_visibility'):
|
||||
readonly += ['is_visible']
|
||||
if not request.user.has_perm('judge.contest_problem_label'):
|
||||
readonly += ['problem_label_script']
|
||||
return readonly
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
|
@ -185,9 +192,9 @@ class ContestAdmin(VersionAdmin):
|
|||
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:
|
||||
if obj is None:
|
||||
return True
|
||||
return obj.organizers.filter(id=request.profile.id).exists()
|
||||
return obj.is_editable_by(request.user)
|
||||
|
||||
def _rescore(self, contest_key):
|
||||
from judge.tasks import rescore_contest
|
||||
|
@ -232,14 +239,10 @@ class ContestAdmin(VersionAdmin):
|
|||
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()
|
||||
with connection.cursor() as 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'):
|
||||
for contest in Contest.objects.filter(is_rated=True, end_time__lte=timezone.now()).order_by('end_time'):
|
||||
rate_contest(contest)
|
||||
return HttpResponseRedirect(reverse('admin:judge_contest_changelist'))
|
||||
|
||||
|
@ -247,16 +250,21 @@ class ContestAdmin(VersionAdmin):
|
|||
if not request.user.has_perm('judge.contest_rating'):
|
||||
raise PermissionDenied()
|
||||
contest = get_object_or_404(Contest, id=id)
|
||||
if not contest.is_rated:
|
||||
if not contest.is_rated or not contest.ended:
|
||||
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)
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
form = super(ContestAdmin, self).get_form(request, obj, **kwargs)
|
||||
if 'problem_label_script' in form.base_fields:
|
||||
# form.base_fields['problem_label_script'] does not exist when the user has only view permission
|
||||
# on the model.
|
||||
form.base_fields['problem_label_script'].widget = AceWidget('lua', request.profile.ace_theme)
|
||||
|
||||
perms = ('edit_own_contest', 'edit_all_contest')
|
||||
form.base_fields['organizers'].queryset = Profile.objects.filter(
|
||||
form.base_fields['curators'].queryset = Profile.objects.filter(
|
||||
Q(user__is_superuser=True) |
|
||||
Q(user__groups__permissions__codename__in=perms) |
|
||||
Q(user__user_permissions__codename__in=perms),
|
||||
|
|
|
@ -82,6 +82,14 @@ class BaseContestFormat(six.with_metaclass(ABCMeta)):
|
|||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def get_contest_problem_label_script(self):
|
||||
"""
|
||||
Returns the default Lua script to generate contest problem labels.
|
||||
:return: A string, the Lua script.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def best_solution_state(cls, points, total):
|
||||
if not points:
|
||||
|
|
|
@ -68,3 +68,10 @@ class DefaultContestFormat(BaseContestFormat):
|
|||
|
||||
def get_problem_breakdown(self, participation, contest_problems):
|
||||
return [(participation.format_data or {}).get(str(contest_problem.id)) for contest_problem in contest_problems]
|
||||
|
||||
def get_contest_problem_label_script(self):
|
||||
return '''
|
||||
function(n)
|
||||
return tostring(math.floor(n + 1))
|
||||
end
|
||||
'''
|
129
judge/contest_format/icpc.py
Normal file
129
judge/contest_format/icpc.py
Normal file
|
@ -0,0 +1,129 @@
|
|||
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('icpc')
|
||||
class ICPCContestFormat(DefaultContestFormat):
|
||||
name = gettext_lazy('ICPC')
|
||||
config_defaults = {'penalty': 20}
|
||||
config_validators = {'penalty': lambda x: x >= 0}
|
||||
'''
|
||||
penalty: Number of penalty minutes each incorrect submission adds. Defaults to 20.
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def validate(cls, config):
|
||||
if config is None:
|
||||
return
|
||||
|
||||
if not isinstance(config, dict):
|
||||
raise ValidationError('ICPC-styled contest expects no config or dict as config')
|
||||
|
||||
for key, value in config.items():
|
||||
if key not in cls.config_defaults:
|
||||
raise ValidationError('unknown config key "%s"' % key)
|
||||
if not isinstance(value, type(cls.config_defaults[key])):
|
||||
raise ValidationError('invalid type for config key "%s"' % key)
|
||||
if not cls.config_validators[key](value):
|
||||
raise ValidationError('invalid value "%s" for config key "%s"' % (value, key))
|
||||
|
||||
def __init__(self, contest, config):
|
||||
self.config = self.config_defaults.copy()
|
||||
self.config.update(config or {})
|
||||
self.contest = contest
|
||||
|
||||
def update_participation(self, participation):
|
||||
cumtime = 0
|
||||
last = 0
|
||||
penalty = 0
|
||||
score = 0
|
||||
format_data = {}
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('''
|
||||
SELECT MAX(cs.points) as `points`, (
|
||||
SELECT MIN(csub.date)
|
||||
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
||||
judge_submission csub ON (csub.id = ccs.submission_id)
|
||||
WHERE ccs.problem_id = cp.id AND ccs.participation_id = %s AND ccs.points = MAX(cs.points)
|
||||
) AS `time`, cp.id AS `prob`
|
||||
FROM judge_contestproblem cp INNER JOIN
|
||||
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
||||
judge_submission sub ON (sub.id = cs.submission_id)
|
||||
GROUP BY cp.id
|
||||
''', (participation.id, participation.id))
|
||||
|
||||
for points, time, prob in cursor.fetchall():
|
||||
time = from_database_time(time)
|
||||
dt = (time - participation.start).total_seconds()
|
||||
|
||||
# Compute penalty
|
||||
if self.config['penalty']:
|
||||
# An IE can have a submission result of `None`
|
||||
subs = participation.submissions.exclude(submission__result__isnull=True) \
|
||||
.exclude(submission__result__in=['IE', 'CE']) \
|
||||
.filter(problem_id=prob)
|
||||
if points:
|
||||
prev = subs.filter(submission__date__lte=time).count() - 1
|
||||
penalty += prev * self.config['penalty'] * 60
|
||||
else:
|
||||
# We should always display the penalty, even if the user has a score of 0
|
||||
prev = subs.count()
|
||||
else:
|
||||
prev = 0
|
||||
|
||||
if points:
|
||||
cumtime += dt
|
||||
last = max(last, dt)
|
||||
|
||||
format_data[str(prob)] = {'time': dt, 'points': points, 'penalty': prev}
|
||||
score += points
|
||||
|
||||
participation.cumtime = cumtime + penalty
|
||||
participation.score = score
|
||||
participation.tiebreaker = last # field is sorted from least to greatest
|
||||
participation.format_data = format_data
|
||||
participation.save()
|
||||
|
||||
def display_user_problem(self, participation, contest_problem):
|
||||
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>')
|
||||
|
||||
def get_contest_problem_label_script(self):
|
||||
return '''
|
||||
function(n)
|
||||
n = n + 1
|
||||
ret = ""
|
||||
while n > 0 do
|
||||
ret = string.char((n - 1) % 26 + 65) .. ret
|
||||
n = math.floor((n - 1) / 26)
|
||||
end
|
||||
return ret
|
||||
end
|
||||
'''
|
63
judge/migrations/0115_auto_20210525_0222.py
Normal file
63
judge/migrations/0115_auto_20210525_0222.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
# Generated by Django 2.2.17 on 2021-05-24 19:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
def hide_scoreboard_eq_true(apps, schema_editor):
|
||||
Contest = apps.get_model('judge', 'Contest')
|
||||
Contest.objects.filter(hide_scoreboard=True).update(scoreboard_visibility='C')
|
||||
|
||||
|
||||
def scoreboard_visibility_eq_contest(apps, schema_editor):
|
||||
Contest = apps.get_model('judge', 'Contest')
|
||||
Contest.objects.filter(scoreboard_visibility__in=('C', 'P')).update(hide_scoreboard=True)
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('judge', '0114_auto_20201228_1041'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='contest',
|
||||
options={'permissions': (('see_private_contest', 'See private contests'), ('edit_own_contest', 'Edit own contests'), ('edit_all_contest', 'Edit all contests'), ('clone_contest', 'Clone contest'), ('moss_contest', 'MOSS contest'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests'), ('change_contest_visibility', 'Change contest visibility'), ('contest_problem_label', 'Edit contest problem label script')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='contest',
|
||||
name='hide_scoreboard',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='contest',
|
||||
name='organizers',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contest',
|
||||
name='authors',
|
||||
field=models.ManyToManyField(help_text='These users will be able to edit the contest.', related_name='_contest_authors_+', to='judge.Profile'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contest',
|
||||
name='curators',
|
||||
field=models.ManyToManyField(blank=True, help_text='These users will be able to edit the contest, but will not be listed as authors.', related_name='_contest_curators_+', to='judge.Profile'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contest',
|
||||
name='problem_label_script',
|
||||
field=models.TextField(blank=True, help_text='A custom Lua function to generate problem labels. Requires a single function with an integer parameter, the zero-indexed contest problem index, and returns a string, the label.', verbose_name='contest problem label script'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contest',
|
||||
name='scoreboard_visibility',
|
||||
field=models.CharField(choices=[('V', 'Visible'), ('C', 'Hidden for duration of contest'), ('P', 'Hidden for duration of participation')], default='V', help_text='Scoreboard visibility through the duration of the contest', max_length=1, verbose_name='scoreboard visibility'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contest',
|
||||
name='testers',
|
||||
field=models.ManyToManyField(blank=True, help_text='These users will be able to view the contest, but not edit it.', related_name='_contest_testers_+', to='judge.Profile'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contestparticipation',
|
||||
name='tiebreaker',
|
||||
field=models.FloatField(default=0.0, verbose_name='tie-breaking field'),
|
||||
),
|
||||
]
|
|
@ -1,12 +1,13 @@
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
|
||||
from django.db import models, transaction
|
||||
from django.db.models import CASCADE
|
||||
from django.db.models import CASCADE, Q
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from jsonfield import JSONField
|
||||
from lupa import LuaRuntime
|
||||
from moss import MOSS_LANG_C, MOSS_LANG_CC, MOSS_LANG_JAVA, MOSS_LANG_PYTHON, MOSS_LANG_PASCAL
|
||||
|
||||
from judge import contest_format
|
||||
|
@ -48,11 +49,25 @@ class ContestTag(models.Model):
|
|||
|
||||
|
||||
class Contest(models.Model):
|
||||
SCOREBOARD_VISIBLE = 'V'
|
||||
SCOREBOARD_AFTER_CONTEST = 'C'
|
||||
SCOREBOARD_AFTER_PARTICIPATION = 'P'
|
||||
SCOREBOARD_VISIBILITY = (
|
||||
(SCOREBOARD_VISIBLE, _('Visible')),
|
||||
(SCOREBOARD_AFTER_CONTEST, _('Hidden for duration of contest')),
|
||||
(SCOREBOARD_AFTER_PARTICIPATION, _('Hidden for duration of participation')),
|
||||
)
|
||||
key = models.CharField(max_length=20, verbose_name=_('contest id'), unique=True,
|
||||
validators=[RegexValidator('^[a-z0-9]+$', _('Contest id must be ^[a-z0-9]+$'))])
|
||||
name = models.CharField(max_length=100, verbose_name=_('contest name'), db_index=True)
|
||||
organizers = models.ManyToManyField(Profile, help_text=_('These people will be able to edit the contest.'),
|
||||
related_name='organizers+')
|
||||
authors = models.ManyToManyField(Profile, help_text=_('These users will be able to edit the contest.'),
|
||||
related_name='authors+')
|
||||
curators = models.ManyToManyField(Profile, help_text=_('These users will be able to edit the contest, '
|
||||
'but will not be listed as authors.'),
|
||||
related_name='curators+', blank=True)
|
||||
testers = models.ManyToManyField(Profile, help_text=_('These users will be able to view the contest, '
|
||||
'but not edit it.'),
|
||||
blank=True, related_name='testers+')
|
||||
description = models.TextField(verbose_name=_('description'), blank=True)
|
||||
problems = models.ManyToManyField(Problem, verbose_name=_('problems'), through='ContestProblem')
|
||||
start_time = models.DateTimeField(verbose_name=_('start time'), db_index=True)
|
||||
|
@ -64,10 +79,9 @@ class Contest(models.Model):
|
|||
'specified organizations.'))
|
||||
is_rated = models.BooleanField(verbose_name=_('contest rated'), help_text=_('Whether this contest can be rated.'),
|
||||
default=False)
|
||||
hide_scoreboard = models.BooleanField(verbose_name=_('hide scoreboard'),
|
||||
help_text=_('Whether the scoreboard should remain hidden for the duration '
|
||||
'of the contest.'),
|
||||
default=False)
|
||||
scoreboard_visibility = models.CharField(verbose_name=_('scoreboard visibility'), default=SCOREBOARD_VISIBLE,
|
||||
max_length=1, help_text=_('Scoreboard visibility through the duration '
|
||||
'of the contest'), choices=SCOREBOARD_VISIBILITY)
|
||||
view_contest_scoreboard = models.ManyToManyField(Profile, verbose_name=_('view contest scoreboard'), blank=True,
|
||||
related_name='view_contest_scoreboard',
|
||||
help_text=_('These users will be able to view the scoreboard.'))
|
||||
|
@ -116,6 +130,10 @@ class Contest(models.Model):
|
|||
help_text=_('A JSON object to serve as the configuration for the chosen contest format '
|
||||
'module. Leave empty to use None. Exact format depends on the contest format '
|
||||
'selected.'))
|
||||
problem_label_script = models.TextField(verbose_name='contest problem label script', blank=True,
|
||||
help_text='A custom Lua function to generate problem labels. Requires a '
|
||||
'single function with an integer parameter, the zero-indexed '
|
||||
'contest problem index, and returns a string, the label.')
|
||||
points_precision = models.IntegerField(verbose_name=_('precision points'), default=2,
|
||||
validators=[MinValueValidator(0), MaxValueValidator(10)],
|
||||
help_text=_('Number of digits to round points to.'))
|
||||
|
@ -128,30 +146,72 @@ class Contest(models.Model):
|
|||
def format(self):
|
||||
return self.format_class(self, self.format_config)
|
||||
|
||||
@cached_property
|
||||
def get_label_for_problem(self):
|
||||
def DENY_ALL(obj, attr_name, is_setting):
|
||||
raise AttributeError()
|
||||
lua = LuaRuntime(attribute_filter=DENY_ALL, register_eval=False, register_builtins=False)
|
||||
return lua.eval(self.problem_label_script or self.format.get_contest_problem_label_script())
|
||||
|
||||
def clean(self):
|
||||
# Django will complain if you didn't fill in start_time or end_time, so we don't have to.
|
||||
if self.start_time and self.end_time and self.start_time >= self.end_time:
|
||||
raise ValidationError('What is this? A contest that ended before it starts?')
|
||||
self.format_class.validate(self.format_config)
|
||||
|
||||
try:
|
||||
# a contest should have at least one problem, with contest problem index 0
|
||||
# so test it to see if the script returns a valid label.
|
||||
label = self.get_label_for_problem(0)
|
||||
except Exception as e:
|
||||
raise ValidationError('Contest problem label script: %s' % e)
|
||||
else:
|
||||
if not isinstance(label, str):
|
||||
raise ValidationError('Contest problem label script: script should return a string.')
|
||||
|
||||
def is_in_contest(self, user):
|
||||
if user.is_authenticated:
|
||||
profile = user.profile
|
||||
return profile and profile.current_contest is not None and profile.current_contest.contest == self
|
||||
return False
|
||||
|
||||
def can_see_scoreboard(self, user):
|
||||
if user.has_perm('judge.see_private_contest'):
|
||||
def can_see_own_scoreboard(self, user):
|
||||
if self.can_see_full_scoreboard(user):
|
||||
return True
|
||||
if user.is_authenticated and self.organizers.filter(id=user.profile.id).exists():
|
||||
return True
|
||||
if user.is_authenticated and self.view_contest_scoreboard.filter(id=user.profile.id).exists():
|
||||
return True
|
||||
if not self.is_visible:
|
||||
if not self.can_join:
|
||||
return False
|
||||
if self.start_time is not None and self.start_time > timezone.now():
|
||||
if not self.show_scoreboard and not self.is_in_contest(user):
|
||||
return False
|
||||
if self.hide_scoreboard and not self.is_in_contest(user) and self.end_time > timezone.now():
|
||||
return True
|
||||
|
||||
def can_see_full_scoreboard(self, user):
|
||||
if self.show_scoreboard:
|
||||
return True
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
if user.has_perm('judge.see_private_contest') or user.has_perm('judge.edit_all_contest'):
|
||||
return True
|
||||
if user.profile.id in self.editor_ids:
|
||||
return True
|
||||
if self.view_contest_scoreboard.filter(id=user.profile.id).exists():
|
||||
return True
|
||||
if self.scoreboard_visibility == self.SCOREBOARD_AFTER_PARTICIPATION and self.has_completed_contest(user):
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_completed_contest(self, user):
|
||||
if user.is_authenticated:
|
||||
participation = self.users.filter(virtual=ContestParticipation.LIVE, user=user.profile).first()
|
||||
if participation and participation.ended:
|
||||
return True
|
||||
return False
|
||||
|
||||
@cached_property
|
||||
def show_scoreboard(self):
|
||||
if not self.can_join:
|
||||
return False
|
||||
if (self.scoreboard_visibility in (self.SCOREBOARD_AFTER_CONTEST, self.SCOREBOARD_AFTER_PARTICIPATION) and
|
||||
not self.ended):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
@ -186,6 +246,19 @@ class Contest(models.Model):
|
|||
def ended(self):
|
||||
return self.end_time < self._now
|
||||
|
||||
@cached_property
|
||||
def author_ids(self):
|
||||
return Contest.authors.through.objects.filter(contest=self).values_list('profile_id', flat=True)
|
||||
|
||||
@cached_property
|
||||
def editor_ids(self):
|
||||
return self.author_ids.union(
|
||||
Contest.curators.through.objects.filter(contest=self).values_list('profile_id', flat=True))
|
||||
|
||||
@cached_property
|
||||
def tester_ids(self):
|
||||
return Contest.testers.through.objects.filter(contest=self).values_list('profile_id', flat=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
@ -198,50 +271,111 @@ class Contest(models.Model):
|
|||
|
||||
update_user_count.alters_data = True
|
||||
|
||||
@cached_property
|
||||
def show_scoreboard(self):
|
||||
if self.hide_scoreboard and not self.ended:
|
||||
return False
|
||||
return True
|
||||
class Inaccessible(Exception):
|
||||
pass
|
||||
|
||||
class PrivateContest(Exception):
|
||||
pass
|
||||
|
||||
def access_check(self, user):
|
||||
# Do unauthenticated check here so we can skip authentication checks later on.
|
||||
if not user.is_authenticated:
|
||||
# Unauthenticated users can only see visible, non-private contests
|
||||
if not self.is_visible:
|
||||
raise self.Inaccessible()
|
||||
if self.is_private or self.is_organization_private:
|
||||
raise self.PrivateContest()
|
||||
return
|
||||
|
||||
# If the user can view or edit all contests
|
||||
if user.has_perm('judge.see_private_contest') or user.has_perm('judge.edit_all_contest'):
|
||||
return
|
||||
|
||||
# User is organizer or curator for contest
|
||||
if user.profile.id in self.editor_ids:
|
||||
return
|
||||
|
||||
# User is tester for contest
|
||||
if user.profile.id in self.tester_ids:
|
||||
return
|
||||
|
||||
# Contest is not publicly visible
|
||||
if not self.is_visible:
|
||||
raise self.Inaccessible()
|
||||
|
||||
# Contest is not private
|
||||
if not self.is_private and not self.is_organization_private:
|
||||
return
|
||||
|
||||
if self.view_contest_scoreboard.filter(id=user.profile.id).exists():
|
||||
return
|
||||
|
||||
in_org = self.organizations.filter(id__in=user.profile.organizations.all()).exists()
|
||||
in_users = self.private_contestants.filter(id=user.profile.id).exists()
|
||||
|
||||
if not self.is_private and self.is_organization_private:
|
||||
if in_org:
|
||||
return
|
||||
raise self.PrivateContest()
|
||||
|
||||
if self.is_private and not self.is_organization_private:
|
||||
if in_users:
|
||||
return
|
||||
raise self.PrivateContest()
|
||||
|
||||
if self.is_private and self.is_organization_private:
|
||||
if in_org and in_users:
|
||||
return
|
||||
raise self.PrivateContest()
|
||||
|
||||
def is_accessible_by(self, user):
|
||||
# Contest is publicly visible
|
||||
if self.is_visible:
|
||||
# Contest is not private
|
||||
if not self.is_private and not self.is_organization_private:
|
||||
return True
|
||||
if user.is_authenticated:
|
||||
# User is in the organizations it is private to
|
||||
if self.organizations.filter(id__in=user.profile.organizations.all()).exists():
|
||||
return True
|
||||
# User is in the group of private contestants
|
||||
if self.private_contestants.filter(id=user.profile.id).exists():
|
||||
return True
|
||||
if self.view_contest_scoreboard.filter(id=user.profile.id).exists():
|
||||
return True
|
||||
|
||||
# If the user can view all contests
|
||||
if user.has_perm('judge.see_private_contest'):
|
||||
try:
|
||||
self.access_check(user)
|
||||
except (self.Inaccessible, self.PrivateContest):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
# User can edit the contest
|
||||
return self.is_editable_by(user)
|
||||
|
||||
def is_editable_by(self, user):
|
||||
# If the user can edit all contests
|
||||
if user.has_perm('judge.edit_all_contest'):
|
||||
return True
|
||||
|
||||
# If the user is a contest organizer
|
||||
if user.has_perm('judge.edit_own_contest') and \
|
||||
self.organizers.filter(id=user.profile.id).exists():
|
||||
# If the user is a contest organizer or curator
|
||||
if user.has_perm('judge.edit_own_contest') and user.profile.id in self.editor_ids:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_visible_contests(cls, user):
|
||||
if not user.is_authenticated:
|
||||
return cls.objects.filter(is_visible=True, is_organization_private=False, is_private=False) \
|
||||
.defer('description').distinct()
|
||||
|
||||
queryset = cls.objects.defer('description')
|
||||
if not (user.has_perm('judge.see_private_contest') or user.has_perm('judge.edit_all_contest')):
|
||||
q = Q(is_visible=True)
|
||||
q &= (
|
||||
Q(view_contest_scoreboard=user.profile) |
|
||||
Q(is_organization_private=False, is_private=False) |
|
||||
Q(is_organization_private=False, is_private=True, private_contestants=user.profile) |
|
||||
Q(is_organization_private=True, is_private=False, organizations__in=user.profile.organizations.all()) |
|
||||
Q(is_organization_private=True, is_private=True, organizations__in=user.profile.organizations.all(),
|
||||
private_contestants=user.profile)
|
||||
)
|
||||
|
||||
q |= Q(authors=user.profile)
|
||||
q |= Q(curators=user.profile)
|
||||
q |= Q(testers=user.profile)
|
||||
queryset = queryset.filter(q)
|
||||
return queryset.distinct()
|
||||
|
||||
def rate(self):
|
||||
Rating.objects.filter(contest__end_time__gte=self.end_time).delete()
|
||||
for contest in Contest.objects.filter(is_rated=True, end_time__gte=self.end_time).order_by('end_time'):
|
||||
Rating.objects.filter(contest__end_time__range=(self.end_time, self._now)).delete()
|
||||
for contest in Contest.objects.filter(
|
||||
is_rated=True, end_time__range=(self.end_time, self._now),
|
||||
).order_by('end_time'):
|
||||
rate_contest(contest)
|
||||
|
||||
class Meta:
|
||||
|
@ -255,6 +389,7 @@ class Contest(models.Model):
|
|||
('contest_access_code', _('Contest access codes')),
|
||||
('create_private_contest', _('Create private contests')),
|
||||
('change_contest_visibility', _('Change contest visibility')),
|
||||
('contest_problem_label', _('Edit contest problem label script')),
|
||||
)
|
||||
verbose_name = _('contest')
|
||||
verbose_name_plural = _('contests')
|
||||
|
@ -271,6 +406,7 @@ class ContestParticipation(models.Model):
|
|||
cumtime = models.PositiveIntegerField(verbose_name=_('cumulative time'), default=0)
|
||||
is_disqualified = models.BooleanField(verbose_name=_('is disqualified'), default=False,
|
||||
help_text=_('Whether this participation is disqualified.'))
|
||||
tiebreaker = models.FloatField(verbose_name=_('tie-breaking field'), default=0.0)
|
||||
virtual = models.IntegerField(verbose_name=_('virtual participation id'), default=LIVE,
|
||||
help_text=_('0 means non-virtual, otherwise the n-th virtual participation.'))
|
||||
format_data = JSONField(verbose_name=_('contest format specific data'), null=True, blank=True)
|
||||
|
|
|
@ -5,7 +5,7 @@ from django.contrib.contenttypes.fields import GenericRelation
|
|||
from django.core.cache import cache
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import CASCADE, F, QuerySet, SET_NULL
|
||||
from django.db.models import CASCADE, F, Q, QuerySet, SET_NULL
|
||||
from django.db.models.expressions import RawSQL
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.urls import reverse
|
||||
|
@ -220,6 +220,43 @@ class Problem(models.Model):
|
|||
def is_subs_manageable_by(self, user):
|
||||
return user.is_staff and user.has_perm('judge.rejudge_submission') and self.is_editable_by(user)
|
||||
|
||||
@classmethod
|
||||
def get_visible_problems(cls, user):
|
||||
# Do unauthenticated check here so we can skip authentication checks later on.
|
||||
if not user.is_authenticated:
|
||||
return cls.get_public_problems()
|
||||
|
||||
# Conditions for visible problem:
|
||||
# - `judge.edit_all_problem` or `judge.see_private_problem`
|
||||
# - otherwise
|
||||
# - not is_public problems
|
||||
# - author or curator or tester
|
||||
# - is_public problems
|
||||
# - not is_organization_private or in organization or `judge.see_organization_problem`
|
||||
# - author or curator or tester
|
||||
queryset = cls.objects.defer('description')
|
||||
|
||||
if not (user.has_perm('judge.see_private_problem') or user.has_perm('judge.edit_all_problem')):
|
||||
q = Q(is_public=True)
|
||||
if not user.has_perm('judge.see_organization_problem'):
|
||||
# Either not organization private or in the organization.
|
||||
q &= (
|
||||
Q(is_organization_private=False) |
|
||||
Q(is_organization_private=True, organizations__in=user.profile.organizations.all())
|
||||
)
|
||||
|
||||
# Authors, curators, and testers should always have access, so OR at the very end.
|
||||
q |= Q(authors=user.profile)
|
||||
q |= Q(curators=user.profile)
|
||||
q |= Q(testers=user.profile)
|
||||
queryset = queryset.filter(q)
|
||||
|
||||
return queryset.distinct()
|
||||
|
||||
@classmethod
|
||||
def get_public_problems(cls):
|
||||
return cls.objects.filter(is_public=True, is_organization_private=False).defer('description').distinct()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
|
|
@ -136,12 +136,15 @@ class Profile(models.Model):
|
|||
|
||||
def calculate_points(self, table=_pp_table):
|
||||
from judge.models import Problem
|
||||
data = (Problem.objects.filter(submission__user=self, submission__points__isnull=False, is_public=True,
|
||||
is_organization_private=False)
|
||||
.annotate(max_points=Max('submission__points')).order_by('-max_points')
|
||||
.values_list('max_points', flat=True).filter(max_points__gt=0))
|
||||
extradata = Problem.objects.filter(submission__user=self, submission__result='AC', is_public=True) \
|
||||
.values('id').distinct().count()
|
||||
public_problems = Problem.get_public_problems()
|
||||
data = (
|
||||
public_problems.filter(submission__user=self, submission__points__isnull=False)
|
||||
.annotate(max_points=Max('submission__points')).order_by('-max_points')
|
||||
.values_list('max_points', flat=True).filter(max_points__gt=0)
|
||||
)
|
||||
extradata = (
|
||||
public_problems.filter(submission__user=self, submission__result='AC').values('id').count()
|
||||
)
|
||||
bonus_function = settings.DMOJ_PP_BONUS_FUNCTION
|
||||
points = sum(data)
|
||||
problems = len(data)
|
||||
|
|
|
@ -11,7 +11,7 @@ class ProblemSitemap(Sitemap):
|
|||
priority = 0.8
|
||||
|
||||
def items(self):
|
||||
return Problem.objects.filter(is_public=True, is_organization_private=False).values_list('code')
|
||||
return Problem.get_public_problems().values_list('code')
|
||||
|
||||
def location(self, obj):
|
||||
return reverse('problem_detail', args=obj)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from judge.tasks.contest import *
|
||||
from judge.tasks.demo import *
|
||||
from judge.tasks.contest import *
|
||||
from judge.tasks.submission import *
|
||||
|
|
|
@ -112,8 +112,8 @@ def hot_problems(duration, limit):
|
|||
cache_key = 'hot_problems:%d:%d' % (duration.total_seconds(), limit)
|
||||
qs = cache.get(cache_key)
|
||||
if qs is None:
|
||||
qs = Problem.objects.filter(is_public=True, is_organization_private=False,
|
||||
submission__date__gt=timezone.now() - duration, points__gt=3, points__lt=25)
|
||||
qs = Problem.get_public_problems() \
|
||||
.filter(submission__date__gt=timezone.now() - duration, points__gt=3, points__lt=25)
|
||||
qs0 = qs.annotate(k=Count('submission__user', distinct=True)).order_by('-k').values_list('k', flat=True)
|
||||
|
||||
if not qs0:
|
||||
|
|
|
@ -16,9 +16,8 @@ def sane_time_repr(delta):
|
|||
|
||||
|
||||
def api_v1_contest_list(request):
|
||||
queryset = Contest.objects.filter(is_visible=True, is_private=False,
|
||||
is_organization_private=False).prefetch_related(
|
||||
Prefetch('tags', queryset=ContestTag.objects.only('name'), to_attr='tag_list')).defer('description')
|
||||
queryset = Contest.get_visible_contests(request.user).prefetch_related(
|
||||
Prefetch('tags', queryset=ContestTag.objects.only('name'), to_attr='tag_list'))
|
||||
|
||||
return JsonResponse({c.key: {
|
||||
'name': c.name,
|
||||
|
@ -33,10 +32,7 @@ def api_v1_contest_detail(request, contest):
|
|||
contest = get_object_or_404(Contest, key=contest)
|
||||
|
||||
in_contest = contest.is_in_contest(request.user)
|
||||
can_see_rankings = contest.can_see_scoreboard(request.user)
|
||||
if contest.hide_scoreboard and in_contest:
|
||||
can_see_rankings = False
|
||||
|
||||
can_see_rankings = contest.can_see_full_scoreboard(request.user)
|
||||
problems = list(contest.contest_problems.select_related('problem')
|
||||
.defer('problem__description').order_by('order'))
|
||||
participations = (contest.users.filter(virtual=0, user__is_unlisted=False)
|
||||
|
|
|
@ -82,16 +82,13 @@ class PostList(ListView):
|
|||
.order_by('-latest')
|
||||
[:settings.DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT])
|
||||
|
||||
visible_contests = Contest.objects.filter(is_visible=True).order_by('start_time')
|
||||
q = Q(is_private=False, is_organization_private=False)
|
||||
if self.request.user.is_authenticated:
|
||||
q |= Q(is_organization_private=True, organizations__in=user.organizations.all())
|
||||
q |= Q(is_private=True, private_contestants=user)
|
||||
q |= Q(view_contest_scoreboard=user)
|
||||
visible_contests = visible_contests.filter(q)
|
||||
context['current_contests'] = visible_contests.filter(start_time__lte=now, end_time__gt=now).distinct()
|
||||
context['future_contests'] = visible_contests.filter(start_time__gt=now).distinct()
|
||||
visible_contests = Contest.get_visible_contests(self.request.user).filter(is_visible=True) \
|
||||
.order_by('start_time')
|
||||
|
||||
context['current_contests'] = visible_contests.filter(start_time__lte=now, end_time__gt=now)
|
||||
context['future_contests'] = visible_contests.filter(start_time__gt=now)
|
||||
|
||||
visible_contests = Contest.get_visible_contests(self.request.user).filter(is_visible=True)
|
||||
if self.request.user.is_authenticated:
|
||||
profile = self.request.profile
|
||||
context['own_open_tickets'] = (Ticket.objects.filter(Q(user=profile) | Q(assignees__in=[profile]), is_open=True).order_by('-id')
|
||||
|
|
|
@ -12,7 +12,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMix
|
|||
from django.core.cache import cache
|
||||
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Case, Count, FloatField, IntegerField, Max, Min, Q, Sum, Value, When
|
||||
from django.db.models import Case, Count, F, FloatField, IntegerField, Max, Min, Q, Sum, Value, When
|
||||
from django.db.models.expressions import CombinedExpression
|
||||
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
|
@ -59,20 +59,7 @@ def _find_contest(request, key, private_check=True):
|
|||
|
||||
class ContestListMixin(object):
|
||||
def get_queryset(self):
|
||||
queryset = Contest.objects.all()
|
||||
if not self.request.user.has_perm('judge.see_private_contest'):
|
||||
q = Q(is_visible=True)
|
||||
if self.request.user.is_authenticated:
|
||||
q |= Q(organizers=self.request.profile)
|
||||
queryset = queryset.filter(q)
|
||||
if not self.request.user.has_perm('judge.edit_all_contest'):
|
||||
q = Q(is_private=False, is_organization_private=False)
|
||||
if self.request.user.is_authenticated:
|
||||
q |= Q(is_organization_private=True, organizations__in=self.request.profile.organizations.all())
|
||||
q |= Q(is_private=True, private_contestants=self.request.profile)
|
||||
q |= Q(view_contest_scoreboard=self.request.profile)
|
||||
queryset = queryset.filter(q)
|
||||
return queryset.distinct()
|
||||
return Contest.get_visible_contests(self.request.user)
|
||||
|
||||
|
||||
class ContestList(DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView):
|
||||
|
@ -101,7 +88,7 @@ class ContestList(DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView):
|
|||
|
||||
def _get_queryset(self):
|
||||
queryset = super(ContestList, self).get_queryset() \
|
||||
.order_by('-start_time', 'key').prefetch_related('tags', 'organizations', 'organizers')
|
||||
.order_by('-start_time', 'key').prefetch_related('tags', 'organizations', 'authors', 'curators', 'testers')
|
||||
|
||||
if 'contest' in self.request.GET:
|
||||
self.contest_query = query = ' '.join(self.request.GET.getlist('contest')).strip()
|
||||
|
@ -128,7 +115,9 @@ class ContestList(DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView):
|
|||
if self.request.user.is_authenticated:
|
||||
for participation in ContestParticipation.objects.filter(virtual=0, user=self.request.profile,
|
||||
contest_id__in=present) \
|
||||
.select_related('contest').prefetch_related('contest__organizers'):
|
||||
.select_related('contest') \
|
||||
.prefetch_related('contest__authors', 'contest__curators', 'contest__testers')\
|
||||
.annotate(key=F('contest__key')):
|
||||
if not participation.ended:
|
||||
active.append(participation)
|
||||
present.remove(participation.contest)
|
||||
|
@ -164,37 +153,44 @@ class ContestMixin(object):
|
|||
slug_url_kwarg = 'contest'
|
||||
|
||||
@cached_property
|
||||
def is_organizer(self):
|
||||
return self.check_organizer()
|
||||
def is_editor(self):
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
return self.request.profile.id in self.object.editor_ids
|
||||
|
||||
def check_organizer(self, contest=None, user=None):
|
||||
if user is None:
|
||||
user = self.request.user
|
||||
return (contest or self.object).is_editable_by(user)
|
||||
@cached_property
|
||||
def is_tester(self):
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
return self.request.profile.id in self.object.tester_ids
|
||||
|
||||
@cached_property
|
||||
def can_edit(self):
|
||||
return self.object.is_editable_by(self.request.user)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ContestMixin, self).get_context_data(**kwargs)
|
||||
if self.request.user.is_authenticated:
|
||||
profile = self.request.profile
|
||||
in_contest = context['in_contest'] = (profile.current_contest is not None and
|
||||
profile.current_contest.contest == self.object)
|
||||
if in_contest:
|
||||
context['participation'] = profile.current_contest
|
||||
context['participating'] = True
|
||||
try:
|
||||
context['live_participation'] = (
|
||||
self.request.profile.contest_history.get(
|
||||
contest=self.object,
|
||||
virtual=ContestParticipation.LIVE,
|
||||
)
|
||||
)
|
||||
except ContestParticipation.DoesNotExist:
|
||||
context['live_participation'] = None
|
||||
context['has_joined'] = False
|
||||
else:
|
||||
try:
|
||||
context['participation'] = profile.contest_history.get(contest=self.object, virtual=0)
|
||||
except ContestParticipation.DoesNotExist:
|
||||
context['participating'] = False
|
||||
context['participation'] = None
|
||||
else:
|
||||
context['participating'] = True
|
||||
context['has_joined'] = True
|
||||
else:
|
||||
context['participating'] = False
|
||||
context['participation'] = None
|
||||
context['in_contest'] = False
|
||||
context['live_participation'] = None
|
||||
context['has_joined'] = False
|
||||
|
||||
context['now'] = timezone.now()
|
||||
context['is_organizer'] = self.is_organizer
|
||||
context['is_editor'] = self.is_editor
|
||||
context['is_tester'] = self.is_tester
|
||||
context['can_edit'] = self.can_edit
|
||||
|
||||
if not self.object.og_image or not self.object.summary:
|
||||
metadata = generate_opengraph('generated-meta-contest:%d' % self.object.id,
|
||||
|
@ -210,17 +206,21 @@ class ContestMixin(object):
|
|||
|
||||
def get_object(self, queryset=None):
|
||||
contest = super(ContestMixin, self).get_object(queryset)
|
||||
user = self.request.user
|
||||
profile = self.request.profile
|
||||
|
||||
if (profile is not None and
|
||||
ContestParticipation.objects.filter(id=profile.current_contest_id, contest_id=contest.id).exists()):
|
||||
return contest
|
||||
|
||||
if not contest.is_visible and not user.has_perm('judge.see_private_contest') and (
|
||||
not user.has_perm('judge.edit_own_contest') or
|
||||
not self.check_organizer(contest, user)):
|
||||
try:
|
||||
contest.access_check(self.request.user)
|
||||
except Contest.PrivateContest:
|
||||
raise PrivateContestError(contest.name, contest.is_private, contest.is_organization_private,
|
||||
contest.organizations.all())
|
||||
except Contest.Inaccessible:
|
||||
raise Http404()
|
||||
else:
|
||||
return contest
|
||||
|
||||
if contest.is_private or contest.is_organization_private:
|
||||
private_contest_error = PrivateContestError(contest.name, contest.is_private,
|
||||
|
@ -297,7 +297,7 @@ class ContestClone(ContestMixin, PermissionRequiredMixin, TitleMixin, SingleObje
|
|||
contest.organizations.set(organizations)
|
||||
contest.private_contestants.set(private_contestants)
|
||||
contest.view_contest_scoreboard.set(view_contest_scoreboard)
|
||||
contest.organizers.add(self.request.profile)
|
||||
contest.authors.add(self.request.profile)
|
||||
|
||||
for problem in contest_problems:
|
||||
problem.contest = contest
|
||||
|
@ -337,7 +337,7 @@ class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView):
|
|||
def join_contest(self, request, access_code=None):
|
||||
contest = self.object
|
||||
|
||||
if not contest.can_join and not self.is_organizer:
|
||||
if not contest.can_join and not (self.is_editor or self.is_tester):
|
||||
return generic_message(request, _('Contest not ongoing'),
|
||||
_('"%s" is not currently ongoing.') % contest.name)
|
||||
|
||||
|
@ -351,8 +351,7 @@ class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView):
|
|||
_('You have been declared persona non grata for this contest. '
|
||||
'You are permanently barred from joining this contest.'))
|
||||
|
||||
requires_access_code = (not (request.user.is_superuser or self.is_organizer) and
|
||||
contest.access_code and access_code != contest.access_code)
|
||||
requires_access_code = (not self.can_edit and contest.access_code and access_code != contest.access_code)
|
||||
if contest.ended:
|
||||
if requires_access_code:
|
||||
raise ContestAccessDenied()
|
||||
|
@ -371,22 +370,24 @@ class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView):
|
|||
else:
|
||||
break
|
||||
else:
|
||||
SPECTATE = ContestParticipation.SPECTATE
|
||||
LIVE = ContestParticipation.LIVE
|
||||
try:
|
||||
participation = ContestParticipation.objects.get(
|
||||
contest=contest, user=profile, virtual=(-1 if self.is_organizer else 0),
|
||||
contest=contest, user=profile, virtual=(SPECTATE if self.is_editor or self.is_tester else LIVE),
|
||||
)
|
||||
except ContestParticipation.DoesNotExist:
|
||||
if requires_access_code:
|
||||
raise ContestAccessDenied()
|
||||
|
||||
participation = ContestParticipation.objects.create(
|
||||
contest=contest, user=profile, virtual=(-1 if self.is_organizer else 0),
|
||||
contest=contest, user=profile, virtual=(SPECTATE if self.is_editor or self.is_tester else LIVE),
|
||||
real_start=timezone.now(),
|
||||
)
|
||||
else:
|
||||
if participation.ended:
|
||||
participation = ContestParticipation.objects.get_or_create(
|
||||
contest=contest, user=profile, virtual=-1,
|
||||
contest=contest, user=profile, virtual=SPECTATE,
|
||||
defaults={'real_start': timezone.now()},
|
||||
)[0]
|
||||
|
||||
|
@ -449,7 +450,7 @@ class ContestCalendar(TitleMixin, ContestListMixin, TemplateView):
|
|||
def get_contest_data(self, start, end):
|
||||
end += timedelta(days=1)
|
||||
contests = self.get_queryset().filter(Q(start_time__gte=start, start_time__lt=end) |
|
||||
Q(end_time__gte=start, end_time__lt=end)).defer('description')
|
||||
Q(end_time__gte=start, end_time__lt=end))
|
||||
starts, ends, oneday = (defaultdict(list) for i in range(3))
|
||||
for contest in contests:
|
||||
start_date = timezone.localtime(contest.start_time).date()
|
||||
|
@ -530,7 +531,7 @@ class ContestStats(TitleMixin, ContestMixin, DetailView):
|
|||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
if not (self.object.ended or self.object.is_editable_by(self.request.user)):
|
||||
if not (self.object.ended or self.can_edit):
|
||||
raise Http404()
|
||||
|
||||
queryset = Submission.objects.filter(contest_object=self.object)
|
||||
|
@ -542,9 +543,10 @@ class ContestStats(TitleMixin, ContestMixin, DetailView):
|
|||
queryset.values('problem__code', 'result').annotate(count=Count('result'))
|
||||
.values_list('problem__code', 'result', 'count'),
|
||||
)
|
||||
labels, codes = zip(
|
||||
*self.object.contest_problems.order_by('order').values_list('problem__name', 'problem__code'),
|
||||
)
|
||||
labels, codes = [], []
|
||||
contest_problems = self.object.contest_problems.order_by('order').values_list('problem__name', 'problem__code')
|
||||
if contest_problems:
|
||||
labels, codes = zip(*contest_problems)
|
||||
num_problems = len(labels)
|
||||
status_counts = [[] for i in range(num_problems)]
|
||||
for problem_code, result, count in status_count_queryset:
|
||||
|
@ -630,10 +632,6 @@ def get_contest_ranking_list(request, contest, participation=None, ranking_list=
|
|||
show_current_virtual=True, ranker=ranker):
|
||||
problems = list(contest.contest_problems.select_related('problem').defer('problem__description').order_by('order'))
|
||||
|
||||
if contest.hide_scoreboard and contest.is_in_contest(request.user):
|
||||
return ([(_('???'), make_contest_ranking_profile(contest, request.profile.current_contest, problems))],
|
||||
problems)
|
||||
|
||||
users = ranker(ranking_list(contest, problems), key=attrgetter('points', 'cumtime'))
|
||||
|
||||
if show_current_virtual:
|
||||
|
@ -651,7 +649,7 @@ def contest_ranking_ajax(request, contest, participation=None):
|
|||
if not exists:
|
||||
return HttpResponseBadRequest('Invalid contest', content_type='text/plain')
|
||||
|
||||
if not contest.can_see_scoreboard(request.user):
|
||||
if not contest.can_see_full_scoreboard(request.user):
|
||||
raise Http404()
|
||||
|
||||
users, problems = get_contest_ranking_list(request, contest, participation)
|
||||
|
@ -679,7 +677,7 @@ class ContestRankingBase(ContestMixin, TitleMixin, DetailView):
|
|||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
if not self.object.can_see_scoreboard(self.request.user):
|
||||
if not self.object.can_see_own_scoreboard(self.request.user):
|
||||
raise Http404()
|
||||
|
||||
users, problems = self.get_ranking_list()
|
||||
|
@ -697,6 +695,14 @@ class ContestRanking(ContestRankingBase):
|
|||
return _('%s Rankings') % self.object.name
|
||||
|
||||
def get_ranking_list(self):
|
||||
if not self.object.can_see_full_scoreboard(self.request.user):
|
||||
queryset = self.object.users.filter(user=self.request.profile, virtual=ContestParticipation.LIVE)
|
||||
return get_contest_ranking_list(
|
||||
self.request, self.object,
|
||||
ranking_list=partial(base_contest_ranking_list, queryset=queryset),
|
||||
ranker=lambda users, key: ((_('???'), user) for user in users),
|
||||
)
|
||||
|
||||
return get_contest_ranking_list(self.request, self.object)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -714,6 +720,9 @@ class ContestParticipationList(LoginRequiredMixin, ContestRankingBase):
|
|||
return _("%s's participation in %s") % (self.profile.username, self.object.name)
|
||||
|
||||
def get_ranking_list(self):
|
||||
if not self.object.can_see_full_scoreboard(self.request.user) and self.profile != self.request.profile:
|
||||
raise Http404()
|
||||
|
||||
queryset = self.object.users.filter(user=self.profile, virtual__gte=0).order_by('-virtual')
|
||||
live_link = format_html('<a href="{2}#!{1}">{0}</a>', _('Live'), self.profile.username,
|
||||
reverse('contest_ranking', args=[self.object.key]))
|
||||
|
@ -741,7 +750,7 @@ class ContestParticipationList(LoginRequiredMixin, ContestRankingBase):
|
|||
class ContestParticipationDisqualify(ContestMixin, SingleObjectMixin, View):
|
||||
def get_object(self, queryset=None):
|
||||
contest = super().get_object(queryset)
|
||||
if not contest.is_editable_by(self.request.user):
|
||||
if not self.can_edit:
|
||||
raise Http404()
|
||||
return contest
|
||||
|
||||
|
@ -762,7 +771,7 @@ class ContestMossMixin(ContestMixin, PermissionRequiredMixin):
|
|||
|
||||
def get_object(self, queryset=None):
|
||||
contest = super().get_object(queryset)
|
||||
if settings.MOSS_API_KEY is None:
|
||||
if settings.MOSS_API_KEY is None or not self.can_edit:
|
||||
raise Http404()
|
||||
if not contest.is_editable_by(self.request.user):
|
||||
raise Http404()
|
||||
|
|
|
@ -438,7 +438,8 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
|
|||
else:
|
||||
context['hot_problems'] = None
|
||||
context['point_start'], context['point_end'], context['point_values'] = 0, 0, {}
|
||||
context['hide_contest_scoreboard'] = self.contest.hide_scoreboard
|
||||
context['hide_contest_scoreboard'] = self.contest.scoreboard_visibility in \
|
||||
(self.contest.SCOREBOARD_AFTER_CONTEST, self.contest.SCOREBOARD_AFTER_PARTICIPATION)
|
||||
return context
|
||||
|
||||
def get_noui_slider_points(self):
|
||||
|
|
|
@ -54,29 +54,14 @@ class OrganizationSelect2View(Select2View):
|
|||
|
||||
class ProblemSelect2View(Select2View):
|
||||
def get_queryset(self):
|
||||
queryset = Problem.objects.filter(Q(code__icontains=self.term) | Q(name__icontains=self.term))
|
||||
if not self.request.user.has_perm('judge.see_private_problem'):
|
||||
filter = Q(is_public=True)
|
||||
if self.request.user.is_authenticated:
|
||||
filter |= Q(authors=self.request.profile) | Q(curators=self.request.profile)
|
||||
queryset = queryset.filter(filter).distinct()
|
||||
return queryset.distinct()
|
||||
return Problem.get_visible_problems(self.request.user) \
|
||||
.filter(Q(code__icontains=self.term) | Q(name__icontains=self.term))
|
||||
|
||||
|
||||
class ContestSelect2View(Select2View):
|
||||
def get_queryset(self):
|
||||
queryset = Contest.objects.filter(Q(key__icontains=self.term) | Q(name__icontains=self.term))
|
||||
if not self.request.user.has_perm('judge.see_private_contest'):
|
||||
queryset = queryset.filter(is_visible=True)
|
||||
if not self.request.user.has_perm('judge.edit_all_contest'):
|
||||
q = Q(is_private=False, is_organization_private=False)
|
||||
if self.request.user.is_authenticated:
|
||||
q |= Q(is_organization_private=True,
|
||||
organizations__in=self.request.profile.organizations.all())
|
||||
q |= Q(is_private=True, private_contestants=self.request.profile)
|
||||
q |= Q(view_contest_scoreboard=self.request.profile)
|
||||
queryset = queryset.filter(q)
|
||||
return queryset
|
||||
return Contest.get_visible_contests(self.request.user) \
|
||||
.filter(Q(key__icontains=self.term) | Q(name__icontains=self.term))
|
||||
|
||||
|
||||
class CommentSelect2View(Select2View):
|
||||
|
@ -119,8 +104,7 @@ class UserSearchSelect2View(BaseListView):
|
|||
class ContestUserSearchSelect2View(UserSearchSelect2View):
|
||||
def get_queryset(self):
|
||||
contest = get_object_or_404(Contest, key=self.kwargs['contest'])
|
||||
if not contest.can_see_scoreboard(self.request.user) or \
|
||||
contest.hide_scoreboard and contest.is_in_contest(self.request.user):
|
||||
if not contest.is_accessible_by(self.request.user) or not contest.can_see_full_scoreboard(self.request.user):
|
||||
raise Http404()
|
||||
|
||||
return Profile.objects.filter(contest_history__contest=contest,
|
||||
|
|
|
@ -43,7 +43,7 @@ from judge.utils.problems import get_result_data
|
|||
from judge.utils.problems import user_authored_ids
|
||||
from judge.utils.problems import user_completed_ids
|
||||
from judge.utils.problems import user_editable_ids
|
||||
from judge.utils.raw_sql import use_straight_join
|
||||
from judge.utils.raw_sql import join_sql_subquery, use_straight_join
|
||||
from judge.utils.views import DiggPaginatorMixin
|
||||
from judge.utils.views import TitleMixin
|
||||
|
||||
|
@ -292,11 +292,9 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
|
|||
queryset=ProblemTranslation.objects.filter(
|
||||
language=self.request.LANGUAGE_CODE), to_attr='_trans'))
|
||||
if self.in_contest:
|
||||
queryset = queryset.filter(
|
||||
contest__participation__contest_id=self.contest.id)
|
||||
if self.contest.hide_scoreboard and self.contest.is_in_contest(self.request.user):
|
||||
queryset = queryset.filter(
|
||||
contest__participation__user=self.request.profile)
|
||||
queryset = queryset.filter(contest_object=self.contest)
|
||||
if not self.contest.can_see_full_scoreboard(self.request.user):
|
||||
queryset = queryset.filter(user=self.request.profile)
|
||||
else:
|
||||
queryset = queryset.select_related(
|
||||
'contest_object').defer('contest_object__description')
|
||||
|
@ -304,12 +302,18 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
|
|||
# This is not technically correct since contest organizers *should* see these, but
|
||||
# the join would be far too messy
|
||||
if not self.request.user.has_perm('judge.see_private_contest'):
|
||||
queryset = queryset.exclude(
|
||||
contest_object_id__in=Contest.objects.filter(hide_scoreboard=True))
|
||||
# Show submissions for any contest you can edit or visible scoreboard
|
||||
contest_queryset = Contest.objects.filter(Q(authors=self.request.profile) |
|
||||
Q(curators=self.request.profile) |
|
||||
Q(scoreboard_visibility=Contest.SCOREBOARD_VISIBLE) |
|
||||
Q(end_time__lt=timezone.now())).distinct()
|
||||
queryset = queryset.filter(Q(user=self.request.profile) |
|
||||
Q(contest_object__in=contest_queryset) |
|
||||
Q(contest_object__isnull=True))
|
||||
|
||||
if self.selected_languages:
|
||||
queryset = queryset.filter(
|
||||
language_id__in=Language.objects.filter(key__in=self.selected_languages))
|
||||
language__in=Language.objects.filter(key__in=self.selected_languages))
|
||||
if self.selected_statuses:
|
||||
queryset = queryset.filter(result__in=self.selected_statuses)
|
||||
|
||||
|
@ -318,14 +322,13 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
|
|||
def get_queryset(self):
|
||||
queryset = self._get_queryset()
|
||||
if not self.in_contest:
|
||||
if not self.request.user.has_perm('judge.see_private_problem'):
|
||||
queryset = queryset.filter(problem__is_public=True)
|
||||
if not self.request.user.has_perm('judge.see_organization_problem'):
|
||||
filter = Q(problem__is_organization_private=False)
|
||||
if self.request.user.is_authenticated:
|
||||
filter |= Q(
|
||||
problem__organizations__in=self.request.profile.organizations.all())
|
||||
queryset = queryset.filter(filter)
|
||||
join_sql_subquery(
|
||||
queryset,
|
||||
subquery=str(Problem.get_visible_problems(self.request.user).distinct().only('id').query),
|
||||
params=[],
|
||||
join_fields=[('problem_id', 'id')],
|
||||
alias='visible_problems',
|
||||
)
|
||||
return queryset
|
||||
|
||||
def get_my_submissions_page(self):
|
||||
|
@ -452,7 +455,7 @@ class ProblemSubmissionsBase(SubmissionsListBase):
|
|||
reverse('problem_detail', args=[self.problem.code]))
|
||||
|
||||
def access_check_contest(self, request):
|
||||
if self.in_contest and not self.contest.can_see_scoreboard(request.user):
|
||||
if self.in_contest and not self.contest.can_see_own_scoreboard(request.user):
|
||||
raise Http404()
|
||||
|
||||
def access_check(self, request):
|
||||
|
|
|
@ -37,3 +37,4 @@ django-newsletter
|
|||
python-memcached
|
||||
netaddr
|
||||
redis
|
||||
lupa
|
|
@ -50,7 +50,9 @@ select[id^=id_contest_problems] {
|
|||
width: 20em !important;
|
||||
}
|
||||
|
||||
select#id_organizers.django-select2,
|
||||
select#id_authors.django-select2,
|
||||
select#id_curators.django-select2,
|
||||
select#id_testers.django-select2,
|
||||
select#id_organizations.django-select2,
|
||||
select#id_tags.django-select2 {
|
||||
width: 20em;
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
{% endblock extrahead %}
|
||||
|
||||
{% block after_field_sets %}{{ block.super }}
|
||||
{% if original and original.is_rated and perms.judge.contest_rating %}
|
||||
{% if original and original.is_rated and original.ended and perms.judge.contest_rating %}
|
||||
<a style="display: none" title="{% trans "Rate" %}" href="{% url 'admin:judge_contest_rate' original.pk %}"
|
||||
class="button rerate-link">
|
||||
<i class="fa fa-lg fa-signal"></i>
|
||||
|
|
|
@ -2,21 +2,21 @@
|
|||
|
||||
{% block tabs %}
|
||||
{{ make_tab('detail', 'fa-info-circle', url('contest_view', contest.key), _('Info')) }}
|
||||
{% if contest.ended or contest.is_editable_by(request.user) %}
|
||||
{% if contest.ended or can_edit %}
|
||||
{{ make_tab('stats', 'fa-pie-chart', url('contest_stats', contest.key), _('Statistics')) }}
|
||||
{% endif %}
|
||||
|
||||
{% if contest.start_time <= now or perms.judge.see_private_contest %}
|
||||
{% if contest.show_scoreboard or contest.can_see_scoreboard(request.user) %}
|
||||
{% if contest.can_see_own_scoreboard(request.user) %}
|
||||
{{ make_tab('ranking', 'fa-bar-chart', url('contest_ranking', contest.key), _('Rankings')) }}
|
||||
{% if request.user.is_authenticated %}
|
||||
{{ make_tab('participation', 'fa-users', url('contest_participation_own', contest.key), _('Participation')) }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ make_tab('ranking', 'fa-bar-chart', None, _('Hidden Rankings')) }}
|
||||
{% endif %}
|
||||
{% if contest.show_scoreboard and request.user.is_authenticated %}
|
||||
{{ make_tab('participation', 'fa-users', url('contest_participation_own', contest.key), _('Participation')) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if contest.is_editable_by(request.user) %}
|
||||
{% if can_edit %}
|
||||
{% if perms.judge.moss_contest and has_moss_api_key %}
|
||||
{{ make_tab('moss', 'fa-gavel', url('contest_moss', contest.key), _('MOSS')) }}
|
||||
{% endif %}
|
||||
|
@ -27,17 +27,18 @@
|
|||
{% endif %}
|
||||
|
||||
{% if request.user.is_authenticated %}
|
||||
{% if contest.can_join or participating or is_organizer %}
|
||||
{% if contest.can_join or is_editor or is_tester %}
|
||||
{% set in_contest = contest.is_in_contest(request.user) %}
|
||||
{% if contest.ended %}
|
||||
{# Allow users to leave the virtual contest #}
|
||||
{% if in_contest %}
|
||||
{# They're in the contest because they're participating virtually #}
|
||||
<form action="{{ url('contest_leave', contest.key) }}" method="post"
|
||||
class="contest-join-pseudotab unselectable button full">
|
||||
{% csrf_token %}
|
||||
<input type="submit" class="leaving-forever" value="{{ _('Leave contest') }}">
|
||||
</form>
|
||||
{% else %}
|
||||
{# They're in the contest because they're participating virtually #}
|
||||
{# Allow users to virtual join #}
|
||||
<form action="{{ url('contest_join', contest.key) }}" method="post"
|
||||
class="contest-join-pseudotab unselectable button full">
|
||||
{% csrf_token %}
|
||||
|
@ -45,35 +46,30 @@
|
|||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{# Allow users to leave the contest #}
|
||||
{% if in_contest %}
|
||||
{# Allow people with ended participations to continue spectating #}
|
||||
<form action="{{ url('contest_leave', contest.key) }}" method="post"
|
||||
class="contest-join-pseudotab unselectable button full">
|
||||
{% csrf_token %}
|
||||
<input type="submit" value="
|
||||
{%- if participating and participation.ended or request.profile in contest.organizers.all() %}
|
||||
{%- if request.participation.spectate %}
|
||||
{{- _('Stop spectating') -}}
|
||||
{% else %}
|
||||
{{- _('Leave contest') -}}
|
||||
{% endif %}">
|
||||
</form>
|
||||
{% elif participating and participation.ended or is_organizer %}
|
||||
{% elif is_editor or is_tester or live_participation.ended %}
|
||||
<form action="{{ url('contest_join', contest.key) }}" method="post"
|
||||
class="contest-join-pseudotab unselectable button full">
|
||||
{% csrf_token %}
|
||||
<input type="submit" value="{{ _('Spectate contest') }}">
|
||||
</form>
|
||||
{% elif participating %}
|
||||
<form action="{{ url('contest_join', contest.key) }}" method="post"
|
||||
class="contest-join-pseudotab unselectable button full">
|
||||
{% csrf_token %}
|
||||
<input type="submit" value="{{ _('Join contest') }}">
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="{{ url('contest_join', contest.key) }}" method="post"
|
||||
class="contest-join-pseudotab unselectable button full">
|
||||
{% csrf_token %}
|
||||
<input type="submit" class="first-join" value="{{ _('Join contest') }}">
|
||||
<input type="submit" {% if not has_joined %}class="first-join"{% endif %}
|
||||
value="{{ _('Join contest') }}">
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
|
|
@ -28,11 +28,11 @@
|
|||
<div id="banner">
|
||||
<a href="https://www.timeanddate.com/worldclock/fixedtime.html?msg={{ contest.name|urlquote('') }}&iso=
|
||||
{{- contest.start_time|utc|date('Y-m-d\TH:i:s') }}" class="date">
|
||||
{%- if participating and participation.virtual and not participation.ended -%}
|
||||
{% if participation.spectate %}
|
||||
{%- if contest.is_in_contest(request.user) and not request.participation.live -%}
|
||||
{% if request.participation.spectate %}
|
||||
{{- _('Spectating, contest ends in %(countdown)s.', countdown=contest.time_before_end|as_countdown) -}}
|
||||
{% elif participation.end_time %}
|
||||
{{- _('Participating virtually, %(countdown)s remaining.', countdown=participation.time_remaining|as_countdown) -}}
|
||||
{% elif request.participation.end_time %}
|
||||
{{- _('Participating virtually, %(countdown)s remaining.', countdown=request.participation.time_remaining|as_countdown) -}}
|
||||
{% else %}
|
||||
{{- _('Participating virtually.') -}}
|
||||
{% endif %}
|
||||
|
@ -42,11 +42,11 @@
|
|||
{% elif contest.end_time < now %}
|
||||
{{- _('Contest is over.') -}}
|
||||
{% else %}
|
||||
{%- if participating -%}
|
||||
{% if participation.ended %}
|
||||
{%- if has_joined -%}
|
||||
{% if live_participation.ended %}
|
||||
{{- _('Your time is up! Contest ends in %(countdown)s.', countdown=contest.time_before_end|as_countdown) -}}
|
||||
{% else %}
|
||||
{{- _('You have %(countdown)s remaining.', countdown=participation.time_remaining|as_countdown) -}}
|
||||
{{- _('You have %(countdown)s remaining.', countdown=live_participation.time_remaining|as_countdown) -}}
|
||||
{% endif %}
|
||||
{%- else -%}
|
||||
{{ _('Contest ends in %(countdown)s.', countdown=contest.time_before_end|as_countdown) }}
|
||||
|
@ -73,7 +73,7 @@
|
|||
{% endcache %}
|
||||
</div>
|
||||
|
||||
{% if contest.ended or request.user.is_superuser or is_organizer %}
|
||||
{% if contest.ended or request.user.is_superuser or is_editor or is_tester %}
|
||||
<hr>
|
||||
<div class="contest-problems">
|
||||
<h2 style="margin-bottom: 0.2em"><i class="fa fa-fw fa-question-circle"></i>{{ _('Problems') }} </h2>
|
||||
|
|
|
@ -185,7 +185,7 @@
|
|||
{% endmacro %}
|
||||
|
||||
{% macro user_count(contest, user) %}
|
||||
{% if contest.show_scoreboard or contest.can_see_scoreboard(user) %}
|
||||
{% if contest.can_see_own_scoreboard(user) %}
|
||||
<a href="{{ url('contest_ranking', contest.key) }}">{{ contest.user_count }}</a>
|
||||
{% else %}
|
||||
{{ contest.user_count }}
|
||||
|
@ -195,7 +195,7 @@
|
|||
{% macro contest_join(contest, request) %}
|
||||
{% if not request.in_contest %}
|
||||
<td>
|
||||
{% if request.profile in contest.organizers.all() %}
|
||||
{% if request.profile in contest.authors.all() or request.profile in contest.curators.all() or request.profile in contest.testers.all() %}
|
||||
<form action="{{ url('contest_join', contest.key) }}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="submit" class="unselectable button full participate-button"
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block user_data %}
|
||||
{% if is_organizer %}
|
||||
{% if can_edit %}
|
||||
<span class="contest-participation-operation">
|
||||
<form action="{{ url('contest_participation_disqualify', contest.key) }}" method="post">
|
||||
{% csrf_token %}
|
||||
|
@ -56,7 +56,7 @@
|
|||
{% block before_point_head %}
|
||||
{% for problem in problems %}
|
||||
<th class="points header problem-score-col"><a href="{{ url('contest_ranked_submissions', contest.key, problem.problem.code) }}">
|
||||
{{- loop.index }}
|
||||
{{- contest.get_label_for_problem(loop.index0) }}
|
||||
<div class="point-denominator">{{ problem.points }}</div>
|
||||
</a></th>
|
||||
{% endfor %}
|
||||
|
|
|
@ -179,7 +179,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block users_js_media %}
|
||||
{% if is_organizer %}
|
||||
{% if can_edit %}
|
||||
<script type="text/javascript">
|
||||
$(function () {
|
||||
$('a.disqualify-participation').click(function (e) {
|
||||
|
@ -349,7 +349,7 @@
|
|||
{% block users_table %}
|
||||
<div style="margin-bottom: 0.5em">
|
||||
{% if tab == 'participation' %}
|
||||
{% if contest.start_time <= now or perms.judge.see_private_contest %}
|
||||
{% if contest.can_see_full_scoreboard(request.user) %}
|
||||
<input id="search-contest" type="text" placeholder="{{ _('View user participation') }}">
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
|
Loading…
Reference in a new issue