Cloned DMOJ
This commit is contained in:
parent
f623974b58
commit
49dc9ff10c
513 changed files with 132349 additions and 39 deletions
1
judge/__init__.py
Normal file
1
judge/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
default_app_config = 'judge.apps.JudgeAppConfig'
|
37
judge/admin/__init__.py
Normal file
37
judge/admin/__init__.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.admin.models import LogEntry
|
||||
|
||||
from judge.admin.comments import CommentAdmin
|
||||
from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin
|
||||
from judge.admin.interface import BlogPostAdmin, LicenseAdmin, LogEntryAdmin, NavigationBarAdmin
|
||||
from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin
|
||||
from judge.admin.problem import ProblemAdmin
|
||||
from judge.admin.profile import ProfileAdmin
|
||||
from judge.admin.runtime import JudgeAdmin, LanguageAdmin
|
||||
from judge.admin.submission import SubmissionAdmin
|
||||
from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin
|
||||
from judge.admin.ticket import TicketAdmin
|
||||
from judge.models import BlogPost, Comment, CommentLock, Contest, ContestParticipation, \
|
||||
ContestTag, Judge, Language, License, MiscConfig, NavigationBar, Organization, \
|
||||
OrganizationRequest, Problem, ProblemGroup, ProblemType, Profile, Submission, Ticket
|
||||
|
||||
admin.site.register(BlogPost, BlogPostAdmin)
|
||||
admin.site.register(Comment, CommentAdmin)
|
||||
admin.site.register(CommentLock)
|
||||
admin.site.register(Contest, ContestAdmin)
|
||||
admin.site.register(ContestParticipation, ContestParticipationAdmin)
|
||||
admin.site.register(ContestTag, ContestTagAdmin)
|
||||
admin.site.register(Judge, JudgeAdmin)
|
||||
admin.site.register(Language, LanguageAdmin)
|
||||
admin.site.register(License, LicenseAdmin)
|
||||
admin.site.register(LogEntry, LogEntryAdmin)
|
||||
admin.site.register(MiscConfig)
|
||||
admin.site.register(NavigationBar, NavigationBarAdmin)
|
||||
admin.site.register(Organization, OrganizationAdmin)
|
||||
admin.site.register(OrganizationRequest, OrganizationRequestAdmin)
|
||||
admin.site.register(Problem, ProblemAdmin)
|
||||
admin.site.register(ProblemGroup, ProblemGroupAdmin)
|
||||
admin.site.register(ProblemType, ProblemTypeAdmin)
|
||||
admin.site.register(Profile, ProfileAdmin)
|
||||
admin.site.register(Submission, SubmissionAdmin)
|
||||
admin.site.register(Ticket, TicketAdmin)
|
64
judge/admin/comments.py
Normal file
64
judge/admin/comments.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
from django.forms import ModelForm
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _, ungettext
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from judge.models import Comment
|
||||
from judge.widgets import AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
|
||||
|
||||
|
||||
class CommentForm(ModelForm):
|
||||
class Meta:
|
||||
widgets = {
|
||||
'author': AdminHeavySelect2Widget(data_view='profile_select2'),
|
||||
'parent': AdminHeavySelect2Widget(data_view='comment_select2'),
|
||||
}
|
||||
if HeavyPreviewAdminPageDownWidget is not None:
|
||||
widgets['body'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('comment_preview'))
|
||||
|
||||
|
||||
class CommentAdmin(VersionAdmin):
|
||||
fieldsets = (
|
||||
(None, {'fields': ('author', 'page', 'parent', 'score', 'hidden')}),
|
||||
('Content', {'fields': ('body',)}),
|
||||
)
|
||||
list_display = ['author', 'linked_page', 'time']
|
||||
search_fields = ['author__user__username', 'page', 'body']
|
||||
actions = ['hide_comment', 'unhide_comment']
|
||||
list_filter = ['hidden']
|
||||
actions_on_top = True
|
||||
actions_on_bottom = True
|
||||
form = CommentForm
|
||||
date_hierarchy = 'time'
|
||||
|
||||
def get_queryset(self, request):
|
||||
return Comment.objects.order_by('-time')
|
||||
|
||||
def hide_comment(self, request, queryset):
|
||||
count = queryset.update(hidden=True)
|
||||
self.message_user(request, ungettext('%d comment successfully hidden.',
|
||||
'%d comments successfully hidden.',
|
||||
count) % count)
|
||||
hide_comment.short_description = _('Hide comments')
|
||||
|
||||
def unhide_comment(self, request, queryset):
|
||||
count = queryset.update(hidden=False)
|
||||
self.message_user(request, ungettext('%d comment successfully unhidden.',
|
||||
'%d comments successfully unhidden.',
|
||||
count) % count)
|
||||
unhide_comment.short_description = _('Unhide comments')
|
||||
|
||||
def linked_page(self, obj):
|
||||
link = obj.link
|
||||
if link is not None:
|
||||
return format_html('<a href="{0}">{1}</a>', link, obj.page)
|
||||
else:
|
||||
return format_html('{0}', obj.page)
|
||||
linked_page.short_description = _('Associated page')
|
||||
linked_page.admin_order_field = 'page'
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
super(CommentAdmin, self).save_model(request, obj, form, change)
|
||||
if obj.hidden:
|
||||
obj.get_descendants().update(hidden=obj.hidden)
|
269
judge/admin/contest.py
Normal file
269
judge/admin/contest.py
Normal file
|
@ -0,0 +1,269 @@
|
|||
from django.conf.urls import url
|
||||
from django.contrib import admin
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import connection, transaction
|
||||
from django.db.models import Q, TextField
|
||||
from django.forms import ModelForm, ModelMultipleChoiceField
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _, ungettext
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from judge.models import Contest, ContestProblem, ContestSubmission, Profile, Rating
|
||||
from judge.ratings import rate_contest
|
||||
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, AdminPagedownWidget, \
|
||||
AdminSelect2MultipleWidget, AdminSelect2Widget, HeavyPreviewAdminPageDownWidget
|
||||
|
||||
|
||||
class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
|
||||
@property
|
||||
def is_hidden(self):
|
||||
return False
|
||||
|
||||
|
||||
class ContestTagForm(ModelForm):
|
||||
contests = ModelMultipleChoiceField(
|
||||
label=_('Included contests'),
|
||||
queryset=Contest.objects.all(),
|
||||
required=False,
|
||||
widget=AdminHeavySelect2MultipleWidget(data_view='contest_select2'))
|
||||
|
||||
|
||||
class ContestTagAdmin(admin.ModelAdmin):
|
||||
fields = ('name', 'color', 'description', 'contests')
|
||||
list_display = ('name', 'color')
|
||||
actions_on_top = True
|
||||
actions_on_bottom = True
|
||||
form = ContestTagForm
|
||||
|
||||
if AdminPagedownWidget is not None:
|
||||
formfield_overrides = {
|
||||
TextField: {'widget': AdminPagedownWidget},
|
||||
}
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
super(ContestTagAdmin, self).save_model(request, obj, form, change)
|
||||
obj.contests.set(form.cleaned_data['contests'])
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
form = super(ContestTagAdmin, self).get_form(request, obj, **kwargs)
|
||||
if obj is not None:
|
||||
form.base_fields['contests'].initial = obj.contests.all()
|
||||
return form
|
||||
|
||||
|
||||
class ContestProblemInlineForm(ModelForm):
|
||||
class Meta:
|
||||
widgets = {'problem': AdminHeavySelect2Widget(data_view='problem_select2')}
|
||||
|
||||
|
||||
class ContestProblemInline(admin.TabularInline):
|
||||
model = ContestProblem
|
||||
verbose_name = _('Problem')
|
||||
verbose_name_plural = 'Problems'
|
||||
fields = ('problem', 'points', 'partial', 'is_pretested', 'max_submissions', 'output_prefix_override', 'order',
|
||||
'rejudge_column')
|
||||
readonly_fields = ('rejudge_column',)
|
||||
form = ContestProblemInlineForm
|
||||
|
||||
def rejudge_column(self, obj):
|
||||
if obj.id is None:
|
||||
return ''
|
||||
return format_html('<a class="button rejudge-link" href="{}">Rejudge</a>',
|
||||
reverse('admin:judge_contest_rejudge', args=(obj.contest.id, obj.id)))
|
||||
rejudge_column.short_description = ''
|
||||
|
||||
|
||||
class ContestForm(ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ContestForm, self).__init__(*args, **kwargs)
|
||||
if 'rate_exclude' in self.fields:
|
||||
if self.instance and self.instance.id:
|
||||
self.fields['rate_exclude'].queryset = \
|
||||
Profile.objects.filter(contest_history__contest=self.instance).distinct()
|
||||
else:
|
||||
self.fields['rate_exclude'].queryset = Profile.objects.none()
|
||||
self.fields['banned_users'].widget.can_add_related = False
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(ContestForm, self).clean()
|
||||
cleaned_data['banned_users'].filter(current_contest__contest=self.instance).update(current_contest=None)
|
||||
|
||||
class Meta:
|
||||
widgets = {
|
||||
'organizers': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
|
||||
'private_contestants': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
|
||||
attrs={'style': 'width: 100%'}),
|
||||
'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2'),
|
||||
'tags': AdminSelect2MultipleWidget,
|
||||
'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
|
||||
attrs={'style': 'width: 100%'}),
|
||||
}
|
||||
|
||||
if HeavyPreviewAdminPageDownWidget is not None:
|
||||
widgets['description'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('contest_preview'))
|
||||
|
||||
|
||||
class ContestAdmin(VersionAdmin):
|
||||
fieldsets = (
|
||||
(None, {'fields': ('key', 'name', 'organizers')}),
|
||||
(_('Settings'), {'fields': ('is_visible', 'use_clarifications', 'hide_problem_tags', 'hide_scoreboard',
|
||||
'run_pretests_only')}),
|
||||
(_('Scheduling'), {'fields': ('start_time', 'end_time', 'time_limit')}),
|
||||
(_('Details'), {'fields': ('description', 'og_image', 'logo_override_image', 'tags', 'summary')}),
|
||||
(_('Format'), {'fields': ('format_name', 'format_config')}),
|
||||
(_('Rating'), {'fields': ('is_rated', 'rate_all', 'rating_floor', 'rating_ceiling', 'rate_exclude')}),
|
||||
(_('Access'), {'fields': ('access_code', 'is_private', 'private_contestants', 'is_organization_private',
|
||||
'organizations')}),
|
||||
(_('Justice'), {'fields': ('banned_users',)}),
|
||||
)
|
||||
list_display = ('key', 'name', 'is_visible', 'is_rated', 'start_time', 'end_time', 'time_limit', 'user_count')
|
||||
actions = ['make_visible', 'make_hidden']
|
||||
inlines = [ContestProblemInline]
|
||||
actions_on_top = True
|
||||
actions_on_bottom = True
|
||||
form = ContestForm
|
||||
change_list_template = 'admin/judge/contest/change_list.html'
|
||||
filter_horizontal = ['rate_exclude']
|
||||
date_hierarchy = 'start_time'
|
||||
|
||||
def get_queryset(self, request):
|
||||
queryset = Contest.objects.all()
|
||||
if request.user.has_perm('judge.edit_all_contest'):
|
||||
return queryset
|
||||
else:
|
||||
return queryset.filter(organizers__id=request.profile.id)
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
readonly = []
|
||||
if not request.user.has_perm('judge.contest_rating'):
|
||||
readonly += ['is_rated', 'rate_all', 'rate_exclude']
|
||||
if not request.user.has_perm('judge.contest_access_code'):
|
||||
readonly += ['access_code']
|
||||
if not request.user.has_perm('judge.create_private_contest'):
|
||||
readonly += ['is_private', 'private_contestants', 'is_organization_private', 'organizations']
|
||||
return readonly
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
if not request.user.has_perm('judge.edit_own_contest'):
|
||||
return False
|
||||
if request.user.has_perm('judge.edit_all_contest') or obj is None:
|
||||
return True
|
||||
return obj.organizers.filter(id=request.profile.id).exists()
|
||||
|
||||
def make_visible(self, request, queryset):
|
||||
count = queryset.update(is_visible=True)
|
||||
self.message_user(request, ungettext('%d contest successfully marked as visible.',
|
||||
'%d contests successfully marked as visible.',
|
||||
count) % count)
|
||||
make_visible.short_description = _('Mark contests as visible')
|
||||
|
||||
def make_hidden(self, request, queryset):
|
||||
count = queryset.update(is_visible=False)
|
||||
self.message_user(request, ungettext('%d contest successfully marked as hidden.',
|
||||
'%d contests successfully marked as hidden.',
|
||||
count) % count)
|
||||
make_hidden.short_description = _('Mark contests as hidden')
|
||||
|
||||
def get_urls(self):
|
||||
return [
|
||||
url(r'^rate/all/$', self.rate_all_view, name='judge_contest_rate_all'),
|
||||
url(r'^(\d+)/rate/$', self.rate_view, name='judge_contest_rate'),
|
||||
url(r'^(\d+)/judge/(\d+)/$', self.rejudge_view, name='judge_contest_rejudge'),
|
||||
] + super(ContestAdmin, self).get_urls()
|
||||
|
||||
def rejudge_view(self, request, contest_id, problem_id):
|
||||
queryset = ContestSubmission.objects.filter(problem_id=problem_id).select_related('submission')
|
||||
for model in queryset:
|
||||
model.submission.judge(rejudge=True)
|
||||
|
||||
self.message_user(request, ungettext('%d submission was successfully scheduled for rejudging.',
|
||||
'%d submissions were successfully scheduled for rejudging.',
|
||||
len(queryset)) % len(queryset))
|
||||
return HttpResponseRedirect(reverse('admin:judge_contest_change', args=(contest_id,)))
|
||||
|
||||
def rate_all_view(self, request):
|
||||
if not request.user.has_perm('judge.contest_rating'):
|
||||
raise PermissionDenied()
|
||||
with transaction.atomic():
|
||||
if connection.vendor == 'sqlite':
|
||||
Rating.objects.all().delete()
|
||||
else:
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('TRUNCATE TABLE `%s`' % Rating._meta.db_table)
|
||||
cursor.close()
|
||||
Profile.objects.update(rating=None)
|
||||
for contest in Contest.objects.filter(is_rated=True).order_by('end_time'):
|
||||
rate_contest(contest)
|
||||
return HttpResponseRedirect(reverse('admin:judge_contest_changelist'))
|
||||
|
||||
def rate_view(self, request, id):
|
||||
if not request.user.has_perm('judge.contest_rating'):
|
||||
raise PermissionDenied()
|
||||
contest = get_object_or_404(Contest, id=id)
|
||||
if not contest.is_rated:
|
||||
raise Http404()
|
||||
with transaction.atomic():
|
||||
contest.rate()
|
||||
return HttpResponseRedirect(request.META.get('HTTP_REFERER', reverse('admin:judge_contest_changelist')))
|
||||
|
||||
def get_form(self, *args, **kwargs):
|
||||
form = super(ContestAdmin, self).get_form(*args, **kwargs)
|
||||
perms = ('edit_own_contest', 'edit_all_contest')
|
||||
form.base_fields['organizers'].queryset = Profile.objects.filter(
|
||||
Q(user__is_superuser=True) |
|
||||
Q(user__groups__permissions__codename__in=perms) |
|
||||
Q(user__user_permissions__codename__in=perms),
|
||||
).distinct()
|
||||
return form
|
||||
|
||||
|
||||
class ContestParticipationForm(ModelForm):
|
||||
class Meta:
|
||||
widgets = {
|
||||
'contest': AdminSelect2Widget(),
|
||||
'user': AdminHeavySelect2Widget(data_view='profile_select2'),
|
||||
}
|
||||
|
||||
|
||||
class ContestParticipationAdmin(admin.ModelAdmin):
|
||||
fields = ('contest', 'user', 'real_start', 'virtual', 'is_disqualified')
|
||||
list_display = ('contest', 'username', 'show_virtual', 'real_start', 'score', 'cumtime')
|
||||
actions = ['recalculate_results']
|
||||
actions_on_bottom = actions_on_top = True
|
||||
search_fields = ('contest__key', 'contest__name', 'user__user__username')
|
||||
form = ContestParticipationForm
|
||||
date_hierarchy = 'real_start'
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super(ContestParticipationAdmin, self).get_queryset(request).only(
|
||||
'contest__name', 'contest__format_name', 'contest__format_config',
|
||||
'user__user__username', 'real_start', 'score', 'cumtime', 'virtual',
|
||||
)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
super().save_model(request, obj, form, change)
|
||||
if form.changed_data and 'is_disqualified' in form.changed_data:
|
||||
obj.set_disqualified(obj.is_disqualified)
|
||||
|
||||
def recalculate_results(self, request, queryset):
|
||||
count = 0
|
||||
for participation in queryset:
|
||||
participation.recompute_results()
|
||||
count += 1
|
||||
self.message_user(request, ungettext('%d participation recalculated.',
|
||||
'%d participations recalculated.',
|
||||
count) % count)
|
||||
recalculate_results.short_description = _('Recalculate results')
|
||||
|
||||
def username(self, obj):
|
||||
return obj.user.username
|
||||
username.short_description = _('username')
|
||||
username.admin_order_field = 'user__user__username'
|
||||
|
||||
def show_virtual(self, obj):
|
||||
return obj.virtual or '-'
|
||||
show_virtual.short_description = _('virtual')
|
||||
show_virtual.admin_order_field = 'virtual'
|
151
judge/admin/interface.py
Normal file
151
judge/admin/interface.py
Normal file
|
@ -0,0 +1,151 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.auth.models import User
|
||||
from django.forms import ModelForm
|
||||
from django.urls import NoReverseMatch, reverse, reverse_lazy
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from mptt.admin import DraggableMPTTAdmin
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from judge.dblock import LockModel
|
||||
from judge.models import NavigationBar
|
||||
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
|
||||
|
||||
|
||||
class NavigationBarAdmin(DraggableMPTTAdmin):
|
||||
list_display = DraggableMPTTAdmin.list_display + ('key', 'linked_path')
|
||||
fields = ('key', 'label', 'path', 'order', 'regex', 'parent')
|
||||
list_editable = () # Bug in SortableModelAdmin: 500 without list_editable being set
|
||||
mptt_level_indent = 20
|
||||
sortable = 'order'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NavigationBarAdmin, self).__init__(*args, **kwargs)
|
||||
self.__save_model_calls = 0
|
||||
|
||||
def linked_path(self, obj):
|
||||
return format_html(u'<a href="{0}" target="_blank">{0}</a>', obj.path)
|
||||
linked_path.short_description = _('link path')
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
self.__save_model_calls += 1
|
||||
return super(NavigationBarAdmin, self).save_model(request, obj, form, change)
|
||||
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
self.__save_model_calls = 0
|
||||
with NavigationBar.objects.disable_mptt_updates():
|
||||
result = super(NavigationBarAdmin, self).changelist_view(request, extra_context)
|
||||
if self.__save_model_calls:
|
||||
with LockModel(write=(NavigationBar,)):
|
||||
NavigationBar.objects.rebuild()
|
||||
return result
|
||||
|
||||
|
||||
class BlogPostForm(ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BlogPostForm, self).__init__(*args, **kwargs)
|
||||
self.fields['authors'].widget.can_add_related = False
|
||||
|
||||
class Meta:
|
||||
widgets = {
|
||||
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||
}
|
||||
|
||||
if HeavyPreviewAdminPageDownWidget is not None:
|
||||
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('blog_preview'))
|
||||
widgets['summary'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('blog_preview'))
|
||||
|
||||
|
||||
class BlogPostAdmin(VersionAdmin):
|
||||
fieldsets = (
|
||||
(None, {'fields': ('title', 'slug', 'authors', 'visible', 'sticky', 'publish_on')}),
|
||||
(_('Content'), {'fields': ('content', 'og_image')}),
|
||||
(_('Summary'), {'classes': ('collapse',), 'fields': ('summary',)}),
|
||||
)
|
||||
prepopulated_fields = {'slug': ('title',)}
|
||||
list_display = ('id', 'title', 'visible', 'sticky', 'publish_on')
|
||||
list_display_links = ('id', 'title')
|
||||
ordering = ('-publish_on',)
|
||||
form = BlogPostForm
|
||||
date_hierarchy = 'publish_on'
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return (request.user.has_perm('judge.edit_all_post') or
|
||||
request.user.has_perm('judge.change_blogpost') and (
|
||||
obj is None or
|
||||
obj.authors.filter(id=request.profile.id).exists()))
|
||||
|
||||
|
||||
class SolutionForm(ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SolutionForm, self).__init__(*args, **kwargs)
|
||||
self.fields['authors'].widget.can_add_related = False
|
||||
|
||||
class Meta:
|
||||
widgets = {
|
||||
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||
'problem': AdminHeavySelect2Widget(data_view='problem_select2', attrs={'style': 'width: 250px'}),
|
||||
}
|
||||
|
||||
if HeavyPreviewAdminPageDownWidget is not None:
|
||||
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('solution_preview'))
|
||||
|
||||
|
||||
class LicenseForm(ModelForm):
|
||||
class Meta:
|
||||
if HeavyPreviewAdminPageDownWidget is not None:
|
||||
widgets = {'text': HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('license_preview'))}
|
||||
|
||||
|
||||
class LicenseAdmin(admin.ModelAdmin):
|
||||
fields = ('key', 'link', 'name', 'display', 'icon', 'text')
|
||||
list_display = ('name', 'key')
|
||||
form = LicenseForm
|
||||
|
||||
|
||||
class UserListFilter(admin.SimpleListFilter):
|
||||
title = _('user')
|
||||
parameter_name = 'user'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return User.objects.filter(is_staff=True).values_list('id', 'username')
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value():
|
||||
return queryset.filter(user_id=self.value(), user__is_staff=True)
|
||||
return queryset
|
||||
|
||||
|
||||
class LogEntryAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ('user', 'content_type', 'object_id', 'object_repr', 'action_flag', 'change_message')
|
||||
list_display = ('__str__', 'action_time', 'user', 'content_type', 'object_link')
|
||||
search_fields = ('object_repr', 'change_message')
|
||||
list_filter = (UserListFilter, 'content_type')
|
||||
list_display_links = None
|
||||
actions = None
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return obj is None and request.user.is_superuser
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def object_link(self, obj):
|
||||
if obj.is_deletion():
|
||||
link = obj.object_repr
|
||||
else:
|
||||
ct = obj.content_type
|
||||
try:
|
||||
link = format_html('<a href="{1}">{0}</a>', obj.object_repr,
|
||||
reverse('admin:%s_%s_change' % (ct.app_label, ct.model), args=(obj.object_id,)))
|
||||
except NoReverseMatch:
|
||||
link = obj.object_repr
|
||||
return link
|
||||
object_link.admin_order_field = 'object_repr'
|
||||
object_link.short_description = _('object')
|
||||
|
||||
def queryset(self, request):
|
||||
return super().queryset(request).prefetch_related('content_type')
|
66
judge/admin/organization.py
Normal file
66
judge/admin/organization.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
from django.contrib import admin
|
||||
from django.forms import ModelForm
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from judge.models import Organization
|
||||
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
|
||||
|
||||
|
||||
class OrganizationForm(ModelForm):
|
||||
class Meta:
|
||||
widgets = {
|
||||
'admins': AdminHeavySelect2MultipleWidget(data_view='profile_select2'),
|
||||
'registrant': AdminHeavySelect2Widget(data_view='profile_select2'),
|
||||
}
|
||||
if HeavyPreviewAdminPageDownWidget is not None:
|
||||
widgets['about'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('organization_preview'))
|
||||
|
||||
|
||||
class OrganizationAdmin(VersionAdmin):
|
||||
readonly_fields = ('creation_date',)
|
||||
fields = ('name', 'slug', 'short_name', 'is_open', 'about', 'logo_override_image', 'slots', 'registrant',
|
||||
'creation_date', 'admins')
|
||||
list_display = ('name', 'short_name', 'is_open', 'slots', 'registrant', 'show_public')
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
actions_on_top = True
|
||||
actions_on_bottom = True
|
||||
form = OrganizationForm
|
||||
|
||||
def show_public(self, obj):
|
||||
return format_html('<a href="{0}" style="white-space:nowrap;">{1}</a>',
|
||||
obj.get_absolute_url(), gettext('View on site'))
|
||||
|
||||
show_public.short_description = ''
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
fields = self.readonly_fields
|
||||
if not request.user.has_perm('judge.organization_admin'):
|
||||
return fields + ('registrant', 'admins', 'is_open', 'slots')
|
||||
return fields
|
||||
|
||||
def get_queryset(self, request):
|
||||
queryset = Organization.objects.all()
|
||||
if request.user.has_perm('judge.edit_all_organization'):
|
||||
return queryset
|
||||
else:
|
||||
return queryset.filter(admins=request.profile.id)
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
if not request.user.has_perm('judge.change_organization'):
|
||||
return False
|
||||
if request.user.has_perm('judge.edit_all_organization') or obj is None:
|
||||
return True
|
||||
return obj.admins.filter(id=request.profile.id).exists()
|
||||
|
||||
|
||||
class OrganizationRequestAdmin(admin.ModelAdmin):
|
||||
list_display = ('username', 'organization', 'state', 'time')
|
||||
readonly_fields = ('user', 'organization')
|
||||
|
||||
def username(self, obj):
|
||||
return obj.user.user.username
|
||||
username.short_description = _('username')
|
||||
username.admin_order_field = 'user__user__username'
|
238
judge/admin/problem.py
Normal file
238
judge/admin/problem.py
Normal file
|
@ -0,0 +1,238 @@
|
|||
from operator import attrgetter
|
||||
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.forms import ModelForm
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext, gettext_lazy as _, ungettext
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from judge.models import LanguageLimit, Problem, ProblemClarification, ProblemTranslation, Profile, Solution
|
||||
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminSelect2MultipleWidget, AdminSelect2Widget, \
|
||||
CheckboxSelectMultipleWithSelectAll, HeavyPreviewAdminPageDownWidget, HeavyPreviewPageDownWidget
|
||||
|
||||
|
||||
class ProblemForm(ModelForm):
|
||||
change_message = forms.CharField(max_length=256, label='Edit reason', required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ProblemForm, self).__init__(*args, **kwargs)
|
||||
self.fields['authors'].widget.can_add_related = False
|
||||
self.fields['curators'].widget.can_add_related = False
|
||||
self.fields['testers'].widget.can_add_related = False
|
||||
self.fields['banned_users'].widget.can_add_related = False
|
||||
self.fields['change_message'].widget.attrs.update({
|
||||
'placeholder': gettext('Describe the changes you made (optional)'),
|
||||
})
|
||||
|
||||
class Meta:
|
||||
widgets = {
|
||||
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||
'curators': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||
'testers': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||
'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2',
|
||||
attrs={'style': 'width: 100%'}),
|
||||
'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2',
|
||||
attrs={'style': 'width: 100%'}),
|
||||
'types': AdminSelect2MultipleWidget,
|
||||
'group': AdminSelect2Widget,
|
||||
}
|
||||
if HeavyPreviewAdminPageDownWidget is not None:
|
||||
widgets['description'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('problem_preview'))
|
||||
|
||||
|
||||
class ProblemCreatorListFilter(admin.SimpleListFilter):
|
||||
title = parameter_name = 'creator'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
queryset = Profile.objects.exclude(authored_problems=None).values_list('user__username', flat=True)
|
||||
return [(name, name) for name in queryset]
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() is None:
|
||||
return queryset
|
||||
return queryset.filter(authors__user__username=self.value())
|
||||
|
||||
|
||||
class LanguageLimitInlineForm(ModelForm):
|
||||
class Meta:
|
||||
widgets = {'language': AdminSelect2Widget}
|
||||
|
||||
|
||||
class LanguageLimitInline(admin.TabularInline):
|
||||
model = LanguageLimit
|
||||
fields = ('language', 'time_limit', 'memory_limit')
|
||||
form = LanguageLimitInlineForm
|
||||
|
||||
|
||||
class ProblemClarificationForm(ModelForm):
|
||||
class Meta:
|
||||
if HeavyPreviewPageDownWidget is not None:
|
||||
widgets = {'description': HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'))}
|
||||
|
||||
|
||||
class ProblemClarificationInline(admin.StackedInline):
|
||||
model = ProblemClarification
|
||||
fields = ('description',)
|
||||
form = ProblemClarificationForm
|
||||
extra = 0
|
||||
|
||||
|
||||
class ProblemSolutionForm(ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ProblemSolutionForm, self).__init__(*args, **kwargs)
|
||||
self.fields['authors'].widget.can_add_related = False
|
||||
|
||||
class Meta:
|
||||
widgets = {
|
||||
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||
}
|
||||
|
||||
if HeavyPreviewAdminPageDownWidget is not None:
|
||||
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('solution_preview'))
|
||||
|
||||
|
||||
class ProblemSolutionInline(admin.StackedInline):
|
||||
model = Solution
|
||||
fields = ('is_public', 'publish_on', 'authors', 'content')
|
||||
form = ProblemSolutionForm
|
||||
extra = 0
|
||||
|
||||
|
||||
class ProblemTranslationForm(ModelForm):
|
||||
class Meta:
|
||||
if HeavyPreviewAdminPageDownWidget is not None:
|
||||
widgets = {'description': HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('problem_preview'))}
|
||||
|
||||
|
||||
class ProblemTranslationInline(admin.StackedInline):
|
||||
model = ProblemTranslation
|
||||
fields = ('language', 'name', 'description')
|
||||
form = ProblemTranslationForm
|
||||
extra = 0
|
||||
|
||||
|
||||
class ProblemAdmin(VersionAdmin):
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': (
|
||||
'code', 'name', 'is_public', 'is_manually_managed', 'date', 'authors', 'curators', 'testers',
|
||||
'is_organization_private', 'organizations', 'description', 'license',
|
||||
),
|
||||
}),
|
||||
(_('Social Media'), {'classes': ('collapse',), 'fields': ('og_image', 'summary')}),
|
||||
(_('Taxonomy'), {'fields': ('types', 'group')}),
|
||||
(_('Points'), {'fields': (('points', 'partial'), 'short_circuit')}),
|
||||
(_('Limits'), {'fields': ('time_limit', 'memory_limit')}),
|
||||
(_('Language'), {'fields': ('allowed_languages',)}),
|
||||
(_('Justice'), {'fields': ('banned_users',)}),
|
||||
(_('History'), {'fields': ('change_message',)}),
|
||||
)
|
||||
list_display = ['code', 'name', 'show_authors', 'points', 'is_public', 'show_public']
|
||||
ordering = ['code']
|
||||
search_fields = ('code', 'name', 'authors__user__username', 'curators__user__username')
|
||||
inlines = [LanguageLimitInline, ProblemClarificationInline, ProblemSolutionInline, ProblemTranslationInline]
|
||||
list_max_show_all = 1000
|
||||
actions_on_top = True
|
||||
actions_on_bottom = True
|
||||
list_filter = ('is_public', ProblemCreatorListFilter)
|
||||
form = ProblemForm
|
||||
date_hierarchy = 'date'
|
||||
|
||||
def get_actions(self, request):
|
||||
actions = super(ProblemAdmin, self).get_actions(request)
|
||||
|
||||
if request.user.has_perm('judge.change_public_visibility'):
|
||||
func, name, desc = self.get_action('make_public')
|
||||
actions[name] = (func, name, desc)
|
||||
|
||||
func, name, desc = self.get_action('make_private')
|
||||
actions[name] = (func, name, desc)
|
||||
|
||||
return actions
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
fields = self.readonly_fields
|
||||
if not request.user.has_perm('judge.change_public_visibility'):
|
||||
fields += ('is_public',)
|
||||
if not request.user.has_perm('judge.change_manually_managed'):
|
||||
fields += ('is_manually_managed',)
|
||||
return fields
|
||||
|
||||
def show_authors(self, obj):
|
||||
return ', '.join(map(attrgetter('user.username'), obj.authors.all()))
|
||||
|
||||
show_authors.short_description = _('Authors')
|
||||
|
||||
def show_public(self, obj):
|
||||
return format_html('<a href="{1}">{0}</a>', gettext('View on site'), obj.get_absolute_url())
|
||||
|
||||
show_public.short_description = ''
|
||||
|
||||
def _rescore(self, request, problem_id):
|
||||
from judge.tasks import rescore_problem
|
||||
transaction.on_commit(rescore_problem.s(problem_id).delay)
|
||||
|
||||
def make_public(self, request, queryset):
|
||||
count = queryset.update(is_public=True)
|
||||
for problem_id in queryset.values_list('id', flat=True):
|
||||
self._rescore(request, problem_id)
|
||||
self.message_user(request, ungettext('%d problem successfully marked as public.',
|
||||
'%d problems successfully marked as public.',
|
||||
count) % count)
|
||||
|
||||
make_public.short_description = _('Mark problems as public')
|
||||
|
||||
def make_private(self, request, queryset):
|
||||
count = queryset.update(is_public=False)
|
||||
for problem_id in queryset.values_list('id', flat=True):
|
||||
self._rescore(request, problem_id)
|
||||
self.message_user(request, ungettext('%d problem successfully marked as private.',
|
||||
'%d problems successfully marked as private.',
|
||||
count) % count)
|
||||
|
||||
make_private.short_description = _('Mark problems as private')
|
||||
|
||||
def get_queryset(self, request):
|
||||
queryset = Problem.objects.prefetch_related('authors__user')
|
||||
if request.user.has_perm('judge.edit_all_problem'):
|
||||
return queryset
|
||||
|
||||
access = Q()
|
||||
if request.user.has_perm('judge.edit_public_problem'):
|
||||
access |= Q(is_public=True)
|
||||
if request.user.has_perm('judge.edit_own_problem'):
|
||||
access |= Q(authors__id=request.profile.id) | Q(curators__id=request.profile.id)
|
||||
return queryset.filter(access).distinct() if access else queryset.none()
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
if request.user.has_perm('judge.edit_all_problem') or obj is None:
|
||||
return True
|
||||
if request.user.has_perm('judge.edit_public_problem') and obj.is_public:
|
||||
return True
|
||||
if not request.user.has_perm('judge.edit_own_problem'):
|
||||
return False
|
||||
return obj.is_editor(request.profile)
|
||||
|
||||
def formfield_for_manytomany(self, db_field, request=None, **kwargs):
|
||||
if db_field.name == 'allowed_languages':
|
||||
kwargs['widget'] = CheckboxSelectMultipleWithSelectAll()
|
||||
return super(ProblemAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)
|
||||
|
||||
def get_form(self, *args, **kwargs):
|
||||
form = super(ProblemAdmin, self).get_form(*args, **kwargs)
|
||||
form.base_fields['authors'].queryset = Profile.objects.all()
|
||||
return form
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
super(ProblemAdmin, self).save_model(request, obj, form, change)
|
||||
if form.changed_data and any(f in form.changed_data for f in ('is_public', 'points', 'partial')):
|
||||
self._rescore(request, obj.id)
|
||||
|
||||
def construct_change_message(self, request, form, *args, **kwargs):
|
||||
if form.cleaned_data.get('change_message'):
|
||||
return form.cleaned_data['change_message']
|
||||
return super(ProblemAdmin, self).construct_change_message(request, form, *args, **kwargs)
|
118
judge/admin/profile.py
Normal file
118
judge/admin/profile.py
Normal file
|
@ -0,0 +1,118 @@
|
|||
from django.contrib import admin
|
||||
from django.forms import ModelForm
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext, gettext_lazy as _, ungettext
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from django_ace import AceWidget
|
||||
from judge.models import Profile
|
||||
from judge.widgets import AdminPagedownWidget, AdminSelect2Widget
|
||||
|
||||
|
||||
class ProfileForm(ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ProfileForm, self).__init__(*args, **kwargs)
|
||||
if 'current_contest' in self.base_fields:
|
||||
# form.fields['current_contest'] does not exist when the user has only view permission on the model.
|
||||
self.fields['current_contest'].queryset = self.instance.contest_history.select_related('contest') \
|
||||
.only('contest__name', 'user_id', 'virtual')
|
||||
self.fields['current_contest'].label_from_instance = \
|
||||
lambda obj: '%s v%d' % (obj.contest.name, obj.virtual) if obj.virtual else obj.contest.name
|
||||
|
||||
class Meta:
|
||||
widgets = {
|
||||
'timezone': AdminSelect2Widget,
|
||||
'language': AdminSelect2Widget,
|
||||
'ace_theme': AdminSelect2Widget,
|
||||
'current_contest': AdminSelect2Widget,
|
||||
}
|
||||
if AdminPagedownWidget is not None:
|
||||
widgets['about'] = AdminPagedownWidget
|
||||
|
||||
|
||||
class TimezoneFilter(admin.SimpleListFilter):
|
||||
title = _('timezone')
|
||||
parameter_name = 'timezone'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return Profile.objects.values_list('timezone', 'timezone').distinct().order_by('timezone')
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() is None:
|
||||
return queryset
|
||||
return queryset.filter(timezone=self.value())
|
||||
|
||||
|
||||
class ProfileAdmin(VersionAdmin):
|
||||
fields = ('user', 'display_rank', 'about', 'organizations', 'timezone', 'language', 'ace_theme',
|
||||
'math_engine', 'last_access', 'ip', 'mute', 'is_unlisted', 'notes', 'is_totp_enabled', 'user_script',
|
||||
'current_contest')
|
||||
readonly_fields = ('user',)
|
||||
list_display = ('admin_user_admin', 'email', 'is_totp_enabled', 'timezone_full',
|
||||
'date_joined', 'last_access', 'ip', 'show_public')
|
||||
ordering = ('user__username',)
|
||||
search_fields = ('user__username', 'ip', 'user__email')
|
||||
list_filter = ('language', TimezoneFilter)
|
||||
actions = ('recalculate_points',)
|
||||
actions_on_top = True
|
||||
actions_on_bottom = True
|
||||
form = ProfileForm
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super(ProfileAdmin, self).get_queryset(request).select_related('user')
|
||||
|
||||
def get_fields(self, request, obj=None):
|
||||
if request.user.has_perm('judge.totp'):
|
||||
fields = list(self.fields)
|
||||
fields.insert(fields.index('is_totp_enabled') + 1, 'totp_key')
|
||||
return tuple(fields)
|
||||
else:
|
||||
return self.fields
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
fields = self.readonly_fields
|
||||
if not request.user.has_perm('judge.totp'):
|
||||
fields += ('is_totp_enabled',)
|
||||
return fields
|
||||
|
||||
def show_public(self, obj):
|
||||
return format_html('<a href="{0}" style="white-space:nowrap;">{1}</a>',
|
||||
obj.get_absolute_url(), gettext('View on site'))
|
||||
show_public.short_description = ''
|
||||
|
||||
def admin_user_admin(self, obj):
|
||||
return obj.username
|
||||
admin_user_admin.admin_order_field = 'user__username'
|
||||
admin_user_admin.short_description = _('User')
|
||||
|
||||
def email(self, obj):
|
||||
return obj.user.email
|
||||
email.admin_order_field = 'user__email'
|
||||
email.short_description = _('Email')
|
||||
|
||||
def timezone_full(self, obj):
|
||||
return obj.timezone
|
||||
timezone_full.admin_order_field = 'timezone'
|
||||
timezone_full.short_description = _('Timezone')
|
||||
|
||||
def date_joined(self, obj):
|
||||
return obj.user.date_joined
|
||||
date_joined.admin_order_field = 'user__date_joined'
|
||||
date_joined.short_description = _('date joined')
|
||||
|
||||
def recalculate_points(self, request, queryset):
|
||||
count = 0
|
||||
for profile in queryset:
|
||||
profile.calculate_points()
|
||||
count += 1
|
||||
self.message_user(request, ungettext('%d user have scores recalculated.',
|
||||
'%d users have scores recalculated.',
|
||||
count) % count)
|
||||
recalculate_points.short_description = _('Recalculate scores')
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
form = super(ProfileAdmin, self).get_form(request, obj, **kwargs)
|
||||
if 'user_script' in form.base_fields:
|
||||
# form.base_fields['user_script'] does not exist when the user has only view permission on the model.
|
||||
form.base_fields['user_script'].widget = AceWidget('javascript', request.profile.ace_theme)
|
||||
return form
|
120
judge/admin/runtime.py
Normal file
120
judge/admin/runtime.py
Normal file
|
@ -0,0 +1,120 @@
|
|||
from django.conf.urls import url
|
||||
from django.db.models import TextField
|
||||
from django.forms import ModelForm, ModelMultipleChoiceField, TextInput
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from django_ace import AceWidget
|
||||
from judge.models import Judge, Problem
|
||||
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminPagedownWidget
|
||||
|
||||
|
||||
class LanguageForm(ModelForm):
|
||||
problems = ModelMultipleChoiceField(
|
||||
label=_('Disallowed problems'),
|
||||
queryset=Problem.objects.all(),
|
||||
required=False,
|
||||
help_text=_('These problems are NOT allowed to be submitted in this language'),
|
||||
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'))
|
||||
|
||||
class Meta:
|
||||
if AdminPagedownWidget is not None:
|
||||
widgets = {'description': AdminPagedownWidget}
|
||||
|
||||
|
||||
class LanguageAdmin(VersionAdmin):
|
||||
fields = ('key', 'name', 'short_name', 'common_name', 'ace', 'pygments', 'info', 'description',
|
||||
'template', 'problems')
|
||||
list_display = ('key', 'name', 'common_name', 'info')
|
||||
form = LanguageForm
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
super(LanguageAdmin, self).save_model(request, obj, form, change)
|
||||
obj.problem_set.set(Problem.objects.exclude(id__in=form.cleaned_data['problems'].values('id')))
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
self.form.base_fields['problems'].initial = \
|
||||
Problem.objects.exclude(id__in=obj.problem_set.values('id')).values_list('pk', flat=True) if obj else []
|
||||
form = super(LanguageAdmin, self).get_form(request, obj, **kwargs)
|
||||
if obj is not None:
|
||||
form.base_fields['template'].widget = AceWidget(obj.ace, request.profile.ace_theme)
|
||||
return form
|
||||
|
||||
|
||||
class GenerateKeyTextInput(TextInput):
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
text = super(TextInput, self).render(name, value, attrs)
|
||||
return mark_safe(text + format_html(
|
||||
'''\
|
||||
<a href="#" onclick="return false;" class="button" id="id_{0}_regen">Regenerate</a>
|
||||
<script type="text/javascript">
|
||||
django.jQuery(document).ready(function ($) {{
|
||||
$('#id_{0}_regen').click(function () {{
|
||||
var length = 100,
|
||||
charset = "abcdefghijklnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`~!@#$%^&*()_+-=|[]{{}};:,<>./?",
|
||||
key = "";
|
||||
for (var i = 0, n = charset.length; i < length; ++i) {{
|
||||
key += charset.charAt(Math.floor(Math.random() * n));
|
||||
}}
|
||||
$('#id_{0}').val(key);
|
||||
}});
|
||||
}});
|
||||
</script>
|
||||
''', name))
|
||||
|
||||
|
||||
class JudgeAdminForm(ModelForm):
|
||||
class Meta:
|
||||
widgets = {'auth_key': GenerateKeyTextInput}
|
||||
if AdminPagedownWidget is not None:
|
||||
widgets['description'] = AdminPagedownWidget
|
||||
|
||||
|
||||
class JudgeAdmin(VersionAdmin):
|
||||
form = JudgeAdminForm
|
||||
readonly_fields = ('created', 'online', 'start_time', 'ping', 'load', 'last_ip', 'runtimes', 'problems')
|
||||
fieldsets = (
|
||||
(None, {'fields': ('name', 'auth_key', 'is_blocked')}),
|
||||
(_('Description'), {'fields': ('description',)}),
|
||||
(_('Information'), {'fields': ('created', 'online', 'last_ip', 'start_time', 'ping', 'load')}),
|
||||
(_('Capabilities'), {'fields': ('runtimes', 'problems')}),
|
||||
)
|
||||
list_display = ('name', 'online', 'start_time', 'ping', 'load', 'last_ip')
|
||||
ordering = ['-online', 'name']
|
||||
|
||||
def get_urls(self):
|
||||
return ([url(r'^(\d+)/disconnect/$', self.disconnect_view, name='judge_judge_disconnect'),
|
||||
url(r'^(\d+)/terminate/$', self.terminate_view, name='judge_judge_terminate')] +
|
||||
super(JudgeAdmin, self).get_urls())
|
||||
|
||||
def disconnect_judge(self, id, force=False):
|
||||
judge = get_object_or_404(Judge, id=id)
|
||||
judge.disconnect(force=force)
|
||||
return HttpResponseRedirect(reverse('admin:judge_judge_changelist'))
|
||||
|
||||
def disconnect_view(self, request, id):
|
||||
return self.disconnect_judge(id)
|
||||
|
||||
def terminate_view(self, request, id):
|
||||
return self.disconnect_judge(id, force=True)
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
if obj is not None and obj.online:
|
||||
return self.readonly_fields + ('name',)
|
||||
return self.readonly_fields
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
result = super(JudgeAdmin, self).has_delete_permission(request, obj)
|
||||
if result and obj is not None:
|
||||
return not obj.online
|
||||
return result
|
||||
|
||||
if AdminPagedownWidget is not None:
|
||||
formfield_overrides = {
|
||||
TextField: {'widget': AdminPagedownWidget},
|
||||
}
|
251
judge/admin/submission.py
Normal file
251
judge/admin/submission.py
Normal file
|
@ -0,0 +1,251 @@
|
|||
from functools import partial
|
||||
from operator import itemgetter
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
from django.contrib import admin, messages
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext, gettext_lazy as _, pgettext, ungettext
|
||||
|
||||
from django_ace import AceWidget
|
||||
from judge.models import ContestParticipation, ContestProblem, ContestSubmission, Profile, Submission, \
|
||||
SubmissionSource, SubmissionTestCase
|
||||
from judge.utils.raw_sql import use_straight_join
|
||||
|
||||
|
||||
class SubmissionStatusFilter(admin.SimpleListFilter):
|
||||
parameter_name = title = 'status'
|
||||
__lookups = (('None', _('None')), ('NotDone', _('Not done')), ('EX', _('Exceptional'))) + Submission.STATUS
|
||||
__handles = set(map(itemgetter(0), Submission.STATUS))
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return self.__lookups
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() == 'None':
|
||||
return queryset.filter(status=None)
|
||||
elif self.value() == 'NotDone':
|
||||
return queryset.exclude(status__in=['D', 'IE', 'CE', 'AB'])
|
||||
elif self.value() == 'EX':
|
||||
return queryset.exclude(status__in=['D', 'CE', 'G', 'AB'])
|
||||
elif self.value() in self.__handles:
|
||||
return queryset.filter(status=self.value())
|
||||
|
||||
|
||||
class SubmissionResultFilter(admin.SimpleListFilter):
|
||||
parameter_name = title = 'result'
|
||||
__lookups = (('None', _('None')), ('BAD', _('Unaccepted'))) + Submission.RESULT
|
||||
__handles = set(map(itemgetter(0), Submission.RESULT))
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return self.__lookups
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() == 'None':
|
||||
return queryset.filter(result=None)
|
||||
elif self.value() == 'BAD':
|
||||
return queryset.exclude(result='AC')
|
||||
elif self.value() in self.__handles:
|
||||
return queryset.filter(result=self.value())
|
||||
|
||||
|
||||
class SubmissionTestCaseInline(admin.TabularInline):
|
||||
fields = ('case', 'batch', 'status', 'time', 'memory', 'points', 'total')
|
||||
readonly_fields = ('case', 'batch', 'total')
|
||||
model = SubmissionTestCase
|
||||
can_delete = False
|
||||
max_num = 0
|
||||
|
||||
|
||||
class ContestSubmissionInline(admin.StackedInline):
|
||||
fields = ('problem', 'participation', 'points')
|
||||
model = ContestSubmission
|
||||
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
kwargs['formfield_callback'] = partial(self.formfield_for_dbfield, request=request, obj=obj)
|
||||
return super(ContestSubmissionInline, self).get_formset(request, obj, **kwargs)
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
submission = kwargs.pop('obj', None)
|
||||
label = None
|
||||
if submission:
|
||||
if db_field.name == 'participation':
|
||||
kwargs['queryset'] = ContestParticipation.objects.filter(user=submission.user,
|
||||
contest__problems=submission.problem) \
|
||||
.only('id', 'contest__name')
|
||||
|
||||
def label(obj):
|
||||
return obj.contest.name
|
||||
elif db_field.name == 'problem':
|
||||
kwargs['queryset'] = ContestProblem.objects.filter(problem=submission.problem) \
|
||||
.only('id', 'problem__name', 'contest__name')
|
||||
|
||||
def label(obj):
|
||||
return pgettext('contest problem', '%(problem)s in %(contest)s') % {
|
||||
'problem': obj.problem.name, 'contest': obj.contest.name,
|
||||
}
|
||||
field = super(ContestSubmissionInline, self).formfield_for_dbfield(db_field, **kwargs)
|
||||
if label is not None:
|
||||
field.label_from_instance = label
|
||||
return field
|
||||
|
||||
|
||||
class SubmissionSourceInline(admin.StackedInline):
|
||||
fields = ('source',)
|
||||
model = SubmissionSource
|
||||
can_delete = False
|
||||
extra = 0
|
||||
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
kwargs.setdefault('widgets', {})['source'] = AceWidget(mode=obj and obj.language.ace,
|
||||
theme=request.profile.ace_theme)
|
||||
return super().get_formset(request, obj, **kwargs)
|
||||
|
||||
|
||||
class SubmissionAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ('user', 'problem', 'date')
|
||||
fields = ('user', 'problem', 'date', 'time', 'memory', 'points', 'language', 'status', 'result',
|
||||
'case_points', 'case_total', 'judged_on', 'error')
|
||||
actions = ('judge', 'recalculate_score')
|
||||
list_display = ('id', 'problem_code', 'problem_name', 'user_column', 'execution_time', 'pretty_memory',
|
||||
'points', 'language_column', 'status', 'result', 'judge_column')
|
||||
list_filter = ('language', SubmissionStatusFilter, SubmissionResultFilter)
|
||||
search_fields = ('problem__code', 'problem__name', 'user__user__username')
|
||||
actions_on_top = True
|
||||
actions_on_bottom = True
|
||||
inlines = [SubmissionSourceInline, SubmissionTestCaseInline, ContestSubmissionInline]
|
||||
|
||||
def get_queryset(self, request):
|
||||
queryset = Submission.objects.select_related('problem', 'user__user', 'language').only(
|
||||
'problem__code', 'problem__name', 'user__user__username', 'language__name',
|
||||
'time', 'memory', 'points', 'status', 'result',
|
||||
)
|
||||
use_straight_join(queryset)
|
||||
if not request.user.has_perm('judge.edit_all_problem'):
|
||||
id = request.profile.id
|
||||
queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id)).distinct()
|
||||
return queryset
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
if not request.user.has_perm('judge.edit_own_problem'):
|
||||
return False
|
||||
if request.user.has_perm('judge.edit_all_problem') or obj is None:
|
||||
return True
|
||||
return obj.problem.is_editor(request.profile)
|
||||
|
||||
def lookup_allowed(self, key, value):
|
||||
return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in ('problem__code',)
|
||||
|
||||
def judge(self, request, queryset):
|
||||
if not request.user.has_perm('judge.rejudge_submission') or not request.user.has_perm('judge.edit_own_problem'):
|
||||
self.message_user(request, gettext('You do not have the permission to rejudge submissions.'),
|
||||
level=messages.ERROR)
|
||||
return
|
||||
queryset = queryset.order_by('id')
|
||||
if not request.user.has_perm('judge.rejudge_submission_lot') and \
|
||||
queryset.count() > settings.DMOJ_SUBMISSIONS_REJUDGE_LIMIT:
|
||||
self.message_user(request, gettext('You do not have the permission to rejudge THAT many submissions.'),
|
||||
level=messages.ERROR)
|
||||
return
|
||||
if not request.user.has_perm('judge.edit_all_problem'):
|
||||
id = request.profile.id
|
||||
queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id))
|
||||
judged = len(queryset)
|
||||
for model in queryset:
|
||||
model.judge(rejudge=True, batch_rejudge=True)
|
||||
self.message_user(request, ungettext('%d submission was successfully scheduled for rejudging.',
|
||||
'%d submissions were successfully scheduled for rejudging.',
|
||||
judged) % judged)
|
||||
judge.short_description = _('Rejudge the selected submissions')
|
||||
|
||||
def recalculate_score(self, request, queryset):
|
||||
if not request.user.has_perm('judge.rejudge_submission'):
|
||||
self.message_user(request, gettext('You do not have the permission to rejudge submissions.'),
|
||||
level=messages.ERROR)
|
||||
return
|
||||
submissions = list(queryset.defer(None).select_related(None).select_related('problem')
|
||||
.only('points', 'case_points', 'case_total', 'problem__partial', 'problem__points'))
|
||||
for submission in submissions:
|
||||
submission.points = round(submission.case_points / submission.case_total * submission.problem.points
|
||||
if submission.case_total else 0, 1)
|
||||
if not submission.problem.partial and submission.points < submission.problem.points:
|
||||
submission.points = 0
|
||||
submission.save()
|
||||
submission.update_contest()
|
||||
|
||||
for profile in Profile.objects.filter(id__in=queryset.values_list('user_id', flat=True).distinct()):
|
||||
profile.calculate_points()
|
||||
cache.delete('user_complete:%d' % profile.id)
|
||||
cache.delete('user_attempted:%d' % profile.id)
|
||||
|
||||
for participation in ContestParticipation.objects.filter(
|
||||
id__in=queryset.values_list('contest__participation_id')).prefetch_related('contest'):
|
||||
participation.recompute_results()
|
||||
|
||||
self.message_user(request, ungettext('%d submission were successfully rescored.',
|
||||
'%d submissions were successfully rescored.',
|
||||
len(submissions)) % len(submissions))
|
||||
recalculate_score.short_description = _('Rescore the selected submissions')
|
||||
|
||||
def problem_code(self, obj):
|
||||
return obj.problem.code
|
||||
problem_code.short_description = _('Problem code')
|
||||
problem_code.admin_order_field = 'problem__code'
|
||||
|
||||
def problem_name(self, obj):
|
||||
return obj.problem.name
|
||||
problem_name.short_description = _('Problem name')
|
||||
problem_name.admin_order_field = 'problem__name'
|
||||
|
||||
def user_column(self, obj):
|
||||
return obj.user.user.username
|
||||
user_column.admin_order_field = 'user__user__username'
|
||||
user_column.short_description = _('User')
|
||||
|
||||
def execution_time(self, obj):
|
||||
return round(obj.time, 2) if obj.time is not None else 'None'
|
||||
execution_time.short_description = _('Time')
|
||||
execution_time.admin_order_field = 'time'
|
||||
|
||||
def pretty_memory(self, obj):
|
||||
memory = obj.memory
|
||||
if memory is None:
|
||||
return gettext('None')
|
||||
if memory < 1000:
|
||||
return gettext('%d KB') % memory
|
||||
else:
|
||||
return gettext('%.2f MB') % (memory / 1024)
|
||||
pretty_memory.admin_order_field = 'memory'
|
||||
pretty_memory.short_description = _('Memory')
|
||||
|
||||
def language_column(self, obj):
|
||||
return obj.language.name
|
||||
language_column.admin_order_field = 'language__name'
|
||||
language_column.short_description = _('Language')
|
||||
|
||||
def judge_column(self, obj):
|
||||
return format_html('<input type="button" value="Rejudge" onclick="location.href=\'{}/judge/\'" />', obj.id)
|
||||
judge_column.short_description = ''
|
||||
|
||||
def get_urls(self):
|
||||
return [
|
||||
url(r'^(\d+)/judge/$', self.judge_view, name='judge_submission_rejudge'),
|
||||
] + super(SubmissionAdmin, self).get_urls()
|
||||
|
||||
def judge_view(self, request, id):
|
||||
if not request.user.has_perm('judge.rejudge_submission') or not request.user.has_perm('judge.edit_own_problem'):
|
||||
raise PermissionDenied()
|
||||
submission = get_object_or_404(Submission, id=id)
|
||||
if not request.user.has_perm('judge.edit_all_problem') and \
|
||||
not submission.problem.is_editor(request.profile):
|
||||
raise PermissionDenied()
|
||||
submission.judge(rejudge=True)
|
||||
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
|
52
judge/admin/taxon.py
Normal file
52
judge/admin/taxon.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
from django.contrib import admin
|
||||
from django.forms import ModelForm, ModelMultipleChoiceField
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from judge.models import Problem
|
||||
from judge.widgets import AdminHeavySelect2MultipleWidget
|
||||
|
||||
|
||||
class ProblemGroupForm(ModelForm):
|
||||
problems = ModelMultipleChoiceField(
|
||||
label=_('Included problems'),
|
||||
queryset=Problem.objects.all(),
|
||||
required=False,
|
||||
help_text=_('These problems are included in this group of problems'),
|
||||
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'))
|
||||
|
||||
|
||||
class ProblemGroupAdmin(admin.ModelAdmin):
|
||||
fields = ('name', 'full_name', 'problems')
|
||||
form = ProblemGroupForm
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
super(ProblemGroupAdmin, self).save_model(request, obj, form, change)
|
||||
obj.problem_set.set(form.cleaned_data['problems'])
|
||||
obj.save()
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
self.form.base_fields['problems'].initial = [o.pk for o in obj.problem_set.all()] if obj else []
|
||||
return super(ProblemGroupAdmin, self).get_form(request, obj, **kwargs)
|
||||
|
||||
|
||||
class ProblemTypeForm(ModelForm):
|
||||
problems = ModelMultipleChoiceField(
|
||||
label=_('Included problems'),
|
||||
queryset=Problem.objects.all(),
|
||||
required=False,
|
||||
help_text=_('These problems are included in this type of problems'),
|
||||
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2'))
|
||||
|
||||
|
||||
class ProblemTypeAdmin(admin.ModelAdmin):
|
||||
fields = ('name', 'full_name', 'problems')
|
||||
form = ProblemTypeForm
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
super(ProblemTypeAdmin, self).save_model(request, obj, form, change)
|
||||
obj.problem_set.set(form.cleaned_data['problems'])
|
||||
obj.save()
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
self.form.base_fields['problems'].initial = [o.pk for o in obj.problem_set.all()] if obj else []
|
||||
return super(ProblemTypeAdmin, self).get_form(request, obj, **kwargs)
|
39
judge/admin/ticket.py
Normal file
39
judge/admin/ticket.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
from django.contrib.admin import ModelAdmin
|
||||
from django.contrib.admin.options import StackedInline
|
||||
from django.forms import ModelForm
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from judge.models import TicketMessage
|
||||
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget
|
||||
|
||||
|
||||
class TicketMessageForm(ModelForm):
|
||||
class Meta:
|
||||
widgets = {
|
||||
'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||
}
|
||||
if HeavyPreviewAdminPageDownWidget is not None:
|
||||
widgets['body'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('ticket_preview'))
|
||||
|
||||
|
||||
class TicketMessageInline(StackedInline):
|
||||
model = TicketMessage
|
||||
form = TicketMessageForm
|
||||
fields = ('user', 'body')
|
||||
|
||||
|
||||
class TicketForm(ModelForm):
|
||||
class Meta:
|
||||
widgets = {
|
||||
'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||
'assignees': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}),
|
||||
}
|
||||
|
||||
|
||||
class TicketAdmin(ModelAdmin):
|
||||
fields = ('title', 'time', 'user', 'assignees', 'content_type', 'object_id', 'notes')
|
||||
readonly_fields = ('time',)
|
||||
list_display = ('title', 'user', 'time', 'linked_item')
|
||||
inlines = [TicketMessageInline]
|
||||
form = TicketForm
|
||||
date_hierarchy = 'time'
|
39
judge/apps.py
Normal file
39
judge/apps.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
from django.apps import AppConfig
|
||||
from django.db import DatabaseError
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
|
||||
class JudgeAppConfig(AppConfig):
|
||||
name = 'judge'
|
||||
verbose_name = gettext_lazy('Online Judge')
|
||||
|
||||
def ready(self):
|
||||
# WARNING: AS THIS IS NOT A FUNCTIONAL PROGRAMMING LANGUAGE,
|
||||
# OPERATIONS MAY HAVE SIDE EFFECTS.
|
||||
# DO NOT REMOVE THINKING THE IMPORT IS UNUSED.
|
||||
# noinspection PyUnresolvedReferences
|
||||
from . import signals, jinja2 # noqa: F401, imported for side effects
|
||||
|
||||
from django.contrib.flatpages.models import FlatPage
|
||||
from django.contrib.flatpages.admin import FlatPageAdmin
|
||||
from django.contrib import admin
|
||||
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
class FlatPageVersionAdmin(VersionAdmin, FlatPageAdmin):
|
||||
pass
|
||||
|
||||
admin.site.unregister(FlatPage)
|
||||
admin.site.register(FlatPage, FlatPageVersionAdmin)
|
||||
|
||||
from judge.models import Language, Profile
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
try:
|
||||
lang = Language.get_python3()
|
||||
for user in User.objects.filter(profile=None):
|
||||
# These poor profileless users
|
||||
profile = Profile(user=user, language=lang)
|
||||
profile.save()
|
||||
except DatabaseError:
|
||||
pass
|
6
judge/bridge/__init__.py
Normal file
6
judge/bridge/__init__.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from .djangohandler import DjangoHandler
|
||||
from .djangoserver import DjangoServer
|
||||
from .judgecallback import DjangoJudgeHandler
|
||||
from .judgehandler import JudgeHandler
|
||||
from .judgelist import JudgeList
|
||||
from .judgeserver import JudgeServer
|
67
judge/bridge/djangohandler.py
Normal file
67
judge/bridge/djangohandler.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
import json
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from event_socket_server import ZlibPacketHandler
|
||||
|
||||
logger = logging.getLogger('judge.bridge')
|
||||
size_pack = struct.Struct('!I')
|
||||
|
||||
|
||||
class DjangoHandler(ZlibPacketHandler):
|
||||
def __init__(self, server, socket):
|
||||
super(DjangoHandler, self).__init__(server, socket)
|
||||
|
||||
self.handlers = {
|
||||
'submission-request': self.on_submission,
|
||||
'terminate-submission': self.on_termination,
|
||||
'disconnect-judge': self.on_disconnect,
|
||||
}
|
||||
self._to_kill = True
|
||||
# self.server.schedule(5, self._kill_if_no_request)
|
||||
|
||||
def _kill_if_no_request(self):
|
||||
if self._to_kill:
|
||||
logger.info('Killed inactive connection: %s', self._socket.getpeername())
|
||||
self.close()
|
||||
|
||||
def _format_send(self, data):
|
||||
return super(DjangoHandler, self)._format_send(json.dumps(data, separators=(',', ':')))
|
||||
|
||||
def packet(self, packet):
|
||||
self._to_kill = False
|
||||
packet = json.loads(packet)
|
||||
try:
|
||||
result = self.handlers.get(packet.get('name', None), self.on_malformed)(packet)
|
||||
except Exception:
|
||||
logger.exception('Error in packet handling (Django-facing)')
|
||||
result = {'name': 'bad-request'}
|
||||
self.send(result, self._schedule_close)
|
||||
|
||||
def _schedule_close(self):
|
||||
self.server.schedule(0, self.close)
|
||||
|
||||
def on_submission(self, data):
|
||||
id = data['submission-id']
|
||||
problem = data['problem-id']
|
||||
language = data['language']
|
||||
source = data['source']
|
||||
priority = data['priority']
|
||||
if not self.server.judges.check_priority(priority):
|
||||
return {'name': 'bad-request'}
|
||||
self.server.judges.judge(id, problem, language, source, priority)
|
||||
return {'name': 'submission-received', 'submission-id': id}
|
||||
|
||||
def on_termination(self, data):
|
||||
return {'name': 'submission-received', 'judge-aborted': self.server.judges.abort(data['submission-id'])}
|
||||
|
||||
def on_disconnect(self, data):
|
||||
judge_id = data['judge-id']
|
||||
force = data['force']
|
||||
self.server.judges.disconnect(judge_id, force=force)
|
||||
|
||||
def on_malformed(self, packet):
|
||||
logger.error('Malformed packet: %s', packet)
|
||||
|
||||
def on_close(self):
|
||||
self._to_kill = False
|
7
judge/bridge/djangoserver.py
Normal file
7
judge/bridge/djangoserver.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from event_socket_server import get_preferred_engine
|
||||
|
||||
|
||||
class DjangoServer(get_preferred_engine()):
|
||||
def __init__(self, judges, *args, **kwargs):
|
||||
super(DjangoServer, self).__init__(*args, **kwargs)
|
||||
self.judges = judges
|
411
judge/bridge/judgecallback.py
Normal file
411
judge/bridge/judgecallback.py
Normal file
|
@ -0,0 +1,411 @@
|
|||
import json
|
||||
import logging
|
||||
import time
|
||||
from operator import itemgetter
|
||||
|
||||
from django import db
|
||||
from django.utils import timezone
|
||||
|
||||
from judge import event_poster as event
|
||||
from judge.caching import finished_submission
|
||||
from judge.models import Judge, Language, LanguageLimit, Problem, RuntimeVersion, Submission, SubmissionTestCase
|
||||
from .judgehandler import JudgeHandler, SubmissionData
|
||||
|
||||
logger = logging.getLogger('judge.bridge')
|
||||
json_log = logging.getLogger('judge.json.bridge')
|
||||
|
||||
UPDATE_RATE_LIMIT = 5
|
||||
UPDATE_RATE_TIME = 0.5
|
||||
|
||||
|
||||
def _ensure_connection():
|
||||
try:
|
||||
db.connection.cursor().execute('SELECT 1').fetchall()
|
||||
except Exception:
|
||||
db.connection.close()
|
||||
|
||||
|
||||
class DjangoJudgeHandler(JudgeHandler):
|
||||
def __init__(self, server, socket):
|
||||
super(DjangoJudgeHandler, self).__init__(server, socket)
|
||||
|
||||
# each value is (updates, last reset)
|
||||
self.update_counter = {}
|
||||
self.judge = None
|
||||
self.judge_address = None
|
||||
|
||||
self._submission_cache_id = None
|
||||
self._submission_cache = {}
|
||||
|
||||
json_log.info(self._make_json_log(action='connect'))
|
||||
|
||||
def on_close(self):
|
||||
super(DjangoJudgeHandler, self).on_close()
|
||||
json_log.info(self._make_json_log(action='disconnect', info='judge disconnected'))
|
||||
if self._working:
|
||||
Submission.objects.filter(id=self._working).update(status='IE', result='IE')
|
||||
json_log.error(self._make_json_log(sub=self._working, action='close', info='IE due to shutdown on grading'))
|
||||
|
||||
def on_malformed(self, packet):
|
||||
super(DjangoJudgeHandler, self).on_malformed(packet)
|
||||
json_log.exception(self._make_json_log(sub=self._working, info='malformed zlib packet'))
|
||||
|
||||
def _packet_exception(self):
|
||||
json_log.exception(self._make_json_log(sub=self._working, info='packet processing exception'))
|
||||
|
||||
def get_related_submission_data(self, submission):
|
||||
_ensure_connection() # We are called from the django-facing daemon thread. Guess what happens.
|
||||
|
||||
try:
|
||||
pid, time, memory, short_circuit, lid, is_pretested, sub_date, uid, part_virtual, part_id = (
|
||||
Submission.objects.filter(id=submission)
|
||||
.values_list('problem__id', 'problem__time_limit', 'problem__memory_limit',
|
||||
'problem__short_circuit', 'language__id', 'is_pretested', 'date', 'user__id',
|
||||
'contest__participation__virtual', 'contest__participation__id')).get()
|
||||
except Submission.DoesNotExist:
|
||||
logger.error('Submission vanished: %s', submission)
|
||||
json_log.error(self._make_json_log(
|
||||
sub=self._working, action='request',
|
||||
info='submission vanished when fetching info',
|
||||
))
|
||||
return
|
||||
|
||||
attempt_no = Submission.objects.filter(problem__id=pid, contest__participation__id=part_id, user__id=uid,
|
||||
date__lt=sub_date).exclude(status__in=('CE', 'IE')).count() + 1
|
||||
|
||||
try:
|
||||
time, memory = (LanguageLimit.objects.filter(problem__id=pid, language__id=lid)
|
||||
.values_list('time_limit', 'memory_limit').get())
|
||||
except LanguageLimit.DoesNotExist:
|
||||
pass
|
||||
|
||||
return SubmissionData(
|
||||
time=time,
|
||||
memory=memory,
|
||||
short_circuit=short_circuit,
|
||||
pretests_only=is_pretested,
|
||||
contest_no=part_virtual,
|
||||
attempt_no=attempt_no,
|
||||
user_id=uid,
|
||||
)
|
||||
|
||||
def _authenticate(self, id, key):
|
||||
result = Judge.objects.filter(name=id, auth_key=key, is_blocked=False).exists()
|
||||
if not result:
|
||||
json_log.warning(self._make_json_log(action='auth', judge=id, info='judge failed authentication'))
|
||||
return result
|
||||
|
||||
def _connected(self):
|
||||
judge = self.judge = Judge.objects.get(name=self.name)
|
||||
judge.start_time = timezone.now()
|
||||
judge.online = True
|
||||
judge.problems.set(Problem.objects.filter(code__in=list(self.problems.keys())))
|
||||
judge.runtimes.set(Language.objects.filter(key__in=list(self.executors.keys())))
|
||||
|
||||
# Delete now in case we somehow crashed and left some over from the last connection
|
||||
RuntimeVersion.objects.filter(judge=judge).delete()
|
||||
versions = []
|
||||
for lang in judge.runtimes.all():
|
||||
versions += [
|
||||
RuntimeVersion(language=lang, name=name, version='.'.join(map(str, version)), priority=idx, judge=judge)
|
||||
for idx, (name, version) in enumerate(self.executors[lang.key])
|
||||
]
|
||||
RuntimeVersion.objects.bulk_create(versions)
|
||||
judge.last_ip = self.client_address[0]
|
||||
judge.save()
|
||||
self.judge_address = '[%s]:%s' % (self.client_address[0], self.client_address[1])
|
||||
json_log.info(self._make_json_log(action='auth', info='judge successfully authenticated',
|
||||
executors=list(self.executors.keys())))
|
||||
|
||||
def _disconnected(self):
|
||||
Judge.objects.filter(id=self.judge.id).update(online=False)
|
||||
RuntimeVersion.objects.filter(judge=self.judge).delete()
|
||||
|
||||
def _update_ping(self):
|
||||
try:
|
||||
Judge.objects.filter(name=self.name).update(ping=self.latency, load=self.load)
|
||||
except Exception as e:
|
||||
# What can I do? I don't want to tie this to MySQL.
|
||||
if e.__class__.__name__ == 'OperationalError' and e.__module__ == '_mysql_exceptions' and e.args[0] == 2006:
|
||||
db.connection.close()
|
||||
|
||||
def _post_update_submission(self, id, state, done=False):
|
||||
if self._submission_cache_id == id:
|
||||
data = self._submission_cache
|
||||
else:
|
||||
self._submission_cache = data = Submission.objects.filter(id=id).values(
|
||||
'problem__is_public', 'contest__participation__contest__key',
|
||||
'user_id', 'problem_id', 'status', 'language__key',
|
||||
).get()
|
||||
self._submission_cache_id = id
|
||||
|
||||
if data['problem__is_public']:
|
||||
event.post('submissions', {
|
||||
'type': 'done-submission' if done else 'update-submission',
|
||||
'state': state, 'id': id,
|
||||
'contest': data['contest__participation__contest__key'],
|
||||
'user': data['user_id'], 'problem': data['problem_id'],
|
||||
'status': data['status'], 'language': data['language__key'],
|
||||
})
|
||||
|
||||
def on_submission_processing(self, packet):
|
||||
id = packet['submission-id']
|
||||
if Submission.objects.filter(id=id).update(status='P', judged_on=self.judge):
|
||||
event.post('sub_%s' % Submission.get_id_secret(id), {'type': 'processing'})
|
||||
self._post_update_submission(id, 'processing')
|
||||
json_log.info(self._make_json_log(packet, action='processing'))
|
||||
else:
|
||||
logger.warning('Unknown submission: %s', id)
|
||||
json_log.error(self._make_json_log(packet, action='processing', info='unknown submission'))
|
||||
|
||||
def on_submission_wrong_acknowledge(self, packet, expected, got):
|
||||
json_log.error(self._make_json_log(packet, action='processing', info='wrong-acknowledge', expected=expected))
|
||||
|
||||
def on_grading_begin(self, packet):
|
||||
super(DjangoJudgeHandler, self).on_grading_begin(packet)
|
||||
if Submission.objects.filter(id=packet['submission-id']).update(
|
||||
status='G', is_pretested=packet['pretested'],
|
||||
current_testcase=1, batch=False):
|
||||
SubmissionTestCase.objects.filter(submission_id=packet['submission-id']).delete()
|
||||
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'grading-begin'})
|
||||
self._post_update_submission(packet['submission-id'], 'grading-begin')
|
||||
json_log.info(self._make_json_log(packet, action='grading-begin'))
|
||||
else:
|
||||
logger.warning('Unknown submission: %s', packet['submission-id'])
|
||||
json_log.error(self._make_json_log(packet, action='grading-begin', info='unknown submission'))
|
||||
|
||||
def _submission_is_batch(self, id):
|
||||
if not Submission.objects.filter(id=id).update(batch=True):
|
||||
logger.warning('Unknown submission: %s', id)
|
||||
|
||||
def on_grading_end(self, packet):
|
||||
super(DjangoJudgeHandler, self).on_grading_end(packet)
|
||||
|
||||
try:
|
||||
submission = Submission.objects.get(id=packet['submission-id'])
|
||||
except Submission.DoesNotExist:
|
||||
logger.warning('Unknown submission: %s', packet['submission-id'])
|
||||
json_log.error(self._make_json_log(packet, action='grading-end', info='unknown submission'))
|
||||
return
|
||||
|
||||
time = 0
|
||||
memory = 0
|
||||
points = 0.0
|
||||
total = 0
|
||||
status = 0
|
||||
status_codes = ['SC', 'AC', 'WA', 'MLE', 'TLE', 'IR', 'RTE', 'OLE']
|
||||
batches = {} # batch number: (points, total)
|
||||
|
||||
for case in SubmissionTestCase.objects.filter(submission=submission):
|
||||
time += case.time
|
||||
if not case.batch:
|
||||
points += case.points
|
||||
total += case.total
|
||||
else:
|
||||
if case.batch in batches:
|
||||
batches[case.batch][0] = min(batches[case.batch][0], case.points)
|
||||
batches[case.batch][1] = max(batches[case.batch][1], case.total)
|
||||
else:
|
||||
batches[case.batch] = [case.points, case.total]
|
||||
memory = max(memory, case.memory)
|
||||
i = status_codes.index(case.status)
|
||||
if i > status:
|
||||
status = i
|
||||
|
||||
for i in batches:
|
||||
points += batches[i][0]
|
||||
total += batches[i][1]
|
||||
|
||||
points = round(points, 1)
|
||||
total = round(total, 1)
|
||||
submission.case_points = points
|
||||
submission.case_total = total
|
||||
|
||||
problem = submission.problem
|
||||
sub_points = round(points / total * problem.points if total > 0 else 0, 3)
|
||||
if not problem.partial and sub_points != problem.points:
|
||||
sub_points = 0
|
||||
|
||||
submission.status = 'D'
|
||||
submission.time = time
|
||||
submission.memory = memory
|
||||
submission.points = sub_points
|
||||
submission.result = status_codes[status]
|
||||
submission.save()
|
||||
|
||||
json_log.info(self._make_json_log(
|
||||
packet, action='grading-end', time=time, memory=memory,
|
||||
points=sub_points, total=problem.points, result=submission.result,
|
||||
case_points=points, case_total=total, user=submission.user_id,
|
||||
problem=problem.code, finish=True,
|
||||
))
|
||||
|
||||
submission.user._updating_stats_only = True
|
||||
submission.user.calculate_points()
|
||||
problem._updating_stats_only = True
|
||||
problem.update_stats()
|
||||
submission.update_contest()
|
||||
|
||||
finished_submission(submission)
|
||||
|
||||
event.post('sub_%s' % submission.id_secret, {
|
||||
'type': 'grading-end',
|
||||
'time': time,
|
||||
'memory': memory,
|
||||
'points': float(points),
|
||||
'total': float(problem.points),
|
||||
'result': submission.result,
|
||||
})
|
||||
if hasattr(submission, 'contest'):
|
||||
participation = submission.contest.participation
|
||||
event.post('contest_%d' % participation.contest_id, {'type': 'update'})
|
||||
self._post_update_submission(submission.id, 'grading-end', done=True)
|
||||
|
||||
def on_compile_error(self, packet):
|
||||
super(DjangoJudgeHandler, self).on_compile_error(packet)
|
||||
|
||||
if Submission.objects.filter(id=packet['submission-id']).update(status='CE', result='CE', error=packet['log']):
|
||||
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {
|
||||
'type': 'compile-error',
|
||||
'log': packet['log'],
|
||||
})
|
||||
self._post_update_submission(packet['submission-id'], 'compile-error', done=True)
|
||||
json_log.info(self._make_json_log(packet, action='compile-error', log=packet['log'],
|
||||
finish=True, result='CE'))
|
||||
else:
|
||||
logger.warning('Unknown submission: %s', packet['submission-id'])
|
||||
json_log.error(self._make_json_log(packet, action='compile-error', info='unknown submission',
|
||||
log=packet['log'], finish=True, result='CE'))
|
||||
|
||||
def on_compile_message(self, packet):
|
||||
super(DjangoJudgeHandler, self).on_compile_message(packet)
|
||||
|
||||
if Submission.objects.filter(id=packet['submission-id']).update(error=packet['log']):
|
||||
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'compile-message'})
|
||||
json_log.info(self._make_json_log(packet, action='compile-message', log=packet['log']))
|
||||
else:
|
||||
logger.warning('Unknown submission: %s', packet['submission-id'])
|
||||
json_log.error(self._make_json_log(packet, action='compile-message', info='unknown submission',
|
||||
log=packet['log']))
|
||||
|
||||
def on_internal_error(self, packet):
|
||||
super(DjangoJudgeHandler, self).on_internal_error(packet)
|
||||
|
||||
id = packet['submission-id']
|
||||
if Submission.objects.filter(id=id).update(status='IE', result='IE', error=packet['message']):
|
||||
event.post('sub_%s' % Submission.get_id_secret(id), {'type': 'internal-error'})
|
||||
self._post_update_submission(id, 'internal-error', done=True)
|
||||
json_log.info(self._make_json_log(packet, action='internal-error', message=packet['message'],
|
||||
finish=True, result='IE'))
|
||||
else:
|
||||
logger.warning('Unknown submission: %s', id)
|
||||
json_log.error(self._make_json_log(packet, action='internal-error', info='unknown submission',
|
||||
message=packet['message'], finish=True, result='IE'))
|
||||
|
||||
def on_submission_terminated(self, packet):
|
||||
super(DjangoJudgeHandler, self).on_submission_terminated(packet)
|
||||
|
||||
if Submission.objects.filter(id=packet['submission-id']).update(status='AB', result='AB'):
|
||||
event.post('sub_%s' % Submission.get_id_secret(packet['submission-id']), {'type': 'aborted-submission'})
|
||||
self._post_update_submission(packet['submission-id'], 'terminated', done=True)
|
||||
json_log.info(self._make_json_log(packet, action='aborted', finish=True, result='AB'))
|
||||
else:
|
||||
logger.warning('Unknown submission: %s', packet['submission-id'])
|
||||
json_log.error(self._make_json_log(packet, action='aborted', info='unknown submission',
|
||||
finish=True, result='AB'))
|
||||
|
||||
def on_batch_begin(self, packet):
|
||||
super(DjangoJudgeHandler, self).on_batch_begin(packet)
|
||||
json_log.info(self._make_json_log(packet, action='batch-begin', batch=self.batch_id))
|
||||
|
||||
def on_batch_end(self, packet):
|
||||
super(DjangoJudgeHandler, self).on_batch_end(packet)
|
||||
json_log.info(self._make_json_log(packet, action='batch-end', batch=self.batch_id))
|
||||
|
||||
def on_test_case(self, packet, max_feedback=SubmissionTestCase._meta.get_field('feedback').max_length):
|
||||
super(DjangoJudgeHandler, self).on_test_case(packet)
|
||||
id = packet['submission-id']
|
||||
updates = packet['cases']
|
||||
max_position = max(map(itemgetter('position'), updates))
|
||||
|
||||
if not Submission.objects.filter(id=id).update(current_testcase=max_position + 1):
|
||||
logger.warning('Unknown submission: %s', id)
|
||||
json_log.error(self._make_json_log(packet, action='test-case', info='unknown submission'))
|
||||
return
|
||||
|
||||
bulk_test_case_updates = []
|
||||
for result in updates:
|
||||
test_case = SubmissionTestCase(submission_id=id, case=result['position'])
|
||||
status = result['status']
|
||||
if status & 4:
|
||||
test_case.status = 'TLE'
|
||||
elif status & 8:
|
||||
test_case.status = 'MLE'
|
||||
elif status & 64:
|
||||
test_case.status = 'OLE'
|
||||
elif status & 2:
|
||||
test_case.status = 'RTE'
|
||||
elif status & 16:
|
||||
test_case.status = 'IR'
|
||||
elif status & 1:
|
||||
test_case.status = 'WA'
|
||||
elif status & 32:
|
||||
test_case.status = 'SC'
|
||||
else:
|
||||
test_case.status = 'AC'
|
||||
test_case.time = result['time']
|
||||
test_case.memory = result['memory']
|
||||
test_case.points = result['points']
|
||||
test_case.total = result['total-points']
|
||||
test_case.batch = self.batch_id if self.in_batch else None
|
||||
test_case.feedback = (result.get('feedback') or '')[:max_feedback]
|
||||
test_case.extended_feedback = result.get('extended-feedback') or ''
|
||||
test_case.output = result['output']
|
||||
bulk_test_case_updates.append(test_case)
|
||||
|
||||
json_log.info(self._make_json_log(
|
||||
packet, action='test-case', case=test_case.case, batch=test_case.batch,
|
||||
time=test_case.time, memory=test_case.memory, feedback=test_case.feedback,
|
||||
extended_feedback=test_case.extended_feedback, output=test_case.output,
|
||||
points=test_case.points, total=test_case.total, status=test_case.status,
|
||||
))
|
||||
|
||||
do_post = True
|
||||
|
||||
if id in self.update_counter:
|
||||
cnt, reset = self.update_counter[id]
|
||||
cnt += 1
|
||||
if time.monotonic() - reset > UPDATE_RATE_TIME:
|
||||
del self.update_counter[id]
|
||||
else:
|
||||
self.update_counter[id] = (cnt, reset)
|
||||
if cnt > UPDATE_RATE_LIMIT:
|
||||
do_post = False
|
||||
if id not in self.update_counter:
|
||||
self.update_counter[id] = (1, time.monotonic())
|
||||
|
||||
if do_post:
|
||||
event.post('sub_%s' % Submission.get_id_secret(id), {
|
||||
'type': 'test-case',
|
||||
'id': max_position,
|
||||
})
|
||||
self._post_update_submission(id, state='test-case')
|
||||
|
||||
SubmissionTestCase.objects.bulk_create(bulk_test_case_updates)
|
||||
|
||||
def on_supported_problems(self, packet):
|
||||
super(DjangoJudgeHandler, self).on_supported_problems(packet)
|
||||
self.judge.problems.set(Problem.objects.filter(code__in=list(self.problems.keys())))
|
||||
json_log.info(self._make_json_log(action='update-problems', count=len(self.problems)))
|
||||
|
||||
def _make_json_log(self, packet=None, sub=None, **kwargs):
|
||||
data = {
|
||||
'judge': self.name,
|
||||
'address': self.judge_address,
|
||||
}
|
||||
if sub is None and packet is not None:
|
||||
sub = packet.get('submission-id')
|
||||
if sub is not None:
|
||||
data['submission'] = sub
|
||||
data.update(kwargs)
|
||||
return json.dumps(data)
|
268
judge/bridge/judgehandler.py
Normal file
268
judge/bridge/judgehandler.py
Normal file
|
@ -0,0 +1,268 @@
|
|||
import json
|
||||
import logging
|
||||
import time
|
||||
from collections import deque, namedtuple
|
||||
|
||||
from event_socket_server import ProxyProtocolMixin, ZlibPacketHandler
|
||||
|
||||
logger = logging.getLogger('judge.bridge')
|
||||
|
||||
SubmissionData = namedtuple('SubmissionData', 'time memory short_circuit pretests_only contest_no attempt_no user_id')
|
||||
|
||||
|
||||
class JudgeHandler(ProxyProtocolMixin, ZlibPacketHandler):
|
||||
def __init__(self, server, socket):
|
||||
super(JudgeHandler, self).__init__(server, socket)
|
||||
|
||||
self.handlers = {
|
||||
'grading-begin': self.on_grading_begin,
|
||||
'grading-end': self.on_grading_end,
|
||||
'compile-error': self.on_compile_error,
|
||||
'compile-message': self.on_compile_message,
|
||||
'batch-begin': self.on_batch_begin,
|
||||
'batch-end': self.on_batch_end,
|
||||
'test-case-status': self.on_test_case,
|
||||
'internal-error': self.on_internal_error,
|
||||
'submission-terminated': self.on_submission_terminated,
|
||||
'submission-acknowledged': self.on_submission_acknowledged,
|
||||
'ping-response': self.on_ping_response,
|
||||
'supported-problems': self.on_supported_problems,
|
||||
'handshake': self.on_handshake,
|
||||
}
|
||||
self._to_kill = True
|
||||
self._working = False
|
||||
self._no_response_job = None
|
||||
self._problems = []
|
||||
self.executors = []
|
||||
self.problems = {}
|
||||
self.latency = None
|
||||
self.time_delta = None
|
||||
self.load = 1e100
|
||||
self.name = None
|
||||
self.batch_id = None
|
||||
self.in_batch = False
|
||||
self._ping_average = deque(maxlen=6) # 1 minute average, just like load
|
||||
self._time_delta = deque(maxlen=6)
|
||||
|
||||
self.server.schedule(15, self._kill_if_no_auth)
|
||||
logger.info('Judge connected from: %s', self.client_address)
|
||||
|
||||
def _kill_if_no_auth(self):
|
||||
if self._to_kill:
|
||||
logger.info('Judge not authenticated: %s', self.client_address)
|
||||
self.close()
|
||||
|
||||
def on_close(self):
|
||||
self._to_kill = False
|
||||
if self._no_response_job:
|
||||
self.server.unschedule(self._no_response_job)
|
||||
self.server.judges.remove(self)
|
||||
if self.name is not None:
|
||||
self._disconnected()
|
||||
logger.info('Judge disconnected from: %s', self.client_address)
|
||||
|
||||
def _authenticate(self, id, key):
|
||||
return False
|
||||
|
||||
def _connected(self):
|
||||
pass
|
||||
|
||||
def _disconnected(self):
|
||||
pass
|
||||
|
||||
def _update_ping(self):
|
||||
pass
|
||||
|
||||
def _format_send(self, data):
|
||||
return super(JudgeHandler, self)._format_send(json.dumps(data, separators=(',', ':')))
|
||||
|
||||
def on_handshake(self, packet):
|
||||
if 'id' not in packet or 'key' not in packet:
|
||||
logger.warning('Malformed handshake: %s', self.client_address)
|
||||
self.close()
|
||||
return
|
||||
|
||||
if not self._authenticate(packet['id'], packet['key']):
|
||||
logger.warning('Authentication failure: %s', self.client_address)
|
||||
self.close()
|
||||
return
|
||||
|
||||
self._to_kill = False
|
||||
self._problems = packet['problems']
|
||||
self.problems = dict(self._problems)
|
||||
self.executors = packet['executors']
|
||||
self.name = packet['id']
|
||||
|
||||
self.send({'name': 'handshake-success'})
|
||||
logger.info('Judge authenticated: %s (%s)', self.client_address, packet['id'])
|
||||
self.server.judges.register(self)
|
||||
self._connected()
|
||||
|
||||
def can_judge(self, problem, executor):
|
||||
return problem in self.problems and executor in self.executors
|
||||
|
||||
@property
|
||||
def working(self):
|
||||
return bool(self._working)
|
||||
|
||||
def get_related_submission_data(self, submission):
|
||||
return SubmissionData(
|
||||
time=2,
|
||||
memory=16384,
|
||||
short_circuit=False,
|
||||
pretests_only=False,
|
||||
contest_no=None,
|
||||
attempt_no=1,
|
||||
user_id=None,
|
||||
)
|
||||
|
||||
def disconnect(self, force=False):
|
||||
if force:
|
||||
# Yank the power out.
|
||||
self.close()
|
||||
else:
|
||||
self.send({'name': 'disconnect'})
|
||||
|
||||
def submit(self, id, problem, language, source):
|
||||
data = self.get_related_submission_data(id)
|
||||
self._working = id
|
||||
self._no_response_job = self.server.schedule(20, self._kill_if_no_response)
|
||||
self.send({
|
||||
'name': 'submission-request',
|
||||
'submission-id': id,
|
||||
'problem-id': problem,
|
||||
'language': language,
|
||||
'source': source,
|
||||
'time-limit': data.time,
|
||||
'memory-limit': data.memory,
|
||||
'short-circuit': data.short_circuit,
|
||||
'meta': {
|
||||
'pretests-only': data.pretests_only,
|
||||
'in-contest': data.contest_no,
|
||||
'attempt-no': data.attempt_no,
|
||||
'user': data.user_id,
|
||||
},
|
||||
})
|
||||
|
||||
def _kill_if_no_response(self):
|
||||
logger.error('Judge seems dead: %s: %s', self.name, self._working)
|
||||
self.close()
|
||||
|
||||
def malformed_packet(self, exception):
|
||||
logger.exception('Judge sent malformed packet: %s', self.name)
|
||||
super(JudgeHandler, self).malformed_packet(exception)
|
||||
|
||||
def on_submission_processing(self, packet):
|
||||
pass
|
||||
|
||||
def on_submission_wrong_acknowledge(self, packet, expected, got):
|
||||
pass
|
||||
|
||||
def on_submission_acknowledged(self, packet):
|
||||
if not packet.get('submission-id', None) == self._working:
|
||||
logger.error('Wrong acknowledgement: %s: %s, expected: %s', self.name, packet.get('submission-id', None),
|
||||
self._working)
|
||||
self.on_submission_wrong_acknowledge(packet, self._working, packet.get('submission-id', None))
|
||||
self.close()
|
||||
logger.info('Submission acknowledged: %d', self._working)
|
||||
if self._no_response_job:
|
||||
self.server.unschedule(self._no_response_job)
|
||||
self._no_response_job = None
|
||||
self.on_submission_processing(packet)
|
||||
|
||||
def abort(self):
|
||||
self.send({'name': 'terminate-submission'})
|
||||
|
||||
def get_current_submission(self):
|
||||
return self._working or None
|
||||
|
||||
def ping(self):
|
||||
self.send({'name': 'ping', 'when': time.time()})
|
||||
|
||||
def packet(self, data):
|
||||
try:
|
||||
try:
|
||||
data = json.loads(data)
|
||||
if 'name' not in data:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
self.on_malformed(data)
|
||||
else:
|
||||
handler = self.handlers.get(data['name'], self.on_malformed)
|
||||
handler(data)
|
||||
except Exception:
|
||||
logger.exception('Error in packet handling (Judge-side): %s', self.name)
|
||||
self._packet_exception()
|
||||
# You can't crash here because you aren't so sure about the judges
|
||||
# not being malicious or simply malforms. THIS IS A SERVER!
|
||||
|
||||
def _packet_exception(self):
|
||||
pass
|
||||
|
||||
def _submission_is_batch(self, id):
|
||||
pass
|
||||
|
||||
def on_supported_problems(self, packet):
|
||||
logger.info('%s: Updated problem list', self.name)
|
||||
self._problems = packet['problems']
|
||||
self.problems = dict(self._problems)
|
||||
if not self.working:
|
||||
self.server.judges.update_problems(self)
|
||||
|
||||
def on_grading_begin(self, packet):
|
||||
logger.info('%s: Grading has begun on: %s', self.name, packet['submission-id'])
|
||||
self.batch_id = None
|
||||
|
||||
def on_grading_end(self, packet):
|
||||
logger.info('%s: Grading has ended on: %s', self.name, packet['submission-id'])
|
||||
self._free_self(packet)
|
||||
self.batch_id = None
|
||||
|
||||
def on_compile_error(self, packet):
|
||||
logger.info('%s: Submission failed to compile: %s', self.name, packet['submission-id'])
|
||||
self._free_self(packet)
|
||||
|
||||
def on_compile_message(self, packet):
|
||||
logger.info('%s: Submission generated compiler messages: %s', self.name, packet['submission-id'])
|
||||
|
||||
def on_internal_error(self, packet):
|
||||
try:
|
||||
raise ValueError('\n\n' + packet['message'])
|
||||
except ValueError:
|
||||
logger.exception('Judge %s failed while handling submission %s', self.name, packet['submission-id'])
|
||||
self._free_self(packet)
|
||||
|
||||
def on_submission_terminated(self, packet):
|
||||
logger.info('%s: Submission aborted: %s', self.name, packet['submission-id'])
|
||||
self._free_self(packet)
|
||||
|
||||
def on_batch_begin(self, packet):
|
||||
logger.info('%s: Batch began on: %s', self.name, packet['submission-id'])
|
||||
self.in_batch = True
|
||||
if self.batch_id is None:
|
||||
self.batch_id = 0
|
||||
self._submission_is_batch(packet['submission-id'])
|
||||
self.batch_id += 1
|
||||
|
||||
def on_batch_end(self, packet):
|
||||
self.in_batch = False
|
||||
logger.info('%s: Batch ended on: %s', self.name, packet['submission-id'])
|
||||
|
||||
def on_test_case(self, packet):
|
||||
logger.info('%s: %d test case(s) completed on: %s', self.name, len(packet['cases']), packet['submission-id'])
|
||||
|
||||
def on_malformed(self, packet):
|
||||
logger.error('%s: Malformed packet: %s', self.name, packet)
|
||||
|
||||
def on_ping_response(self, packet):
|
||||
end = time.time()
|
||||
self._ping_average.append(end - packet['when'])
|
||||
self._time_delta.append((end + packet['when']) / 2 - packet['time'])
|
||||
self.latency = sum(self._ping_average) / len(self._ping_average)
|
||||
self.time_delta = sum(self._time_delta) / len(self._time_delta)
|
||||
self.load = packet['load']
|
||||
self._update_ping()
|
||||
|
||||
def _free_self(self, packet):
|
||||
self._working = False
|
||||
self.server.judges.on_judge_free(self, packet['submission-id'])
|
123
judge/bridge/judgelist.py
Normal file
123
judge/bridge/judgelist.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
import logging
|
||||
from collections import namedtuple
|
||||
from operator import attrgetter
|
||||
from threading import RLock
|
||||
|
||||
try:
|
||||
from llist import dllist
|
||||
except ImportError:
|
||||
from pyllist import dllist
|
||||
|
||||
logger = logging.getLogger('judge.bridge')
|
||||
|
||||
PriorityMarker = namedtuple('PriorityMarker', 'priority')
|
||||
|
||||
|
||||
class JudgeList(object):
|
||||
priorities = 4
|
||||
|
||||
def __init__(self):
|
||||
self.queue = dllist()
|
||||
self.priority = [self.queue.append(PriorityMarker(i)) for i in range(self.priorities)]
|
||||
self.judges = set()
|
||||
self.node_map = {}
|
||||
self.submission_map = {}
|
||||
self.lock = RLock()
|
||||
|
||||
def _handle_free_judge(self, judge):
|
||||
with self.lock:
|
||||
node = self.queue.first
|
||||
while node:
|
||||
if not isinstance(node.value, PriorityMarker):
|
||||
id, problem, language, source = node.value
|
||||
if judge.can_judge(problem, language):
|
||||
self.submission_map[id] = judge
|
||||
logger.info('Dispatched queued submission %d: %s', id, judge.name)
|
||||
try:
|
||||
judge.submit(id, problem, language, source)
|
||||
except Exception:
|
||||
logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name)
|
||||
self.judges.remove(judge)
|
||||
return
|
||||
self.queue.remove(node)
|
||||
del self.node_map[id]
|
||||
break
|
||||
node = node.next
|
||||
|
||||
def register(self, judge):
|
||||
with self.lock:
|
||||
# Disconnect all judges with the same name, see <https://github.com/DMOJ/online-judge/issues/828>
|
||||
self.disconnect(judge, force=True)
|
||||
self.judges.add(judge)
|
||||
self._handle_free_judge(judge)
|
||||
|
||||
def disconnect(self, judge_id, force=False):
|
||||
for judge in self.judges:
|
||||
if judge.name == judge_id:
|
||||
judge.disconnect(force=force)
|
||||
|
||||
def update_problems(self, judge):
|
||||
with self.lock:
|
||||
self._handle_free_judge(judge)
|
||||
|
||||
def remove(self, judge):
|
||||
with self.lock:
|
||||
sub = judge.get_current_submission()
|
||||
if sub is not None:
|
||||
try:
|
||||
del self.submission_map[sub]
|
||||
except KeyError:
|
||||
pass
|
||||
self.judges.discard(judge)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.judges)
|
||||
|
||||
def on_judge_free(self, judge, submission):
|
||||
with self.lock:
|
||||
logger.info('Judge available after grading %d: %s', submission, judge.name)
|
||||
del self.submission_map[submission]
|
||||
self._handle_free_judge(judge)
|
||||
|
||||
def abort(self, submission):
|
||||
with self.lock:
|
||||
logger.info('Abort request: %d', submission)
|
||||
try:
|
||||
self.submission_map[submission].abort()
|
||||
return True
|
||||
except KeyError:
|
||||
try:
|
||||
node = self.node_map[submission]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self.queue.remove(node)
|
||||
del self.node_map[submission]
|
||||
return False
|
||||
|
||||
def check_priority(self, priority):
|
||||
return 0 <= priority < self.priorities
|
||||
|
||||
def judge(self, id, problem, language, source, priority):
|
||||
with self.lock:
|
||||
if id in self.submission_map or id in self.node_map:
|
||||
# Already judging, don't queue again. This can happen during batch rejudges, rejudges should be
|
||||
# idempotent.
|
||||
return
|
||||
|
||||
candidates = [judge for judge in self.judges if not judge.working and judge.can_judge(problem, language)]
|
||||
logger.info('Free judges: %d', len(candidates))
|
||||
if candidates:
|
||||
# Schedule the submission on the judge reporting least load.
|
||||
judge = min(candidates, key=attrgetter('load'))
|
||||
logger.info('Dispatched submission %d to: %s', id, judge.name)
|
||||
self.submission_map[id] = judge
|
||||
try:
|
||||
judge.submit(id, problem, language, source)
|
||||
except Exception:
|
||||
logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name)
|
||||
self.judges.discard(judge)
|
||||
return self.judge(id, problem, language, source, priority)
|
||||
else:
|
||||
self.node_map[id] = self.queue.insert((id, problem, language, source), self.priority[priority])
|
||||
logger.info('Queued submission: %d', id)
|
68
judge/bridge/judgeserver.py
Normal file
68
judge/bridge/judgeserver.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
from event_socket_server import get_preferred_engine
|
||||
from judge.models import Judge
|
||||
from .judgelist import JudgeList
|
||||
|
||||
logger = logging.getLogger('judge.bridge')
|
||||
|
||||
|
||||
def reset_judges():
|
||||
Judge.objects.update(online=False, ping=None, load=None)
|
||||
|
||||
|
||||
class JudgeServer(get_preferred_engine()):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(JudgeServer, self).__init__(*args, **kwargs)
|
||||
reset_judges()
|
||||
self.judges = JudgeList()
|
||||
self.ping_judge_thread = threading.Thread(target=self.ping_judge, args=())
|
||||
self.ping_judge_thread.daemon = True
|
||||
self.ping_judge_thread.start()
|
||||
|
||||
def on_shutdown(self):
|
||||
super(JudgeServer, self).on_shutdown()
|
||||
reset_judges()
|
||||
|
||||
def ping_judge(self):
|
||||
try:
|
||||
while True:
|
||||
for judge in self.judges:
|
||||
judge.ping()
|
||||
time.sleep(10)
|
||||
except Exception:
|
||||
logger.exception('Ping error')
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
import logging
|
||||
from .judgehandler import JudgeHandler
|
||||
|
||||
format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s'
|
||||
logging.basicConfig(format=format)
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
handler = logging.FileHandler(os.path.join(os.path.dirname(__file__), 'judgeserver.log'), encoding='utf-8')
|
||||
handler.setFormatter(logging.Formatter(format))
|
||||
handler.setLevel(logging.INFO)
|
||||
logging.getLogger().addHandler(handler)
|
||||
|
||||
parser = argparse.ArgumentParser(description='''
|
||||
Runs the bridge between DMOJ website and judges.
|
||||
''')
|
||||
parser.add_argument('judge_host', nargs='+', action='append',
|
||||
help='host to listen for the judge')
|
||||
parser.add_argument('-p', '--judge-port', type=int, action='append',
|
||||
help='port to listen for the judge')
|
||||
|
||||
args = parser.parse_args()
|
||||
server = JudgeServer(list(zip(args.judge_host, args.judge_port)), JudgeHandler)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
10
judge/caching.py
Normal file
10
judge/caching.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from django.core.cache import cache
|
||||
|
||||
|
||||
def finished_submission(sub):
|
||||
keys = ['user_complete:%d' % sub.user_id, 'user_attempted:%s' % sub.user_id]
|
||||
if hasattr(sub, 'contest'):
|
||||
participation = sub.contest.participation
|
||||
keys += ['contest_complete:%d' % participation.id]
|
||||
keys += ['contest_attempted:%d' % participation.id]
|
||||
cache.delete_many(keys)
|
122
judge/comments.py
Normal file
122
judge/comments.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Count
|
||||
from django.db.models.expressions import F, Value
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.forms import ModelForm
|
||||
from django.http import HttpResponseForbidden, HttpResponseNotFound, HttpResponseRedirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import View
|
||||
from django.views.generic.base import TemplateResponseMixin
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from reversion import revisions
|
||||
from reversion.models import Revision, Version
|
||||
|
||||
from judge.dblock import LockModel
|
||||
from judge.models import Comment, CommentLock, CommentVote
|
||||
from judge.utils.raw_sql import RawSQLColumn, unique_together_left_join
|
||||
from judge.widgets import HeavyPreviewPageDownWidget
|
||||
|
||||
|
||||
class CommentForm(ModelForm):
|
||||
class Meta:
|
||||
model = Comment
|
||||
fields = ['body', 'parent']
|
||||
widgets = {
|
||||
'parent': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
if HeavyPreviewPageDownWidget is not None:
|
||||
widgets['body'] = HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'),
|
||||
preview_timeout=1000, hide_preview_button=True)
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
self.request = request
|
||||
super(CommentForm, self).__init__(*args, **kwargs)
|
||||
self.fields['body'].widget.attrs.update({'placeholder': _('Comment body')})
|
||||
|
||||
def clean(self):
|
||||
if self.request is not None and self.request.user.is_authenticated:
|
||||
profile = self.request.profile
|
||||
if profile.mute:
|
||||
raise ValidationError(_('Your part is silent, little toad.'))
|
||||
elif (not self.request.user.is_staff and
|
||||
not profile.submission_set.filter(points=F('problem__points')).exists()):
|
||||
raise ValidationError(_('You need to have solved at least one problem '
|
||||
'before your voice can be heard.'))
|
||||
return super(CommentForm, self).clean()
|
||||
|
||||
|
||||
class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
|
||||
comment_page = None
|
||||
|
||||
def get_comment_page(self):
|
||||
if self.comment_page is None:
|
||||
raise NotImplementedError()
|
||||
return self.comment_page
|
||||
|
||||
def is_comment_locked(self):
|
||||
return (CommentLock.objects.filter(page=self.get_comment_page()).exists() and
|
||||
not self.request.user.has_perm('judge.override_comment_lock'))
|
||||
|
||||
@method_decorator(login_required)
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
page = self.get_comment_page()
|
||||
|
||||
if self.is_comment_locked():
|
||||
return HttpResponseForbidden()
|
||||
|
||||
parent = request.POST.get('parent')
|
||||
if parent:
|
||||
try:
|
||||
parent = int(parent)
|
||||
except ValueError:
|
||||
return HttpResponseNotFound()
|
||||
else:
|
||||
if not Comment.objects.filter(hidden=False, id=parent, page=page).exists():
|
||||
return HttpResponseNotFound()
|
||||
|
||||
form = CommentForm(request, request.POST)
|
||||
if form.is_valid():
|
||||
comment = form.save(commit=False)
|
||||
comment.author = request.profile
|
||||
comment.page = page
|
||||
with LockModel(write=(Comment, Revision, Version), read=(ContentType,)), revisions.create_revision():
|
||||
revisions.set_user(request.user)
|
||||
revisions.set_comment(_('Posted comment'))
|
||||
comment.save()
|
||||
return HttpResponseRedirect(request.path)
|
||||
|
||||
context = self.get_context_data(object=self.object, comment_form=form)
|
||||
return self.render_to_response(context)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
return self.render_to_response(self.get_context_data(
|
||||
object=self.object,
|
||||
comment_form=CommentForm(request, initial={'page': self.get_comment_page(), 'parent': None}),
|
||||
))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(CommentedDetailView, self).get_context_data(**kwargs)
|
||||
queryset = Comment.objects.filter(hidden=False, page=self.get_comment_page())
|
||||
context['has_comments'] = queryset.exists()
|
||||
context['comment_lock'] = self.is_comment_locked()
|
||||
queryset = queryset.select_related('author__user').defer('author__about').annotate(revisions=Count('versions'))
|
||||
|
||||
if self.request.user.is_authenticated:
|
||||
queryset = queryset.annotate(vote_score=Coalesce(RawSQLColumn(CommentVote, 'score'), Value(0)))
|
||||
profile = self.request.profile
|
||||
unique_together_left_join(queryset, CommentVote, 'comment', 'voter', profile.id)
|
||||
context['is_new_user'] = (not self.request.user.is_staff and
|
||||
not profile.submission_set.filter(points=F('problem__points')).exists())
|
||||
context['comment_list'] = queryset
|
||||
context['vote_hide_threshold'] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD
|
||||
|
||||
return context
|
5
judge/contest_format/__init__.py
Normal file
5
judge/contest_format/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from judge.contest_format.atcoder import AtCoderContestFormat
|
||||
from judge.contest_format.default import DefaultContestFormat
|
||||
from judge.contest_format.ecoo import ECOOContestFormat
|
||||
from judge.contest_format.ioi import IOIContestFormat
|
||||
from judge.contest_format.registry import choices, formats
|
113
judge/contest_format/atcoder.py
Normal file
113
judge/contest_format/atcoder.py
Normal file
|
@ -0,0 +1,113 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import connection
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from judge.contest_format.default import DefaultContestFormat
|
||||
from judge.contest_format.registry import register_contest_format
|
||||
from judge.timezone import from_database_time
|
||||
from judge.utils.timedelta import nice_repr
|
||||
|
||||
|
||||
@register_contest_format('atcoder')
|
||||
class AtCoderContestFormat(DefaultContestFormat):
|
||||
name = gettext_lazy('AtCoder')
|
||||
config_defaults = {'penalty': 5}
|
||||
config_validators = {'penalty': lambda x: x >= 0}
|
||||
'''
|
||||
penalty: Number of penalty minutes each incorrect submission adds. Defaults to 5.
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def validate(cls, config):
|
||||
if config is None:
|
||||
return
|
||||
|
||||
if not isinstance(config, dict):
|
||||
raise ValidationError('AtCoder-styled contest expects no config or dict as config')
|
||||
|
||||
for key, value in config.items():
|
||||
if key not in cls.config_defaults:
|
||||
raise ValidationError('unknown config key "%s"' % key)
|
||||
if not isinstance(value, type(cls.config_defaults[key])):
|
||||
raise ValidationError('invalid type for config key "%s"' % key)
|
||||
if not cls.config_validators[key](value):
|
||||
raise ValidationError('invalid value "%s" for config key "%s"' % (value, key))
|
||||
|
||||
def __init__(self, contest, config):
|
||||
self.config = self.config_defaults.copy()
|
||||
self.config.update(config or {})
|
||||
self.contest = contest
|
||||
|
||||
def update_participation(self, participation):
|
||||
cumtime = 0
|
||||
penalty = 0
|
||||
points = 0
|
||||
format_data = {}
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('''
|
||||
SELECT MAX(cs.points) as `score`, (
|
||||
SELECT MIN(csub.date)
|
||||
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
||||
judge_submission csub ON (csub.id = ccs.submission_id)
|
||||
WHERE ccs.problem_id = cp.id AND ccs.participation_id = %s AND ccs.points = MAX(cs.points)
|
||||
) AS `time`, cp.id AS `prob`
|
||||
FROM judge_contestproblem cp INNER JOIN
|
||||
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
||||
judge_submission sub ON (sub.id = cs.submission_id)
|
||||
GROUP BY cp.id
|
||||
''', (participation.id, participation.id))
|
||||
|
||||
for score, time, prob in cursor.fetchall():
|
||||
time = from_database_time(time)
|
||||
dt = (time - participation.start).total_seconds()
|
||||
|
||||
# Compute penalty
|
||||
if self.config['penalty']:
|
||||
# An IE can have a submission result of `None`
|
||||
subs = participation.submissions.exclude(submission__result__isnull=True) \
|
||||
.exclude(submission__result__in=['IE', 'CE']) \
|
||||
.filter(problem_id=prob)
|
||||
if score:
|
||||
prev = subs.filter(submission__date__lte=time).count() - 1
|
||||
penalty += prev * self.config['penalty'] * 60
|
||||
else:
|
||||
# We should always display the penalty, even if the user has a score of 0
|
||||
prev = subs.count()
|
||||
else:
|
||||
prev = 0
|
||||
|
||||
if score:
|
||||
cumtime = max(cumtime, dt)
|
||||
|
||||
format_data[str(prob)] = {'time': dt, 'points': score, 'penalty': prev}
|
||||
points += score
|
||||
|
||||
participation.cumtime = cumtime + penalty
|
||||
participation.score = points
|
||||
participation.format_data = format_data
|
||||
participation.save()
|
||||
|
||||
def display_user_problem(self, participation, contest_problem):
|
||||
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
||||
if format_data:
|
||||
penalty = format_html('<small style="color:red"> ({penalty})</small>',
|
||||
penalty=floatformat(format_data['penalty'])) if format_data['penalty'] else ''
|
||||
return format_html(
|
||||
'<td class="{state}"><a href="{url}">{points}{penalty}<div class="solving-time">{time}</div></a></td>',
|
||||
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
|
||||
self.best_solution_state(format_data['points'], contest_problem.points)),
|
||||
url=reverse('contest_user_submissions',
|
||||
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
|
||||
points=floatformat(format_data['points']),
|
||||
penalty=penalty,
|
||||
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'),
|
||||
)
|
||||
else:
|
||||
return mark_safe('<td></td>')
|
91
judge/contest_format/base.py
Normal file
91
judge/contest_format/base.py
Normal file
|
@ -0,0 +1,91 @@
|
|||
from abc import ABCMeta, abstractmethod, abstractproperty
|
||||
|
||||
from django.utils import six
|
||||
|
||||
|
||||
class abstractclassmethod(classmethod):
|
||||
__isabstractmethod__ = True
|
||||
|
||||
def __init__(self, callable):
|
||||
callable.__isabstractmethod__ = True
|
||||
super(abstractclassmethod, self).__init__(callable)
|
||||
|
||||
|
||||
class BaseContestFormat(six.with_metaclass(ABCMeta)):
|
||||
@abstractmethod
|
||||
def __init__(self, contest, config):
|
||||
self.config = config
|
||||
self.contest = contest
|
||||
|
||||
@abstractproperty
|
||||
def name(self):
|
||||
"""
|
||||
Name of this contest format. Should be invoked with gettext_lazy.
|
||||
|
||||
:return: str
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractclassmethod
|
||||
def validate(cls, config):
|
||||
"""
|
||||
Validates the contest format configuration.
|
||||
|
||||
:param config: A dictionary containing the configuration for this contest format.
|
||||
:return: None
|
||||
:raises: ValidationError
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def update_participation(self, participation):
|
||||
"""
|
||||
Updates a ContestParticipation object's score, cumtime, and format_data fields based on this contest format.
|
||||
Implementations should call ContestParticipation.save().
|
||||
|
||||
:param participation: A ContestParticipation object.
|
||||
:return: None
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def display_user_problem(self, participation, contest_problem):
|
||||
"""
|
||||
Returns the HTML fragment to show a user's performance on an individual problem. This is expected to use
|
||||
information from the format_data field instead of computing it from scratch.
|
||||
|
||||
:param participation: The ContestParticipation object linking the user to the contest.
|
||||
:param contest_problem: The ContestProblem object representing the problem in question.
|
||||
:return: An HTML fragment, marked as safe for Jinja2.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def display_participation_result(self, participation):
|
||||
"""
|
||||
Returns the HTML fragment to show a user's performance on the whole contest. This is expected to use
|
||||
information from the format_data field instead of computing it from scratch.
|
||||
|
||||
:param participation: The ContestParticipation object.
|
||||
:return: An HTML fragment, marked as safe for Jinja2.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def get_problem_breakdown(self, participation, contest_problems):
|
||||
"""
|
||||
Returns a machine-readable breakdown for the user's performance on every problem.
|
||||
|
||||
:param participation: The ContestParticipation object.
|
||||
:param contest_problems: The list of ContestProblem objects to display performance for.
|
||||
:return: A list of dictionaries, whose content is to be determined by the contest system.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def best_solution_state(cls, points, total):
|
||||
if not points:
|
||||
return 'failed-score'
|
||||
if points == total:
|
||||
return 'full-score'
|
||||
return 'partial-score'
|
70
judge/contest_format/default.py
Normal file
70
judge/contest_format/default.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Max
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from judge.contest_format.base import BaseContestFormat
|
||||
from judge.contest_format.registry import register_contest_format
|
||||
from judge.utils.timedelta import nice_repr
|
||||
|
||||
|
||||
@register_contest_format('default')
|
||||
class DefaultContestFormat(BaseContestFormat):
|
||||
name = gettext_lazy('Default')
|
||||
|
||||
@classmethod
|
||||
def validate(cls, config):
|
||||
if config is not None and (not isinstance(config, dict) or config):
|
||||
raise ValidationError('default contest expects no config or empty dict as config')
|
||||
|
||||
def __init__(self, contest, config):
|
||||
super(DefaultContestFormat, self).__init__(contest, config)
|
||||
|
||||
def update_participation(self, participation):
|
||||
cumtime = 0
|
||||
points = 0
|
||||
format_data = {}
|
||||
|
||||
for result in participation.submissions.values('problem_id').annotate(
|
||||
time=Max('submission__date'), points=Max('points'),
|
||||
):
|
||||
dt = (result['time'] - participation.start).total_seconds()
|
||||
if result['points']:
|
||||
cumtime += dt
|
||||
format_data[str(result['problem_id'])] = {'time': dt, 'points': result['points']}
|
||||
points += result['points']
|
||||
|
||||
participation.cumtime = max(cumtime, 0)
|
||||
participation.score = points
|
||||
participation.format_data = format_data
|
||||
participation.save()
|
||||
|
||||
def display_user_problem(self, participation, contest_problem):
|
||||
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
||||
if format_data:
|
||||
return format_html(
|
||||
u'<td class="{state}"><a href="{url}">{points}<div class="solving-time">{time}</div></a></td>',
|
||||
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
|
||||
self.best_solution_state(format_data['points'], contest_problem.points)),
|
||||
url=reverse('contest_user_submissions',
|
||||
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
|
||||
points=floatformat(format_data['points']),
|
||||
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'),
|
||||
)
|
||||
else:
|
||||
return mark_safe('<td></td>')
|
||||
|
||||
def display_participation_result(self, participation):
|
||||
return format_html(
|
||||
u'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
|
||||
points=floatformat(participation.score),
|
||||
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday'),
|
||||
)
|
||||
|
||||
def get_problem_breakdown(self, participation, contest_problems):
|
||||
return [(participation.format_data or {}).get(str(contest_problem.id)) for contest_problem in contest_problems]
|
122
judge/contest_format/ecoo.py
Normal file
122
judge/contest_format/ecoo.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import connection
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from judge.contest_format.default import DefaultContestFormat
|
||||
from judge.contest_format.registry import register_contest_format
|
||||
from judge.timezone import from_database_time
|
||||
from judge.utils.timedelta import nice_repr
|
||||
|
||||
|
||||
@register_contest_format('ecoo')
|
||||
class ECOOContestFormat(DefaultContestFormat):
|
||||
name = gettext_lazy('ECOO')
|
||||
config_defaults = {'cumtime': False, 'first_ac_bonus': 10, 'time_bonus': 5}
|
||||
config_validators = {'cumtime': lambda x: True, 'first_ac_bonus': lambda x: x >= 0, 'time_bonus': lambda x: x >= 0}
|
||||
'''
|
||||
cumtime: Specify True if cumulative time is to be used in breaking ties. Defaults to False.
|
||||
first_ac_bonus: The number of points to award if a solution gets AC on its first non-IE/CE run. Defaults to 10.
|
||||
time_bonus: Number of minutes to award an extra point for submitting before the contest end.
|
||||
Specify 0 to disable. Defaults to 5.
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def validate(cls, config):
|
||||
if config is None:
|
||||
return
|
||||
|
||||
if not isinstance(config, dict):
|
||||
raise ValidationError('ECOO-styled contest expects no config or dict as config')
|
||||
|
||||
for key, value in config.items():
|
||||
if key not in cls.config_defaults:
|
||||
raise ValidationError('unknown config key "%s"' % key)
|
||||
if not isinstance(value, type(cls.config_defaults[key])):
|
||||
raise ValidationError('invalid type for config key "%s"' % key)
|
||||
if not cls.config_validators[key](value):
|
||||
raise ValidationError('invalid value "%s" for config key "%s"' % (value, key))
|
||||
|
||||
def __init__(self, contest, config):
|
||||
self.config = self.config_defaults.copy()
|
||||
self.config.update(config or {})
|
||||
self.contest = contest
|
||||
|
||||
def update_participation(self, participation):
|
||||
cumtime = 0
|
||||
points = 0
|
||||
format_data = {}
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('''
|
||||
SELECT (
|
||||
SELECT MAX(ccs.points)
|
||||
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
||||
judge_submission csub ON (csub.id = ccs.submission_id)
|
||||
WHERE ccs.problem_id = cp.id AND ccs.participation_id = %s AND csub.date = MAX(sub.date)
|
||||
) AS `score`, MAX(sub.date) AS `time`, cp.id AS `prob`, (
|
||||
SELECT COUNT(ccs.id)
|
||||
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
||||
judge_submission csub ON (csub.id = ccs.submission_id)
|
||||
WHERE ccs.problem_id = cp.id AND ccs.participation_id = %s AND csub.result NOT IN ('IE', 'CE')
|
||||
) AS `subs`, cp.points AS `max_score`
|
||||
FROM judge_contestproblem cp INNER JOIN
|
||||
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
||||
judge_submission sub ON (sub.id = cs.submission_id)
|
||||
GROUP BY cp.id
|
||||
''', (participation.id, participation.id, participation.id))
|
||||
|
||||
for score, time, prob, subs, max_score in cursor.fetchall():
|
||||
time = from_database_time(time)
|
||||
dt = (time - participation.start).total_seconds()
|
||||
if self.config['cumtime']:
|
||||
cumtime += dt
|
||||
|
||||
bonus = 0
|
||||
if score > 0:
|
||||
# First AC bonus
|
||||
if subs == 1 and score == max_score:
|
||||
bonus += self.config['first_ac_bonus']
|
||||
# Time bonus
|
||||
if self.config['time_bonus']:
|
||||
bonus += (participation.end_time - time).total_seconds() // 60 // self.config['time_bonus']
|
||||
points += bonus
|
||||
|
||||
format_data[str(prob)] = {'time': dt, 'points': score, 'bonus': bonus}
|
||||
points += score
|
||||
|
||||
participation.cumtime = cumtime
|
||||
participation.score = points
|
||||
participation.format_data = format_data
|
||||
participation.save()
|
||||
|
||||
def display_user_problem(self, participation, contest_problem):
|
||||
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
||||
if format_data:
|
||||
bonus = format_html('<small> +{bonus}</small>',
|
||||
bonus=floatformat(format_data['bonus'])) if format_data['bonus'] else ''
|
||||
|
||||
return format_html(
|
||||
'<td class="{state}"><a href="{url}">{points}{bonus}<div class="solving-time">{time}</div></a></td>',
|
||||
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
|
||||
self.best_solution_state(format_data['points'], contest_problem.points)),
|
||||
url=reverse('contest_user_submissions',
|
||||
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
|
||||
points=floatformat(format_data['points']),
|
||||
bonus=bonus,
|
||||
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'),
|
||||
)
|
||||
else:
|
||||
return mark_safe('<td></td>')
|
||||
|
||||
def display_participation_result(self, participation):
|
||||
return format_html(
|
||||
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
|
||||
points=floatformat(participation.score),
|
||||
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') if self.config['cumtime'] else '',
|
||||
)
|
99
judge/contest_format/ioi.py
Normal file
99
judge/contest_format/ioi.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import connection
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from judge.contest_format.default import DefaultContestFormat
|
||||
from judge.contest_format.registry import register_contest_format
|
||||
from judge.timezone import from_database_time
|
||||
from judge.utils.timedelta import nice_repr
|
||||
|
||||
|
||||
@register_contest_format('ioi')
|
||||
class IOIContestFormat(DefaultContestFormat):
|
||||
name = gettext_lazy('IOI')
|
||||
config_defaults = {'cumtime': False}
|
||||
'''
|
||||
cumtime: Specify True if time penalties are to be computed. Defaults to False.
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def validate(cls, config):
|
||||
if config is None:
|
||||
return
|
||||
|
||||
if not isinstance(config, dict):
|
||||
raise ValidationError('IOI-styled contest expects no config or dict as config')
|
||||
|
||||
for key, value in config.items():
|
||||
if key not in cls.config_defaults:
|
||||
raise ValidationError('unknown config key "%s"' % key)
|
||||
if not isinstance(value, type(cls.config_defaults[key])):
|
||||
raise ValidationError('invalid type for config key "%s"' % key)
|
||||
|
||||
def __init__(self, contest, config):
|
||||
self.config = self.config_defaults.copy()
|
||||
self.config.update(config or {})
|
||||
self.contest = contest
|
||||
|
||||
def update_participation(self, participation):
|
||||
cumtime = 0
|
||||
points = 0
|
||||
format_data = {}
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('''
|
||||
SELECT MAX(cs.points) as `score`, (
|
||||
SELECT MIN(csub.date)
|
||||
FROM judge_contestsubmission ccs LEFT OUTER JOIN
|
||||
judge_submission csub ON (csub.id = ccs.submission_id)
|
||||
WHERE ccs.problem_id = cp.id AND ccs.participation_id = %s AND ccs.points = MAX(cs.points)
|
||||
) AS `time`, cp.id AS `prob`
|
||||
FROM judge_contestproblem cp INNER JOIN
|
||||
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
|
||||
judge_submission sub ON (sub.id = cs.submission_id)
|
||||
GROUP BY cp.id
|
||||
''', (participation.id, participation.id))
|
||||
|
||||
for score, time, prob in cursor.fetchall():
|
||||
if self.config['cumtime']:
|
||||
dt = (from_database_time(time) - participation.start).total_seconds()
|
||||
if score:
|
||||
cumtime += dt
|
||||
else:
|
||||
dt = 0
|
||||
|
||||
format_data[str(prob)] = {'time': dt, 'points': score}
|
||||
points += score
|
||||
|
||||
participation.cumtime = max(cumtime, 0)
|
||||
participation.score = points
|
||||
participation.format_data = format_data
|
||||
participation.save()
|
||||
|
||||
def display_user_problem(self, participation, contest_problem):
|
||||
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
||||
if format_data:
|
||||
return format_html(
|
||||
'<td class="{state}"><a href="{url}">{points}<div class="solving-time">{time}</div></a></td>',
|
||||
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') +
|
||||
self.best_solution_state(format_data['points'], contest_problem.points)),
|
||||
url=reverse('contest_user_submissions',
|
||||
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]),
|
||||
points=floatformat(format_data['points']),
|
||||
time=nice_repr(timedelta(seconds=format_data['time']), 'noday') if self.config['cumtime'] else '',
|
||||
)
|
||||
else:
|
||||
return mark_safe('<td></td>')
|
||||
|
||||
def display_participation_result(self, participation):
|
||||
return format_html(
|
||||
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
|
||||
points=floatformat(participation.score),
|
||||
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') if self.config['cumtime'] else '',
|
||||
)
|
16
judge/contest_format/registry.py
Normal file
16
judge/contest_format/registry.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from django.utils import six
|
||||
|
||||
formats = {}
|
||||
|
||||
|
||||
def register_contest_format(name):
|
||||
def register_class(contest_format_class):
|
||||
assert name not in formats
|
||||
formats[name] = contest_format_class
|
||||
return contest_format_class
|
||||
|
||||
return register_class
|
||||
|
||||
|
||||
def choices():
|
||||
return [(key, value.name) for key, value in sorted(six.iteritems(formats))]
|
23
judge/dblock.py
Normal file
23
judge/dblock.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
from itertools import chain
|
||||
|
||||
from django.db import connection, transaction
|
||||
|
||||
|
||||
class LockModel(object):
|
||||
def __init__(self, write, read=()):
|
||||
self.tables = ', '.join(chain(
|
||||
('`%s` WRITE' % model._meta.db_table for model in write),
|
||||
('`%s` READ' % model._meta.db_table for model in read),
|
||||
))
|
||||
self.cursor = connection.cursor()
|
||||
|
||||
def __enter__(self):
|
||||
self.cursor.execute('LOCK TABLES ' + self.tables)
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if exc_type is None:
|
||||
transaction.commit()
|
||||
else:
|
||||
transaction.rollback()
|
||||
self.cursor.execute('UNLOCK TABLES')
|
||||
self.cursor.close()
|
18
judge/event_poster.py
Normal file
18
judge/event_poster.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from django.conf import settings
|
||||
|
||||
__all__ = ['last', 'post']
|
||||
|
||||
if not settings.EVENT_DAEMON_USE:
|
||||
real = False
|
||||
|
||||
def post(channel, message):
|
||||
return 0
|
||||
|
||||
def last():
|
||||
return 0
|
||||
elif hasattr(settings, 'EVENT_DAEMON_AMQP'):
|
||||
from .event_poster_amqp import last, post
|
||||
real = True
|
||||
else:
|
||||
from .event_poster_ws import last, post
|
||||
real = True
|
55
judge/event_poster_amqp.py
Normal file
55
judge/event_poster_amqp.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
import json
|
||||
import threading
|
||||
from time import time
|
||||
|
||||
import pika
|
||||
from django.conf import settings
|
||||
from pika.exceptions import AMQPError
|
||||
|
||||
__all__ = ['EventPoster', 'post', 'last']
|
||||
|
||||
|
||||
class EventPoster(object):
|
||||
def __init__(self):
|
||||
self._connect()
|
||||
self._exchange = settings.EVENT_DAEMON_AMQP_EXCHANGE
|
||||
|
||||
def _connect(self):
|
||||
self._conn = pika.BlockingConnection(pika.URLParameters(settings.EVENT_DAEMON_AMQP))
|
||||
self._chan = self._conn.channel()
|
||||
|
||||
def post(self, channel, message, tries=0):
|
||||
try:
|
||||
id = int(time() * 1000000)
|
||||
self._chan.basic_publish(self._exchange, '',
|
||||
json.dumps({'id': id, 'channel': channel, 'message': message}))
|
||||
return id
|
||||
except AMQPError:
|
||||
if tries > 10:
|
||||
raise
|
||||
self._connect()
|
||||
return self.post(channel, message, tries + 1)
|
||||
|
||||
|
||||
_local = threading.local()
|
||||
|
||||
|
||||
def _get_poster():
|
||||
if 'poster' not in _local.__dict__:
|
||||
_local.poster = EventPoster()
|
||||
return _local.poster
|
||||
|
||||
|
||||
def post(channel, message):
|
||||
try:
|
||||
return _get_poster().post(channel, message)
|
||||
except AMQPError:
|
||||
try:
|
||||
del _local.poster
|
||||
except AttributeError:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
def last():
|
||||
return int(time() * 1000000)
|
82
judge/event_poster_ws.py
Normal file
82
judge/event_poster_ws.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
import json
|
||||
import socket
|
||||
import threading
|
||||
|
||||
from django.conf import settings
|
||||
from websocket import WebSocketException, create_connection
|
||||
|
||||
__all__ = ['EventPostingError', 'EventPoster', 'post', 'last']
|
||||
_local = threading.local()
|
||||
|
||||
|
||||
class EventPostingError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class EventPoster(object):
|
||||
def __init__(self):
|
||||
self._connect()
|
||||
|
||||
def _connect(self):
|
||||
self._conn = create_connection(settings.EVENT_DAEMON_POST)
|
||||
if settings.EVENT_DAEMON_KEY is not None:
|
||||
self._conn.send(json.dumps({'command': 'auth', 'key': settings.EVENT_DAEMON_KEY}))
|
||||
resp = json.loads(self._conn.recv())
|
||||
if resp['status'] == 'error':
|
||||
raise EventPostingError(resp['code'])
|
||||
|
||||
def post(self, channel, message, tries=0):
|
||||
try:
|
||||
self._conn.send(json.dumps({'command': 'post', 'channel': channel, 'message': message}))
|
||||
resp = json.loads(self._conn.recv())
|
||||
if resp['status'] == 'error':
|
||||
raise EventPostingError(resp['code'])
|
||||
else:
|
||||
return resp['id']
|
||||
except WebSocketException:
|
||||
if tries > 10:
|
||||
raise
|
||||
self._connect()
|
||||
return self.post(channel, message, tries + 1)
|
||||
|
||||
def last(self, tries=0):
|
||||
try:
|
||||
self._conn.send('{"command": "last-msg"}')
|
||||
resp = json.loads(self._conn.recv())
|
||||
if resp['status'] == 'error':
|
||||
raise EventPostingError(resp['code'])
|
||||
else:
|
||||
return resp['id']
|
||||
except WebSocketException:
|
||||
if tries > 10:
|
||||
raise
|
||||
self._connect()
|
||||
return self.last(tries + 1)
|
||||
|
||||
|
||||
def _get_poster():
|
||||
if 'poster' not in _local.__dict__:
|
||||
_local.poster = EventPoster()
|
||||
return _local.poster
|
||||
|
||||
|
||||
def post(channel, message):
|
||||
try:
|
||||
return _get_poster().post(channel, message)
|
||||
except (WebSocketException, socket.error):
|
||||
try:
|
||||
del _local.poster
|
||||
except AttributeError:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
def last():
|
||||
try:
|
||||
return _get_poster().last()
|
||||
except (WebSocketException, socket.error):
|
||||
try:
|
||||
del _local.poster
|
||||
except AttributeError:
|
||||
pass
|
||||
return 0
|
99
judge/feed.py
Normal file
99
judge/feed.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.syndication.views import Feed
|
||||
from django.core.cache import cache
|
||||
from django.utils import timezone
|
||||
from django.utils.feedgenerator import Atom1Feed
|
||||
|
||||
from judge.jinja2.markdown import markdown
|
||||
from judge.models import BlogPost, Comment, Problem
|
||||
|
||||
|
||||
class ProblemFeed(Feed):
|
||||
title = 'Recently Added %s Problems' % settings.SITE_NAME
|
||||
link = '/'
|
||||
description = 'The latest problems added on the %s website' % settings.SITE_LONG_NAME
|
||||
|
||||
def items(self):
|
||||
return Problem.objects.filter(is_public=True, is_organization_private=False).order_by('-date', '-id')[:25]
|
||||
|
||||
def item_title(self, problem):
|
||||
return problem.name
|
||||
|
||||
def item_description(self, problem):
|
||||
key = 'problem_feed:%d' % problem.id
|
||||
desc = cache.get(key)
|
||||
if desc is None:
|
||||
desc = str(markdown(problem.description, 'problem'))[:500] + '...'
|
||||
cache.set(key, desc, 86400)
|
||||
return desc
|
||||
|
||||
def item_pubdate(self, problem):
|
||||
return problem.date
|
||||
|
||||
item_updateddate = item_pubdate
|
||||
|
||||
|
||||
class AtomProblemFeed(ProblemFeed):
|
||||
feed_type = Atom1Feed
|
||||
subtitle = ProblemFeed.description
|
||||
|
||||
|
||||
class CommentFeed(Feed):
|
||||
title = 'Latest %s Comments' % settings.SITE_NAME
|
||||
link = '/'
|
||||
description = 'The latest comments on the %s website' % settings.SITE_LONG_NAME
|
||||
|
||||
def items(self):
|
||||
return Comment.most_recent(AnonymousUser(), 25)
|
||||
|
||||
def item_title(self, comment):
|
||||
return '%s -> %s' % (comment.author.user.username, comment.page_title)
|
||||
|
||||
def item_description(self, comment):
|
||||
key = 'comment_feed:%d' % comment.id
|
||||
desc = cache.get(key)
|
||||
if desc is None:
|
||||
desc = str(markdown(comment.body, 'comment'))
|
||||
cache.set(key, desc, 86400)
|
||||
return desc
|
||||
|
||||
def item_pubdate(self, comment):
|
||||
return comment.time
|
||||
|
||||
item_updateddate = item_pubdate
|
||||
|
||||
|
||||
class AtomCommentFeed(CommentFeed):
|
||||
feed_type = Atom1Feed
|
||||
subtitle = CommentFeed.description
|
||||
|
||||
|
||||
class BlogFeed(Feed):
|
||||
title = 'Latest %s Blog Posts' % settings.SITE_NAME
|
||||
link = '/'
|
||||
description = 'The latest blog posts from the %s' % settings.SITE_LONG_NAME
|
||||
|
||||
def items(self):
|
||||
return BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()).order_by('-sticky', '-publish_on')
|
||||
|
||||
def item_title(self, post):
|
||||
return post.title
|
||||
|
||||
def item_description(self, post):
|
||||
key = 'blog_feed:%d' % post.id
|
||||
summary = cache.get(key)
|
||||
if summary is None:
|
||||
summary = str(markdown(post.summary or post.content, 'blog'))
|
||||
cache.set(key, summary, 86400)
|
||||
return summary
|
||||
|
||||
def item_pubdate(self, post):
|
||||
return post.publish_on
|
||||
|
||||
item_updateddate = item_pubdate
|
||||
|
||||
|
||||
class AtomBlogFeed(BlogFeed):
|
||||
feed_type = Atom1Feed
|
||||
subtitle = BlogFeed.description
|
173
judge/fixtures/demo.json
Normal file
173
judge/fixtures/demo.json
Normal file
|
@ -0,0 +1,173 @@
|
|||
[
|
||||
{
|
||||
"fields": {
|
||||
"about": "",
|
||||
"ace_theme": "github",
|
||||
"current_contest": null,
|
||||
"display_rank": "admin",
|
||||
"ip": "10.0.2.2",
|
||||
"language": 1,
|
||||
"last_access": "2017-12-02T08:57:10.093Z",
|
||||
"math_engine": "auto",
|
||||
"mute": false,
|
||||
"organizations": [
|
||||
1
|
||||
],
|
||||
"performance_points": 0.0,
|
||||
"points": 0.0,
|
||||
"problem_count": 0,
|
||||
"rating": null,
|
||||
"timezone": "America/Toronto",
|
||||
"user": 1,
|
||||
"user_script": ""
|
||||
},
|
||||
"model": "judge.profile",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"date_joined": "2017-12-02T08:34:17.408Z",
|
||||
"email": "",
|
||||
"first_name": "",
|
||||
"groups": [
|
||||
|
||||
],
|
||||
"is_active": true,
|
||||
"is_staff": true,
|
||||
"is_superuser": true,
|
||||
"last_login": "2017-12-02T08:34:31.840Z",
|
||||
"last_name": "",
|
||||
"password": "pbkdf2_sha256$36000$eFRRZq4DgktS$md1gk0bBJb7PH/+3YEXkcCW8K+KmiI+y/amqR32G2DY=",
|
||||
"user_permissions": [
|
||||
|
||||
],
|
||||
"username": "admin"
|
||||
},
|
||||
"model": "auth.user",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"about": "This is a sample organization. You can use organizations to split up your user base, host private contests, and more.",
|
||||
"access_code": null,
|
||||
"admins": [
|
||||
1
|
||||
],
|
||||
"creation_date": "2017-12-02T08:50:25.199Z",
|
||||
"is_open": true,
|
||||
"slug": "dmoj",
|
||||
"name": "DMOJ: Modern Online Judge",
|
||||
"registrant": 1,
|
||||
"short_name": "DMOJ",
|
||||
"slots": null
|
||||
},
|
||||
"model": "judge.organization",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"full_name": "Simple Math",
|
||||
"name": "Simple Math"
|
||||
},
|
||||
"model": "judge.problemtype",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"full_name": "Uncategorized",
|
||||
"name": "Uncategorized"
|
||||
},
|
||||
"model": "judge.problemgroup",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"ac_rate": 0.0,
|
||||
"allowed_languages": [
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
2,
|
||||
7,
|
||||
1,
|
||||
8
|
||||
],
|
||||
"authors": [
|
||||
1
|
||||
],
|
||||
"banned_users": [
|
||||
|
||||
],
|
||||
"code": "aplusb",
|
||||
"curators": [
|
||||
|
||||
],
|
||||
"date": "2017-12-02T05:00:00Z",
|
||||
"description": "Tudor is sitting in math class, on his laptop. Clearly, he is not paying attention in this situation. However, he gets called on by his math teacher to do some problems. Since his math teacher did not expect much from Tudor, he only needs to do some simple addition problems. However, simple for you and I may not be simple for Tudor , so please help him!\n\n## Input Specification\n\nThe first line will contain an integer ~N~ (~1 \\le N \\le 100\\,000~), the number of addition problems Tudor needs to do. The next ~N~ lines will each contain two space-separated integers whose absolute value is less than ~1\\,000\\,000\\,000~, the two integers Tudor needs to add.\n\n## Output Specification\n\nOutput ~N~ lines of one integer each, the solutions to the addition problems in order.\n\n## Sample Input\n\n 2\n 1 1\n -1 0\n\n## Sample Output\n\n 2\n -1",
|
||||
"group": 1,
|
||||
"is_manually_managed": false,
|
||||
"is_public": true,
|
||||
"license": null,
|
||||
"memory_limit": 65536,
|
||||
"name": "A Plus B",
|
||||
"og_image": "",
|
||||
"partial": true,
|
||||
"points": 5.0,
|
||||
"short_circuit": false,
|
||||
"summary": "",
|
||||
"testers": [
|
||||
|
||||
],
|
||||
"time_limit": 2.0,
|
||||
"types": [
|
||||
1
|
||||
],
|
||||
"user_count": 0
|
||||
},
|
||||
"model": "judge.problem",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"authors": [
|
||||
1
|
||||
],
|
||||
"content": "Welcome to DMOJ!\n\n```python\nprint \"Hello, World!\"\n```\n\nYou can get started by checking out [this problem we've added for you](/problem/aplusb).",
|
||||
"og_image": "",
|
||||
"publish_on": "2017-12-02T05:00:00Z",
|
||||
"slug": "first-post",
|
||||
"sticky": true,
|
||||
"summary": "",
|
||||
"title": "First Post",
|
||||
"visible": true
|
||||
},
|
||||
"model": "judge.blogpost",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"author": 1,
|
||||
"body": "This is your first comment!",
|
||||
"hidden": false,
|
||||
"level": 0,
|
||||
"lft": 1,
|
||||
"page": "b:1",
|
||||
"parent": null,
|
||||
"rght": 2,
|
||||
"score": 0,
|
||||
"time": "2017-12-02T08:46:54.007Z",
|
||||
"tree_id": 1
|
||||
},
|
||||
"model": "judge.comment",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"domain": "localhost:8081",
|
||||
"name": "DMOJ: Modern Online Judge"
|
||||
},
|
||||
"model": "sites.site",
|
||||
"pk": 1
|
||||
}
|
||||
]
|
122
judge/fixtures/language_small.json
Normal file
122
judge/fixtures/language_small.json
Normal file
|
@ -0,0 +1,122 @@
|
|||
[
|
||||
{
|
||||
"fields": {
|
||||
"ace": "python",
|
||||
"common_name": "Python",
|
||||
"description": "",
|
||||
"extension": "",
|
||||
"info": "python 2.7.9",
|
||||
"key": "PY2",
|
||||
"name": "Python 2",
|
||||
"pygments": "python",
|
||||
"short_name": ""
|
||||
},
|
||||
"model": "judge.language",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"ace": "assembly_x86",
|
||||
"common_name": "Assembly",
|
||||
"description": "",
|
||||
"extension": "",
|
||||
"info": "binutils 2.25",
|
||||
"key": "GAS64",
|
||||
"name": "Assembly (x64)",
|
||||
"pygments": "gas",
|
||||
"short_name": ""
|
||||
},
|
||||
"model": "judge.language",
|
||||
"pk": 2
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"ace": "AWK",
|
||||
"common_name": "Awk",
|
||||
"description": "",
|
||||
"extension": "",
|
||||
"info": "mawk 1.3.3",
|
||||
"key": "AWK",
|
||||
"name": "AWK",
|
||||
"pygments": "awk",
|
||||
"short_name": "AWK"
|
||||
},
|
||||
"model": "judge.language",
|
||||
"pk": 3
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"ace": "c_cpp",
|
||||
"common_name": "C",
|
||||
"description": "Compile options: `gcc -std=c99 -Wall -O2 -lm -march=native -s`\r\n",
|
||||
"extension": "",
|
||||
"info": "gcc 4.9.2",
|
||||
"key": "C",
|
||||
"name": "C",
|
||||
"pygments": "c",
|
||||
"short_name": ""
|
||||
},
|
||||
"model": "judge.language",
|
||||
"pk": 4
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"ace": "c_cpp",
|
||||
"common_name": "C++",
|
||||
"description": "Compile options: `g++ -Wall -O2 -lm -march=native -s`\r\n",
|
||||
"extension": "",
|
||||
"info": "g++ 4.9.2",
|
||||
"key": "CPP03",
|
||||
"name": "C++03",
|
||||
"pygments": "cpp",
|
||||
"short_name": "C++03"
|
||||
},
|
||||
"model": "judge.language",
|
||||
"pk": 5
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"ace": "c_cpp",
|
||||
"common_name": "C++",
|
||||
"description": "Compile options: `g++ -std=c++11 -Wall -O2 -lm -march=native -s`\r\n",
|
||||
"extension": "",
|
||||
"info": "g++-4.9.2 -std=c++11",
|
||||
"key": "CPP11",
|
||||
"name": "C++11",
|
||||
"pygments": "cpp",
|
||||
"short_name": "C++11"
|
||||
},
|
||||
"model": "judge.language",
|
||||
"pk": 6
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"ace": "perl",
|
||||
"common_name": "Perl",
|
||||
"description": "",
|
||||
"extension": "",
|
||||
"info": "perl 5.10.1",
|
||||
"key": "PERL",
|
||||
"name": "Perl",
|
||||
"pygments": "perl",
|
||||
"short_name": ""
|
||||
},
|
||||
"model": "judge.language",
|
||||
"pk": 7
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"ace": "python",
|
||||
"common_name": "Python",
|
||||
"description": "",
|
||||
"extension": "",
|
||||
"info": "python 3.4.2",
|
||||
"key": "PY3",
|
||||
"name": "Python 3",
|
||||
"pygments": "python3",
|
||||
"short_name": ""
|
||||
},
|
||||
"model": "judge.language",
|
||||
"pk": 8
|
||||
}
|
||||
]
|
98
judge/fixtures/navbar.json
Normal file
98
judge/fixtures/navbar.json
Normal file
|
@ -0,0 +1,98 @@
|
|||
[
|
||||
{
|
||||
"fields": {
|
||||
"key": "problems",
|
||||
"label": "Problems",
|
||||
"level": 0,
|
||||
"lft": 1,
|
||||
"order": 1,
|
||||
"parent": null,
|
||||
"path": "/problems/",
|
||||
"regex": "^/problem",
|
||||
"rght": 2,
|
||||
"tree_id": 1
|
||||
},
|
||||
"model": "judge.navigationbar",
|
||||
"pk": 1
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"key": "submit",
|
||||
"label": "Submissions",
|
||||
"level": 0,
|
||||
"lft": 1,
|
||||
"order": 2,
|
||||
"parent": null,
|
||||
"path": "/submissions/",
|
||||
"regex": "^/submi|^/src/",
|
||||
"rght": 2,
|
||||
"tree_id": 2
|
||||
},
|
||||
"model": "judge.navigationbar",
|
||||
"pk": 2
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"key": "user",
|
||||
"label": "Users",
|
||||
"level": 0,
|
||||
"lft": 1,
|
||||
"order": 3,
|
||||
"parent": null,
|
||||
"path": "/users/",
|
||||
"regex": "^/user",
|
||||
"rght": 2,
|
||||
"tree_id": 3
|
||||
},
|
||||
"model": "judge.navigationbar",
|
||||
"pk": 3
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"key": "contest",
|
||||
"label": "Contests",
|
||||
"level": 0,
|
||||
"lft": 1,
|
||||
"order": 5,
|
||||
"parent": null,
|
||||
"path": "/contests/",
|
||||
"regex": "^/contest",
|
||||
"rght": 2,
|
||||
"tree_id": 4
|
||||
},
|
||||
"model": "judge.navigationbar",
|
||||
"pk": 5
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"key": "about",
|
||||
"label": "About",
|
||||
"level": 0,
|
||||
"lft": 1,
|
||||
"order": 6,
|
||||
"parent": null,
|
||||
"path": "/about/",
|
||||
"regex": "^/about/$",
|
||||
"rght": 4,
|
||||
"tree_id": 5
|
||||
},
|
||||
"model": "judge.navigationbar",
|
||||
"pk": 6
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"key": "status",
|
||||
"label": "Status",
|
||||
"level": 1,
|
||||
"lft": 2,
|
||||
"order": 7,
|
||||
"parent": 6,
|
||||
"path": "/status/",
|
||||
"regex": "^/status/$|^/judge/",
|
||||
"rght": 3,
|
||||
"tree_id": 5
|
||||
},
|
||||
"model": "judge.navigationbar",
|
||||
"pk": 7
|
||||
}
|
||||
]
|
159
judge/forms.py
Normal file
159
judge/forms.py
Normal file
|
@ -0,0 +1,159 @@
|
|||
from operator import attrgetter
|
||||
|
||||
import pyotp
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db.models import Q
|
||||
from django.forms import CharField, Form, ModelForm
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django_ace import AceWidget
|
||||
from judge.models import Contest, Language, Organization, PrivateMessage, Problem, Profile, Submission
|
||||
from judge.utils.subscription import newsletter_id
|
||||
from judge.widgets import HeavyPreviewPageDownWidget, MathJaxPagedownWidget, PagedownWidget, Select2MultipleWidget, \
|
||||
Select2Widget
|
||||
|
||||
|
||||
def fix_unicode(string, unsafe=tuple('\u202a\u202b\u202d\u202e')):
|
||||
return string + (sum(k in unsafe for k in string) - string.count('\u202c')) * '\u202c'
|
||||
|
||||
|
||||
class ProfileForm(ModelForm):
|
||||
if newsletter_id is not None:
|
||||
newsletter = forms.BooleanField(label=_('Subscribe to contest updates'), initial=False, required=False)
|
||||
test_site = forms.BooleanField(label=_('Enable experimental features'), initial=False, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = ['about', 'organizations', 'timezone', 'language', 'ace_theme', 'user_script']
|
||||
widgets = {
|
||||
'user_script': AceWidget(theme='github'),
|
||||
'timezone': Select2Widget(attrs={'style': 'width:200px'}),
|
||||
'language': Select2Widget(attrs={'style': 'width:200px'}),
|
||||
'ace_theme': Select2Widget(attrs={'style': 'width:200px'}),
|
||||
}
|
||||
|
||||
has_math_config = bool(settings.MATHOID_URL)
|
||||
if has_math_config:
|
||||
fields.append('math_engine')
|
||||
widgets['math_engine'] = Select2Widget(attrs={'style': 'width:200px'})
|
||||
|
||||
if HeavyPreviewPageDownWidget is not None:
|
||||
widgets['about'] = HeavyPreviewPageDownWidget(
|
||||
preview=reverse_lazy('profile_preview'),
|
||||
attrs={'style': 'max-width:700px;min-width:700px;width:700px'},
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
organizations = self.cleaned_data.get('organizations') or []
|
||||
max_orgs = settings.DMOJ_USER_MAX_ORGANIZATION_COUNT
|
||||
|
||||
if sum(org.is_open for org in organizations) > max_orgs:
|
||||
raise ValidationError(
|
||||
_('You may not be part of more than {count} public organizations.').format(count=max_orgs))
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop('user', None)
|
||||
super(ProfileForm, self).__init__(*args, **kwargs)
|
||||
if not user.has_perm('judge.edit_all_organization'):
|
||||
self.fields['organizations'].queryset = Organization.objects.filter(
|
||||
Q(is_open=True) | Q(id__in=user.profile.organizations.all()),
|
||||
)
|
||||
|
||||
|
||||
class ProblemSubmitForm(ModelForm):
|
||||
source = CharField(max_length=65536, widget=AceWidget(theme='twilight', no_ace_media=True))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ProblemSubmitForm, self).__init__(*args, **kwargs)
|
||||
self.fields['problem'].empty_label = None
|
||||
self.fields['problem'].widget = forms.HiddenInput()
|
||||
self.fields['language'].empty_label = None
|
||||
self.fields['language'].label_from_instance = attrgetter('display_name')
|
||||
self.fields['language'].queryset = Language.objects.filter(judges__online=True).distinct()
|
||||
|
||||
class Meta:
|
||||
model = Submission
|
||||
fields = ['problem', 'language']
|
||||
|
||||
|
||||
class EditOrganizationForm(ModelForm):
|
||||
class Meta:
|
||||
model = Organization
|
||||
fields = ['about', 'logo_override_image', 'admins']
|
||||
widgets = {'admins': Select2MultipleWidget()}
|
||||
if HeavyPreviewPageDownWidget is not None:
|
||||
widgets['about'] = HeavyPreviewPageDownWidget(preview=reverse_lazy('organization_preview'))
|
||||
|
||||
|
||||
class NewMessageForm(ModelForm):
|
||||
class Meta:
|
||||
model = PrivateMessage
|
||||
fields = ['title', 'content']
|
||||
widgets = {}
|
||||
if PagedownWidget is not None:
|
||||
widgets['content'] = MathJaxPagedownWidget()
|
||||
|
||||
|
||||
class CustomAuthenticationForm(AuthenticationForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CustomAuthenticationForm, self).__init__(*args, **kwargs)
|
||||
self.fields['username'].widget.attrs.update({'placeholder': _('Username')})
|
||||
self.fields['password'].widget.attrs.update({'placeholder': _('Password')})
|
||||
|
||||
self.has_google_auth = self._has_social_auth('GOOGLE_OAUTH2')
|
||||
self.has_facebook_auth = self._has_social_auth('FACEBOOK')
|
||||
self.has_github_auth = self._has_social_auth('GITHUB_SECURE')
|
||||
|
||||
def _has_social_auth(self, key):
|
||||
return (getattr(settings, 'SOCIAL_AUTH_%s_KEY' % key, None) and
|
||||
getattr(settings, 'SOCIAL_AUTH_%s_SECRET' % key, None))
|
||||
|
||||
|
||||
class NoAutoCompleteCharField(forms.CharField):
|
||||
def widget_attrs(self, widget):
|
||||
attrs = super(NoAutoCompleteCharField, self).widget_attrs(widget)
|
||||
attrs['autocomplete'] = 'off'
|
||||
return attrs
|
||||
|
||||
|
||||
class TOTPForm(Form):
|
||||
TOLERANCE = settings.DMOJ_TOTP_TOLERANCE_HALF_MINUTES
|
||||
|
||||
totp_token = NoAutoCompleteCharField(validators=[
|
||||
RegexValidator('^[0-9]{6}$', _('Two Factor Authentication tokens must be 6 decimal digits.')),
|
||||
])
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.totp_key = kwargs.pop('totp_key')
|
||||
super(TOTPForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def clean_totp_token(self):
|
||||
if not pyotp.TOTP(self.totp_key).verify(self.cleaned_data['totp_token'], valid_window=self.TOLERANCE):
|
||||
raise ValidationError(_('Invalid Two Factor Authentication token.'))
|
||||
|
||||
|
||||
class ProblemCloneForm(Form):
|
||||
code = CharField(max_length=20, validators=[RegexValidator('^[a-z0-9]+$', _('Problem code must be ^[a-z0-9]+$'))])
|
||||
|
||||
def clean_code(self):
|
||||
code = self.cleaned_data['code']
|
||||
if Problem.objects.filter(code=code).exists():
|
||||
raise ValidationError(_('Problem with code already exists.'))
|
||||
return code
|
||||
|
||||
|
||||
class ContestCloneForm(Form):
|
||||
key = CharField(max_length=20, validators=[RegexValidator('^[a-z0-9]+$', _('Contest id must be ^[a-z0-9]+$'))])
|
||||
|
||||
def clean_key(self):
|
||||
key = self.cleaned_data['key']
|
||||
if Contest.objects.filter(key=key).exists():
|
||||
raise ValidationError(_('Contest with key already exists.'))
|
||||
return key
|
55
judge/fulltext.py
Normal file
55
judge/fulltext.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
# From: http://www.mercurytide.co.uk/news/article/django-full-text-search/
|
||||
|
||||
from django.db import connection, models
|
||||
from django.db.models.query import QuerySet
|
||||
|
||||
|
||||
class SearchQuerySet(QuerySet):
|
||||
DEFAULT = ''
|
||||
BOOLEAN = ' IN BOOLEAN MODE'
|
||||
NATURAL_LANGUAGE = ' IN NATURAL LANGUAGE MODE'
|
||||
QUERY_EXPANSION = ' WITH QUERY EXPANSION'
|
||||
|
||||
def __init__(self, fields=None, **kwargs):
|
||||
super(SearchQuerySet, self).__init__(**kwargs)
|
||||
self._search_fields = fields
|
||||
|
||||
def _clone(self, *args, **kwargs):
|
||||
queryset = super(SearchQuerySet, self)._clone(*args, **kwargs)
|
||||
queryset._search_fields = self._search_fields
|
||||
return queryset
|
||||
|
||||
def search(self, query, mode=DEFAULT):
|
||||
meta = self.model._meta
|
||||
|
||||
# Get the table name and column names from the model
|
||||
# in `table_name`.`column_name` style
|
||||
columns = [meta.get_field(name).column for name in self._search_fields]
|
||||
full_names = ['%s.%s' %
|
||||
(connection.ops.quote_name(meta.db_table),
|
||||
connection.ops.quote_name(column))
|
||||
for column in columns]
|
||||
|
||||
# Create the MATCH...AGAINST expressions
|
||||
fulltext_columns = ', '.join(full_names)
|
||||
match_expr = ('MATCH(%s) AGAINST (%%s%s)' % (fulltext_columns, mode))
|
||||
|
||||
# Add the extra SELECT and WHERE options
|
||||
return self.extra(select={'relevance': match_expr},
|
||||
select_params=[query],
|
||||
where=[match_expr],
|
||||
params=[query])
|
||||
|
||||
|
||||
class SearchManager(models.Manager):
|
||||
def __init__(self, fields=None):
|
||||
super(SearchManager, self).__init__()
|
||||
self._search_fields = fields
|
||||
|
||||
def get_queryset(self):
|
||||
if self._search_fields is not None:
|
||||
return SearchQuerySet(model=self.model, fields=self._search_fields)
|
||||
return super(SearchManager, self).get_queryset()
|
||||
|
||||
def search(self, *args, **kwargs):
|
||||
return self.get_queryset().search(*args, **kwargs)
|
37
judge/highlight_code.py
Normal file
37
judge/highlight_code.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
from django.utils.html import escape, mark_safe
|
||||
|
||||
__all__ = ['highlight_code']
|
||||
|
||||
|
||||
def _make_pre_code(code):
|
||||
return mark_safe('<pre>' + escape(code) + '</pre>')
|
||||
|
||||
|
||||
def _wrap_code(inner):
|
||||
yield 0, "<code>"
|
||||
for tup in inner:
|
||||
yield tup
|
||||
yield 0, "</code>"
|
||||
|
||||
|
||||
try:
|
||||
import pygments
|
||||
import pygments.lexers
|
||||
import pygments.formatters.html
|
||||
import pygments.util
|
||||
except ImportError:
|
||||
def highlight_code(code, language, cssclass=None):
|
||||
return _make_pre_code(code)
|
||||
else:
|
||||
class HtmlCodeFormatter(pygments.formatters.HtmlFormatter):
|
||||
def wrap(self, source, outfile):
|
||||
return self._wrap_div(self._wrap_pre(_wrap_code(source)))
|
||||
|
||||
def highlight_code(code, language, cssclass='codehilite'):
|
||||
try:
|
||||
lexer = pygments.lexers.get_lexer_by_name(language)
|
||||
except pygments.util.ClassNotFound:
|
||||
return _make_pre_code(code)
|
||||
|
||||
# return mark_safe(pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass, linenos='table')))
|
||||
return mark_safe(pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass)))
|
36
judge/jinja2/__init__.py
Normal file
36
judge/jinja2/__init__.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
import itertools
|
||||
import json
|
||||
|
||||
from django.utils.http import urlquote
|
||||
from jinja2.ext import Extension
|
||||
from mptt.utils import get_cached_trees
|
||||
from statici18n.templatetags.statici18n import inlinei18n
|
||||
|
||||
from judge.highlight_code import highlight_code
|
||||
from judge.user_translations import gettext
|
||||
from . import (camo, datetime, filesize, gravatar, language, markdown, rating, reference, render, social,
|
||||
spaceless, submission, timedelta)
|
||||
from . import registry
|
||||
|
||||
registry.function('str', str)
|
||||
registry.filter('str', str)
|
||||
registry.filter('json', json.dumps)
|
||||
registry.filter('highlight', highlight_code)
|
||||
registry.filter('urlquote', urlquote)
|
||||
registry.filter('roundfloat', round)
|
||||
registry.function('inlinei18n', inlinei18n)
|
||||
registry.function('mptt_tree', get_cached_trees)
|
||||
registry.function('user_trans', gettext)
|
||||
|
||||
|
||||
@registry.function
|
||||
def counter(start=1):
|
||||
return itertools.count(start).__next__
|
||||
|
||||
|
||||
class DMOJExtension(Extension):
|
||||
def __init__(self, env):
|
||||
super(DMOJExtension, self).__init__(env)
|
||||
env.globals.update(registry.globals)
|
||||
env.filters.update(registry.filters)
|
||||
env.tests.update(registry.tests)
|
9
judge/jinja2/camo.py
Normal file
9
judge/jinja2/camo.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from judge.utils.camo import client as camo_client
|
||||
from . import registry
|
||||
|
||||
|
||||
@registry.filter
|
||||
def camo(url):
|
||||
if camo_client is None:
|
||||
return url
|
||||
return camo_client.rewrite_url(url)
|
27
judge/jinja2/datetime.py
Normal file
27
judge/jinja2/datetime.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
import functools
|
||||
|
||||
from django.template.defaultfilters import date, time
|
||||
from django.templatetags.tz import localtime
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from . import registry
|
||||
|
||||
|
||||
def localtime_wrapper(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(datetime, *args, **kwargs):
|
||||
if getattr(datetime, 'convert_to_local_time', True):
|
||||
datetime = localtime(datetime)
|
||||
return func(datetime, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
registry.filter(localtime_wrapper(date))
|
||||
registry.filter(localtime_wrapper(time))
|
||||
|
||||
|
||||
@registry.function
|
||||
@registry.render_with('widgets/relative-time.html')
|
||||
def relative_time(time, format=_('N j, Y, g:i a'), rel=_('{time}'), abs=_('on {time}')):
|
||||
return {'time': time, 'format': format, 'rel_format': rel, 'abs_format': abs}
|
36
judge/jinja2/filesize.py
Normal file
36
judge/jinja2/filesize.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
from django.utils.html import avoid_wrapping
|
||||
|
||||
from . import registry
|
||||
|
||||
|
||||
def _format_size(bytes, callback):
|
||||
bytes = float(bytes)
|
||||
|
||||
KB = 1 << 10
|
||||
MB = 1 << 20
|
||||
GB = 1 << 30
|
||||
TB = 1 << 40
|
||||
PB = 1 << 50
|
||||
|
||||
if bytes < KB:
|
||||
return callback('', bytes)
|
||||
elif bytes < MB:
|
||||
return callback('K', bytes / KB)
|
||||
elif bytes < GB:
|
||||
return callback('M', bytes / MB)
|
||||
elif bytes < TB:
|
||||
return callback('G', bytes / GB)
|
||||
elif bytes < PB:
|
||||
return callback('T', bytes / TB)
|
||||
else:
|
||||
return callback('P', bytes / PB)
|
||||
|
||||
|
||||
@registry.filter
|
||||
def kbdetailformat(bytes):
|
||||
return avoid_wrapping(_format_size(bytes * 1024, lambda x, y: ['%d %sB', '%.2f %sB'][bool(x)] % (y, x)))
|
||||
|
||||
|
||||
@registry.filter
|
||||
def kbsimpleformat(kb):
|
||||
return _format_size(kb * 1024, lambda x, y: '%.0f%s' % (y, x or 'B'))
|
25
judge/jinja2/gravatar.py
Normal file
25
judge/jinja2/gravatar.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
import hashlib
|
||||
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.utils.http import urlencode
|
||||
|
||||
from judge.models import Profile
|
||||
from judge.utils.unicode import utf8bytes
|
||||
from . import registry
|
||||
|
||||
|
||||
@registry.function
|
||||
def gravatar(email, size=80, default=None):
|
||||
if isinstance(email, Profile):
|
||||
if default is None:
|
||||
default = email.mute
|
||||
email = email.user.email
|
||||
elif isinstance(email, AbstractUser):
|
||||
email = email.email
|
||||
|
||||
gravatar_url = '//www.gravatar.com/avatar/' + hashlib.md5(utf8bytes(email.strip().lower())).hexdigest() + '?'
|
||||
args = {'d': 'identicon', 's': str(size)}
|
||||
if default:
|
||||
args['f'] = 'y'
|
||||
gravatar_url += urlencode(args)
|
||||
return gravatar_url
|
18
judge/jinja2/language.py
Normal file
18
judge/jinja2/language.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from django.utils import translation
|
||||
|
||||
from . import registry
|
||||
|
||||
|
||||
@registry.function('language_info')
|
||||
def get_language_info(language):
|
||||
# ``language`` is either a language code string or a sequence
|
||||
# with the language code as its first item
|
||||
if len(language[0]) > 1:
|
||||
return translation.get_language_info(language[0])
|
||||
else:
|
||||
return translation.get_language_info(str(language))
|
||||
|
||||
|
||||
@registry.function('language_info_list')
|
||||
def get_language_info_list(langs):
|
||||
return [get_language_info(lang) for lang in langs]
|
142
judge/jinja2/markdown/__init__.py
Normal file
142
judge/jinja2/markdown/__init__.py
Normal file
|
@ -0,0 +1,142 @@
|
|||
import logging
|
||||
import re
|
||||
from html.parser import HTMLParser
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import mistune
|
||||
from django.conf import settings
|
||||
from jinja2 import Markup
|
||||
from lxml import html
|
||||
from lxml.etree import ParserError, XMLSyntaxError
|
||||
|
||||
from judge.highlight_code import highlight_code
|
||||
from judge.jinja2.markdown.lazy_load import lazy_load as lazy_load_processor
|
||||
from judge.jinja2.markdown.math import MathInlineGrammar, MathInlineLexer, MathRenderer
|
||||
from judge.utils.camo import client as camo_client
|
||||
from judge.utils.texoid import TEXOID_ENABLED, TexoidRenderer
|
||||
from .. import registry
|
||||
|
||||
logger = logging.getLogger('judge.html')
|
||||
|
||||
NOFOLLOW_WHITELIST = settings.NOFOLLOW_EXCLUDED
|
||||
|
||||
|
||||
class CodeSafeInlineGrammar(mistune.InlineGrammar):
|
||||
double_emphasis = re.compile(r'^\*{2}([\s\S]+?)()\*{2}(?!\*)') # **word**
|
||||
emphasis = re.compile(r'^\*((?:\*\*|[^\*])+?)()\*(?!\*)') # *word*
|
||||
|
||||
|
||||
class AwesomeInlineGrammar(MathInlineGrammar, CodeSafeInlineGrammar):
|
||||
pass
|
||||
|
||||
|
||||
class AwesomeInlineLexer(MathInlineLexer, mistune.InlineLexer):
|
||||
grammar_class = AwesomeInlineGrammar
|
||||
|
||||
|
||||
class AwesomeRenderer(MathRenderer, mistune.Renderer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.nofollow = kwargs.pop('nofollow', True)
|
||||
self.texoid = TexoidRenderer() if kwargs.pop('texoid', False) else None
|
||||
self.parser = HTMLParser()
|
||||
super(AwesomeRenderer, self).__init__(*args, **kwargs)
|
||||
|
||||
def _link_rel(self, href):
|
||||
if href:
|
||||
try:
|
||||
url = urlparse(href)
|
||||
except ValueError:
|
||||
return ' rel="nofollow"'
|
||||
else:
|
||||
if url.netloc and url.netloc not in NOFOLLOW_WHITELIST:
|
||||
return ' rel="nofollow"'
|
||||
return ''
|
||||
|
||||
def autolink(self, link, is_email=False):
|
||||
text = link = mistune.escape(link)
|
||||
if is_email:
|
||||
link = 'mailto:%s' % link
|
||||
return '<a href="%s"%s>%s</a>' % (link, self._link_rel(link), text)
|
||||
|
||||
def table(self, header, body):
|
||||
return (
|
||||
'<table class="table">\n<thead>%s</thead>\n'
|
||||
'<tbody>\n%s</tbody>\n</table>\n'
|
||||
) % (header, body)
|
||||
|
||||
def link(self, link, title, text):
|
||||
link = mistune.escape_link(link)
|
||||
if not title:
|
||||
return '<a href="%s"%s>%s</a>' % (link, self._link_rel(link), text)
|
||||
title = mistune.escape(title, quote=True)
|
||||
return '<a href="%s" title="%s"%s>%s</a>' % (link, title, self._link_rel(link), text)
|
||||
|
||||
def block_code(self, code, lang=None):
|
||||
if not lang:
|
||||
return '\n<pre><code>%s</code></pre>\n' % mistune.escape(code).rstrip()
|
||||
return highlight_code(code, lang)
|
||||
|
||||
def block_html(self, html):
|
||||
if self.texoid and html.startswith('<latex'):
|
||||
attr = html[6:html.index('>')]
|
||||
latex = html[html.index('>') + 1:html.rindex('<')]
|
||||
latex = self.parser.unescape(latex)
|
||||
result = self.texoid.get_result(latex)
|
||||
if not result:
|
||||
return '<pre>%s</pre>' % mistune.escape(latex, smart_amp=False)
|
||||
elif 'error' not in result:
|
||||
img = ('''<img src="%(svg)s" onerror="this.src='%(png)s';this.onerror=null"'''
|
||||
'width="%(width)s" height="%(height)s"%(tail)s>') % {
|
||||
'svg': result['svg'], 'png': result['png'],
|
||||
'width': result['meta']['width'], 'height': result['meta']['height'],
|
||||
'tail': ' /' if self.options.get('use_xhtml') else '',
|
||||
}
|
||||
style = ['max-width: 100%',
|
||||
'height: %s' % result['meta']['height'],
|
||||
'max-height: %s' % result['meta']['height'],
|
||||
'width: %s' % result['meta']['height']]
|
||||
if 'inline' in attr:
|
||||
tag = 'span'
|
||||
else:
|
||||
tag = 'div'
|
||||
style += ['text-align: center']
|
||||
return '<%s style="%s">%s</%s>' % (tag, ';'.join(style), img, tag)
|
||||
else:
|
||||
return '<pre>%s</pre>' % mistune.escape(result['error'], smart_amp=False)
|
||||
return super(AwesomeRenderer, self).block_html(html)
|
||||
|
||||
def header(self, text, level, *args, **kwargs):
|
||||
return super(AwesomeRenderer, self).header(text, level + 2, *args, **kwargs)
|
||||
|
||||
|
||||
@registry.filter
|
||||
def markdown(value, style, math_engine=None, lazy_load=False):
|
||||
styles = settings.MARKDOWN_STYLES.get(style, settings.MARKDOWN_DEFAULT_STYLE)
|
||||
escape = styles.get('safe_mode', True)
|
||||
nofollow = styles.get('nofollow', True)
|
||||
texoid = TEXOID_ENABLED and styles.get('texoid', False)
|
||||
math = hasattr(settings, 'MATHOID_URL') and styles.get('math', False)
|
||||
|
||||
post_processors = []
|
||||
if styles.get('use_camo', False) and camo_client is not None:
|
||||
post_processors.append(camo_client.update_tree)
|
||||
if lazy_load:
|
||||
post_processors.append(lazy_load_processor)
|
||||
|
||||
renderer = AwesomeRenderer(escape=escape, nofollow=nofollow, texoid=texoid,
|
||||
math=math and math_engine is not None, math_engine=math_engine)
|
||||
markdown = mistune.Markdown(renderer=renderer, inline=AwesomeInlineLexer,
|
||||
parse_block_html=1, parse_inline_html=1)
|
||||
result = markdown(value)
|
||||
|
||||
if post_processors:
|
||||
try:
|
||||
tree = html.fromstring(result, parser=html.HTMLParser(recover=True))
|
||||
except (XMLSyntaxError, ParserError) as e:
|
||||
if result and (not isinstance(e, ParserError) or e.args[0] != 'Document is empty'):
|
||||
logger.exception('Failed to parse HTML string')
|
||||
tree = html.Element('div')
|
||||
for processor in post_processors:
|
||||
processor(tree)
|
||||
result = html.tostring(tree, encoding='unicode')
|
||||
return Markup(result)
|
20
judge/jinja2/markdown/lazy_load.py
Normal file
20
judge/jinja2/markdown/lazy_load.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
from copy import deepcopy
|
||||
|
||||
from django.templatetags.static import static
|
||||
from lxml import html
|
||||
|
||||
|
||||
def lazy_load(tree):
|
||||
blank = static('blank.gif')
|
||||
for img in tree.xpath('.//img'):
|
||||
src = img.get('src', '')
|
||||
if src.startswith('data') or '-math' in img.get('class', ''):
|
||||
continue
|
||||
noscript = html.Element('noscript')
|
||||
copy = deepcopy(img)
|
||||
copy.tail = ''
|
||||
noscript.append(copy)
|
||||
img.addprevious(noscript)
|
||||
img.set('data-src', src)
|
||||
img.set('src', blank)
|
||||
img.set('class', img.get('class') + ' unveil' if img.get('class') else 'unveil')
|
65
judge/jinja2/markdown/math.py
Normal file
65
judge/jinja2/markdown/math.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
import re
|
||||
|
||||
import mistune
|
||||
|
||||
from judge.utils.mathoid import MathoidMathParser
|
||||
|
||||
mistune._pre_tags.append('latex')
|
||||
|
||||
|
||||
class MathInlineGrammar(mistune.InlineGrammar):
|
||||
block_math = re.compile(r'^\$\$(.*?)\$\$|^\\\[(.*?)\\\]', re.DOTALL)
|
||||
math = re.compile(r'^~(.*?)~|^\\\((.*?)\\\)', re.DOTALL)
|
||||
text = re.compile(r'^[\s\S]+?(?=[\\<!\[_*`~$]|\\[\[(]|https?://| {2,}\n|$)')
|
||||
|
||||
|
||||
class MathInlineLexer(mistune.InlineLexer):
|
||||
grammar_class = MathInlineGrammar
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.default_rules = self.default_rules[:]
|
||||
self.inline_html_rules = self.default_rules
|
||||
self.default_rules.insert(self.default_rules.index('strikethrough') + 1, 'math')
|
||||
self.default_rules.insert(self.default_rules.index('strikethrough') + 1, 'block_math')
|
||||
super(MathInlineLexer, self).__init__(*args, **kwargs)
|
||||
|
||||
def output_block_math(self, m):
|
||||
return self.renderer.block_math(m.group(1) or m.group(2))
|
||||
|
||||
def output_math(self, m):
|
||||
return self.renderer.math(m.group(1) or m.group(2))
|
||||
|
||||
def output_inline_html(self, m):
|
||||
tag = m.group(1)
|
||||
text = m.group(3)
|
||||
if self._parse_inline_html and text:
|
||||
if tag == 'a':
|
||||
self._in_link = True
|
||||
text = self.output(text)
|
||||
self._in_link = False
|
||||
else:
|
||||
text = self.output(text)
|
||||
extra = m.group(2) or ''
|
||||
html = '<%s%s>%s</%s>' % (tag, extra, text, tag)
|
||||
else:
|
||||
html = m.group(0)
|
||||
return self.renderer.inline_html(html)
|
||||
|
||||
|
||||
class MathRenderer(mistune.Renderer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if kwargs.pop('math', False):
|
||||
self.mathoid = MathoidMathParser(kwargs.pop('math_engine', None) or 'svg')
|
||||
else:
|
||||
self.mathoid = None
|
||||
super(MathRenderer, self).__init__(*args, **kwargs)
|
||||
|
||||
def block_math(self, math):
|
||||
if self.mathoid is None or not math:
|
||||
return r'\[%s\]' % mistune.escape(str(math))
|
||||
return self.mathoid.display_math(math)
|
||||
|
||||
def math(self, math):
|
||||
if self.mathoid is None or not math:
|
||||
return r'\(%s\)' % mistune.escape(str(math))
|
||||
return self.mathoid.inline_math(math)
|
35
judge/jinja2/rating.py
Normal file
35
judge/jinja2/rating.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
from django.utils import six
|
||||
|
||||
from judge.ratings import rating_class, rating_name, rating_progress
|
||||
from . import registry
|
||||
|
||||
|
||||
def _get_rating_value(func, obj):
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
if isinstance(obj, six.integer_types):
|
||||
return func(obj)
|
||||
else:
|
||||
return func(obj.rating)
|
||||
|
||||
|
||||
@registry.function('rating_class')
|
||||
def get_rating_class(obj):
|
||||
return _get_rating_value(rating_class, obj) or 'rate-none'
|
||||
|
||||
|
||||
@registry.function(name='rating_name')
|
||||
def get_name(obj):
|
||||
return _get_rating_value(rating_name, obj) or 'Unrated'
|
||||
|
||||
|
||||
@registry.function(name='rating_progress')
|
||||
def get_progress(obj):
|
||||
return _get_rating_value(rating_progress, obj) or 0.0
|
||||
|
||||
|
||||
@registry.function
|
||||
@registry.render_with('user/rating.html')
|
||||
def rating_number(obj):
|
||||
return {'rating': obj}
|
187
judge/jinja2/reference.py
Normal file
187
judge/jinja2/reference.py
Normal file
|
@ -0,0 +1,187 @@
|
|||
import re
|
||||
from collections import defaultdict
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from ansi2html import Ansi2HTMLConverter
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from lxml.html import Element
|
||||
|
||||
from judge import lxml_tree
|
||||
from judge.models import Contest, Problem, Profile
|
||||
from judge.ratings import rating_class, rating_progress
|
||||
from . import registry
|
||||
|
||||
rereference = re.compile(r'\[(r?user):(\w+)\]')
|
||||
|
||||
|
||||
def get_user(username, data):
|
||||
if not data:
|
||||
element = Element('span')
|
||||
element.text = username
|
||||
return element
|
||||
|
||||
element = Element('span', {'class': Profile.get_user_css_class(*data)})
|
||||
link = Element('a', {'href': reverse('user_page', args=[username])})
|
||||
link.text = username
|
||||
element.append(link)
|
||||
return element
|
||||
|
||||
|
||||
def get_user_rating(username, data):
|
||||
if not data:
|
||||
element = Element('span')
|
||||
element.text = username
|
||||
return element
|
||||
|
||||
rating = data[1]
|
||||
element = Element('a', {'class': 'rate-group', 'href': reverse('user_page', args=[username])})
|
||||
if rating:
|
||||
rating_css = rating_class(rating)
|
||||
rate_box = Element('span', {'class': 'rate-box ' + rating_css})
|
||||
rate_box.append(Element('span', {'style': 'height: %3.fem' % rating_progress(rating)}))
|
||||
user = Element('span', {'class': 'rating ' + rating_css})
|
||||
user.text = username
|
||||
element.append(rate_box)
|
||||
element.append(user)
|
||||
else:
|
||||
element.text = username
|
||||
return element
|
||||
|
||||
|
||||
def get_user_info(usernames):
|
||||
return {name: (rank, rating) for name, rank, rating in
|
||||
Profile.objects.filter(user__username__in=usernames)
|
||||
.values_list('user__username', 'display_rank', 'rating')}
|
||||
|
||||
|
||||
reference_map = {
|
||||
'user': (get_user, get_user_info),
|
||||
'ruser': (get_user_rating, get_user_info),
|
||||
}
|
||||
|
||||
|
||||
def process_reference(text):
|
||||
# text/tail -> text/tail + elements
|
||||
last = 0
|
||||
tail = text
|
||||
prev = None
|
||||
elements = []
|
||||
for piece in rereference.finditer(text):
|
||||
if prev is None:
|
||||
tail = text[last:piece.start()]
|
||||
else:
|
||||
prev.append(text[last:piece.start()])
|
||||
prev = list(piece.groups())
|
||||
elements.append(prev)
|
||||
last = piece.end()
|
||||
if prev is not None:
|
||||
prev.append(text[last:])
|
||||
return tail, elements
|
||||
|
||||
|
||||
def populate_list(queries, list, element, tail, children):
|
||||
if children:
|
||||
for elem in children:
|
||||
queries[elem[0]].append(elem[1])
|
||||
list.append((element, tail, children))
|
||||
|
||||
|
||||
def update_tree(list, results, is_tail=False):
|
||||
for element, text, children in list:
|
||||
after = []
|
||||
for type, name, tail in children:
|
||||
child = reference_map[type][0](name, results[type].get(name))
|
||||
child.tail = tail
|
||||
after.append(child)
|
||||
|
||||
after = iter(reversed(after))
|
||||
if is_tail:
|
||||
element.tail = text
|
||||
link = element.getnext()
|
||||
if link is None:
|
||||
link = next(after)
|
||||
element.getparent().append(link)
|
||||
else:
|
||||
element.text = text
|
||||
link = next(after)
|
||||
element.insert(0, link)
|
||||
for child in after:
|
||||
link.addprevious(child)
|
||||
link = child
|
||||
|
||||
|
||||
@registry.filter
|
||||
def reference(text):
|
||||
tree = lxml_tree.fromstring(text)
|
||||
texts = []
|
||||
tails = []
|
||||
queries = defaultdict(list)
|
||||
for element in tree.iter():
|
||||
if element.text:
|
||||
populate_list(queries, texts, element, *process_reference(element.text))
|
||||
if element.tail:
|
||||
populate_list(queries, tails, element, *process_reference(element.tail))
|
||||
|
||||
results = {type: reference_map[type][1](values) for type, values in queries.items()}
|
||||
update_tree(texts, results, is_tail=False)
|
||||
update_tree(tails, results, is_tail=True)
|
||||
return tree
|
||||
|
||||
|
||||
@registry.filter
|
||||
def item_title(item):
|
||||
if isinstance(item, Problem):
|
||||
return item.name
|
||||
elif isinstance(item, Contest):
|
||||
return item.name
|
||||
return '<Unknown>'
|
||||
|
||||
|
||||
@registry.function
|
||||
@registry.render_with('user/link.html')
|
||||
def link_user(user):
|
||||
if isinstance(user, Profile):
|
||||
user, profile = user.user, user
|
||||
elif isinstance(user, AbstractUser):
|
||||
profile = user.profile
|
||||
elif type(user).__name__ == 'ContestRankingProfile':
|
||||
user, profile = user.user, user
|
||||
else:
|
||||
raise ValueError('Expected profile or user, got %s' % (type(user),))
|
||||
return {'user': user, 'profile': profile}
|
||||
|
||||
|
||||
@registry.function
|
||||
@registry.render_with('user/link-list.html')
|
||||
def link_users(users):
|
||||
return {'users': users}
|
||||
|
||||
|
||||
@registry.function
|
||||
@registry.render_with('runtime-version-fragment.html')
|
||||
def runtime_versions(versions):
|
||||
return {'runtime_versions': versions}
|
||||
|
||||
|
||||
@registry.filter(name='absolutify')
|
||||
def absolute_links(text, url):
|
||||
tree = lxml_tree.fromstring(text)
|
||||
for anchor in tree.xpath('.//a'):
|
||||
href = anchor.get('href')
|
||||
if href:
|
||||
anchor.set('href', urljoin(url, href))
|
||||
return tree
|
||||
|
||||
|
||||
@registry.function(name='urljoin')
|
||||
def join(first, second, *rest):
|
||||
if not rest:
|
||||
return urljoin(first, second)
|
||||
return urljoin(urljoin(first, second), *rest)
|
||||
|
||||
|
||||
@registry.filter(name='ansi2html')
|
||||
def ansi2html(s):
|
||||
return mark_safe(Ansi2HTMLConverter(inline=True).convert(s, full=False))
|
53
judge/jinja2/registry.py
Normal file
53
judge/jinja2/registry.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
from django_jinja.library import render_with
|
||||
|
||||
globals = {}
|
||||
tests = {}
|
||||
filters = {}
|
||||
extensions = []
|
||||
|
||||
__all__ = ['render_with', 'function', 'filter', 'test', 'extension']
|
||||
|
||||
|
||||
def _store_function(store, func, name=None):
|
||||
if name is None:
|
||||
name = func.__name__
|
||||
store[name] = func
|
||||
|
||||
|
||||
def _register_function(store, name, func):
|
||||
if name is None and func is None:
|
||||
def decorator(func):
|
||||
_store_function(store, func)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
elif name is not None and func is None:
|
||||
if callable(name):
|
||||
_store_function(store, name)
|
||||
return name
|
||||
else:
|
||||
def decorator(func):
|
||||
_store_function(store, func, name)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
else:
|
||||
_store_function(store, func, name)
|
||||
return func
|
||||
|
||||
|
||||
def filter(name=None, func=None):
|
||||
return _register_function(filters, name, func)
|
||||
|
||||
|
||||
def function(name=None, func=None):
|
||||
return _register_function(globals, name, func)
|
||||
|
||||
|
||||
def test(name=None, func=None):
|
||||
return _register_function(tests, name, func)
|
||||
|
||||
|
||||
def extension(cls):
|
||||
extensions.append(cls)
|
||||
return cls
|
27
judge/jinja2/render.py
Normal file
27
judge/jinja2/render.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
from django.template import (Context, Template as DjangoTemplate, TemplateSyntaxError as DjangoTemplateSyntaxError,
|
||||
VariableDoesNotExist)
|
||||
|
||||
from . import registry
|
||||
|
||||
MAX_CACHE = 100
|
||||
django_cache = {}
|
||||
|
||||
|
||||
def compile_template(code):
|
||||
if code in django_cache:
|
||||
return django_cache[code]
|
||||
|
||||
# If this works for re.compile, it works for us too.
|
||||
if len(django_cache) > MAX_CACHE:
|
||||
django_cache.clear()
|
||||
|
||||
t = django_cache[code] = DjangoTemplate(code)
|
||||
return t
|
||||
|
||||
|
||||
@registry.function
|
||||
def render_django(template, **context):
|
||||
try:
|
||||
return compile_template(template).render(Context(context))
|
||||
except (VariableDoesNotExist, DjangoTemplateSyntaxError):
|
||||
return 'Error rendering: %r' % template
|
34
judge/jinja2/social.py
Normal file
34
judge/jinja2/social.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
from django.template.loader import get_template
|
||||
from django.utils.safestring import mark_safe
|
||||
from django_social_share.templatetags.social_share import post_to_facebook_url, post_to_gplus_url, post_to_twitter_url
|
||||
|
||||
from . import registry
|
||||
|
||||
SHARES = [
|
||||
('post_to_twitter', 'django_social_share/templatetags/post_to_twitter.html', post_to_twitter_url),
|
||||
('post_to_facebook', 'django_social_share/templatetags/post_to_facebook.html', post_to_facebook_url),
|
||||
('post_to_gplus', 'django_social_share/templatetags/post_to_gplus.html', post_to_gplus_url),
|
||||
# For future versions:
|
||||
# ('post_to_linkedin', 'django_social_share/templatetags/post_to_linkedin.html', post_to_linkedin_url),
|
||||
# ('post_to_reddit', 'django_social_share/templatetags/post_to_reddit.html', post_to_reddit_url),
|
||||
]
|
||||
|
||||
|
||||
def make_func(name, template, url_func):
|
||||
def func(request, *args):
|
||||
link_text = args[-1]
|
||||
context = {'request': request, 'link_text': mark_safe(link_text)}
|
||||
context = url_func(context, *args[:-1])
|
||||
return mark_safe(get_template(template).render(context))
|
||||
|
||||
func.__name__ = name
|
||||
registry.function(name, func)
|
||||
|
||||
|
||||
for name, template, url_func in SHARES:
|
||||
make_func(name, template, url_func)
|
||||
|
||||
|
||||
@registry.function
|
||||
def recaptcha_init(language=None):
|
||||
return get_template('snowpenguin/recaptcha/recaptcha_init.html').render({'explicit': False, 'language': language})
|
29
judge/jinja2/spaceless.py
Normal file
29
judge/jinja2/spaceless.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
import re
|
||||
|
||||
from jinja2 import Markup, nodes
|
||||
from jinja2.ext import Extension
|
||||
|
||||
|
||||
class SpacelessExtension(Extension):
|
||||
"""
|
||||
Removes whitespace between HTML tags at compile time, including tab and newline characters.
|
||||
It does not remove whitespace between jinja2 tags or variables. Neither does it remove whitespace between tags
|
||||
and their text content.
|
||||
Adapted from coffin:
|
||||
https://github.com/coffin/coffin/blob/master/coffin/template/defaulttags.py
|
||||
Adapted from StackOverflow:
|
||||
https://stackoverflow.com/a/23741298/1090657
|
||||
"""
|
||||
|
||||
tags = {'spaceless'}
|
||||
|
||||
def parse(self, parser):
|
||||
lineno = next(parser.stream).lineno
|
||||
body = parser.parse_statements(['name:endspaceless'], drop_needle=True)
|
||||
return nodes.CallBlock(
|
||||
self.call_method('_strip_spaces', [], [], None, None),
|
||||
[], [], body,
|
||||
).set_lineno(lineno)
|
||||
|
||||
def _strip_spaces(self, caller=None):
|
||||
return Markup(re.sub(r'>\s+<', '><', caller().unescape().strip()))
|
21
judge/jinja2/submission.py
Normal file
21
judge/jinja2/submission.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
from . import registry
|
||||
|
||||
|
||||
@registry.function
|
||||
def submission_layout(submission, profile_id, user, editable_problem_ids, completed_problem_ids):
|
||||
problem_id = submission.problem_id
|
||||
can_view = False
|
||||
|
||||
if problem_id in editable_problem_ids:
|
||||
can_view = True
|
||||
|
||||
if profile_id == submission.user_id:
|
||||
can_view = True
|
||||
|
||||
if user.has_perm('judge.change_submission'):
|
||||
can_view = True
|
||||
|
||||
if submission.problem_id in completed_problem_ids:
|
||||
can_view |= submission.problem.is_public or profile_id in submission.problem.tester_ids
|
||||
|
||||
return can_view
|
28
judge/jinja2/timedelta.py
Normal file
28
judge/jinja2/timedelta.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
import datetime
|
||||
|
||||
from judge.utils.timedelta import nice_repr
|
||||
from . import registry
|
||||
|
||||
|
||||
@registry.filter
|
||||
def timedelta(value, display='long'):
|
||||
if value is None:
|
||||
return value
|
||||
return nice_repr(value, display)
|
||||
|
||||
|
||||
@registry.filter
|
||||
def timestampdelta(value, display='long'):
|
||||
value = datetime.timedelta(seconds=value)
|
||||
return timedelta(value, display)
|
||||
|
||||
|
||||
@registry.filter
|
||||
def seconds(timedelta):
|
||||
return timedelta.total_seconds()
|
||||
|
||||
|
||||
@registry.filter
|
||||
@registry.render_with('time-remaining-fragment.html')
|
||||
def as_countdown(timedelta):
|
||||
return {'countdown': timedelta}
|
117
judge/judgeapi.py
Normal file
117
judge/judgeapi.py
Normal file
|
@ -0,0 +1,117 @@
|
|||
import json
|
||||
import logging
|
||||
import socket
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from judge import event_poster as event
|
||||
|
||||
logger = logging.getLogger('judge.judgeapi')
|
||||
size_pack = struct.Struct('!I')
|
||||
|
||||
|
||||
def _post_update_submission(submission, done=False):
|
||||
if submission.problem.is_public:
|
||||
event.post('submissions', {'type': 'done-submission' if done else 'update-submission',
|
||||
'id': submission.id,
|
||||
'contest': submission.contest_key,
|
||||
'user': submission.user_id, 'problem': submission.problem_id,
|
||||
'status': submission.status, 'language': submission.language.key})
|
||||
|
||||
|
||||
def judge_request(packet, reply=True):
|
||||
sock = socket.create_connection(settings.BRIDGED_DJANGO_CONNECT or
|
||||
settings.BRIDGED_DJANGO_ADDRESS[0])
|
||||
|
||||
output = json.dumps(packet, separators=(',', ':'))
|
||||
output = zlib.compress(output.encode('utf-8'))
|
||||
writer = sock.makefile('wb')
|
||||
writer.write(size_pack.pack(len(output)))
|
||||
writer.write(output)
|
||||
writer.close()
|
||||
|
||||
if reply:
|
||||
reader = sock.makefile('rb', -1)
|
||||
input = reader.read(size_pack.size)
|
||||
if not input:
|
||||
raise ValueError('Judge did not respond')
|
||||
length = size_pack.unpack(input)[0]
|
||||
input = reader.read(length)
|
||||
if not input:
|
||||
raise ValueError('Judge did not respond')
|
||||
reader.close()
|
||||
sock.close()
|
||||
|
||||
result = json.loads(zlib.decompress(input).decode('utf-8'))
|
||||
return result
|
||||
|
||||
|
||||
def judge_submission(submission, rejudge, batch_rejudge=False):
|
||||
from .models import ContestSubmission, Submission, SubmissionTestCase
|
||||
|
||||
CONTEST_SUBMISSION_PRIORITY = 0
|
||||
DEFAULT_PRIORITY = 1
|
||||
REJUDGE_PRIORITY = 2
|
||||
BATCH_REJUDGE_PRIORITY = 3
|
||||
|
||||
updates = {'time': None, 'memory': None, 'points': None, 'result': None, 'error': None,
|
||||
'was_rejudged': rejudge, 'status': 'QU'}
|
||||
try:
|
||||
# This is set proactively; it might get unset in judgecallback's on_grading_begin if the problem doesn't
|
||||
# actually have pretests stored on the judge.
|
||||
updates['is_pretested'] = ContestSubmission.objects.filter(submission=submission) \
|
||||
.values_list('problem__contest__run_pretests_only', flat=True)[0]
|
||||
except IndexError:
|
||||
priority = DEFAULT_PRIORITY
|
||||
else:
|
||||
priority = CONTEST_SUBMISSION_PRIORITY
|
||||
|
||||
# This should prevent double rejudge issues by permitting only the judging of
|
||||
# QU (which is the initial state) and D (which is the final state).
|
||||
# Even though the bridge will not queue a submission already being judged,
|
||||
# we will destroy the current state by deleting all SubmissionTestCase objects.
|
||||
# However, we can't drop the old state immediately before a submission is set for judging,
|
||||
# as that would prevent people from knowing a submission is being scheduled for rejudging.
|
||||
# It is worth noting that this mechanism does not prevent a new rejudge from being scheduled
|
||||
# while already queued, but that does not lead to data corruption.
|
||||
if not Submission.objects.filter(id=submission.id).exclude(status__in=('P', 'G')).update(**updates):
|
||||
return False
|
||||
|
||||
SubmissionTestCase.objects.filter(submission_id=submission.id).delete()
|
||||
|
||||
try:
|
||||
response = judge_request({
|
||||
'name': 'submission-request',
|
||||
'submission-id': submission.id,
|
||||
'problem-id': submission.problem.code,
|
||||
'language': submission.language.key,
|
||||
'source': submission.source.source,
|
||||
'priority': BATCH_REJUDGE_PRIORITY if batch_rejudge else REJUDGE_PRIORITY if rejudge else priority,
|
||||
})
|
||||
except BaseException:
|
||||
logger.exception('Failed to send request to judge')
|
||||
Submission.objects.filter(id=submission.id).update(status='IE')
|
||||
success = False
|
||||
else:
|
||||
if response['name'] != 'submission-received' or response['submission-id'] != submission.id:
|
||||
Submission.objects.filter(id=submission.id).update(status='IE')
|
||||
_post_update_submission(submission)
|
||||
success = True
|
||||
return success
|
||||
|
||||
|
||||
def disconnect_judge(judge, force=False):
|
||||
judge_request({'name': 'disconnect-judge', 'judge-id': judge.name, 'force': force}, reply=False)
|
||||
|
||||
|
||||
def abort_submission(submission):
|
||||
from .models import Submission
|
||||
response = judge_request({'name': 'terminate-submission', 'submission-id': submission.id})
|
||||
# This defaults to true, so that in the case the judgelist fails to remove the submission from the queue,
|
||||
# and returns a bad-request, the submission is not falsely shown as "Aborted" when it will still be judged.
|
||||
if not response.get('judge-aborted', True):
|
||||
Submission.objects.filter(id=submission.id).update(status='AB', result='AB')
|
||||
event.post('sub_%s' % Submission.get_id_secret(submission.id), {'type': 'aborted-submission'})
|
||||
_post_update_submission(submission, done=True)
|
59
judge/lxml_tree.py
Normal file
59
judge/lxml_tree.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
import logging
|
||||
|
||||
from django.utils.safestring import SafeData, mark_safe
|
||||
from lxml import html
|
||||
from lxml.etree import ParserError, XMLSyntaxError
|
||||
|
||||
logger = logging.getLogger('judge.html')
|
||||
|
||||
|
||||
class HTMLTreeString(SafeData):
|
||||
def __init__(self, str):
|
||||
try:
|
||||
self._tree = html.fromstring(str, parser=html.HTMLParser(recover=True))
|
||||
except (XMLSyntaxError, ParserError) as e:
|
||||
if str and (not isinstance(e, ParserError) or e.args[0] != 'Document is empty'):
|
||||
logger.exception('Failed to parse HTML string')
|
||||
self._tree = html.Element('div')
|
||||
|
||||
def __getattr__(self, attr):
|
||||
try:
|
||||
return getattr(self._tree, attr)
|
||||
except AttributeError:
|
||||
return getattr(str(self), attr)
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if key[0] == '_':
|
||||
super(HTMLTreeString, self).__setattr__(key, value)
|
||||
setattr(self._tree, key, value)
|
||||
|
||||
def __repr__(self):
|
||||
return '<HTMLTreeString %r>' % str(self)
|
||||
|
||||
def __str__(self):
|
||||
return mark_safe(html.tostring(self._tree, encoding='unicode'))
|
||||
|
||||
def __radd__(self, other):
|
||||
return other + str(self)
|
||||
|
||||
def __add__(self, other):
|
||||
return str(self) + other
|
||||
|
||||
def __getitem__(self, item):
|
||||
return str(self)[item]
|
||||
|
||||
def __getstate__(self):
|
||||
return str(self)
|
||||
|
||||
def __setstate__(self, state):
|
||||
self._tree = html.fromstring(state)
|
||||
|
||||
@property
|
||||
def tree(self):
|
||||
return self._tree
|
||||
|
||||
|
||||
def fromstring(str):
|
||||
if isinstance(str, HTMLTreeString):
|
||||
return str
|
||||
return HTMLTreeString(str)
|
0
judge/management/__init__.py
Normal file
0
judge/management/__init__.py
Normal file
0
judge/management/commands/__init__.py
Normal file
0
judge/management/commands/__init__.py
Normal file
17
judge/management/commands/addjudge.py
Normal file
17
judge/management/commands/addjudge.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
|
||||
from judge.models import Judge
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'create a judge'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('name', help='the name of the judge')
|
||||
parser.add_argument('auth_key', help='authentication key for the judge')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
judge = Judge()
|
||||
judge.name = options['name']
|
||||
judge.auth_key = options['auth_key']
|
||||
judge.save()
|
32
judge/management/commands/adduser.py
Normal file
32
judge/management/commands/adduser.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from judge.models import Language, Profile
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'creates a user'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('name', help='username')
|
||||
parser.add_argument('email', help='email, not necessary to be resolvable')
|
||||
parser.add_argument('password', help='password for the user')
|
||||
parser.add_argument('language', nargs='?', default=settings.DEFAULT_USER_LANGUAGE,
|
||||
help='default language ID for user')
|
||||
|
||||
parser.add_argument('--superuser', action='store_true', default=False,
|
||||
help="if specified, creates user with superuser privileges")
|
||||
parser.add_argument('--staff', action='store_true', default=False,
|
||||
help="if specified, creates user with staff privileges")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
usr = User(username=options['name'], email=options['email'], is_active=True)
|
||||
usr.set_password(options['password'])
|
||||
usr.is_superuser = options['superuser']
|
||||
usr.is_staff = options['staff']
|
||||
usr.save()
|
||||
|
||||
profile = Profile(user=usr)
|
||||
profile.language = Language.objects.get(key=options['language'])
|
||||
profile.save()
|
16
judge/management/commands/camo.py
Normal file
16
judge/management/commands/camo.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from judge.utils.camo import client as camo_client
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'obtains the camo url for the specified url'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('url', help='url to use camo on')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if camo_client is None:
|
||||
raise CommandError('Camo not available')
|
||||
|
||||
print(camo_client.image_url(options['url']))
|
27
judge/management/commands/copy_language.py
Normal file
27
judge/management/commands/copy_language.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from judge.models import Language, LanguageLimit
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'allows the problems that allow <source> to be submitted in <target>'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('source', help='language to copy from')
|
||||
parser.add_argument('target', help='language to copy to')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
try:
|
||||
source = Language.objects.get(key=options['source'])
|
||||
except Language.DoesNotExist:
|
||||
raise CommandError('Invalid source language: %s' % options['source'])
|
||||
|
||||
try:
|
||||
target = Language.objects.get(key=options['target'])
|
||||
except Language.DoesNotExist:
|
||||
raise CommandError('Invalid target language: %s' % options['target'])
|
||||
|
||||
target.problem_set.set(source.problem_set.all())
|
||||
LanguageLimit.objects.bulk_create(LanguageLimit(problem=ll.problem, language=target, time_limit=ll.time_limit,
|
||||
memory_limit=ll.memory_limit)
|
||||
for ll in LanguageLimit.objects.filter(language=source))
|
23
judge/management/commands/create_problem.py
Normal file
23
judge/management/commands/create_problem.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
|
||||
from judge.models import Problem, ProblemGroup, ProblemType
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'create an empty problem'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('code', help='problem code')
|
||||
parser.add_argument('name', help='problem title')
|
||||
parser.add_argument('body', help='problem description')
|
||||
parser.add_argument('type', help='problem type')
|
||||
parser.add_argument('group', help='problem group')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
problem = Problem()
|
||||
problem.code = options['code']
|
||||
problem.name = options['name']
|
||||
problem.description = options['body']
|
||||
problem.group = ProblemGroup.objects.get(name=options['group'])
|
||||
problem.types = [ProblemType.objects.get(name=options['type'])]
|
||||
problem.save()
|
130
judge/management/commands/makedmojmessages.py
Normal file
130
judge/management/commands/makedmojmessages.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
import glob
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import CommandError
|
||||
from django.core.management.commands.makemessages import Command as MakeMessagesCommand, check_programs
|
||||
|
||||
from judge.models import NavigationBar, ProblemType
|
||||
|
||||
|
||||
class Command(MakeMessagesCommand):
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--locale', '-l', default=[], dest='locale', action='append',
|
||||
help='Creates or updates the message files for the given locale(s) (e.g. pt_BR). '
|
||||
'Can be used multiple times.')
|
||||
parser.add_argument('--exclude', '-x', default=[], dest='exclude', action='append',
|
||||
help='Locales to exclude. Default is none. Can be used multiple times.')
|
||||
parser.add_argument('--all', '-a', action='store_true', dest='all',
|
||||
default=False, help='Updates the message files for all existing locales.')
|
||||
parser.add_argument('--no-wrap', action='store_true', dest='no_wrap',
|
||||
default=False, help="Don't break long message lines into several lines.")
|
||||
parser.add_argument('--no-obsolete', action='store_true', dest='no_obsolete',
|
||||
default=False, help="Remove obsolete message strings.")
|
||||
parser.add_argument('--keep-pot', action='store_true', dest='keep_pot',
|
||||
default=False, help="Keep .pot file after making messages. Useful when debugging.")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
locale = options.get('locale')
|
||||
exclude = options.get('exclude')
|
||||
self.domain = 'dmoj-user'
|
||||
self.verbosity = options.get('verbosity')
|
||||
process_all = options.get('all')
|
||||
|
||||
# Need to ensure that the i18n framework is enabled
|
||||
if settings.configured:
|
||||
settings.USE_I18N = True
|
||||
else:
|
||||
settings.configure(USE_I18N=True)
|
||||
|
||||
# Avoid messing with mutable class variables
|
||||
if options.get('no_wrap'):
|
||||
self.msgmerge_options = self.msgmerge_options[:] + ['--no-wrap']
|
||||
self.msguniq_options = self.msguniq_options[:] + ['--no-wrap']
|
||||
self.msgattrib_options = self.msgattrib_options[:] + ['--no-wrap']
|
||||
self.xgettext_options = self.xgettext_options[:] + ['--no-wrap']
|
||||
if options.get('no_location'):
|
||||
self.msgmerge_options = self.msgmerge_options[:] + ['--no-location']
|
||||
self.msguniq_options = self.msguniq_options[:] + ['--no-location']
|
||||
self.msgattrib_options = self.msgattrib_options[:] + ['--no-location']
|
||||
self.xgettext_options = self.xgettext_options[:] + ['--no-location']
|
||||
|
||||
self.no_obsolete = options.get('no_obsolete')
|
||||
self.keep_pot = options.get('keep_pot')
|
||||
|
||||
if locale is None and not exclude and not process_all:
|
||||
raise CommandError("Type '%s help %s' for usage information." % (
|
||||
os.path.basename(sys.argv[0]), sys.argv[1]))
|
||||
|
||||
self.invoked_for_django = False
|
||||
self.locale_paths = []
|
||||
self.default_locale_path = None
|
||||
if os.path.isdir(os.path.join('conf', 'locale')):
|
||||
self.locale_paths = [os.path.abspath(os.path.join('conf', 'locale'))]
|
||||
self.default_locale_path = self.locale_paths[0]
|
||||
self.invoked_for_django = True
|
||||
else:
|
||||
self.locale_paths.extend(settings.LOCALE_PATHS)
|
||||
# Allow to run makemessages inside an app dir
|
||||
if os.path.isdir('locale'):
|
||||
self.locale_paths.append(os.path.abspath('locale'))
|
||||
if self.locale_paths:
|
||||
self.default_locale_path = self.locale_paths[0]
|
||||
if not os.path.exists(self.default_locale_path):
|
||||
os.makedirs(self.default_locale_path)
|
||||
|
||||
# Build locale list
|
||||
locale_dirs = list(filter(os.path.isdir, glob.glob('%s/*' % self.default_locale_path)))
|
||||
all_locales = list(map(os.path.basename, locale_dirs))
|
||||
|
||||
# Account for excluded locales
|
||||
if process_all:
|
||||
locales = all_locales
|
||||
else:
|
||||
locales = locale or all_locales
|
||||
locales = set(locales) - set(exclude)
|
||||
|
||||
if locales:
|
||||
check_programs('msguniq', 'msgmerge', 'msgattrib')
|
||||
|
||||
check_programs('xgettext')
|
||||
|
||||
try:
|
||||
potfiles = self.build_potfiles()
|
||||
|
||||
# Build po files for each selected locale
|
||||
for locale in locales:
|
||||
if self.verbosity > 0:
|
||||
self.stdout.write("processing locale %s\n" % locale)
|
||||
for potfile in potfiles:
|
||||
self.write_po_file(potfile, locale)
|
||||
finally:
|
||||
if not self.keep_pot:
|
||||
self.remove_potfiles()
|
||||
|
||||
def find_files(self, root):
|
||||
return []
|
||||
|
||||
def _emit_message(self, potfile, string):
|
||||
potfile.write('''
|
||||
msgid "%s"
|
||||
msgstr ""
|
||||
''' % string.replace('\\', r'\\').replace('\t', '\\t').replace('\n', '\\n').replace('"', '\\"'))
|
||||
|
||||
def process_files(self, file_list):
|
||||
with io.open(os.path.join(self.default_locale_path, 'dmoj-user.pot'), 'w', encoding='utf-8') as potfile:
|
||||
if self.verbosity > 1:
|
||||
self.stdout.write('processing navigation bar')
|
||||
for label in NavigationBar.objects.values_list('label', flat=True):
|
||||
if self.verbosity > 2:
|
||||
self.stdout.write('processing navigation item label "%s"\n' % label)
|
||||
self._emit_message(potfile, label)
|
||||
|
||||
if self.verbosity > 1:
|
||||
self.stdout.write('processing problem types')
|
||||
for name in ProblemType.objects.values_list('full_name', flat=True):
|
||||
if self.verbosity > 2:
|
||||
self.stdout.write('processing problem type name "%s"\n' % name)
|
||||
self._emit_message(potfile, name)
|
58
judge/management/commands/render_pdf.py
Normal file
58
judge/management/commands/render_pdf.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.template.loader import get_template
|
||||
from django.utils import translation
|
||||
|
||||
from judge.models import Problem, ProblemTranslation
|
||||
from judge.pdf_problems import DefaultPdfMaker, PhantomJSPdfMaker, PuppeteerPDFRender, SlimerJSPdfMaker
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'renders a PDF file of a problem'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('code', help='code of problem to render')
|
||||
parser.add_argument('directory', nargs='?', help='directory to store temporaries')
|
||||
parser.add_argument('-l', '--language', default=settings.LANGUAGE_CODE,
|
||||
help='language to render PDF in')
|
||||
parser.add_argument('-p', '--phantomjs', action='store_const', const=PhantomJSPdfMaker,
|
||||
default=DefaultPdfMaker, dest='engine')
|
||||
parser.add_argument('-s', '--slimerjs', action='store_const', const=SlimerJSPdfMaker, dest='engine')
|
||||
parser.add_argument('-c', '--chrome', '--puppeteer', action='store_const',
|
||||
const=PuppeteerPDFRender, dest='engine')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
try:
|
||||
problem = Problem.objects.get(code=options['code'])
|
||||
except Problem.DoesNotExist:
|
||||
print('Bad problem code')
|
||||
return
|
||||
|
||||
try:
|
||||
trans = problem.translations.get(language=options['language'])
|
||||
except ProblemTranslation.DoesNotExist:
|
||||
trans = None
|
||||
|
||||
directory = options['directory']
|
||||
with options['engine'](directory, clean_up=directory is None) as maker, \
|
||||
translation.override(options['language']):
|
||||
problem_name = problem.name if trans is None else trans.name
|
||||
maker.html = get_template('problem/raw.html').render({
|
||||
'problem': problem,
|
||||
'problem_name': problem_name,
|
||||
'description': problem.description if trans is None else trans.description,
|
||||
'url': '',
|
||||
'math_engine': maker.math_engine,
|
||||
}).replace('"//', '"https://').replace("'//", "'https://")
|
||||
maker.title = problem_name
|
||||
for file in ('style.css', 'pygment-github.css', 'mathjax_config.js'):
|
||||
maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file))
|
||||
maker.make(debug=True)
|
||||
if not maker.success:
|
||||
print(maker.log, file=sys.stderr)
|
||||
elif directory is None:
|
||||
shutil.move(maker.pdffile, problem.code + '.pdf')
|
33
judge/management/commands/runbridged.py
Normal file
33
judge/management/commands/runbridged.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
import threading
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from judge.bridge import DjangoHandler, DjangoServer
|
||||
from judge.bridge import DjangoJudgeHandler, JudgeServer
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
judge_handler = DjangoJudgeHandler
|
||||
|
||||
try:
|
||||
import netaddr # noqa: F401, imported to see if it exists
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
proxies = settings.BRIDGED_JUDGE_PROXIES
|
||||
if proxies:
|
||||
judge_handler = judge_handler.with_proxy_set(proxies)
|
||||
|
||||
judge_server = JudgeServer(settings.BRIDGED_JUDGE_ADDRESS, judge_handler)
|
||||
django_server = DjangoServer(judge_server.judges, settings.BRIDGED_DJANGO_ADDRESS, DjangoHandler)
|
||||
|
||||
# TODO: Merge the two servers
|
||||
threading.Thread(target=django_server.serve_forever).start()
|
||||
try:
|
||||
judge_server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
django_server.stop()
|
53
judge/management/commands/runmoss.py
Normal file
53
judge/management/commands/runmoss.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from moss import *
|
||||
|
||||
from judge.models import Contest, ContestParticipation, Submission
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Checks for duplicate code using MOSS'
|
||||
|
||||
LANG_MAPPING = {
|
||||
('C++', MOSS_LANG_CC),
|
||||
('C', MOSS_LANG_C),
|
||||
('Java', MOSS_LANG_JAVA),
|
||||
('Python', MOSS_LANG_PYTHON),
|
||||
}
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('contest', help='the id of the contest')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
moss_api_key = settings.MOSS_API_KEY
|
||||
if moss_api_key is None:
|
||||
print('No MOSS API Key supplied')
|
||||
return
|
||||
contest = options['contest']
|
||||
|
||||
for problem in Contest.objects.get(key=contest).problems.order_by('code'):
|
||||
print('========== %s / %s ==========' % (problem.code, problem.name))
|
||||
for dmoj_lang, moss_lang in self.LANG_MAPPING:
|
||||
print("%s: " % dmoj_lang, end=' ')
|
||||
subs = Submission.objects.filter(
|
||||
contest__participation__virtual__in=(ContestParticipation.LIVE, ContestParticipation.SPECTATE),
|
||||
contest__participation__contest__key=contest,
|
||||
result='AC', problem__id=problem.id,
|
||||
language__common_name=dmoj_lang,
|
||||
).values_list('user__user__username', 'source__source')
|
||||
if not subs:
|
||||
print('<no submissions>')
|
||||
continue
|
||||
|
||||
moss_call = MOSS(moss_api_key, language=moss_lang, matching_file_limit=100,
|
||||
comment='%s - %s' % (contest, problem.code))
|
||||
|
||||
users = set()
|
||||
|
||||
for username, source in subs:
|
||||
if username in users:
|
||||
continue
|
||||
users.add(username)
|
||||
moss_call.add_file_from_memory(username, source.encode('utf-8'))
|
||||
|
||||
print('(%d): %s' % (subs.count(), moss_call.process()))
|
63
judge/middleware.py
Normal file
63
judge/middleware.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
from django.conf import settings
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import Resolver404, resolve, reverse
|
||||
from django.utils.http import urlquote
|
||||
|
||||
|
||||
class ShortCircuitMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
try:
|
||||
callback, args, kwargs = resolve(request.path_info, getattr(request, 'urlconf', None))
|
||||
except Resolver404:
|
||||
callback, args, kwargs = None, None, None
|
||||
|
||||
if getattr(callback, 'short_circuit_middleware', False):
|
||||
return callback(request, *args, **kwargs)
|
||||
return self.get_response(request)
|
||||
|
||||
|
||||
class DMOJLoginMiddleware(object):
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
if request.user.is_authenticated:
|
||||
profile = request.profile = request.user.profile
|
||||
login_2fa_path = reverse('login_2fa')
|
||||
if (profile.is_totp_enabled and not request.session.get('2fa_passed', False) and
|
||||
request.path not in (login_2fa_path, reverse('auth_logout')) and
|
||||
not request.path.startswith(settings.STATIC_URL)):
|
||||
return HttpResponseRedirect(login_2fa_path + '?next=' + urlquote(request.get_full_path()))
|
||||
else:
|
||||
request.profile = None
|
||||
return self.get_response(request)
|
||||
|
||||
|
||||
class DMOJImpersonationMiddleware(object):
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
if request.user.is_impersonate:
|
||||
request.no_profile_update = True
|
||||
request.profile = request.user.profile
|
||||
return self.get_response(request)
|
||||
|
||||
|
||||
class ContestMiddleware(object):
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
profile = request.profile
|
||||
if profile:
|
||||
profile.update_contest()
|
||||
request.participation = profile.current_contest
|
||||
request.in_contest = request.participation is not None
|
||||
else:
|
||||
request.in_contest = False
|
||||
request.participation = None
|
||||
return self.get_response(request)
|
761
judge/migrations/0001_squashed_0084_contest_formats.py
Normal file
761
judge/migrations/0001_squashed_0084_contest_formats.py
Normal file
File diff suppressed because one or more lines are too long
39
judge/migrations/0085_submission_source.py
Normal file
39
judge/migrations/0085_submission_source.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.15 on 2019-01-31 22:18
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('judge', '0084_contest_formats'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SubmissionSource',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('source', models.TextField(max_length=65536, verbose_name='source code')),
|
||||
('submission', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='link', to='judge.Submission', verbose_name='associated submission')),
|
||||
],
|
||||
),
|
||||
migrations.RunSQL(
|
||||
['''INSERT INTO judge_submissionsource (source, submission_id)
|
||||
SELECT source, id AS 'submission_id' FROM judge_submission;'''],
|
||||
['''UPDATE judge_submission sub
|
||||
INNER JOIN judge_submissionsource src ON sub.id = src.submission_id
|
||||
SET sub.source = src.source;'''],
|
||||
elidable=True,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='submission',
|
||||
name='source',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='submissionsource',
|
||||
name='submission',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='source', to='judge.Submission', verbose_name='associated submission'),
|
||||
),
|
||||
]
|
23
judge/migrations/0086_rating_ceiling.py
Normal file
23
judge/migrations/0086_rating_ceiling.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.20 on 2019-06-20 16:04
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('judge', '0085_submission_source'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='contest',
|
||||
name='rating_ceiling',
|
||||
field=models.IntegerField(blank=True, help_text='Rating ceiling for contest', null=True, verbose_name='rating ceiling'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contest',
|
||||
name='rating_floor',
|
||||
field=models.IntegerField(blank=True, help_text='Rating floor for contest', null=True, verbose_name='rating floor'),
|
||||
),
|
||||
]
|
24
judge/migrations/0087_problem_resource_limits.py
Normal file
24
judge/migrations/0087_problem_resource_limits.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.20 on 2019-06-09 14:44
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('judge', '0086_rating_ceiling'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='problem',
|
||||
name='memory_limit',
|
||||
field=models.PositiveIntegerField(help_text='The memory limit for this problem, in kilobytes (e.g. 64mb = 65536 kilobytes).', verbose_name='memory limit'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='problem',
|
||||
name='time_limit',
|
||||
field=models.FloatField(help_text='The time limit for this problem, in seconds. Fractional seconds (e.g. 1.5) are supported.', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(2000)], verbose_name='time limit'),
|
||||
),
|
||||
]
|
37
judge/migrations/0088_private_contests.py
Normal file
37
judge/migrations/0088_private_contests.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.20 on 2019-09-02 15:58
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('judge', '0087_problem_resource_limits'),
|
||||
]
|
||||
|
||||
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'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'},
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='contest',
|
||||
old_name='is_public',
|
||||
new_name='is_visible',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contest',
|
||||
name='is_organization_private',
|
||||
field=models.BooleanField(default=False, verbose_name='private to organizations'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contest',
|
||||
name='private_contestants',
|
||||
field=models.ManyToManyField(blank=True, help_text='If private, only these users may see the contest', related_name='_contest_private_contestants_+', to='judge.Profile', verbose_name='private contestants'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='contest',
|
||||
name='is_private',
|
||||
field=models.BooleanField(default=False, verbose_name='private to specific users'),
|
||||
),
|
||||
]
|
27
judge/migrations/0089_submission_to_contest.py
Normal file
27
judge/migrations/0089_submission_to_contest.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 2.1.12 on 2019-09-25 23:28
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('judge', '0088_private_contests'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='submission',
|
||||
name='contest_object',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='judge.Contest', verbose_name='contest'),
|
||||
),
|
||||
migrations.RunSQL('''
|
||||
UPDATE `judge_submission`
|
||||
INNER JOIN `judge_contestsubmission`
|
||||
ON (`judge_submission`.`id` = `judge_contestsubmission`.`submission_id`)
|
||||
INNER JOIN `judge_contestparticipation`
|
||||
ON (`judge_contestsubmission`.`participation_id` = `judge_contestparticipation`.`id`)
|
||||
SET `judge_submission`.`contest_object_id` = `judge_contestparticipation`.`contest_id`
|
||||
''', migrations.RunSQL.noop),
|
||||
]
|
19
judge/migrations/0090_fix_contest_visibility.py
Normal file
19
judge/migrations/0090_fix_contest_visibility.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('judge', '0089_submission_to_contest'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunSQL('''
|
||||
UPDATE `judge_contest`
|
||||
SET `judge_contest`.`is_private` = 0, `judge_contest`.`is_organization_private` = 1
|
||||
WHERE `judge_contest`.`is_private` = 1
|
||||
''', '''
|
||||
UPDATE `judge_contest`
|
||||
SET `judge_contest`.`is_private` = `judge_contest`.`is_organization_private`
|
||||
'''),
|
||||
]
|
23
judge/migrations/0091_compiler_message_ansi2html.py
Normal file
23
judge/migrations/0091_compiler_message_ansi2html.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import lxml.html as lh
|
||||
from django.db import migrations
|
||||
from lxml.html.clean import clean_html
|
||||
|
||||
|
||||
def strip_error_html(apps, schema_editor):
|
||||
Submission = apps.get_model('judge', 'Submission')
|
||||
for sub in Submission.objects.filter(error__isnull=False).iterator():
|
||||
if sub.error:
|
||||
sub.error = clean_html(lh.fromstring(sub.error)).text_content()
|
||||
sub.save(update_fields=['error'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('judge', '0090_fix_contest_visibility'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(strip_error_html, migrations.RunPython.noop, atomic=True),
|
||||
]
|
17
judge/migrations/0092_contest_clone.py
Normal file
17
judge/migrations/0092_contest_clone.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 2.1.12 on 2019-09-30 21:25
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('judge', '0091_compiler_message_ansi2html'),
|
||||
]
|
||||
|
||||
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'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'},
|
||||
),
|
||||
]
|
45
judge/migrations/0093_contest_moss.py
Normal file
45
judge/migrations/0093_contest_moss.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Generated by Django 2.1.12 on 2019-10-17 20:52
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('judge', '0092_contest_clone'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ContestMoss',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('language', models.CharField(max_length=10)),
|
||||
('submission_count', models.PositiveIntegerField(default=0)),
|
||||
('url', models.URLField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'contest moss result',
|
||||
'verbose_name_plural': 'contest moss results',
|
||||
},
|
||||
),
|
||||
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')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contestmoss',
|
||||
name='contest',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moss', to='judge.Contest', verbose_name='contest'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contestmoss',
|
||||
name='problem',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moss', to='judge.Problem', verbose_name='problem'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='contestmoss',
|
||||
unique_together={('contest', 'problem', 'language')},
|
||||
),
|
||||
]
|
14
judge/migrations/0094_submissiontestcase_unique_together.py
Normal file
14
judge/migrations/0094_submissiontestcase_unique_together.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('judge', '0093_contest_moss'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='submissiontestcase',
|
||||
unique_together={('submission', 'case')},
|
||||
),
|
||||
]
|
18
judge/migrations/0095_organization_logo_override.py
Normal file
18
judge/migrations/0095_organization_logo_override.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.1.12 on 2019-10-23 00:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('judge', '0094_submissiontestcase_unique_together'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='logo_override_image',
|
||||
field=models.CharField(blank=True, default='', help_text='This image will replace the default site logo for users viewing the organization.', max_length=150, verbose_name='Logo override image'),
|
||||
),
|
||||
]
|
21
judge/migrations/0096_profile_language_set_default.py
Normal file
21
judge/migrations/0096_profile_language_set_default.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 2.2.6 on 2019-11-08 01:27
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import judge.models.runtime
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('judge', '0095_organization_logo_override'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='language',
|
||||
field=models.ForeignKey(default=judge.models.runtime.Language.get_default_language_pk, on_delete=django.db.models.deletion.SET_DEFAULT, to='judge.Language', verbose_name='preferred language'),
|
||||
),
|
||||
]
|
23
judge/migrations/0097_participation_is_disqualified.py
Normal file
23
judge/migrations/0097_participation_is_disqualified.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 2.2.7 on 2019-11-10 02:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('judge', '0096_profile_language_set_default'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='contestparticipation',
|
||||
name='is_disqualified',
|
||||
field=models.BooleanField(default=False, help_text='Whether this participation is disqualified.', verbose_name='is disqualified'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='contestparticipation',
|
||||
name='virtual',
|
||||
field=models.IntegerField(default=0, help_text='0 means non-virtual, otherwise the n-th virtual participation.', verbose_name='virtual participation id'),
|
||||
),
|
||||
]
|
0
judge/migrations/__init__.py
Normal file
0
judge/migrations/__init__.py
Normal file
29
judge/models/__init__.py
Normal file
29
judge/models/__init__.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
from reversion import revisions
|
||||
|
||||
from judge.models.choices import ACE_THEMES, EFFECTIVE_MATH_ENGINES, MATH_ENGINES_CHOICES, TIMEZONE
|
||||
from judge.models.comment import Comment, CommentLock, CommentVote
|
||||
from judge.models.contest import Contest, ContestMoss, ContestParticipation, ContestProblem, ContestSubmission, \
|
||||
ContestTag, Rating
|
||||
from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex
|
||||
from judge.models.message import PrivateMessage, PrivateMessageThread
|
||||
from judge.models.problem import LanguageLimit, License, Problem, ProblemClarification, ProblemGroup, \
|
||||
ProblemTranslation, ProblemType, Solution, TranslatedProblemForeignKeyQuerySet, TranslatedProblemQuerySet
|
||||
from judge.models.problem_data import CHECKERS, ProblemData, ProblemTestCase, problem_data_storage, \
|
||||
problem_directory_file
|
||||
from judge.models.profile import Organization, OrganizationRequest, Profile
|
||||
from judge.models.runtime import Judge, Language, RuntimeVersion
|
||||
from judge.models.submission import SUBMISSION_RESULT, Submission, SubmissionSource, SubmissionTestCase
|
||||
from judge.models.ticket import Ticket, TicketMessage
|
||||
|
||||
revisions.register(Profile, exclude=['points', 'last_access', 'ip', 'rating'])
|
||||
revisions.register(Problem, follow=['language_limits'])
|
||||
revisions.register(LanguageLimit)
|
||||
revisions.register(Contest, follow=['contest_problems'])
|
||||
revisions.register(ContestProblem)
|
||||
revisions.register(Organization)
|
||||
revisions.register(BlogPost)
|
||||
revisions.register(Solution)
|
||||
revisions.register(Judge, fields=['name', 'created', 'auth_key', 'description'])
|
||||
revisions.register(Language)
|
||||
revisions.register(Comment, fields=['author', 'time', 'page', 'score', 'body', 'hidden', 'parent'])
|
||||
del revisions
|
66
judge/models/choices.py
Normal file
66
judge/models/choices.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
from collections import defaultdict
|
||||
from operator import itemgetter
|
||||
|
||||
import pytz
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
def make_timezones():
|
||||
data = defaultdict(list)
|
||||
for tz in pytz.all_timezones:
|
||||
if '/' in tz:
|
||||
area, loc = tz.split('/', 1)
|
||||
else:
|
||||
area, loc = 'Other', tz
|
||||
if not loc.startswith('GMT'):
|
||||
data[area].append((tz, loc))
|
||||
return sorted(data.items(), key=itemgetter(0))
|
||||
|
||||
|
||||
TIMEZONE = make_timezones()
|
||||
del make_timezones
|
||||
|
||||
ACE_THEMES = (
|
||||
('ambiance', 'Ambiance'),
|
||||
('chaos', 'Chaos'),
|
||||
('chrome', 'Chrome'),
|
||||
('clouds', 'Clouds'),
|
||||
('clouds_midnight', 'Clouds Midnight'),
|
||||
('cobalt', 'Cobalt'),
|
||||
('crimson_editor', 'Crimson Editor'),
|
||||
('dawn', 'Dawn'),
|
||||
('dreamweaver', 'Dreamweaver'),
|
||||
('eclipse', 'Eclipse'),
|
||||
('github', 'Github'),
|
||||
('idle_fingers', 'Idle Fingers'),
|
||||
('katzenmilch', 'Katzenmilch'),
|
||||
('kr_theme', 'KR Theme'),
|
||||
('kuroir', 'Kuroir'),
|
||||
('merbivore', 'Merbivore'),
|
||||
('merbivore_soft', 'Merbivore Soft'),
|
||||
('mono_industrial', 'Mono Industrial'),
|
||||
('monokai', 'Monokai'),
|
||||
('pastel_on_dark', 'Pastel on Dark'),
|
||||
('solarized_dark', 'Solarized Dark'),
|
||||
('solarized_light', 'Solarized Light'),
|
||||
('terminal', 'Terminal'),
|
||||
('textmate', 'Textmate'),
|
||||
('tomorrow', 'Tomorrow'),
|
||||
('tomorrow_night', 'Tomorrow Night'),
|
||||
('tomorrow_night_blue', 'Tomorrow Night Blue'),
|
||||
('tomorrow_night_bright', 'Tomorrow Night Bright'),
|
||||
('tomorrow_night_eighties', 'Tomorrow Night Eighties'),
|
||||
('twilight', 'Twilight'),
|
||||
('vibrant_ink', 'Vibrant Ink'),
|
||||
('xcode', 'XCode'),
|
||||
)
|
||||
|
||||
MATH_ENGINES_CHOICES = (
|
||||
('tex', _('Leave as LaTeX')),
|
||||
('svg', _('SVG with PNG fallback')),
|
||||
('mml', _('MathML only')),
|
||||
('jax', _('MathJax with SVG/PNG fallback')),
|
||||
('auto', _('Detect best quality')),
|
||||
)
|
||||
|
||||
EFFECTIVE_MATH_ENGINES = ('svg', 'mml', 'tex', 'jax')
|
185
judge/models/comment.py
Normal file
185
judge/models/comment.py
Normal file
|
@ -0,0 +1,185 @@
|
|||
import itertools
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import CASCADE
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from mptt.fields import TreeForeignKey
|
||||
from mptt.models import MPTTModel
|
||||
from reversion.models import Version
|
||||
|
||||
from judge.models.contest import Contest
|
||||
from judge.models.interface import BlogPost
|
||||
from judge.models.problem import Problem
|
||||
from judge.models.profile import Profile
|
||||
from judge.utils.cachedict import CacheDict
|
||||
|
||||
__all__ = ['Comment', 'CommentLock', 'CommentVote']
|
||||
|
||||
comment_validator = RegexValidator(r'^[pcs]:[a-z0-9]+$|^b:\d+$',
|
||||
_(r'Page code must be ^[pcs]:[a-z0-9]+$|^b:\d+$'))
|
||||
|
||||
|
||||
class VersionRelation(GenericRelation):
|
||||
def __init__(self):
|
||||
super(VersionRelation, self).__init__(Version, object_id_field='object_id')
|
||||
|
||||
def get_extra_restriction(self, where_class, alias, remote_alias):
|
||||
cond = super(VersionRelation, self).get_extra_restriction(where_class, alias, remote_alias)
|
||||
field = self.remote_field.model._meta.get_field('db')
|
||||
lookup = field.get_lookup('exact')(field.get_col(remote_alias), 'default')
|
||||
cond.add(lookup, 'AND')
|
||||
return cond
|
||||
|
||||
|
||||
class Comment(MPTTModel):
|
||||
author = models.ForeignKey(Profile, verbose_name=_('commenter'), on_delete=CASCADE)
|
||||
time = models.DateTimeField(verbose_name=_('posted time'), auto_now_add=True)
|
||||
page = models.CharField(max_length=30, verbose_name=_('associated page'), db_index=True,
|
||||
validators=[comment_validator])
|
||||
score = models.IntegerField(verbose_name=_('votes'), default=0)
|
||||
body = models.TextField(verbose_name=_('body of comment'), max_length=8192)
|
||||
hidden = models.BooleanField(verbose_name=_('hide the comment'), default=0)
|
||||
parent = TreeForeignKey('self', verbose_name=_('parent'), null=True, blank=True, related_name='replies',
|
||||
on_delete=CASCADE)
|
||||
versions = VersionRelation()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('comment')
|
||||
verbose_name_plural = _('comments')
|
||||
|
||||
class MPTTMeta:
|
||||
order_insertion_by = ['-time']
|
||||
|
||||
@classmethod
|
||||
def most_recent(cls, user, n, batch=None):
|
||||
queryset = cls.objects.filter(hidden=False).select_related('author__user') \
|
||||
.defer('author__about', 'body').order_by('-id')
|
||||
|
||||
problem_access = CacheDict(lambda code: Problem.objects.get(code=code).is_accessible_by(user))
|
||||
contest_access = CacheDict(lambda key: Contest.objects.get(key=key).is_accessible_by(user))
|
||||
blog_access = CacheDict(lambda id: BlogPost.objects.get(id=id).can_see(user))
|
||||
|
||||
if user.is_superuser:
|
||||
return queryset[:n]
|
||||
if batch is None:
|
||||
batch = 2 * n
|
||||
output = []
|
||||
for i in itertools.count(0):
|
||||
slice = queryset[i * batch:i * batch + batch]
|
||||
if not slice:
|
||||
break
|
||||
for comment in slice:
|
||||
if comment.page.startswith('p:') or comment.page.startswith('s:'):
|
||||
try:
|
||||
if problem_access[comment.page[2:]]:
|
||||
output.append(comment)
|
||||
except Problem.DoesNotExist:
|
||||
pass
|
||||
elif comment.page.startswith('c:'):
|
||||
try:
|
||||
if contest_access[comment.page[2:]]:
|
||||
output.append(comment)
|
||||
except Contest.DoesNotExist:
|
||||
pass
|
||||
elif comment.page.startswith('b:'):
|
||||
try:
|
||||
if blog_access[comment.page[2:]]:
|
||||
output.append(comment)
|
||||
except BlogPost.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
output.append(comment)
|
||||
if len(output) >= n:
|
||||
return output
|
||||
return output
|
||||
|
||||
@cached_property
|
||||
def link(self):
|
||||
try:
|
||||
link = None
|
||||
if self.page.startswith('p:'):
|
||||
link = reverse('problem_detail', args=(self.page[2:],))
|
||||
elif self.page.startswith('c:'):
|
||||
link = reverse('contest_view', args=(self.page[2:],))
|
||||
elif self.page.startswith('b:'):
|
||||
key = 'blog_slug:%s' % self.page[2:]
|
||||
slug = cache.get(key)
|
||||
if slug is None:
|
||||
try:
|
||||
slug = BlogPost.objects.get(id=self.page[2:]).slug
|
||||
except ObjectDoesNotExist:
|
||||
slug = ''
|
||||
cache.set(key, slug, 3600)
|
||||
link = reverse('blog_post', args=(self.page[2:], slug))
|
||||
elif self.page.startswith('s:'):
|
||||
link = reverse('problem_editorial', args=(self.page[2:],))
|
||||
except Exception:
|
||||
link = 'invalid'
|
||||
return link
|
||||
|
||||
@classmethod
|
||||
def get_page_title(cls, page):
|
||||
try:
|
||||
if page.startswith('p:'):
|
||||
return Problem.objects.values_list('name', flat=True).get(code=page[2:])
|
||||
elif page.startswith('c:'):
|
||||
return Contest.objects.values_list('name', flat=True).get(key=page[2:])
|
||||
elif page.startswith('b:'):
|
||||
return BlogPost.objects.values_list('title', flat=True).get(id=page[2:])
|
||||
elif page.startswith('s:'):
|
||||
return _('Editorial for %s') % Problem.objects.values_list('name', flat=True).get(code=page[2:])
|
||||
return '<unknown>'
|
||||
except ObjectDoesNotExist:
|
||||
return '<deleted>'
|
||||
|
||||
@cached_property
|
||||
def page_title(self):
|
||||
return self.get_page_title(self.page)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return '%s#comment-%d' % (self.link, self.id)
|
||||
|
||||
def __str__(self):
|
||||
return '%(page)s by %(user)s' % {'page': self.page, 'user': self.author.user.username}
|
||||
|
||||
# Only use this when queried with
|
||||
# .prefetch_related(Prefetch('votes', queryset=CommentVote.objects.filter(voter_id=profile_id)))
|
||||
# It's rather stupid to put a query specific property on the model, but the alternative requires
|
||||
# digging Django internals, and could not be guaranteed to work forever.
|
||||
# Hence it is left here for when the alternative breaks.
|
||||
# @property
|
||||
# def vote_score(self):
|
||||
# queryset = self.votes.all()
|
||||
# if not queryset:
|
||||
# return 0
|
||||
# return queryset[0].score
|
||||
|
||||
|
||||
class CommentVote(models.Model):
|
||||
voter = models.ForeignKey(Profile, related_name='voted_comments', on_delete=CASCADE)
|
||||
comment = models.ForeignKey(Comment, related_name='votes', on_delete=CASCADE)
|
||||
score = models.IntegerField()
|
||||
|
||||
class Meta:
|
||||
unique_together = ['voter', 'comment']
|
||||
verbose_name = _('comment vote')
|
||||
verbose_name_plural = _('comment votes')
|
||||
|
||||
|
||||
class CommentLock(models.Model):
|
||||
page = models.CharField(max_length=30, verbose_name=_('associated page'), db_index=True,
|
||||
validators=[comment_validator])
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('override_comment_lock', _('Override comment lock')),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.page)
|
412
judge/models/contest.py
Normal file
412
judge/models/contest.py
Normal file
|
@ -0,0 +1,412 @@
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinValueValidator, RegexValidator
|
||||
from django.db import models, transaction
|
||||
from django.db.models import CASCADE
|
||||
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 moss import MOSS_LANG_C, MOSS_LANG_CC, MOSS_LANG_JAVA, MOSS_LANG_PYTHON
|
||||
|
||||
from judge import contest_format
|
||||
from judge.models.problem import Problem
|
||||
from judge.models.profile import Organization, Profile
|
||||
from judge.models.submission import Submission
|
||||
from judge.ratings import rate_contest
|
||||
|
||||
__all__ = ['Contest', 'ContestTag', 'ContestParticipation', 'ContestProblem', 'ContestSubmission', 'Rating']
|
||||
|
||||
|
||||
class ContestTag(models.Model):
|
||||
color_validator = RegexValidator('^#(?:[A-Fa-f0-9]{3}){1,2}$', _('Invalid colour.'))
|
||||
|
||||
name = models.CharField(max_length=20, verbose_name=_('tag name'), unique=True,
|
||||
validators=[RegexValidator(r'^[a-z-]+$', message=_('Lowercase letters and hyphens only.'))])
|
||||
color = models.CharField(max_length=7, verbose_name=_('tag colour'), validators=[color_validator])
|
||||
description = models.TextField(verbose_name=_('tag description'), blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('contest_tag', args=[self.name])
|
||||
|
||||
@property
|
||||
def text_color(self, cache={}):
|
||||
if self.color not in cache:
|
||||
if len(self.color) == 4:
|
||||
r, g, b = [ord(bytes.fromhex(i * 2)) for i in self.color[1:]]
|
||||
else:
|
||||
r, g, b = [i for i in bytes.fromhex(self.color[1:])]
|
||||
cache[self.color] = '#000' if 299 * r + 587 * g + 144 * b > 140000 else '#fff'
|
||||
return cache[self.color]
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('contest tag')
|
||||
verbose_name_plural = _('contest tags')
|
||||
|
||||
|
||||
class Contest(models.Model):
|
||||
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+')
|
||||
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)
|
||||
end_time = models.DateTimeField(verbose_name=_('end time'), db_index=True)
|
||||
time_limit = models.DurationField(verbose_name=_('time limit'), blank=True, null=True)
|
||||
is_visible = models.BooleanField(verbose_name=_('publicly visible'), default=False,
|
||||
help_text=_('Should be set even for organization-private contests, where it '
|
||||
'determines whether the contest is visible to members of the '
|
||||
'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)
|
||||
use_clarifications = models.BooleanField(verbose_name=_('no comments'),
|
||||
help_text=_("Use clarification system instead of comments."),
|
||||
default=True)
|
||||
rating_floor = models.IntegerField(verbose_name=('rating floor'), help_text=_('Rating floor for contest'),
|
||||
null=True, blank=True)
|
||||
rating_ceiling = models.IntegerField(verbose_name=('rating ceiling'), help_text=_('Rating ceiling for contest'),
|
||||
null=True, blank=True)
|
||||
rate_all = models.BooleanField(verbose_name=_('rate all'), help_text=_('Rate all users who joined.'), default=False)
|
||||
rate_exclude = models.ManyToManyField(Profile, verbose_name=_('exclude from ratings'), blank=True,
|
||||
related_name='rate_exclude+')
|
||||
is_private = models.BooleanField(verbose_name=_('private to specific users'), default=False)
|
||||
private_contestants = models.ManyToManyField(Profile, blank=True, verbose_name=_('private contestants'),
|
||||
help_text=_('If private, only these users may see the contest'),
|
||||
related_name='private_contestants+')
|
||||
hide_problem_tags = models.BooleanField(verbose_name=_('hide problem tags'),
|
||||
help_text=_('Whether problem tags should be hidden by default.'),
|
||||
default=False)
|
||||
run_pretests_only = models.BooleanField(verbose_name=_('run pretests only'),
|
||||
help_text=_('Whether judges should grade pretests only, versus all '
|
||||
'testcases. Commonly set during a contest, then unset '
|
||||
'prior to rejudging user submissions when the contest ends.'),
|
||||
default=False)
|
||||
is_organization_private = models.BooleanField(verbose_name=_('private to organizations'), default=False)
|
||||
organizations = models.ManyToManyField(Organization, blank=True, verbose_name=_('organizations'),
|
||||
help_text=_('If private, only these organizations may see the contest'))
|
||||
og_image = models.CharField(verbose_name=_('OpenGraph image'), default='', max_length=150, blank=True)
|
||||
logo_override_image = models.CharField(verbose_name=_('Logo override image'), default='', max_length=150,
|
||||
blank=True,
|
||||
help_text=_('This image will replace the default site logo for users '
|
||||
'inside the contest.'))
|
||||
tags = models.ManyToManyField(ContestTag, verbose_name=_('contest tags'), blank=True, related_name='contests')
|
||||
user_count = models.IntegerField(verbose_name=_('the amount of live participants'), default=0)
|
||||
summary = models.TextField(blank=True, verbose_name=_('contest summary'),
|
||||
help_text=_('Plain-text, shown in meta description tag, e.g. for social media.'))
|
||||
access_code = models.CharField(verbose_name=_('access code'), blank=True, default='', max_length=255,
|
||||
help_text=_('An optional code to prompt contestants before they are allowed '
|
||||
'to join the contest. Leave it blank to disable.'))
|
||||
banned_users = models.ManyToManyField(Profile, verbose_name=_('personae non gratae'), blank=True,
|
||||
help_text=_('Bans the selected users from joining this contest.'))
|
||||
format_name = models.CharField(verbose_name=_('contest format'), default='default', max_length=32,
|
||||
choices=contest_format.choices(), help_text=_('The contest format module to use.'))
|
||||
format_config = JSONField(verbose_name=_('contest format configuration'), null=True, blank=True,
|
||||
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.'))
|
||||
|
||||
@cached_property
|
||||
def format_class(self):
|
||||
return contest_format.formats[self.format_name]
|
||||
|
||||
@cached_property
|
||||
def format(self):
|
||||
return self.format_class(self, self.format_config)
|
||||
|
||||
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)
|
||||
|
||||
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'):
|
||||
return True
|
||||
if user.is_authenticated and self.organizers.filter(id=user.profile.id).exists():
|
||||
return True
|
||||
if not self.is_visible:
|
||||
return False
|
||||
if self.start_time is not None and self.start_time > timezone.now():
|
||||
return False
|
||||
if self.hide_scoreboard and not self.is_in_contest(user) and self.end_time > timezone.now():
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def contest_window_length(self):
|
||||
return self.end_time - self.start_time
|
||||
|
||||
@cached_property
|
||||
def _now(self):
|
||||
# This ensures that all methods talk about the same now.
|
||||
return timezone.now()
|
||||
|
||||
@cached_property
|
||||
def can_join(self):
|
||||
return self.start_time <= self._now
|
||||
|
||||
@property
|
||||
def time_before_start(self):
|
||||
if self.start_time >= self._now:
|
||||
return self.start_time - self._now
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def time_before_end(self):
|
||||
if self.end_time >= self._now:
|
||||
return self.end_time - self._now
|
||||
else:
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def ended(self):
|
||||
return self.end_time < self._now
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('contest_view', args=(self.key,))
|
||||
|
||||
def update_user_count(self):
|
||||
self.user_count = self.users.filter(virtual=0).count()
|
||||
self.save()
|
||||
|
||||
update_user_count.alters_data = True
|
||||
|
||||
@cached_property
|
||||
def show_scoreboard(self):
|
||||
if self.hide_scoreboard and not self.ended:
|
||||
return False
|
||||
return True
|
||||
|
||||
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 the user can view all contests
|
||||
if user.has_perm('judge.see_private_contest'):
|
||||
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():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
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'):
|
||||
rate_contest(contest)
|
||||
|
||||
class Meta:
|
||||
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')),
|
||||
)
|
||||
verbose_name = _('contest')
|
||||
verbose_name_plural = _('contests')
|
||||
|
||||
|
||||
class ContestParticipation(models.Model):
|
||||
LIVE = 0
|
||||
SPECTATE = -1
|
||||
|
||||
contest = models.ForeignKey(Contest, verbose_name=_('associated contest'), related_name='users', on_delete=CASCADE)
|
||||
user = models.ForeignKey(Profile, verbose_name=_('user'), related_name='contest_history', on_delete=CASCADE)
|
||||
real_start = models.DateTimeField(verbose_name=_('start time'), default=timezone.now, db_column='start')
|
||||
score = models.IntegerField(verbose_name=_('score'), default=0, db_index=True)
|
||||
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.'))
|
||||
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)
|
||||
|
||||
def recompute_results(self):
|
||||
with transaction.atomic():
|
||||
self.contest.format.update_participation(self)
|
||||
if self.is_disqualified:
|
||||
self.score = -9999
|
||||
self.save(update_fields=['score'])
|
||||
recompute_results.alters_data = True
|
||||
|
||||
def set_disqualified(self, disqualified):
|
||||
self.is_disqualified = disqualified
|
||||
self.recompute_results()
|
||||
if self.contest.is_rated and self.contest.ratings.exists():
|
||||
self.contest.rate()
|
||||
if self.is_disqualified:
|
||||
if self.user.current_contest == self:
|
||||
self.user.remove_contest()
|
||||
self.contest.banned_users.add(self.user)
|
||||
else:
|
||||
self.contest.banned_users.remove(self.user)
|
||||
set_disqualified.alters_data = True
|
||||
|
||||
@property
|
||||
def live(self):
|
||||
return self.virtual == self.LIVE
|
||||
|
||||
@property
|
||||
def spectate(self):
|
||||
return self.virtual == self.SPECTATE
|
||||
|
||||
@cached_property
|
||||
def start(self):
|
||||
contest = self.contest
|
||||
return contest.start_time if contest.time_limit is None and (self.live or self.spectate) else self.real_start
|
||||
|
||||
@cached_property
|
||||
def end_time(self):
|
||||
contest = self.contest
|
||||
if self.spectate:
|
||||
return contest.end_time
|
||||
if self.virtual:
|
||||
if contest.time_limit:
|
||||
return self.real_start + contest.time_limit
|
||||
else:
|
||||
return self.real_start + (contest.end_time - contest.start_time)
|
||||
return contest.end_time if contest.time_limit is None else \
|
||||
min(self.real_start + contest.time_limit, contest.end_time)
|
||||
|
||||
@cached_property
|
||||
def _now(self):
|
||||
# This ensures that all methods talk about the same now.
|
||||
return timezone.now()
|
||||
|
||||
@property
|
||||
def ended(self):
|
||||
return self.end_time is not None and self.end_time < self._now
|
||||
|
||||
@property
|
||||
def time_remaining(self):
|
||||
end = self.end_time
|
||||
if end is not None and end >= self._now:
|
||||
return end - self._now
|
||||
|
||||
def __str__(self):
|
||||
if self.spectate:
|
||||
return gettext('%s spectating in %s') % (self.user.username, self.contest.name)
|
||||
if self.virtual:
|
||||
return gettext('%s in %s, v%d') % (self.user.username, self.contest.name, self.virtual)
|
||||
return gettext('%s in %s') % (self.user.username, self.contest.name)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('contest participation')
|
||||
verbose_name_plural = _('contest participations')
|
||||
|
||||
unique_together = ('contest', 'user', 'virtual')
|
||||
|
||||
|
||||
class ContestProblem(models.Model):
|
||||
problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='contests', on_delete=CASCADE)
|
||||
contest = models.ForeignKey(Contest, verbose_name=_('contest'), related_name='contest_problems', on_delete=CASCADE)
|
||||
points = models.IntegerField(verbose_name=_('points'))
|
||||
partial = models.BooleanField(default=True, verbose_name=_('partial'))
|
||||
is_pretested = models.BooleanField(default=False, verbose_name=_('is pretested'))
|
||||
order = models.PositiveIntegerField(db_index=True, verbose_name=_('order'))
|
||||
output_prefix_override = models.IntegerField(verbose_name=_('output prefix length override'), null=True, blank=True)
|
||||
max_submissions = models.IntegerField(help_text=_('Maximum number of submissions for this problem, '
|
||||
'or 0 for no limit.'), default=0,
|
||||
validators=[MinValueValidator(0, _('Why include a problem you '
|
||||
'can\'t submit to?'))])
|
||||
|
||||
class Meta:
|
||||
unique_together = ('problem', 'contest')
|
||||
verbose_name = _('contest problem')
|
||||
verbose_name_plural = _('contest problems')
|
||||
|
||||
|
||||
class ContestSubmission(models.Model):
|
||||
submission = models.OneToOneField(Submission, verbose_name=_('submission'),
|
||||
related_name='contest', on_delete=CASCADE)
|
||||
problem = models.ForeignKey(ContestProblem, verbose_name=_('problem'), on_delete=CASCADE,
|
||||
related_name='submissions', related_query_name='submission')
|
||||
participation = models.ForeignKey(ContestParticipation, verbose_name=_('participation'), on_delete=CASCADE,
|
||||
related_name='submissions', related_query_name='submission')
|
||||
points = models.FloatField(default=0.0, verbose_name=_('points'))
|
||||
is_pretest = models.BooleanField(verbose_name=_('is pretested'),
|
||||
help_text=_('Whether this submission was ran only on pretests.'),
|
||||
default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('contest submission')
|
||||
verbose_name_plural = _('contest submissions')
|
||||
|
||||
|
||||
class Rating(models.Model):
|
||||
user = models.ForeignKey(Profile, verbose_name=_('user'), related_name='ratings', on_delete=CASCADE)
|
||||
contest = models.ForeignKey(Contest, verbose_name=_('contest'), related_name='ratings', on_delete=CASCADE)
|
||||
participation = models.OneToOneField(ContestParticipation, verbose_name=_('participation'),
|
||||
related_name='rating', on_delete=CASCADE)
|
||||
rank = models.IntegerField(verbose_name=_('rank'))
|
||||
rating = models.IntegerField(verbose_name=_('rating'))
|
||||
volatility = models.IntegerField(verbose_name=_('volatility'))
|
||||
last_rated = models.DateTimeField(db_index=True, verbose_name=_('last rated'))
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'contest')
|
||||
verbose_name = _('contest rating')
|
||||
verbose_name_plural = _('contest ratings')
|
||||
|
||||
|
||||
class ContestMoss(models.Model):
|
||||
LANG_MAPPING = [
|
||||
('C', MOSS_LANG_C),
|
||||
('C++', MOSS_LANG_CC),
|
||||
('Java', MOSS_LANG_JAVA),
|
||||
('Python', MOSS_LANG_PYTHON),
|
||||
]
|
||||
|
||||
contest = models.ForeignKey(Contest, verbose_name=_('contest'), related_name='moss', on_delete=CASCADE)
|
||||
problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='moss', on_delete=CASCADE)
|
||||
language = models.CharField(max_length=10)
|
||||
submission_count = models.PositiveIntegerField(default=0)
|
||||
url = models.URLField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('contest', 'problem', 'language')
|
||||
verbose_name = _('contest moss result')
|
||||
verbose_name_plural = _('contest moss results')
|
94
judge/models/interface.py
Normal file
94
judge/models/interface.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
import re
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from mptt.fields import TreeForeignKey
|
||||
from mptt.models import MPTTModel
|
||||
|
||||
from judge.models.profile import Profile
|
||||
|
||||
__all__ = ['MiscConfig', 'validate_regex', 'NavigationBar', 'BlogPost']
|
||||
|
||||
|
||||
class MiscConfig(models.Model):
|
||||
key = models.CharField(max_length=30, db_index=True)
|
||||
value = models.TextField(blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.key
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('configuration item')
|
||||
verbose_name_plural = _('miscellaneous configuration')
|
||||
|
||||
|
||||
def validate_regex(regex):
|
||||
try:
|
||||
re.compile(regex, re.VERBOSE)
|
||||
except re.error as e:
|
||||
raise ValidationError('Invalid regex: %s' % e.message)
|
||||
|
||||
|
||||
class NavigationBar(MPTTModel):
|
||||
class Meta:
|
||||
verbose_name = _('navigation item')
|
||||
verbose_name_plural = _('navigation bar')
|
||||
|
||||
class MPTTMeta:
|
||||
order_insertion_by = ['order']
|
||||
|
||||
order = models.PositiveIntegerField(db_index=True, verbose_name=_('order'))
|
||||
key = models.CharField(max_length=10, unique=True, verbose_name=_('identifier'))
|
||||
label = models.CharField(max_length=20, verbose_name=_('label'))
|
||||
path = models.CharField(max_length=255, verbose_name=_('link path'))
|
||||
regex = models.TextField(verbose_name=_('highlight regex'), validators=[validate_regex])
|
||||
parent = TreeForeignKey('self', verbose_name=_('parent item'), null=True, blank=True,
|
||||
related_name='children', on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return self.label
|
||||
|
||||
@property
|
||||
def pattern(self, cache={}):
|
||||
# A cache with a bad policy is an alias for memory leak
|
||||
# Thankfully, there will never be too many regexes to cache.
|
||||
if self.regex in cache:
|
||||
return cache[self.regex]
|
||||
else:
|
||||
pattern = cache[self.regex] = re.compile(self.regex, re.VERBOSE)
|
||||
return pattern
|
||||
|
||||
|
||||
class BlogPost(models.Model):
|
||||
title = models.CharField(verbose_name=_('post title'), max_length=100)
|
||||
authors = models.ManyToManyField(Profile, verbose_name=_('authors'), blank=True)
|
||||
slug = models.SlugField(verbose_name=_('slug'))
|
||||
visible = models.BooleanField(verbose_name=_('public visibility'), default=False)
|
||||
sticky = models.BooleanField(verbose_name=_('sticky'), default=False)
|
||||
publish_on = models.DateTimeField(verbose_name=_('publish after'))
|
||||
content = models.TextField(verbose_name=_('post content'))
|
||||
summary = models.TextField(verbose_name=_('post summary'), blank=True)
|
||||
og_image = models.CharField(verbose_name=_('openGraph image'), default='', max_length=150, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('blog_post', args=(self.id, self.slug))
|
||||
|
||||
def can_see(self, user):
|
||||
if self.visible and self.publish_on <= timezone.now():
|
||||
return True
|
||||
if user.has_perm('judge.edit_all_post'):
|
||||
return True
|
||||
return user.is_authenticated and self.authors.filter(id=user.profile.id).exists()
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('edit_all_post', _('Edit all posts')),
|
||||
)
|
||||
verbose_name = _('blog post')
|
||||
verbose_name_plural = _('blog posts')
|
20
judge/models/message.py
Normal file
20
judge/models/message.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
from django.db import models
|
||||
from django.db.models import CASCADE
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from judge.models.profile import Profile
|
||||
|
||||
__all__ = ['PrivateMessage', 'PrivateMessageThread']
|
||||
|
||||
|
||||
class PrivateMessage(models.Model):
|
||||
title = models.CharField(verbose_name=_('message title'), max_length=50)
|
||||
content = models.TextField(verbose_name=_('message body'))
|
||||
sender = models.ForeignKey(Profile, verbose_name=_('sender'), related_name='sent_messages', on_delete=CASCADE)
|
||||
target = models.ForeignKey(Profile, verbose_name=_('target'), related_name='received_messages', on_delete=CASCADE)
|
||||
timestamp = models.DateTimeField(verbose_name=_('message timestamp'), auto_now_add=True)
|
||||
read = models.BooleanField(verbose_name=_('read'), default=False)
|
||||
|
||||
|
||||
class PrivateMessageThread(models.Model):
|
||||
messages = models.ManyToManyField(PrivateMessage, verbose_name=_('messages in the thread'))
|
413
judge/models/problem.py
Normal file
413
judge/models/problem.py
Normal file
|
@ -0,0 +1,413 @@
|
|||
from operator import attrgetter
|
||||
|
||||
from django.conf import settings
|
||||
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.expressions import RawSQL
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from judge.fulltext import SearchQuerySet
|
||||
from judge.models.profile import Organization, Profile
|
||||
from judge.models.runtime import Language
|
||||
from judge.user_translations import gettext as user_gettext
|
||||
from judge.utils.raw_sql import RawSQLColumn, unique_together_left_join
|
||||
|
||||
__all__ = ['ProblemGroup', 'ProblemType', 'Problem', 'ProblemTranslation', 'ProblemClarification',
|
||||
'License', 'Solution', 'TranslatedProblemQuerySet', 'TranslatedProblemForeignKeyQuerySet']
|
||||
|
||||
|
||||
class ProblemType(models.Model):
|
||||
name = models.CharField(max_length=20, verbose_name=_('problem category ID'), unique=True)
|
||||
full_name = models.CharField(max_length=100, verbose_name=_('problem category name'))
|
||||
|
||||
def __str__(self):
|
||||
return self.full_name
|
||||
|
||||
class Meta:
|
||||
ordering = ['full_name']
|
||||
verbose_name = _('problem type')
|
||||
verbose_name_plural = _('problem types')
|
||||
|
||||
|
||||
class ProblemGroup(models.Model):
|
||||
name = models.CharField(max_length=20, verbose_name=_('problem group ID'), unique=True)
|
||||
full_name = models.CharField(max_length=100, verbose_name=_('problem group name'))
|
||||
|
||||
def __str__(self):
|
||||
return self.full_name
|
||||
|
||||
class Meta:
|
||||
ordering = ['full_name']
|
||||
verbose_name = _('problem group')
|
||||
verbose_name_plural = _('problem groups')
|
||||
|
||||
|
||||
class License(models.Model):
|
||||
key = models.CharField(max_length=20, unique=True, verbose_name=_('key'),
|
||||
validators=[RegexValidator(r'^[-\w.]+$', r'License key must be ^[-\w.]+$')])
|
||||
link = models.CharField(max_length=256, verbose_name=_('link'))
|
||||
name = models.CharField(max_length=256, verbose_name=_('full name'))
|
||||
display = models.CharField(max_length=256, blank=True, verbose_name=_('short name'),
|
||||
help_text=_('Displayed on pages under this license'))
|
||||
icon = models.CharField(max_length=256, blank=True, verbose_name=_('icon'), help_text=_('URL to the icon'))
|
||||
text = models.TextField(verbose_name=_('license text'))
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('license', args=(self.key,))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('license')
|
||||
verbose_name_plural = _('licenses')
|
||||
|
||||
|
||||
class TranslatedProblemQuerySet(SearchQuerySet):
|
||||
def __init__(self, **kwargs):
|
||||
super(TranslatedProblemQuerySet, self).__init__(('code', 'name', 'description'), **kwargs)
|
||||
|
||||
def add_i18n_name(self, language):
|
||||
queryset = self._clone()
|
||||
alias = unique_together_left_join(queryset, ProblemTranslation, 'problem', 'language', language)
|
||||
return queryset.annotate(i18n_name=Coalesce(RawSQL('%s.name' % alias, ()), F('name'),
|
||||
output_field=models.CharField()))
|
||||
|
||||
|
||||
class TranslatedProblemForeignKeyQuerySet(QuerySet):
|
||||
def add_problem_i18n_name(self, key, language, name_field=None):
|
||||
queryset = self._clone() if name_field is None else self.annotate(_name=F(name_field))
|
||||
alias = unique_together_left_join(queryset, ProblemTranslation, 'problem', 'language', language,
|
||||
parent_model=Problem)
|
||||
# You must specify name_field if Problem is not yet joined into the QuerySet.
|
||||
kwargs = {key: Coalesce(RawSQL('%s.name' % alias, ()),
|
||||
F(name_field) if name_field else RawSQLColumn(Problem, 'name'),
|
||||
output_field=models.CharField())}
|
||||
return queryset.annotate(**kwargs)
|
||||
|
||||
|
||||
class Problem(models.Model):
|
||||
code = models.CharField(max_length=20, verbose_name=_('problem code'), unique=True,
|
||||
validators=[RegexValidator('^[a-z0-9]+$', _('Problem code must be ^[a-z0-9]+$'))],
|
||||
help_text=_('A short, unique code for the problem, '
|
||||
'used in the url after /problem/'))
|
||||
name = models.CharField(max_length=100, verbose_name=_('problem name'), db_index=True,
|
||||
help_text=_('The full name of the problem, '
|
||||
'as shown in the problem list.'))
|
||||
description = models.TextField(verbose_name=_('problem body'))
|
||||
authors = models.ManyToManyField(Profile, verbose_name=_('creators'), blank=True, related_name='authored_problems',
|
||||
help_text=_('These users will be able to edit the problem, '
|
||||
'and be listed as authors.'))
|
||||
curators = models.ManyToManyField(Profile, verbose_name=_('curators'), blank=True, related_name='curated_problems',
|
||||
help_text=_('These users will be able to edit the problem, '
|
||||
'but not be listed as authors.'))
|
||||
testers = models.ManyToManyField(Profile, verbose_name=_('testers'), blank=True, related_name='tested_problems',
|
||||
help_text=_(
|
||||
'These users will be able to view the private problem, but not edit it.'))
|
||||
types = models.ManyToManyField(ProblemType, verbose_name=_('problem types'),
|
||||
help_text=_('The type of problem, '
|
||||
"as shown on the problem's page."))
|
||||
group = models.ForeignKey(ProblemGroup, verbose_name=_('problem group'), on_delete=CASCADE,
|
||||
help_text=_('The group of problem, shown under Category in the problem list.'))
|
||||
time_limit = models.FloatField(verbose_name=_('time limit'),
|
||||
help_text=_('The time limit for this problem, in seconds. '
|
||||
'Fractional seconds (e.g. 1.5) are supported.'),
|
||||
validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_TIME_LIMIT),
|
||||
MaxValueValidator(settings.DMOJ_PROBLEM_MAX_TIME_LIMIT)])
|
||||
memory_limit = models.PositiveIntegerField(verbose_name=_('memory limit'),
|
||||
help_text=_('The memory limit for this problem, in kilobytes '
|
||||
'(e.g. 64mb = 65536 kilobytes).'),
|
||||
validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_MEMORY_LIMIT),
|
||||
MaxValueValidator(settings.DMOJ_PROBLEM_MAX_MEMORY_LIMIT)])
|
||||
short_circuit = models.BooleanField(default=False)
|
||||
points = models.FloatField(verbose_name=_('points'),
|
||||
help_text=_('Points awarded for problem completion. '
|
||||
"Points are displayed with a 'p' suffix if partial."),
|
||||
validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_PROBLEM_POINTS)])
|
||||
partial = models.BooleanField(verbose_name=_('allows partial points'), default=False)
|
||||
allowed_languages = models.ManyToManyField(Language, verbose_name=_('allowed languages'),
|
||||
help_text=_('List of allowed submission languages.'))
|
||||
is_public = models.BooleanField(verbose_name=_('publicly visible'), db_index=True, default=False)
|
||||
is_manually_managed = models.BooleanField(verbose_name=_('manually managed'), db_index=True, default=False,
|
||||
help_text=_('Whether judges should be allowed to manage data or not.'))
|
||||
date = models.DateTimeField(verbose_name=_('date of publishing'), null=True, blank=True, db_index=True,
|
||||
help_text=_("Doesn't have magic ability to auto-publish due to backward compatibility"))
|
||||
banned_users = models.ManyToManyField(Profile, verbose_name=_('personae non gratae'), blank=True,
|
||||
help_text=_('Bans the selected users from submitting to this problem.'))
|
||||
license = models.ForeignKey(License, null=True, blank=True, on_delete=SET_NULL,
|
||||
help_text=_('The license under which this problem is published.'))
|
||||
og_image = models.CharField(verbose_name=_('OpenGraph image'), max_length=150, blank=True)
|
||||
summary = models.TextField(blank=True, verbose_name=_('problem summary'),
|
||||
help_text=_('Plain-text, shown in meta description tag, e.g. for social media.'))
|
||||
user_count = models.IntegerField(verbose_name=_('number of users'), default=0,
|
||||
help_text=_('The number of users who solved the problem.'))
|
||||
ac_rate = models.FloatField(verbose_name=_('solve rate'), default=0)
|
||||
|
||||
objects = TranslatedProblemQuerySet.as_manager()
|
||||
tickets = GenericRelation('Ticket')
|
||||
|
||||
organizations = models.ManyToManyField(Organization, blank=True, verbose_name=_('organizations'),
|
||||
help_text=_('If private, only these organizations may see the problem.'))
|
||||
is_organization_private = models.BooleanField(verbose_name=_('private to organizations'), default=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Problem, self).__init__(*args, **kwargs)
|
||||
self._translated_name_cache = {}
|
||||
self._i18n_name = None
|
||||
self.__original_code = self.code
|
||||
|
||||
@cached_property
|
||||
def types_list(self):
|
||||
return list(map(user_gettext, map(attrgetter('full_name'), self.types.all())))
|
||||
|
||||
def languages_list(self):
|
||||
return self.allowed_languages.values_list('common_name', flat=True).distinct().order_by('common_name')
|
||||
|
||||
def is_editor(self, profile):
|
||||
return (self.authors.filter(id=profile.id) | self.curators.filter(id=profile.id)).exists()
|
||||
|
||||
def is_editable_by(self, user):
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
if user.has_perm('judge.edit_all_problem') or user.has_perm('judge.edit_public_problem') and self.is_public:
|
||||
return True
|
||||
return user.has_perm('judge.edit_own_problem') and self.is_editor(user.profile)
|
||||
|
||||
def is_accessible_by(self, user):
|
||||
# Problem is public.
|
||||
if self.is_public:
|
||||
# Problem is not private to an organization.
|
||||
if not self.is_organization_private:
|
||||
return True
|
||||
|
||||
# If the user can see all organization private problems.
|
||||
if user.has_perm('judge.see_organization_problem'):
|
||||
return True
|
||||
|
||||
# If the user is in the organization.
|
||||
if user.is_authenticated and \
|
||||
self.organizations.filter(id__in=user.profile.organizations.all()):
|
||||
return True
|
||||
|
||||
# If the user can view all problems.
|
||||
if user.has_perm('judge.see_private_problem'):
|
||||
return True
|
||||
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
|
||||
# If the user authored the problem or is a curator.
|
||||
if user.has_perm('judge.edit_own_problem') and self.is_editor(user.profile):
|
||||
return True
|
||||
|
||||
# If user is a tester.
|
||||
if self.testers.filter(id=user.profile.id).exists():
|
||||
return True
|
||||
|
||||
# If user is currently in a contest containing that problem.
|
||||
current = user.profile.current_contest_id
|
||||
if current is None:
|
||||
return False
|
||||
from judge.models import ContestProblem
|
||||
return ContestProblem.objects.filter(problem_id=self.id, contest__users__id=current).exists()
|
||||
|
||||
def is_subs_manageable_by(self, user):
|
||||
return user.is_staff and user.has_perm('judge.rejudge_submission') and self.is_editable_by(user)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('problem_detail', args=(self.code,))
|
||||
|
||||
@cached_property
|
||||
def author_ids(self):
|
||||
return self.authors.values_list('id', flat=True)
|
||||
|
||||
@cached_property
|
||||
def editor_ids(self):
|
||||
return self.author_ids | self.curators.values_list('id', flat=True)
|
||||
|
||||
@cached_property
|
||||
def tester_ids(self):
|
||||
return self.testers.values_list('id', flat=True)
|
||||
|
||||
@cached_property
|
||||
def usable_common_names(self):
|
||||
return set(self.usable_languages.values_list('common_name', flat=True))
|
||||
|
||||
@property
|
||||
def usable_languages(self):
|
||||
return self.allowed_languages.filter(judges__in=self.judges.filter(online=True)).distinct()
|
||||
|
||||
def translated_name(self, language):
|
||||
if language in self._translated_name_cache:
|
||||
return self._translated_name_cache[language]
|
||||
# Hits database despite prefetch_related.
|
||||
try:
|
||||
name = self.translations.filter(language=language).values_list('name', flat=True)[0]
|
||||
except IndexError:
|
||||
name = self.name
|
||||
self._translated_name_cache[language] = name
|
||||
return name
|
||||
|
||||
@property
|
||||
def i18n_name(self):
|
||||
if self._i18n_name is None:
|
||||
self._i18n_name = self._trans[0].name if self._trans else self.name
|
||||
return self._i18n_name
|
||||
|
||||
@i18n_name.setter
|
||||
def i18n_name(self, value):
|
||||
self._i18n_name = value
|
||||
|
||||
@property
|
||||
def clarifications(self):
|
||||
return ProblemClarification.objects.filter(problem=self)
|
||||
|
||||
def update_stats(self):
|
||||
self.user_count = self.submission_set.filter(points__gte=self.points, result='AC',
|
||||
user__is_unlisted=False).values('user').distinct().count()
|
||||
submissions = self.submission_set.count()
|
||||
if submissions:
|
||||
self.ac_rate = 100.0 * self.submission_set.filter(points__gte=self.points, result='AC',
|
||||
user__is_unlisted=False).count() / submissions
|
||||
else:
|
||||
self.ac_rate = 0
|
||||
self.save()
|
||||
|
||||
update_stats.alters_data = True
|
||||
|
||||
def _get_limits(self, key):
|
||||
global_limit = getattr(self, key)
|
||||
limits = {limit['language_id']: (limit['language__name'], limit[key])
|
||||
for limit in self.language_limits.values('language_id', 'language__name', key)
|
||||
if limit[key] != global_limit}
|
||||
limit_ids = set(limits.keys())
|
||||
common = []
|
||||
|
||||
for cn, ids in Language.get_common_name_map().items():
|
||||
if ids - limit_ids:
|
||||
continue
|
||||
limit = set(limits[id][1] for id in ids)
|
||||
if len(limit) == 1:
|
||||
limit = next(iter(limit))
|
||||
common.append((cn, limit))
|
||||
for id in ids:
|
||||
del limits[id]
|
||||
|
||||
limits = list(limits.values()) + common
|
||||
limits.sort()
|
||||
return limits
|
||||
|
||||
@property
|
||||
def language_time_limit(self):
|
||||
key = 'problem_tls:%d' % self.id
|
||||
result = cache.get(key)
|
||||
if result is not None:
|
||||
return result
|
||||
result = self._get_limits('time_limit')
|
||||
cache.set(key, result)
|
||||
return result
|
||||
|
||||
@property
|
||||
def language_memory_limit(self):
|
||||
key = 'problem_mls:%d' % self.id
|
||||
result = cache.get(key)
|
||||
if result is not None:
|
||||
return result
|
||||
result = self._get_limits('memory_limit')
|
||||
cache.set(key, result)
|
||||
return result
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super(Problem, self).save(*args, **kwargs)
|
||||
if self.code != self.__original_code:
|
||||
try:
|
||||
problem_data = self.data_files
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
problem_data._update_code(self.__original_code, self.code)
|
||||
|
||||
save.alters_data = True
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('see_private_problem', 'See hidden problems'),
|
||||
('edit_own_problem', 'Edit own problems'),
|
||||
('edit_all_problem', 'Edit all problems'),
|
||||
('edit_public_problem', 'Edit all public problems'),
|
||||
('clone_problem', 'Clone problem'),
|
||||
('change_public_visibility', 'Change is_public field'),
|
||||
('change_manually_managed', 'Change is_manually_managed field'),
|
||||
('see_organization_problem', 'See organization-private problems'),
|
||||
)
|
||||
verbose_name = _('problem')
|
||||
verbose_name_plural = _('problems')
|
||||
|
||||
|
||||
class ProblemTranslation(models.Model):
|
||||
problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='translations', on_delete=CASCADE)
|
||||
language = models.CharField(verbose_name=_('language'), max_length=7, choices=settings.LANGUAGES)
|
||||
name = models.CharField(verbose_name=_('translated name'), max_length=100, db_index=True)
|
||||
description = models.TextField(verbose_name=_('translated description'))
|
||||
|
||||
class Meta:
|
||||
unique_together = ('problem', 'language')
|
||||
verbose_name = _('problem translation')
|
||||
verbose_name_plural = _('problem translations')
|
||||
|
||||
|
||||
class ProblemClarification(models.Model):
|
||||
problem = models.ForeignKey(Problem, verbose_name=_('clarified problem'), on_delete=CASCADE)
|
||||
description = models.TextField(verbose_name=_('clarification body'))
|
||||
date = models.DateTimeField(verbose_name=_('clarification timestamp'), auto_now_add=True)
|
||||
|
||||
|
||||
class LanguageLimit(models.Model):
|
||||
problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='language_limits', on_delete=CASCADE)
|
||||
language = models.ForeignKey(Language, verbose_name=_('language'), on_delete=CASCADE)
|
||||
time_limit = models.FloatField(verbose_name=_('time limit'),
|
||||
validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_TIME_LIMIT),
|
||||
MaxValueValidator(settings.DMOJ_PROBLEM_MAX_TIME_LIMIT)])
|
||||
memory_limit = models.IntegerField(verbose_name=_('memory limit'),
|
||||
validators=[MinValueValidator(settings.DMOJ_PROBLEM_MIN_MEMORY_LIMIT),
|
||||
MaxValueValidator(settings.DMOJ_PROBLEM_MAX_MEMORY_LIMIT)])
|
||||
|
||||
class Meta:
|
||||
unique_together = ('problem', 'language')
|
||||
verbose_name = _('language-specific resource limit')
|
||||
verbose_name_plural = _('language-specific resource limits')
|
||||
|
||||
|
||||
class Solution(models.Model):
|
||||
problem = models.OneToOneField(Problem, on_delete=SET_NULL, verbose_name=_('associated problem'),
|
||||
null=True, blank=True, related_name='solution')
|
||||
is_public = models.BooleanField(verbose_name=_('public visibility'), default=False)
|
||||
publish_on = models.DateTimeField(verbose_name=_('publish date'))
|
||||
authors = models.ManyToManyField(Profile, verbose_name=_('authors'), blank=True)
|
||||
content = models.TextField(verbose_name=_('editorial content'))
|
||||
|
||||
def get_absolute_url(self):
|
||||
problem = self.problem
|
||||
if problem is None:
|
||||
return reverse('home')
|
||||
else:
|
||||
return reverse('problem_editorial', args=[problem.code])
|
||||
|
||||
def __str__(self):
|
||||
return _('Editorial for %s') % self.problem.name
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('see_private_solution', 'See hidden solutions'),
|
||||
)
|
||||
verbose_name = _('solution')
|
||||
verbose_name_plural = _('solutions')
|
94
judge/models/problem_data.py
Normal file
94
judge/models/problem_data.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
import errno
|
||||
import os
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from judge.utils.problem_data import ProblemDataStorage
|
||||
|
||||
__all__ = ['problem_data_storage', 'problem_directory_file', 'ProblemData', 'ProblemTestCase', 'CHECKERS']
|
||||
|
||||
problem_data_storage = ProblemDataStorage()
|
||||
|
||||
|
||||
def _problem_directory_file(code, filename):
|
||||
return os.path.join(code, os.path.basename(filename))
|
||||
|
||||
|
||||
def problem_directory_file(data, filename):
|
||||
return _problem_directory_file(data.problem.code, filename)
|
||||
|
||||
|
||||
CHECKERS = (
|
||||
('standard', _('Standard')),
|
||||
('floats', _('Floats')),
|
||||
('floatsabs', _('Floats (absolute)')),
|
||||
('floatsrel', _('Floats (relative)')),
|
||||
('rstripped', _('Non-trailing spaces')),
|
||||
('sorted', _('Unordered')),
|
||||
('identical', _('Byte identical')),
|
||||
('linecount', _('Line-by-line')),
|
||||
)
|
||||
|
||||
|
||||
class ProblemData(models.Model):
|
||||
problem = models.OneToOneField('Problem', verbose_name=_('problem'), related_name='data_files',
|
||||
on_delete=models.CASCADE)
|
||||
zipfile = models.FileField(verbose_name=_('data zip file'), storage=problem_data_storage, null=True, blank=True,
|
||||
upload_to=problem_directory_file)
|
||||
generator = models.FileField(verbose_name=_('generator file'), storage=problem_data_storage, null=True, blank=True,
|
||||
upload_to=problem_directory_file)
|
||||
output_prefix = models.IntegerField(verbose_name=_('output prefix length'), blank=True, null=True)
|
||||
output_limit = models.IntegerField(verbose_name=_('output limit length'), blank=True, null=True)
|
||||
feedback = models.TextField(verbose_name=_('init.yml generation feedback'), blank=True)
|
||||
checker = models.CharField(max_length=10, verbose_name=_('checker'), choices=CHECKERS, blank=True)
|
||||
checker_args = models.TextField(verbose_name=_('checker arguments'), blank=True,
|
||||
help_text=_('checker arguments as a JSON object'))
|
||||
|
||||
__original_zipfile = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ProblemData, self).__init__(*args, **kwargs)
|
||||
self.__original_zipfile = self.zipfile
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.zipfile != self.__original_zipfile:
|
||||
self.__original_zipfile.delete(save=False)
|
||||
return super(ProblemData, self).save(*args, **kwargs)
|
||||
|
||||
def has_yml(self):
|
||||
return problem_data_storage.exists('%s/init.yml' % self.problem.code)
|
||||
|
||||
def _update_code(self, original, new):
|
||||
try:
|
||||
problem_data_storage.rename(original, new)
|
||||
except OSError as e:
|
||||
if e.errno != errno.ENOENT:
|
||||
raise
|
||||
if self.zipfile:
|
||||
self.zipfile.name = _problem_directory_file(new, self.zipfile.name)
|
||||
if self.generator:
|
||||
self.generator.name = _problem_directory_file(new, self.generator.name)
|
||||
self.save()
|
||||
_update_code.alters_data = True
|
||||
|
||||
|
||||
class ProblemTestCase(models.Model):
|
||||
dataset = models.ForeignKey('Problem', verbose_name=_('problem data set'), related_name='cases',
|
||||
on_delete=models.CASCADE)
|
||||
order = models.IntegerField(verbose_name=_('case position'))
|
||||
type = models.CharField(max_length=1, verbose_name=_('case type'),
|
||||
choices=(('C', _('Normal case')),
|
||||
('S', _('Batch start')),
|
||||
('E', _('Batch end'))),
|
||||
default='C')
|
||||
input_file = models.CharField(max_length=100, verbose_name=_('input file name'), blank=True)
|
||||
output_file = models.CharField(max_length=100, verbose_name=_('output file name'), blank=True)
|
||||
generator_args = models.TextField(verbose_name=_('generator arguments'), blank=True)
|
||||
points = models.IntegerField(verbose_name=_('point value'), blank=True, null=True)
|
||||
is_pretest = models.BooleanField(verbose_name=_('case is pretest?'))
|
||||
output_prefix = models.IntegerField(verbose_name=_('output prefix length'), blank=True, null=True)
|
||||
output_limit = models.IntegerField(verbose_name=_('output limit length'), blank=True, null=True)
|
||||
checker = models.CharField(max_length=10, verbose_name=_('checker'), choices=CHECKERS, blank=True)
|
||||
checker_args = models.TextField(verbose_name=_('checker arguments'), blank=True,
|
||||
help_text=_('checker arguments as a JSON object'))
|
204
judge/models/profile.py
Normal file
204
judge/models/profile.py
Normal file
|
@ -0,0 +1,204 @@
|
|||
from operator import mul
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Max
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from fernet_fields import EncryptedCharField
|
||||
from sortedm2m.fields import SortedManyToManyField
|
||||
|
||||
from judge.models.choices import ACE_THEMES, MATH_ENGINES_CHOICES, TIMEZONE
|
||||
from judge.models.runtime import Language
|
||||
from judge.ratings import rating_class
|
||||
|
||||
__all__ = ['Organization', 'Profile', 'OrganizationRequest']
|
||||
|
||||
|
||||
class EncryptedNullCharField(EncryptedCharField):
|
||||
def get_prep_value(self, value):
|
||||
if not value:
|
||||
return None
|
||||
return super(EncryptedNullCharField, self).get_prep_value(value)
|
||||
|
||||
|
||||
class Organization(models.Model):
|
||||
name = models.CharField(max_length=128, verbose_name=_('organization title'))
|
||||
slug = models.SlugField(max_length=128, verbose_name=_('organization slug'),
|
||||
help_text=_('Organization name shown in URL'))
|
||||
short_name = models.CharField(max_length=20, verbose_name=_('short name'),
|
||||
help_text=_('Displayed beside user name during contests'))
|
||||
about = models.TextField(verbose_name=_('organization description'))
|
||||
registrant = models.ForeignKey('Profile', verbose_name=_('registrant'), on_delete=models.CASCADE,
|
||||
related_name='registrant+', help_text=_('User who registered this organization'))
|
||||
admins = models.ManyToManyField('Profile', verbose_name=_('administrators'), related_name='admin_of',
|
||||
help_text=_('Those who can edit this organization'))
|
||||
creation_date = models.DateTimeField(verbose_name=_('creation date'), auto_now_add=True)
|
||||
is_open = models.BooleanField(verbose_name=_('is open organization?'),
|
||||
help_text=_('Allow joining organization'), default=True)
|
||||
slots = models.IntegerField(verbose_name=_('maximum size'), null=True, blank=True,
|
||||
help_text=_('Maximum amount of users in this organization, '
|
||||
'only applicable to private organizations'))
|
||||
access_code = models.CharField(max_length=7, help_text=_('Student access code'),
|
||||
verbose_name=_('access code'), null=True, blank=True)
|
||||
logo_override_image = models.CharField(verbose_name=_('Logo override image'), default='', max_length=150,
|
||||
blank=True,
|
||||
help_text=_('This image will replace the default site logo for users '
|
||||
'viewing the organization.'))
|
||||
|
||||
def __contains__(self, item):
|
||||
if isinstance(item, int):
|
||||
return self.members.filter(id=item).exists()
|
||||
elif isinstance(item, Profile):
|
||||
return self.members.filter(id=item.id).exists()
|
||||
else:
|
||||
raise TypeError('Organization membership test must be Profile or primany key')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('organization_home', args=(self.id, self.slug))
|
||||
|
||||
def get_users_url(self):
|
||||
return reverse('organization_users', args=(self.id, self.slug))
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
permissions = (
|
||||
('organization_admin', 'Administer organizations'),
|
||||
('edit_all_organization', 'Edit all organizations'),
|
||||
)
|
||||
verbose_name = _('organization')
|
||||
verbose_name_plural = _('organizations')
|
||||
|
||||
|
||||
class Profile(models.Model):
|
||||
user = models.OneToOneField(User, verbose_name=_('user associated'), on_delete=models.CASCADE)
|
||||
about = models.TextField(verbose_name=_('self-description'), null=True, blank=True)
|
||||
timezone = models.CharField(max_length=50, verbose_name=_('location'), choices=TIMEZONE,
|
||||
default=settings.DEFAULT_USER_TIME_ZONE)
|
||||
language = models.ForeignKey('Language', verbose_name=_('preferred language'), on_delete=models.SET_DEFAULT,
|
||||
default=Language.get_default_language_pk)
|
||||
points = models.FloatField(default=0, db_index=True)
|
||||
performance_points = models.FloatField(default=0, db_index=True)
|
||||
problem_count = models.IntegerField(default=0, db_index=True)
|
||||
ace_theme = models.CharField(max_length=30, choices=ACE_THEMES, default='github')
|
||||
last_access = models.DateTimeField(verbose_name=_('last access time'), default=now)
|
||||
ip = models.GenericIPAddressField(verbose_name=_('last IP'), blank=True, null=True)
|
||||
organizations = SortedManyToManyField(Organization, verbose_name=_('organization'), blank=True,
|
||||
related_name='members', related_query_name='member')
|
||||
display_rank = models.CharField(max_length=10, default='user', verbose_name=_('display rank'),
|
||||
choices=(('user', 'Normal User'), ('setter', 'Problem Setter'), ('admin', 'Admin')))
|
||||
mute = models.BooleanField(verbose_name=_('comment mute'), help_text=_('Some users are at their best when silent.'),
|
||||
default=False)
|
||||
is_unlisted = models.BooleanField(verbose_name=_('unlisted user'), help_text=_('User will not be ranked.'),
|
||||
default=False)
|
||||
rating = models.IntegerField(null=True, default=None)
|
||||
user_script = models.TextField(verbose_name=_('user script'), default='', blank=True, max_length=65536,
|
||||
help_text=_('User-defined JavaScript for site customization.'))
|
||||
current_contest = models.OneToOneField('ContestParticipation', verbose_name=_('current contest'),
|
||||
null=True, blank=True, related_name='+', on_delete=models.SET_NULL)
|
||||
math_engine = models.CharField(verbose_name=_('math engine'), choices=MATH_ENGINES_CHOICES, max_length=4,
|
||||
default=settings.MATHOID_DEFAULT_TYPE,
|
||||
help_text=_('the rendering engine used to render math'))
|
||||
is_totp_enabled = models.BooleanField(verbose_name=_('2FA enabled'), default=False,
|
||||
help_text=_('check to enable TOTP-based two factor authentication'))
|
||||
totp_key = EncryptedNullCharField(max_length=32, null=True, blank=True, verbose_name=_('TOTP key'),
|
||||
help_text=_('32 character base32-encoded key for TOTP'),
|
||||
validators=[RegexValidator('^$|^[A-Z2-7]{32}$',
|
||||
_('TOTP key must be empty or base32'))])
|
||||
notes = models.TextField(verbose_name=_('internal notes'), null=True, blank=True,
|
||||
help_text=_('Notes for administrators regarding this user.'))
|
||||
|
||||
@cached_property
|
||||
def organization(self):
|
||||
# We do this to take advantage of prefetch_related
|
||||
orgs = self.organizations.all()
|
||||
return orgs[0] if orgs else None
|
||||
|
||||
@cached_property
|
||||
def username(self):
|
||||
return self.user.username
|
||||
|
||||
_pp_table = [pow(settings.DMOJ_PP_STEP, i) for i in range(settings.DMOJ_PP_ENTRIES)]
|
||||
|
||||
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()
|
||||
bonus_function = settings.DMOJ_PP_BONUS_FUNCTION
|
||||
points = sum(data)
|
||||
problems = len(data)
|
||||
entries = min(len(data), len(table))
|
||||
pp = sum(map(mul, table[:entries], data[:entries])) + bonus_function(extradata)
|
||||
if self.points != points or problems != self.problem_count or self.performance_points != pp:
|
||||
self.points = points
|
||||
self.problem_count = problems
|
||||
self.performance_points = pp
|
||||
self.save(update_fields=['points', 'problem_count', 'performance_points'])
|
||||
return points
|
||||
|
||||
calculate_points.alters_data = True
|
||||
|
||||
def remove_contest(self):
|
||||
self.current_contest = None
|
||||
self.save()
|
||||
|
||||
remove_contest.alters_data = True
|
||||
|
||||
def update_contest(self):
|
||||
contest = self.current_contest
|
||||
if contest is not None and (contest.ended or not contest.contest.is_accessible_by(self.user)):
|
||||
self.remove_contest()
|
||||
|
||||
update_contest.alters_data = True
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('user_page', args=(self.user.username,))
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
|
||||
@classmethod
|
||||
def get_user_css_class(cls, display_rank, rating, rating_colors=settings.DMOJ_RATING_COLORS):
|
||||
if rating_colors:
|
||||
return 'rating %s %s' % (rating_class(rating) if rating is not None else 'rate-none', display_rank)
|
||||
return display_rank
|
||||
|
||||
@cached_property
|
||||
def css_class(self):
|
||||
return self.get_user_css_class(self.display_rank, self.rating)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('test_site', 'Shows in-progress development stuff'),
|
||||
('totp', 'Edit TOTP settings'),
|
||||
)
|
||||
verbose_name = _('user profile')
|
||||
verbose_name_plural = _('user profiles')
|
||||
|
||||
|
||||
class OrganizationRequest(models.Model):
|
||||
user = models.ForeignKey(Profile, verbose_name=_('user'), related_name='requests', on_delete=models.CASCADE)
|
||||
organization = models.ForeignKey(Organization, verbose_name=_('organization'), related_name='requests',
|
||||
on_delete=models.CASCADE)
|
||||
time = models.DateTimeField(verbose_name=_('request time'), auto_now_add=True)
|
||||
state = models.CharField(max_length=1, verbose_name=_('state'), choices=(
|
||||
('P', 'Pending'),
|
||||
('A', 'Approved'),
|
||||
('R', 'Rejected'),
|
||||
))
|
||||
reason = models.TextField(verbose_name=_('reason'))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('organization join request')
|
||||
verbose_name_plural = _('organization join requests')
|
176
judge/models/runtime.py
Normal file
176
judge/models/runtime.py
Normal file
|
@ -0,0 +1,176 @@
|
|||
from collections import OrderedDict, defaultdict
|
||||
from operator import attrgetter
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.db.models import CASCADE
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from judge.judgeapi import disconnect_judge
|
||||
|
||||
__all__ = ['Language', 'RuntimeVersion', 'Judge']
|
||||
|
||||
|
||||
class Language(models.Model):
|
||||
key = models.CharField(max_length=6, verbose_name=_('short identifier'),
|
||||
help_text=_('The identifier for this language; the same as its executor id for judges.'),
|
||||
unique=True)
|
||||
name = models.CharField(max_length=20, verbose_name=_('long name'),
|
||||
help_text=_('Longer name for the language, e.g. "Python 2" or "C++11".'))
|
||||
short_name = models.CharField(max_length=10, verbose_name=_('short name'),
|
||||
help_text=_('More readable, but short, name to display publicly; e.g. "PY2" or '
|
||||
'"C++11". If left blank, it will default to the '
|
||||
'short identifier.'),
|
||||
null=True, blank=True)
|
||||
common_name = models.CharField(max_length=10, verbose_name=_('common name'),
|
||||
help_text=_('Common name for the language. For example, the common name for C++03, '
|
||||
'C++11, and C++14 would be "C++"'))
|
||||
ace = models.CharField(max_length=20, verbose_name=_('ace mode name'),
|
||||
help_text=_('Language ID for Ace.js editor highlighting, appended to "mode-" to determine '
|
||||
'the Ace JavaScript file to use, e.g., "python".'))
|
||||
pygments = models.CharField(max_length=20, verbose_name=_('pygments name'),
|
||||
help_text=_('Language ID for Pygments highlighting in source windows.'))
|
||||
template = models.TextField(verbose_name=_('code template'),
|
||||
help_text=_('Code template to display in submission editor.'), blank=True)
|
||||
info = models.CharField(max_length=50, verbose_name=_('runtime info override'), blank=True,
|
||||
help_text=_("Do not set this unless you know what you're doing! It will override the "
|
||||
"usually more specific, judge-provided runtime info!"))
|
||||
description = models.TextField(verbose_name=_('language description'),
|
||||
help_text=_('Use this field to inform users of quirks with your environment, '
|
||||
'additional restrictions, etc.'), blank=True)
|
||||
extension = models.CharField(max_length=10, verbose_name=_('extension'),
|
||||
help_text=_('The extension of source files, e.g., "py" or "cpp".'))
|
||||
|
||||
def runtime_versions(self):
|
||||
runtimes = OrderedDict()
|
||||
# There be dragons here if two judges specify different priorities
|
||||
for runtime in self.runtimeversion_set.all():
|
||||
id = runtime.name
|
||||
if id not in runtimes:
|
||||
runtimes[id] = set()
|
||||
if not runtime.version: # empty str == error determining version on judge side
|
||||
continue
|
||||
runtimes[id].add(runtime.version)
|
||||
|
||||
lang_versions = []
|
||||
for id, version_list in runtimes.items():
|
||||
lang_versions.append((id, sorted(version_list, key=lambda a: tuple(map(int, a.split('.'))))))
|
||||
return lang_versions
|
||||
|
||||
@classmethod
|
||||
def get_common_name_map(cls):
|
||||
result = cache.get('lang:cn_map')
|
||||
if result is not None:
|
||||
return result
|
||||
result = defaultdict(set)
|
||||
for id, cn in Language.objects.values_list('id', 'common_name'):
|
||||
result[cn].add(id)
|
||||
result = {id: cns for id, cns in result.items() if len(cns) > 1}
|
||||
cache.set('lang:cn_map', result, 86400)
|
||||
return result
|
||||
|
||||
@cached_property
|
||||
def short_display_name(self):
|
||||
return self.short_name or self.key
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@cached_property
|
||||
def display_name(self):
|
||||
if self.info:
|
||||
return '%s (%s)' % (self.name, self.info)
|
||||
else:
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def get_python3(cls):
|
||||
# We really need a default language, and this app is in Python 3
|
||||
return Language.objects.get_or_create(key='PY3', defaults={'name': 'Python 3'})[0]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('runtime_list') + '#' + self.key
|
||||
|
||||
@classmethod
|
||||
def get_default_language(cls):
|
||||
return Language.objects.get(key=settings.DEFAULT_USER_LANGUAGE)
|
||||
|
||||
@classmethod
|
||||
def get_default_language_pk(cls):
|
||||
return cls.get_default_language().pk
|
||||
|
||||
class Meta:
|
||||
ordering = ['key']
|
||||
verbose_name = _('language')
|
||||
verbose_name_plural = _('languages')
|
||||
|
||||
|
||||
class RuntimeVersion(models.Model):
|
||||
language = models.ForeignKey(Language, verbose_name=_('language to which this runtime belongs'), on_delete=CASCADE)
|
||||
judge = models.ForeignKey('Judge', verbose_name=_('judge on which this runtime exists'), on_delete=CASCADE)
|
||||
name = models.CharField(max_length=64, verbose_name=_('runtime name'))
|
||||
version = models.CharField(max_length=64, verbose_name=_('runtime version'), blank=True)
|
||||
priority = models.IntegerField(verbose_name=_('order in which to display this runtime'), default=0)
|
||||
|
||||
|
||||
class Judge(models.Model):
|
||||
name = models.CharField(max_length=50, help_text=_('Server name, hostname-style'), unique=True)
|
||||
created = models.DateTimeField(auto_now_add=True, verbose_name=_('time of creation'))
|
||||
auth_key = models.CharField(max_length=100, help_text=_('A key to authenticate this judge'),
|
||||
verbose_name=_('authentication key'))
|
||||
is_blocked = models.BooleanField(verbose_name=_('block judge'), default=False,
|
||||
help_text=_('Whether this judge should be blocked from connecting, '
|
||||
'even if its key is correct.'))
|
||||
online = models.BooleanField(verbose_name=_('judge online status'), default=False)
|
||||
start_time = models.DateTimeField(verbose_name=_('judge start time'), null=True)
|
||||
ping = models.FloatField(verbose_name=_('response time'), null=True)
|
||||
load = models.FloatField(verbose_name=_('system load'), null=True,
|
||||
help_text=_('Load for the last minute, divided by processors to be fair.'))
|
||||
description = models.TextField(blank=True, verbose_name=_('description'))
|
||||
last_ip = models.GenericIPAddressField(verbose_name='Last connected IP', blank=True, null=True)
|
||||
problems = models.ManyToManyField('Problem', verbose_name=_('problems'), related_name='judges')
|
||||
runtimes = models.ManyToManyField(Language, verbose_name=_('judges'), related_name='judges')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def disconnect(self, force=False):
|
||||
disconnect_judge(self, force=force)
|
||||
|
||||
disconnect.alters_data = True
|
||||
|
||||
@cached_property
|
||||
def runtime_versions(self):
|
||||
qs = (self.runtimeversion_set.values('language__key', 'language__name', 'version', 'name')
|
||||
.order_by('language__key', 'priority'))
|
||||
|
||||
ret = OrderedDict()
|
||||
|
||||
for data in qs:
|
||||
key = data['language__key']
|
||||
if key not in ret:
|
||||
ret[key] = {'name': data['language__name'], 'runtime': []}
|
||||
ret[key]['runtime'].append((data['name'], (data['version'],)))
|
||||
|
||||
return list(ret.items())
|
||||
|
||||
@cached_property
|
||||
def uptime(self):
|
||||
return timezone.now() - self.start_time if self.online else 'N/A'
|
||||
|
||||
@cached_property
|
||||
def ping_ms(self):
|
||||
return self.ping * 1000 if self.ping is not None else None
|
||||
|
||||
@cached_property
|
||||
def runtime_list(self):
|
||||
return map(attrgetter('name'), self.runtimes.all())
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
verbose_name = _('judge')
|
||||
verbose_name_plural = _('judges')
|
217
judge/models/submission.py
Normal file
217
judge/models/submission.py
Normal file
|
@ -0,0 +1,217 @@
|
|||
import hashlib
|
||||
import hmac
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from judge.judgeapi import abort_submission, judge_submission
|
||||
from judge.models.problem import Problem, TranslatedProblemForeignKeyQuerySet
|
||||
from judge.models.profile import Profile
|
||||
from judge.models.runtime import Language
|
||||
from judge.utils.unicode import utf8bytes
|
||||
|
||||
__all__ = ['SUBMISSION_RESULT', 'Submission', 'SubmissionSource', 'SubmissionTestCase']
|
||||
|
||||
SUBMISSION_RESULT = (
|
||||
('AC', _('Accepted')),
|
||||
('WA', _('Wrong Answer')),
|
||||
('TLE', _('Time Limit Exceeded')),
|
||||
('MLE', _('Memory Limit Exceeded')),
|
||||
('OLE', _('Output Limit Exceeded')),
|
||||
('IR', _('Invalid Return')),
|
||||
('RTE', _('Runtime Error')),
|
||||
('CE', _('Compile Error')),
|
||||
('IE', _('Internal Error')),
|
||||
('SC', _('Short circuit')),
|
||||
('AB', _('Aborted')),
|
||||
)
|
||||
|
||||
|
||||
class Submission(models.Model):
|
||||
STATUS = (
|
||||
('QU', _('Queued')),
|
||||
('P', _('Processing')),
|
||||
('G', _('Grading')),
|
||||
('D', _('Completed')),
|
||||
('IE', _('Internal Error')),
|
||||
('CE', _('Compile Error')),
|
||||
('AB', _('Aborted')),
|
||||
)
|
||||
IN_PROGRESS_GRADING_STATUS = ('QU', 'P', 'G')
|
||||
RESULT = SUBMISSION_RESULT
|
||||
USER_DISPLAY_CODES = {
|
||||
'AC': _('Accepted'),
|
||||
'WA': _('Wrong Answer'),
|
||||
'SC': "Short Circuited",
|
||||
'TLE': _('Time Limit Exceeded'),
|
||||
'MLE': _('Memory Limit Exceeded'),
|
||||
'OLE': _('Output Limit Exceeded'),
|
||||
'IR': _('Invalid Return'),
|
||||
'RTE': _('Runtime Error'),
|
||||
'CE': _('Compile Error'),
|
||||
'IE': _('Internal Error (judging server error)'),
|
||||
'QU': _('Queued'),
|
||||
'P': _('Processing'),
|
||||
'G': _('Grading'),
|
||||
'D': _('Completed'),
|
||||
'AB': _('Aborted'),
|
||||
}
|
||||
|
||||
user = models.ForeignKey(Profile, on_delete=models.CASCADE)
|
||||
problem = models.ForeignKey(Problem, on_delete=models.CASCADE)
|
||||
date = models.DateTimeField(verbose_name=_('submission time'), auto_now_add=True, db_index=True)
|
||||
time = models.FloatField(verbose_name=_('execution time'), null=True, db_index=True)
|
||||
memory = models.FloatField(verbose_name=_('memory usage'), null=True)
|
||||
points = models.FloatField(verbose_name=_('points granted'), null=True, db_index=True)
|
||||
language = models.ForeignKey(Language, verbose_name=_('submission language'), on_delete=models.CASCADE)
|
||||
status = models.CharField(verbose_name=_('status'), max_length=2, choices=STATUS, default='QU', db_index=True)
|
||||
result = models.CharField(verbose_name=_('result'), max_length=3, choices=SUBMISSION_RESULT,
|
||||
default=None, null=True, blank=True, db_index=True)
|
||||
error = models.TextField(verbose_name=_('compile errors'), null=True, blank=True)
|
||||
current_testcase = models.IntegerField(default=0)
|
||||
batch = models.BooleanField(verbose_name=_('batched cases'), default=False)
|
||||
case_points = models.FloatField(verbose_name=_('test case points'), default=0)
|
||||
case_total = models.FloatField(verbose_name=_('test case total points'), default=0)
|
||||
judged_on = models.ForeignKey('Judge', verbose_name=_('judged on'), null=True, blank=True,
|
||||
on_delete=models.SET_NULL)
|
||||
was_rejudged = models.BooleanField(verbose_name=_('was rejudged by admin'), default=False)
|
||||
is_pretested = models.BooleanField(verbose_name=_('was ran on pretests only'), default=False)
|
||||
contest_object = models.ForeignKey('Contest', verbose_name=_('contest'), null=True, blank=True,
|
||||
on_delete=models.SET_NULL, related_name='+')
|
||||
|
||||
objects = TranslatedProblemForeignKeyQuerySet.as_manager()
|
||||
|
||||
@classmethod
|
||||
def result_class_from_code(cls, result, case_points, case_total):
|
||||
if result == 'AC':
|
||||
if case_points == case_total:
|
||||
return 'AC'
|
||||
return '_AC'
|
||||
return result
|
||||
|
||||
@property
|
||||
def result_class(self):
|
||||
# This exists to save all these conditionals from being executed (slowly) in each row.jade template
|
||||
if self.status in ('IE', 'CE'):
|
||||
return self.status
|
||||
return Submission.result_class_from_code(self.result, self.case_points, self.case_total)
|
||||
|
||||
@property
|
||||
def memory_bytes(self):
|
||||
return self.memory * 1024 if self.memory is not None else 0
|
||||
|
||||
@property
|
||||
def short_status(self):
|
||||
return self.result or self.status
|
||||
|
||||
@property
|
||||
def long_status(self):
|
||||
return Submission.USER_DISPLAY_CODES.get(self.short_status, '')
|
||||
|
||||
def judge(self, rejudge=False, batch_rejudge=False):
|
||||
judge_submission(self, rejudge, batch_rejudge)
|
||||
|
||||
judge.alters_data = True
|
||||
|
||||
def abort(self):
|
||||
abort_submission(self)
|
||||
|
||||
abort.alters_data = True
|
||||
|
||||
def update_contest(self):
|
||||
try:
|
||||
contest = self.contest
|
||||
except AttributeError:
|
||||
return
|
||||
|
||||
contest_problem = contest.problem
|
||||
contest.points = round(self.case_points / self.case_total * contest_problem.points
|
||||
if self.case_total > 0 else 0, 3)
|
||||
if not contest_problem.partial and contest.points != contest_problem.points:
|
||||
contest.points = 0
|
||||
contest.save()
|
||||
contest.participation.recompute_results()
|
||||
|
||||
update_contest.alters_data = True
|
||||
|
||||
@property
|
||||
def is_graded(self):
|
||||
return self.status not in ('QU', 'P', 'G')
|
||||
|
||||
@cached_property
|
||||
def contest_key(self):
|
||||
if hasattr(self, 'contest'):
|
||||
return self.contest_object.key
|
||||
|
||||
def __str__(self):
|
||||
return 'Submission %d of %s by %s' % (self.id, self.problem, self.user.user.username)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('submission_status', args=(self.id,))
|
||||
|
||||
@cached_property
|
||||
def contest_or_none(self):
|
||||
try:
|
||||
return self.contest
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_id_secret(cls, sub_id):
|
||||
return (hmac.new(utf8bytes(settings.EVENT_DAEMON_SUBMISSION_KEY), b'%d' % sub_id, hashlib.sha512)
|
||||
.hexdigest()[:16] + '%08x' % sub_id)
|
||||
|
||||
@cached_property
|
||||
def id_secret(self):
|
||||
return self.get_id_secret(self.id)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('abort_any_submission', 'Abort any submission'),
|
||||
('rejudge_submission', 'Rejudge the submission'),
|
||||
('rejudge_submission_lot', 'Rejudge a lot of submissions'),
|
||||
('spam_submission', 'Submit without limit'),
|
||||
('view_all_submission', 'View all submission'),
|
||||
('resubmit_other', "Resubmit others' submission"),
|
||||
)
|
||||
verbose_name = _('submission')
|
||||
verbose_name_plural = _('submissions')
|
||||
|
||||
|
||||
class SubmissionSource(models.Model):
|
||||
submission = models.OneToOneField(Submission, on_delete=models.CASCADE, verbose_name=_('associated submission'),
|
||||
related_name='source')
|
||||
source = models.TextField(verbose_name=_('source code'), max_length=65536)
|
||||
|
||||
def __str__(self):
|
||||
return 'Source of %s' % self.submission
|
||||
|
||||
|
||||
class SubmissionTestCase(models.Model):
|
||||
RESULT = SUBMISSION_RESULT
|
||||
|
||||
submission = models.ForeignKey(Submission, verbose_name=_('associated submission'),
|
||||
related_name='test_cases', on_delete=models.CASCADE)
|
||||
case = models.IntegerField(verbose_name=_('test case ID'))
|
||||
status = models.CharField(max_length=3, verbose_name=_('status flag'), choices=SUBMISSION_RESULT)
|
||||
time = models.FloatField(verbose_name=_('execution time'), null=True)
|
||||
memory = models.FloatField(verbose_name=_('memory usage'), null=True)
|
||||
points = models.FloatField(verbose_name=_('points granted'), null=True)
|
||||
total = models.FloatField(verbose_name=_('points possible'), null=True)
|
||||
batch = models.IntegerField(verbose_name=_('batch number'), null=True)
|
||||
feedback = models.CharField(max_length=50, verbose_name=_('judging feedback'), blank=True)
|
||||
extended_feedback = models.TextField(verbose_name=_('extended judging feedback'), blank=True)
|
||||
output = models.TextField(verbose_name=_('program output'), blank=True)
|
||||
|
||||
@property
|
||||
def long_status(self):
|
||||
return Submission.USER_DISPLAY_CODES.get(self.status, '')
|
||||
|
||||
class Meta:
|
||||
unique_together = ('submission', 'case')
|
||||
verbose_name = _('submission test case')
|
||||
verbose_name_plural = _('submission test cases')
|
30
judge/models/ticket.py
Normal file
30
judge/models/ticket.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from judge.models.profile import Profile
|
||||
|
||||
|
||||
class Ticket(models.Model):
|
||||
title = models.CharField(max_length=100, verbose_name=_('ticket title'))
|
||||
user = models.ForeignKey(Profile, verbose_name=_('ticket creator'), related_name='tickets',
|
||||
on_delete=models.CASCADE)
|
||||
time = models.DateTimeField(verbose_name=_('creation time'), auto_now_add=True)
|
||||
assignees = models.ManyToManyField(Profile, verbose_name=_('assignees'), related_name='assigned_tickets')
|
||||
notes = models.TextField(verbose_name=_('quick notes'), blank=True,
|
||||
help_text=_('Staff notes for this issue to aid in processing.'))
|
||||
content_type = models.ForeignKey(ContentType, verbose_name=_('linked item type'),
|
||||
on_delete=models.CASCADE)
|
||||
object_id = models.PositiveIntegerField(verbose_name=_('linked item ID'))
|
||||
linked_item = GenericForeignKey()
|
||||
is_open = models.BooleanField(verbose_name=_('is ticket open?'), default=True)
|
||||
|
||||
|
||||
class TicketMessage(models.Model):
|
||||
ticket = models.ForeignKey(Ticket, verbose_name=_('ticket'), related_name='messages',
|
||||
related_query_name='message', on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(Profile, verbose_name=_('poster'), related_name='ticket_messages',
|
||||
on_delete=models.CASCADE)
|
||||
body = models.TextField(verbose_name=_('message body'))
|
||||
time = models.DateTimeField(verbose_name=_('message time'), auto_now_add=True)
|
268
judge/pdf_problems.py
Normal file
268
judge/pdf_problems.py
Normal file
|
@ -0,0 +1,268 @@
|
|||
import errno
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext
|
||||
|
||||
HAS_PHANTOMJS = os.access(settings.PHANTOMJS, os.X_OK)
|
||||
HAS_SLIMERJS = os.access(settings.SLIMERJS, os.X_OK)
|
||||
|
||||
NODE_PATH = settings.NODEJS
|
||||
PUPPETEER_MODULE = settings.PUPPETEER_MODULE
|
||||
HAS_PUPPETEER = os.access(NODE_PATH, os.X_OK) and os.path.isdir(PUPPETEER_MODULE)
|
||||
|
||||
HAS_PDF = (os.path.isdir(settings.DMOJ_PDF_PROBLEM_CACHE) and
|
||||
(HAS_PHANTOMJS or HAS_SLIMERJS or HAS_PUPPETEER))
|
||||
|
||||
EXIFTOOL = settings.EXIFTOOL
|
||||
HAS_EXIFTOOL = os.access(EXIFTOOL, os.X_OK)
|
||||
|
||||
logger = logging.getLogger('judge.problem.pdf')
|
||||
|
||||
|
||||
class BasePdfMaker(object):
|
||||
math_engine = 'jax'
|
||||
title = None
|
||||
|
||||
def __init__(self, dir=None, clean_up=True):
|
||||
self.dir = dir or os.path.join(settings.DMOJ_PDF_PROBLEM_TEMP_DIR, str(uuid.uuid1()))
|
||||
self.proc = None
|
||||
self.log = None
|
||||
self.htmlfile = os.path.join(self.dir, 'input.html')
|
||||
self.pdffile = os.path.join(self.dir, 'output.pdf')
|
||||
self.clean_up = clean_up
|
||||
|
||||
def load(self, file, source):
|
||||
with open(os.path.join(self.dir, file), 'w') as target, open(source) as source:
|
||||
target.write(source.read())
|
||||
|
||||
def make(self, debug=False):
|
||||
self._make(debug)
|
||||
|
||||
if self.title and HAS_EXIFTOOL:
|
||||
try:
|
||||
subprocess.check_output([EXIFTOOL, '-Title=%s' % (self.title,), self.pdffile])
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error('Failed to run exiftool to set title for: %s\n%s', self.title, e.output)
|
||||
|
||||
def _make(self, debug):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def html(self):
|
||||
with io.open(self.htmlfile, encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
@html.setter
|
||||
def html(self, data):
|
||||
with io.open(self.htmlfile, 'w', encoding='utf-8') as f:
|
||||
f.write(data)
|
||||
|
||||
@property
|
||||
def success(self):
|
||||
return self.proc.returncode == 0
|
||||
|
||||
@property
|
||||
def created(self):
|
||||
return os.path.exists(self.pdffile)
|
||||
|
||||
def __enter__(self):
|
||||
try:
|
||||
os.makedirs(self.dir)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.clean_up:
|
||||
shutil.rmtree(self.dir, ignore_errors=True)
|
||||
|
||||
|
||||
class PhantomJSPdfMaker(BasePdfMaker):
|
||||
template = '''\
|
||||
"use strict";
|
||||
var page = require('webpage').create();
|
||||
var param = {params};
|
||||
|
||||
page.paperSize = {
|
||||
format: param.paper, orientation: 'portrait', margin: '1cm',
|
||||
footer: {
|
||||
height: '1cm',
|
||||
contents: phantom.callback(function(num, pages) {
|
||||
return ('<center style="margin: 0 auto; font-family: Segoe UI; font-size: 10px">'
|
||||
+ param.footer.replace('[page]', num).replace('[topage]', pages) + '</center>');
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
page.onCallback = function (data) {
|
||||
if (data.action === 'snapshot') {
|
||||
page.render(param.output);
|
||||
phantom.exit();
|
||||
}
|
||||
}
|
||||
|
||||
page.open(param.input, function (status) {
|
||||
if (status !== 'success') {
|
||||
console.log('Unable to load the address!');
|
||||
phantom.exit(1);
|
||||
} else {
|
||||
page.evaluate(function (zoom) {
|
||||
document.documentElement.style.zoom = zoom;
|
||||
}, param.zoom);
|
||||
window.setTimeout(function () {
|
||||
page.render(param.output);
|
||||
phantom.exit();
|
||||
}, param.timeout);
|
||||
}
|
||||
});
|
||||
'''
|
||||
|
||||
def get_render_script(self):
|
||||
return self.template.replace('{params}', json.dumps({
|
||||
'zoom': settings.PHANTOMJS_PDF_ZOOM,
|
||||
'timeout': int(settings.PHANTOMJS_PDF_TIMEOUT * 1000),
|
||||
'input': 'input.html', 'output': 'output.pdf',
|
||||
'paper': settings.PHANTOMJS_PAPER_SIZE,
|
||||
'footer': gettext('Page [page] of [topage]'),
|
||||
}))
|
||||
|
||||
def _make(self, debug):
|
||||
with io.open(os.path.join(self.dir, '_render.js'), 'w', encoding='utf-8') as f:
|
||||
f.write(self.get_render_script())
|
||||
cmdline = [settings.PHANTOMJS, '_render.js']
|
||||
self.proc = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.dir)
|
||||
self.log = self.proc.communicate()[0]
|
||||
|
||||
|
||||
class SlimerJSPdfMaker(BasePdfMaker):
|
||||
math_engine = 'mml'
|
||||
|
||||
template = '''\
|
||||
"use strict";
|
||||
try {
|
||||
var param = {params};
|
||||
|
||||
var {Cc, Ci} = require('chrome');
|
||||
var prefs = Cc['@mozilla.org/preferences-service;1'].getService(Ci.nsIPrefService);
|
||||
// Changing the serif font so that printed footers show up as Segoe UI.
|
||||
var branch = prefs.getBranch('font.name.serif.');
|
||||
branch.setCharPref('x-western', 'Segoe UI');
|
||||
|
||||
var page = require('webpage').create();
|
||||
|
||||
page.paperSize = {
|
||||
format: param.paper, orientation: 'portrait', margin: '1cm', edge: '0.5cm',
|
||||
footerStr: { left: '', right: '', center: param.footer }
|
||||
};
|
||||
|
||||
page.open(param.input, function (status) {
|
||||
if (status !== 'success') {
|
||||
console.log('Unable to load the address!');
|
||||
slimer.exit(1);
|
||||
} else {
|
||||
page.render(param.output, { ratio: param.zoom });
|
||||
slimer.exit();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
slimer.exit(1);
|
||||
}
|
||||
'''
|
||||
|
||||
def get_render_script(self):
|
||||
return self.template.replace('{params}', json.dumps({
|
||||
'zoom': settings.SLIMERJS_PDF_ZOOM,
|
||||
'input': 'input.html', 'output': 'output.pdf',
|
||||
'paper': settings.SLIMERJS_PAPER_SIZE,
|
||||
'footer': gettext('Page [page] of [topage]').replace('[page]', '&P').replace('[topage]', '&L'),
|
||||
}))
|
||||
|
||||
def _make(self, debug):
|
||||
with io.open(os.path.join(self.dir, '_render.js'), 'w', encoding='utf-8') as f:
|
||||
f.write(self.get_render_script())
|
||||
|
||||
env = None
|
||||
firefox = settings.SLIMERJS_FIREFOX_PATH
|
||||
if firefox:
|
||||
env = os.environ.copy()
|
||||
env['SLIMERJSLAUNCHER'] = firefox
|
||||
|
||||
cmdline = [settings.SLIMERJS, '--headless', '_render.js']
|
||||
self.proc = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.dir, env=env)
|
||||
self.log = self.proc.communicate()[0]
|
||||
|
||||
|
||||
class PuppeteerPDFRender(BasePdfMaker):
|
||||
template = '''\
|
||||
"use strict";
|
||||
const param = {params};
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
puppeteer.launch().then(browser => Promise.resolve()
|
||||
.then(async () => {
|
||||
const page = await browser.newPage();
|
||||
await page.goto(param.input, { waitUntil: 'load' });
|
||||
await page.waitForSelector('.math-loaded', { timeout: 15000 });
|
||||
await page.pdf({
|
||||
path: param.output,
|
||||
format: param.paper,
|
||||
margin: {
|
||||
top: '1cm',
|
||||
bottom: '1cm',
|
||||
left: '1cm',
|
||||
right: '1cm',
|
||||
},
|
||||
printBackground: true,
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: '<div></div>',
|
||||
footerTemplate: '<center style="margin: 0 auto; font-family: Segoe UI; font-size: 10px">' +
|
||||
param.footer.replace('[page]', '<span class="pageNumber"></span>')
|
||||
.replace('[topage]', '<span class="totalPages"></span>')
|
||||
+ '</center>',
|
||||
});
|
||||
await browser.close();
|
||||
})
|
||||
.catch(e => browser.close().then(() => {throw e}))
|
||||
).catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
'''
|
||||
|
||||
def get_render_script(self):
|
||||
return self.template.replace('{params}', json.dumps({
|
||||
'input': 'file://' + os.path.abspath(os.path.join(self.dir, 'input.html')),
|
||||
'output': os.path.abspath(os.path.join(self.dir, 'output.pdf')),
|
||||
'paper': settings.PUPPETEER_PAPER_SIZE,
|
||||
'footer': gettext('Page [page] of [topage]'),
|
||||
}))
|
||||
|
||||
def _make(self, debug):
|
||||
with io.open(os.path.join(self.dir, '_render.js'), 'w', encoding='utf-8') as f:
|
||||
f.write(self.get_render_script())
|
||||
|
||||
env = os.environ.copy()
|
||||
env['NODE_PATH'] = os.path.dirname(PUPPETEER_MODULE)
|
||||
|
||||
cmdline = [NODE_PATH, '_render.js']
|
||||
self.proc = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.dir, env=env)
|
||||
self.log = self.proc.communicate()[0]
|
||||
|
||||
|
||||
if HAS_PUPPETEER:
|
||||
DefaultPdfMaker = PuppeteerPDFRender
|
||||
elif HAS_SLIMERJS:
|
||||
DefaultPdfMaker = SlimerJSPdfMaker
|
||||
elif HAS_PHANTOMJS:
|
||||
DefaultPdfMaker = PhantomJSPdfMaker
|
||||
else:
|
||||
DefaultPdfMaker = None
|
78
judge/performance_points.py
Normal file
78
judge/performance_points.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
from collections import namedtuple
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
|
||||
from judge.models import Submission
|
||||
from judge.timezone import from_database_time
|
||||
|
||||
PP_WEIGHT_TABLE = [pow(settings.DMOJ_PP_STEP, i) for i in range(settings.DMOJ_PP_ENTRIES)]
|
||||
|
||||
PPBreakdown = namedtuple('PPBreakdown', 'points weight scaled_points problem_name problem_code '
|
||||
'sub_id sub_date sub_points sub_total sub_result_class '
|
||||
'sub_short_status sub_long_status sub_lang')
|
||||
|
||||
|
||||
def get_pp_breakdown(user, start=0, end=settings.DMOJ_PP_ENTRIES):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute('''
|
||||
SELECT max_points_table.problem_code,
|
||||
max_points_table.problem_name,
|
||||
max_points_table.max_points,
|
||||
judge_submission.id,
|
||||
judge_submission.date,
|
||||
judge_submission.case_points,
|
||||
judge_submission.case_total,
|
||||
judge_submission.result,
|
||||
judge_language.short_name,
|
||||
judge_language.key
|
||||
FROM judge_submission
|
||||
JOIN (SELECT judge_problem.id problem_id,
|
||||
judge_problem.name problem_name,
|
||||
judge_problem.code problem_code,
|
||||
MAX(judge_submission.points) AS max_points
|
||||
FROM judge_problem
|
||||
INNER JOIN judge_submission ON (judge_problem.id = judge_submission.problem_id)
|
||||
WHERE (judge_problem.is_public = True AND
|
||||
judge_problem.is_organization_private = False AND
|
||||
judge_submission.points IS NOT NULL AND
|
||||
judge_submission.user_id = %s)
|
||||
GROUP BY judge_problem.id
|
||||
HAVING MAX(judge_submission.points) > 0.0) AS max_points_table
|
||||
ON (judge_submission.problem_id = max_points_table.problem_id AND
|
||||
judge_submission.points = max_points_table.max_points AND
|
||||
judge_submission.user_id = %s)
|
||||
JOIN judge_language
|
||||
ON judge_submission.language_id = judge_language.id
|
||||
GROUP BY max_points_table.problem_id
|
||||
ORDER BY max_points DESC, judge_submission.date DESC
|
||||
LIMIT %s OFFSET %s
|
||||
''', (user.id, user.id, end - start + 1, start))
|
||||
data = cursor.fetchall()
|
||||
|
||||
breakdown = []
|
||||
for weight, contrib in zip(PP_WEIGHT_TABLE[start:end], data):
|
||||
code, name, points, id, date, case_points, case_total, result, lang_short_name, lang_key = contrib
|
||||
|
||||
# Replicates a lot of the logic usually done on Submission objects
|
||||
lang_short_display_name = lang_short_name or lang_key
|
||||
result_class = Submission.result_class_from_code(result, case_points, case_total)
|
||||
long_status = Submission.USER_DISPLAY_CODES.get(result, '')
|
||||
|
||||
breakdown.append(PPBreakdown(
|
||||
points=points,
|
||||
weight=weight * 100,
|
||||
scaled_points=points * weight,
|
||||
problem_name=name,
|
||||
problem_code=code,
|
||||
sub_id=id,
|
||||
sub_date=from_database_time(date),
|
||||
sub_points=case_points,
|
||||
sub_total=case_total,
|
||||
sub_short_status=result,
|
||||
sub_long_status=long_status,
|
||||
sub_result_class=result_class,
|
||||
sub_lang=lang_short_display_name,
|
||||
))
|
||||
has_more = end < min(len(PP_WEIGHT_TABLE), start + len(data))
|
||||
return breakdown, has_more
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue