Add contest tester/curator (DMOJ)

This commit is contained in:
cuom1999 2021-05-24 15:00:36 -05:00
parent 06318a97e5
commit 297b8a2a36
25 changed files with 611 additions and 230 deletions

View file

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

View file

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

View file

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

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

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

View file

@ -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()
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
return
if self.view_contest_scoreboard.filter(id=user.profile.id).exists():
return True
return
# If the user can view all contests
if user.has_perm('judge.see_private_contest'):
return True
in_org = self.organizations.filter(id__in=user.profile.organizations.all()).exists()
in_users = self.private_contestants.filter(id=user.profile.id).exists()
# User can edit the contest
return self.is_editable_by(user)
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):
try:
self.access_check(user)
except (self.Inaccessible, self.PrivateContest):
return False
else:
return True
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)

View file

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

View file

@ -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)
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 = Problem.objects.filter(submission__user=self, submission__result='AC', is_public=True) \
.values('id').distinct().count()
.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)

View file

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

View file

@ -1,3 +1,4 @@
from judge.tasks.contest import *
from judge.tasks.demo import *
from judge.tasks.contest import *
from judge.tasks.submission import *

View file

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

View file

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

View file

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

View file

@ -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
else:
try:
context['participation'] = profile.contest_history.get(contest=self.object, virtual=0)
context['live_participation'] = (
self.request.profile.contest_history.get(
contest=self.object,
virtual=ContestParticipation.LIVE,
)
)
except ContestParticipation.DoesNotExist:
context['participating'] = False
context['participation'] = None
context['live_participation'] = None
context['has_joined'] = False
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()

View file

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

View file

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

View file

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

View file

@ -37,3 +37,4 @@ django-newsletter
python-memcached
netaddr
redis
lupa

View file

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

View file

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

View file

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

View file

@ -28,11 +28,11 @@
<div id="banner">
<a href="https://www.timeanddate.com/worldclock/fixedtime.html?msg={{ contest.name|urlquote('') }}&amp;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>

View file

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

View file

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

View file

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