Cloned DMOJ

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

10
judge/views/__init__.py Normal file
View file

@ -0,0 +1,10 @@
from django.views.generic import TemplateView
class TitledTemplateView(TemplateView):
title = None
def get_context_data(self, **kwargs):
if 'title' not in kwargs and self.title is not None:
kwargs['title'] = self.title
return super(TitledTemplateView, self).get_context_data(**kwargs)

View file

@ -0,0 +1,2 @@
from .api_v1 import *
from .api_v2 import *

173
judge/views/api/api_v1.py Normal file
View file

@ -0,0 +1,173 @@
from operator import attrgetter
from django.db.models import F, Prefetch
from django.http import Http404, JsonResponse
from django.shortcuts import get_object_or_404
from dmoj import settings
from judge.models import Contest, ContestParticipation, ContestTag, Problem, Profile, Submission
def sane_time_repr(delta):
days = delta.days
hours = delta.seconds / 3600
minutes = (delta.seconds % 3600) / 60
return '%02d:%02d:%02d' % (days, hours, minutes)
def api_v1_contest_list(request):
queryset = Contest.objects.filter(is_visible=True, is_private=False,
is_organization_private=False).prefetch_related(
Prefetch('tags', queryset=ContestTag.objects.only('name'), to_attr='tag_list')).defer('description')
return JsonResponse({c.key: {
'name': c.name,
'start_time': c.start_time.isoformat(),
'end_time': c.end_time.isoformat(),
'time_limit': c.time_limit and sane_time_repr(c.time_limit),
'labels': list(map(attrgetter('name'), c.tag_list)),
} for c in queryset})
def api_v1_contest_detail(request, contest):
contest = get_object_or_404(Contest, key=contest)
in_contest = contest.is_in_contest(request.user)
can_see_rankings = contest.can_see_scoreboard(request.user)
if contest.hide_scoreboard and in_contest:
can_see_rankings = False
problems = list(contest.contest_problems.select_related('problem')
.defer('problem__description').order_by('order'))
participations = (contest.users.filter(virtual=0, user__is_unlisted=False)
.prefetch_related('user__organizations')
.annotate(username=F('user__user__username'))
.order_by('-score', 'cumtime') if can_see_rankings else [])
can_see_problems = (in_contest or contest.ended or contest.is_editable_by(request.user))
return JsonResponse({
'time_limit': contest.time_limit and contest.time_limit.total_seconds(),
'start_time': contest.start_time.isoformat(),
'end_time': contest.end_time.isoformat(),
'tags': list(contest.tags.values_list('name', flat=True)),
'is_rated': contest.is_rated,
'rate_all': contest.is_rated and contest.rate_all,
'has_rating': contest.ratings.exists(),
'rating_floor': contest.rating_floor,
'rating_ceiling': contest.rating_ceiling,
'format': {
'name': contest.format_name,
'config': contest.format_config,
},
'problems': [
{
'points': int(problem.points),
'partial': problem.partial,
'name': problem.problem.name,
'code': problem.problem.code,
} for problem in problems] if can_see_problems else [],
'rankings': [
{
'user': participation.username,
'points': participation.score,
'cumtime': participation.cumtime,
'is_disqualified': participation.is_disqualified,
'solutions': contest.format.get_problem_breakdown(participation, problems),
} for participation in participations],
})
def api_v1_problem_list(request):
queryset = Problem.objects.filter(is_public=True, is_organization_private=False)
if settings.ENABLE_FTS and 'search' in request.GET:
query = ' '.join(request.GET.getlist('search')).strip()
if query:
queryset = queryset.search(query)
queryset = queryset.values_list('code', 'points', 'partial', 'name', 'group__full_name')
return JsonResponse({code: {
'points': points,
'partial': partial,
'name': name,
'group': group,
} for code, points, partial, name, group in queryset})
def api_v1_problem_info(request, problem):
p = get_object_or_404(Problem, code=problem)
if not p.is_accessible_by(request.user):
raise Http404()
return JsonResponse({
'name': p.name,
'authors': list(p.authors.values_list('user__username', flat=True)),
'types': list(p.types.values_list('full_name', flat=True)),
'group': p.group.full_name,
'time_limit': p.time_limit,
'memory_limit': p.memory_limit,
'points': p.points,
'partial': p.partial,
'languages': list(p.allowed_languages.values_list('key', flat=True)),
})
def api_v1_user_list(request):
queryset = Profile.objects.filter(is_unlisted=False).values_list('user__username', 'points', 'performance_points',
'display_rank')
return JsonResponse({username: {
'points': points,
'performance_points': performance_points,
'rank': rank,
} for username, points, performance_points, rank in queryset})
def api_v1_user_info(request, user):
profile = get_object_or_404(Profile, user__username=user)
submissions = list(Submission.objects.filter(case_points=F('case_total'), user=profile, problem__is_public=True,
problem__is_organization_private=False)
.values('problem').distinct().values_list('problem__code', flat=True))
resp = {
'points': profile.points,
'performance_points': profile.performance_points,
'rank': profile.display_rank,
'solved_problems': submissions,
'organizations': list(profile.organizations.values_list('id', flat=True)),
}
last_rating = profile.ratings.last()
contest_history = {}
if not profile.is_unlisted:
participations = ContestParticipation.objects.filter(user=profile, virtual=0, contest__is_visible=True,
contest__is_private=False,
contest__is_organization_private=False)
for contest_key, rating, volatility in participations.values_list('contest__key', 'rating__rating',
'rating__volatility'):
contest_history[contest_key] = {
'rating': rating,
'volatility': volatility,
}
resp['contests'] = {
'current_rating': last_rating.rating if last_rating else None,
'volatility': last_rating.volatility if last_rating else None,
'history': contest_history,
}
return JsonResponse(resp)
def api_v1_user_submissions(request, user):
profile = get_object_or_404(Profile, user__username=user)
subs = Submission.objects.filter(user=profile, problem__is_public=True, problem__is_organization_private=False)
return JsonResponse({sub['id']: {
'problem': sub['problem__code'],
'time': sub['time'],
'memory': sub['memory'],
'points': sub['points'],
'language': sub['language__key'],
'status': sub['status'],
'result': sub['result'],
} for sub in subs.values('id', 'problem__code', 'time', 'memory', 'points', 'language__key', 'status', 'result')})

122
judge/views/api/api_v2.py Normal file
View file

@ -0,0 +1,122 @@
from operator import attrgetter
from django.db.models import Max
from django.http import JsonResponse
from judge.models import ContestParticipation, Problem, Profile, Submission
from judge.utils.ranker import ranker
from judge.views.contests import contest_ranking_list
def error(message):
return JsonResponse({'error': message}, status=422)
def api_v2_user_info(request):
"""
{
"points": 100.0,
"rating": 2452,
"rank": "user",
"organizations": [],
"solved_problems": ["ccc14s4", ...],
"attempted_problems": [
{
"code": "Hello, World!",
"points": 1.0,
"max_points": 2.0
}
],
"authored_problems": ["dmpg16s4"],
"contest_history": [
{
"contest": {
"code": "halloween14",
"name": "Kemonomimi Party",
"tags": ["seasonal"],
"time_limit": null,
"start_time": "2014-10-31T04:00:00+00:00",
"end_time": "2014-11-10T05:00:00+00:00"
},
"rank": 1,
"rating:": 1800
},
// ...
]
}
"""
try:
username = request.GET['username']
except KeyError:
return error("no username passed")
if not username:
return error("username argument not provided")
try:
profile = Profile.objects.get(user__username=username)
except Profile.DoesNotExist:
return error("no such user")
last_rating = list(profile.ratings.order_by('-contest__end_time'))
resp = {
"rank": profile.display_rank,
"organizations": list(profile.organizations.values_list('key', flat=True)),
}
contest_history = []
for participation in (ContestParticipation.objects.filter(user=profile, virtual=0, contest__is_visible=True)
.order_by('-contest__end_time')):
contest = participation.contest
problems = list(contest.contest_problems.select_related('problem').defer('problem__description')
.order_by('order'))
rank, result = next(filter(lambda data: data[1].user == profile.user,
ranker(contest_ranking_list(contest, problems),
key=attrgetter('points', 'cumtime'))))
contest_history.append({
'contest': {
'code': contest.key,
'name': contest.name,
'tags': list(contest.tags.values_list('name', flat=True)),
'time_limit': contest.time_limit and contest.time_limit.total_seconds(),
'start_time': contest.start_time.isoformat(),
'end_time': contest.end_time.isoformat(),
},
'rank': rank,
'rating': result.participation_rating,
})
resp['contests'] = {
"current_rating": last_rating[0].rating if last_rating else None,
"volatility": last_rating[0].volatility if last_rating else None,
'history': contest_history,
}
solved_problems = []
attempted_problems = []
problem_data = (Submission.objects.filter(points__gt=0, user=profile, problem__is_public=True,
problem__is_organization_private=False)
.annotate(max_pts=Max('points'))
.values_list('max_pts', 'problem__points', 'problem__code')
.distinct())
for awarded_pts, max_pts, problem in problem_data:
if awarded_pts == max_pts:
solved_problems.append(problem)
else:
attempted_problems.append({
'awarded': awarded_pts,
'max': max_pts,
'problem': problem,
})
resp['problems'] = {
'points': profile.points,
'solved': solved_problems,
'attempted': attempted_problems,
'authored': list(Problem.objects.filter(is_public=True, is_organization_private=False, authors=profile)
.values_list('code', flat=True)),
}
return JsonResponse(resp)

126
judge/views/blog.py Normal file
View file

@ -0,0 +1,126 @@
from django.conf import settings
from django.db.models import Count, Max, Q
from django.http import Http404
from django.urls import reverse
from django.utils import timezone
from django.utils.functional import lazy
from django.utils.translation import ugettext as _
from django.views.generic import ListView
from judge.comments import CommentedDetailView
from judge.models import BlogPost, Comment, Contest, Language, Problem, ProblemClarification, Profile, Submission, \
Ticket
from judge.utils.cachedict import CacheDict
from judge.utils.diggpaginator import DiggPaginator
from judge.utils.problems import user_completed_ids
from judge.utils.tickets import filter_visible_tickets
from judge.utils.views import TitleMixin
class PostList(ListView):
model = BlogPost
paginate_by = 10
context_object_name = 'posts'
template_name = 'blog/list.html'
title = None
def get_paginator(self, queryset, per_page, orphans=0,
allow_empty_first_page=True, **kwargs):
return DiggPaginator(queryset, per_page, body=6, padding=2,
orphans=orphans, allow_empty_first_page=allow_empty_first_page, **kwargs)
def get_queryset(self):
return (BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()).order_by('-sticky', '-publish_on')
.prefetch_related('authors__user'))
def get_context_data(self, **kwargs):
context = super(PostList, self).get_context_data(**kwargs)
context['title'] = self.title or _('Page %d of Posts') % context['page_obj'].number
context['first_page_href'] = reverse('home')
context['page_prefix'] = reverse('blog_post_list')
context['comments'] = Comment.most_recent(self.request.user, 10)
context['new_problems'] = Problem.objects.filter(is_public=True, is_organization_private=False) \
.order_by('-date', '-id')[:settings.DMOJ_BLOG_NEW_PROBLEM_COUNT]
context['page_titles'] = CacheDict(lambda page: Comment.get_page_title(page))
context['has_clarifications'] = False
if self.request.user.is_authenticated:
participation = self.request.profile.current_contest
if participation:
clarifications = ProblemClarification.objects.filter(problem__in=participation.contest.problems.all())
context['has_clarifications'] = clarifications.count() > 0
context['clarifications'] = clarifications.order_by('-date')
context['user_count'] = lazy(Profile.objects.count, int, int)
context['problem_count'] = lazy(Problem.objects.filter(is_public=True).count, int, int)
context['submission_count'] = lazy(Submission.objects.count, int, int)
context['language_count'] = lazy(Language.objects.count, int, int)
context['post_comment_counts'] = {
int(page[2:]): count for page, count in
Comment.objects
.filter(page__in=['b:%d' % post.id for post in context['posts']], hidden=False)
.values_list('page').annotate(count=Count('page')).order_by()
}
now = timezone.now()
# Dashboard stuff
if self.request.user.is_authenticated:
user = self.request.profile
context['recently_attempted_problems'] = (Submission.objects.filter(user=user)
.exclude(problem__in=user_completed_ids(user))
.values_list('problem__code', 'problem__name', 'problem__points')
.annotate(points=Max('points'), latest=Max('date'))
.order_by('-latest')
[:settings.DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT])
visible_contests = Contest.objects.filter(is_visible=True).order_by('start_time')
q = Q(is_private=False, is_organization_private=False)
if self.request.user.is_authenticated:
q |= Q(is_organization_private=True, organizations__in=user.organizations.all())
q |= Q(is_private=True, private_contestants=user)
visible_contests = visible_contests.filter(q)
context['current_contests'] = visible_contests.filter(start_time__lte=now, end_time__gt=now)
context['future_contests'] = visible_contests.filter(start_time__gt=now)
if self.request.user.is_authenticated:
profile = self.request.profile
context['own_open_tickets'] = (Ticket.objects.filter(user=profile, is_open=True).order_by('-id')
.prefetch_related('linked_item').select_related('user__user'))
else:
profile = None
context['own_open_tickets'] = []
# Superusers better be staffs, not the spell-casting kind either.
if self.request.user.is_staff:
tickets = (Ticket.objects.order_by('-id').filter(is_open=True).prefetch_related('linked_item')
.select_related('user__user'))
context['open_tickets'] = filter_visible_tickets(tickets, self.request.user, profile)[:10]
else:
context['open_tickets'] = []
return context
class PostView(TitleMixin, CommentedDetailView):
model = BlogPost
pk_url_kwarg = 'id'
context_object_name = 'post'
template_name = 'blog/content.html'
def get_title(self):
return self.object.title
def get_comment_page(self):
return 'b:%s' % self.object.id
def get_context_data(self, **kwargs):
context = super(PostView, self).get_context_data(**kwargs)
context['og_image'] = self.object.og_image
return context
def get_object(self, queryset=None):
post = super(PostView, self).get_object(queryset)
if not post.can_see(self.request.user):
raise Http404()
return post

170
judge/views/comment.py Normal file
View file

@ -0,0 +1,170 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.core.exceptions import PermissionDenied
from django.db import IntegrityError, transaction
from django.db.models import F
from django.forms.models import ModelForm
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as _
from django.views.decorators.http import require_POST
from django.views.generic import DetailView, UpdateView
from reversion import revisions
from reversion.models import Version
from judge.dblock import LockModel
from judge.models import Comment, CommentVote
from judge.utils.views import TitleMixin
from judge.widgets import MathJaxPagedownWidget
__all__ = ['upvote_comment', 'downvote_comment', 'CommentEditAjax', 'CommentContent',
'CommentEdit']
@login_required
def vote_comment(request, delta):
if abs(delta) != 1:
return HttpResponseBadRequest(_('Messing around, are we?'), content_type='text/plain')
if request.method != 'POST':
return HttpResponseForbidden()
if 'id' not in request.POST:
return HttpResponseBadRequest()
if not request.user.is_staff and not request.profile.submission_set.filter(points=F('problem__points')).exists():
return HttpResponseBadRequest(_('You must solve at least one problem before you can vote.'),
content_type='text/plain')
try:
comment_id = int(request.POST['id'])
except ValueError:
return HttpResponseBadRequest()
else:
if not Comment.objects.filter(id=comment_id).exists():
raise Http404()
vote = CommentVote()
vote.comment_id = comment_id
vote.voter = request.profile
vote.score = delta
while True:
try:
vote.save()
except IntegrityError:
with LockModel(write=(CommentVote,)):
try:
vote = CommentVote.objects.get(comment_id=comment_id, voter=request.profile)
except CommentVote.DoesNotExist:
# We must continue racing in case this is exploited to manipulate votes.
continue
if -vote.score != delta:
return HttpResponseBadRequest(_('You already voted.'), content_type='text/plain')
vote.delete()
Comment.objects.filter(id=comment_id).update(score=F('score') - vote.score)
else:
Comment.objects.filter(id=comment_id).update(score=F('score') + delta)
break
return HttpResponse('success', content_type='text/plain')
def upvote_comment(request):
return vote_comment(request, 1)
def downvote_comment(request):
return vote_comment(request, -1)
class CommentMixin(object):
model = Comment
pk_url_kwarg = 'id'
context_object_name = 'comment'
class CommentRevisionAjax(CommentMixin, DetailView):
template_name = 'comments/revision-ajax.html'
def get_context_data(self, **kwargs):
context = super(CommentRevisionAjax, self).get_context_data(**kwargs)
revisions = Version.objects.get_for_object(self.object).order_by('-revision')
try:
wanted = min(max(int(self.request.GET.get('revision', 0)), 0), len(revisions) - 1)
except ValueError:
raise Http404
context['revision'] = revisions[wanted]
return context
def get_object(self, queryset=None):
comment = super(CommentRevisionAjax, self).get_object(queryset)
if comment.hidden and not self.request.user.has_perm('judge.change_comment'):
raise Http404()
return comment
class CommentEditForm(ModelForm):
class Meta:
model = Comment
fields = ['body']
if MathJaxPagedownWidget is not None:
widgets = {'body': MathJaxPagedownWidget(attrs={'id': 'id-edit-comment-body'})}
class CommentEditAjax(LoginRequiredMixin, CommentMixin, UpdateView):
template_name = 'comments/edit-ajax.html'
form_class = CommentEditForm
def form_valid(self, form):
with transaction.atomic(), revisions.create_revision():
revisions.set_comment(_('Edited from site'))
revisions.set_user(self.request.user)
return super(CommentEditAjax, self).form_valid(form)
def get_success_url(self):
return self.object.get_absolute_url()
def get_object(self, queryset=None):
comment = super(CommentEditAjax, self).get_object(queryset)
if self.request.user.has_perm('judge.change_comment'):
return comment
profile = self.request.profile
if profile != comment.author or profile.mute or comment.hidden:
raise Http404()
return comment
class CommentEdit(TitleMixin, CommentEditAjax):
template_name = 'comments/edit.html'
def get_title(self):
return _('Editing comment')
class CommentContent(CommentMixin, DetailView):
template_name = 'comments/content.html'
class CommentVotesAjax(PermissionRequiredMixin, CommentMixin, DetailView):
template_name = 'comments/votes.html'
permission_required = 'judge.change_commentvote'
def get_context_data(self, **kwargs):
context = super(CommentVotesAjax, self).get_context_data(**kwargs)
context['votes'] = (self.object.votes.select_related('voter__user')
.only('id', 'voter__display_rank', 'voter__user__username', 'score'))
return context
@require_POST
def comment_hide(request):
if not request.user.has_perm('judge.change_comment'):
raise PermissionDenied()
try:
comment_id = int(request.POST['id'])
except ValueError:
return HttpResponseBadRequest()
comment = get_object_or_404(Comment, id=comment_id)
comment.get_descendants(include_self=True).update(hidden=True)
return HttpResponse('ok')

793
judge/views/contests.py Normal file
View file

@ -0,0 +1,793 @@
import json
from calendar import Calendar, SUNDAY
from collections import defaultdict, namedtuple
from datetime import date, datetime, time, timedelta
from functools import partial
from itertools import chain
from operator import attrgetter, itemgetter
from django import forms
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.core.cache import cache
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django.db import IntegrityError
from django.db.models import Case, Count, FloatField, IntegerField, Max, Min, Q, Sum, Value, When
from django.db.models.expressions import CombinedExpression
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.template.defaultfilters import date as date_filter
from django.urls import reverse
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.timezone import make_aware
from django.utils.translation import gettext as _, gettext_lazy
from django.views.generic import ListView, TemplateView
from django.views.generic.detail import BaseDetailView, DetailView, SingleObjectMixin, View
from judge import event_poster as event
from judge.comments import CommentedDetailView
from judge.forms import ContestCloneForm
from judge.models import Contest, ContestMoss, ContestParticipation, ContestProblem, ContestTag, \
Problem, Profile, Submission
from judge.tasks import run_moss
from judge.utils.celery import redirect_to_task_status
from judge.utils.opengraph import generate_opengraph
from judge.utils.problems import _get_result_data
from judge.utils.ranker import ranker
from judge.utils.stats import get_bar_chart, get_pie_chart
from judge.utils.views import DiggPaginatorMixin, SingleObjectFormView, TitleMixin, generic_message
__all__ = ['ContestList', 'ContestDetail', 'ContestRanking', 'ContestJoin', 'ContestLeave', 'ContestCalendar',
'ContestClone', 'ContestStats', 'ContestMossView', 'ContestMossDelete', 'contest_ranking_ajax',
'ContestParticipationList', 'ContestParticipationDisqualify', 'get_contest_ranking_list',
'base_contest_ranking_list']
def _find_contest(request, key, private_check=True):
try:
contest = Contest.objects.get(key=key)
if private_check and not contest.is_accessible_by(request.user):
raise ObjectDoesNotExist()
except ObjectDoesNotExist:
return generic_message(request, _('No such contest'),
_('Could not find a contest with the key "%s".') % key, status=404), False
return contest, True
class ContestListMixin(object):
def get_queryset(self):
queryset = Contest.objects.all()
if not self.request.user.has_perm('judge.see_private_contest'):
q = Q(is_visible=True)
if self.request.user.is_authenticated:
q |= Q(organizers=self.request.profile)
queryset = queryset.filter(q)
if not self.request.user.has_perm('judge.edit_all_contest'):
q = Q(is_private=False, is_organization_private=False)
if self.request.user.is_authenticated:
q |= Q(is_organization_private=True, organizations__in=self.request.profile.organizations.all())
q |= Q(is_private=True, private_contestants=self.request.profile)
queryset = queryset.filter(q)
return queryset.distinct()
class ContestList(DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView):
model = Contest
paginate_by = 20
template_name = 'contest/list.html'
title = gettext_lazy('Contests')
context_object_name = 'past_contests'
@cached_property
def _now(self):
return timezone.now()
def _get_queryset(self):
return super(ContestList, self).get_queryset() \
.order_by('-start_time', 'key').prefetch_related('tags', 'organizations', 'organizers')
def get_queryset(self):
return self._get_queryset().filter(end_time__lt=self._now)
def get_context_data(self, **kwargs):
context = super(ContestList, self).get_context_data(**kwargs)
present, active, future = [], [], []
for contest in self._get_queryset().exclude(end_time__lt=self._now):
if contest.start_time > self._now:
future.append(contest)
else:
present.append(contest)
if self.request.user.is_authenticated:
for participation in ContestParticipation.objects.filter(virtual=0, user=self.request.profile,
contest_id__in=present) \
.select_related('contest').prefetch_related('contest__organizers'):
if not participation.ended:
active.append(participation)
present.remove(participation.contest)
active.sort(key=attrgetter('end_time'))
future.sort(key=attrgetter('start_time'))
context['active_participations'] = active
context['current_contests'] = present
context['future_contests'] = future
context['now'] = self._now
context['first_page_href'] = '.'
return context
class PrivateContestError(Exception):
def __init__(self, name, is_private, is_organization_private, orgs):
self.name = name
self.is_private = is_private
self.is_organization_private = is_organization_private
self.orgs = orgs
class ContestMixin(object):
context_object_name = 'contest'
model = Contest
slug_field = 'key'
slug_url_kwarg = 'contest'
@cached_property
def is_organizer(self):
return self.check_organizer()
def check_organizer(self, contest=None, user=None):
if user is None:
user = self.request.user
return (contest or self.object).is_editable_by(user)
def get_context_data(self, **kwargs):
context = super(ContestMixin, self).get_context_data(**kwargs)
if self.request.user.is_authenticated:
profile = self.request.profile
in_contest = context['in_contest'] = (profile.current_contest is not None and
profile.current_contest.contest == self.object)
if in_contest:
context['participation'] = profile.current_contest
context['participating'] = True
else:
try:
context['participation'] = profile.contest_history.get(contest=self.object, virtual=0)
except ContestParticipation.DoesNotExist:
context['participating'] = False
context['participation'] = None
else:
context['participating'] = True
else:
context['participating'] = False
context['participation'] = None
context['in_contest'] = False
context['now'] = timezone.now()
context['is_organizer'] = self.is_organizer
if not self.object.og_image or not self.object.summary:
metadata = generate_opengraph('generated-meta-contest:%d' % self.object.id,
self.object.description, 'contest')
context['meta_description'] = self.object.summary or metadata[0]
context['og_image'] = self.object.og_image or metadata[1]
context['has_moss_api_key'] = settings.MOSS_API_KEY is not None
context['logo_override_image'] = self.object.logo_override_image
if not context['logo_override_image'] and self.object.organizations.count() == 1:
context['logo_override_image'] = self.object.organizations.first().logo_override_image
return context
def get_object(self, queryset=None):
contest = super(ContestMixin, self).get_object(queryset)
user = self.request.user
profile = self.request.profile
if (profile is not None and
ContestParticipation.objects.filter(id=profile.current_contest_id, contest_id=contest.id).exists()):
return contest
if not contest.is_visible and not user.has_perm('judge.see_private_contest') and (
not user.has_perm('judge.edit_own_contest') or
not self.check_organizer(contest, user)):
raise Http404()
if contest.is_private or contest.is_organization_private:
private_contest_error = PrivateContestError(contest.name, contest.is_private,
contest.is_organization_private, contest.organizations.all())
if profile is None:
raise private_contest_error
if user.has_perm('judge.edit_all_contest'):
return contest
if not (contest.is_organization_private and
contest.organizations.filter(id__in=profile.organizations.all()).exists()) and \
not (contest.is_private and contest.private_contestants.filter(id=profile.id).exists()):
raise private_contest_error
return contest
def dispatch(self, request, *args, **kwargs):
try:
return super(ContestMixin, self).dispatch(request, *args, **kwargs)
except Http404:
key = kwargs.get(self.slug_url_kwarg, None)
if key:
return generic_message(request, _('No such contest'),
_('Could not find a contest with the key "%s".') % key)
else:
return generic_message(request, _('No such contest'),
_('Could not find such contest.'))
except PrivateContestError as e:
return render(request, 'contest/private.html', {
'error': e, 'title': _('Access to contest "%s" denied') % e.name,
}, status=403)
class ContestDetail(ContestMixin, TitleMixin, CommentedDetailView):
template_name = 'contest/contest.html'
def get_comment_page(self):
return 'c:%s' % self.object.key
def get_title(self):
return self.object.name
def get_context_data(self, **kwargs):
context = super(ContestDetail, self).get_context_data(**kwargs)
context['contest_problems'] = Problem.objects.filter(contests__contest=self.object) \
.order_by('contests__order').defer('description') \
.annotate(has_public_editorial=Sum(Case(When(solution__is_public=True, then=1),
default=0, output_field=IntegerField()))) \
.add_i18n_name(self.request.LANGUAGE_CODE)
return context
class ContestClone(ContestMixin, PermissionRequiredMixin, TitleMixin, SingleObjectFormView):
title = _('Clone Contest')
template_name = 'contest/clone.html'
form_class = ContestCloneForm
permission_required = 'judge.clone_contest'
def form_valid(self, form):
contest = self.object
tags = contest.tags.all()
organizations = contest.organizations.all()
private_contestants = contest.private_contestants.all()
contest_problems = contest.contest_problems.all()
contest.pk = None
contest.is_visible = False
contest.user_count = 0
contest.key = form.cleaned_data['key']
contest.save()
contest.tags.set(tags)
contest.organizations.set(organizations)
contest.private_contestants.set(private_contestants)
contest.organizers.add(self.request.profile)
for problem in contest_problems:
problem.contest = contest
problem.pk = None
ContestProblem.objects.bulk_create(contest_problems)
return HttpResponseRedirect(reverse('admin:judge_contest_change', args=(contest.id,)))
class ContestAccessDenied(Exception):
pass
class ContestAccessCodeForm(forms.Form):
access_code = forms.CharField(max_length=255)
def __init__(self, *args, **kwargs):
super(ContestAccessCodeForm, self).__init__(*args, **kwargs)
self.fields['access_code'].widget.attrs.update({'autocomplete': 'off'})
class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView):
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return self.ask_for_access_code()
def post(self, request, *args, **kwargs):
self.object = self.get_object()
try:
return self.join_contest(request)
except ContestAccessDenied:
if request.POST.get('access_code'):
return self.ask_for_access_code(ContestAccessCodeForm(request.POST))
else:
return HttpResponseRedirect(request.path)
def join_contest(self, request, access_code=None):
contest = self.object
if not contest.can_join and not self.is_organizer:
return generic_message(request, _('Contest not ongoing'),
_('"%s" is not currently ongoing.') % contest.name)
profile = request.profile
if profile.current_contest is not None:
return generic_message(request, _('Already in contest'),
_('You are already in a contest: "%s".') % profile.current_contest.contest.name)
if not request.user.is_superuser and contest.banned_users.filter(id=profile.id).exists():
return generic_message(request, _('Banned from joining'),
_('You have been declared persona non grata for this contest. '
'You are permanently barred from joining this contest.'))
requires_access_code = (not (request.user.is_superuser or self.is_organizer) and
contest.access_code and access_code != contest.access_code)
if contest.ended:
if requires_access_code:
raise ContestAccessDenied()
while True:
virtual_id = max((ContestParticipation.objects.filter(contest=contest, user=profile)
.aggregate(virtual_id=Max('virtual'))['virtual_id'] or 0) + 1, 1)
try:
participation = ContestParticipation.objects.create(
contest=contest, user=profile, virtual=virtual_id,
real_start=timezone.now(),
)
# There is obviously a race condition here, so we keep trying until we win the race.
except IntegrityError:
pass
else:
break
else:
try:
participation = ContestParticipation.objects.get(
contest=contest, user=profile, virtual=(-1 if self.is_organizer else 0),
)
except ContestParticipation.DoesNotExist:
if requires_access_code:
raise ContestAccessDenied()
participation = ContestParticipation.objects.create(
contest=contest, user=profile, virtual=(-1 if self.is_organizer else 0),
real_start=timezone.now(),
)
else:
if participation.ended:
participation = ContestParticipation.objects.get_or_create(
contest=contest, user=profile, virtual=-1,
defaults={'real_start': timezone.now()},
)[0]
profile.current_contest = participation
profile.save()
contest._updating_stats_only = True
contest.update_user_count()
return HttpResponseRedirect(reverse('problem_list'))
def ask_for_access_code(self, form=None):
contest = self.object
wrong_code = False
if form:
if form.is_valid():
if form.cleaned_data['access_code'] == contest.access_code:
return self.join_contest(self.request, form.cleaned_data['access_code'])
wrong_code = True
else:
form = ContestAccessCodeForm()
return render(self.request, 'contest/access_code.html', {
'form': form, 'wrong_code': wrong_code,
'title': _('Enter access code for "%s"') % contest.name,
})
class ContestLeave(LoginRequiredMixin, ContestMixin, BaseDetailView):
def post(self, request, *args, **kwargs):
contest = self.get_object()
profile = request.profile
if profile.current_contest is None or profile.current_contest.contest_id != contest.id:
return generic_message(request, _('No such contest'),
_('You are not in contest "%s".') % contest.key, 404)
profile.remove_contest()
return HttpResponseRedirect(reverse('contest_view', args=(contest.key,)))
ContestDay = namedtuple('ContestDay', 'date weekday is_pad is_today starts ends oneday')
class ContestCalendar(TitleMixin, ContestListMixin, TemplateView):
firstweekday = SUNDAY
weekday_classes = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
template_name = 'contest/calendar.html'
def get(self, request, *args, **kwargs):
try:
self.year = int(kwargs['year'])
self.month = int(kwargs['month'])
except (KeyError, ValueError):
raise ImproperlyConfigured(_('ContestCalendar requires integer year and month'))
self.today = timezone.now().date()
return self.render()
def render(self):
context = self.get_context_data()
return self.render_to_response(context)
def get_contest_data(self, start, end):
end += timedelta(days=1)
contests = self.get_queryset().filter(Q(start_time__gte=start, start_time__lt=end) |
Q(end_time__gte=start, end_time__lt=end)).defer('description')
starts, ends, oneday = (defaultdict(list) for i in range(3))
for contest in contests:
start_date = timezone.localtime(contest.start_time).date()
end_date = timezone.localtime(contest.end_time - timedelta(seconds=1)).date()
if start_date == end_date:
oneday[start_date].append(contest)
else:
starts[start_date].append(contest)
ends[end_date].append(contest)
return starts, ends, oneday
def get_table(self):
calendar = Calendar(self.firstweekday).monthdatescalendar(self.year, self.month)
starts, ends, oneday = self.get_contest_data(make_aware(datetime.combine(calendar[0][0], time.min)),
make_aware(datetime.combine(calendar[-1][-1], time.min)))
return [[ContestDay(
date=date, weekday=self.weekday_classes[weekday], is_pad=date.month != self.month,
is_today=date == self.today, starts=starts[date], ends=ends[date], oneday=oneday[date],
) for weekday, date in enumerate(week)] for week in calendar]
def get_context_data(self, **kwargs):
context = super(ContestCalendar, self).get_context_data(**kwargs)
try:
month = date(self.year, self.month, 1)
except ValueError:
raise Http404()
else:
context['title'] = _('Contests in %(month)s') % {'month': date_filter(month, _("F Y"))}
dates = Contest.objects.aggregate(min=Min('start_time'), max=Max('end_time'))
min_month = (self.today.year, self.today.month)
if dates['min'] is not None:
min_month = dates['min'].year, dates['min'].month
max_month = (self.today.year, self.today.month)
if dates['max'] is not None:
max_month = max((dates['max'].year, dates['max'].month), (self.today.year, self.today.month))
month = (self.year, self.month)
if month < min_month or month > max_month:
# 404 is valid because it merely declares the lack of existence, without any reason
raise Http404()
context['now'] = timezone.now()
context['calendar'] = self.get_table()
context['curr_month'] = date(self.year, self.month, 1)
if month > min_month:
context['prev_month'] = date(self.year - (self.month == 1), 12 if self.month == 1 else self.month - 1, 1)
else:
context['prev_month'] = None
if month < max_month:
context['next_month'] = date(self.year + (self.month == 12), 1 if self.month == 12 else self.month + 1, 1)
else:
context['next_month'] = None
return context
class CachedContestCalendar(ContestCalendar):
def render(self):
key = 'contest_cal:%d:%d' % (self.year, self.month)
cached = cache.get(key)
if cached is not None:
return HttpResponse(cached)
response = super(CachedContestCalendar, self).render()
response.render()
cached.set(key, response.content)
return response
class ContestStats(TitleMixin, ContestMixin, DetailView):
template_name = 'contest/stats.html'
def get_title(self):
return _('%s Statistics') % self.object.name
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if not (self.object.ended or self.object.is_editable_by(self.request.user)):
raise Http404()
queryset = Submission.objects.filter(contest_object=self.object)
ac_count = Count(Case(When(result='AC', then=Value(1)), output_field=IntegerField()))
ac_rate = CombinedExpression(ac_count / Count('problem'), '*', Value(100.0), output_field=FloatField())
status_count_queryset = list(
queryset.values('problem__code', 'result').annotate(count=Count('result'))
.values_list('problem__code', 'result', 'count'),
)
labels, codes = zip(
*self.object.contest_problems.order_by('order').values_list('problem__name', 'problem__code'),
)
num_problems = len(labels)
status_counts = [[] for i in range(num_problems)]
for problem_code, result, count in status_count_queryset:
if problem_code in codes:
status_counts[codes.index(problem_code)].append((result, count))
result_data = defaultdict(partial(list, [0] * num_problems))
for i in range(num_problems):
for category in _get_result_data(defaultdict(int, status_counts[i]))['categories']:
result_data[category['code']][i] = category['count']
stats = {
'problem_status_count': {
'labels': labels,
'datasets': [
{
'label': name,
'backgroundColor': settings.DMOJ_STATS_SUBMISSION_RESULT_COLORS[name],
'data': data,
}
for name, data in result_data.items()
],
},
'problem_ac_rate': get_bar_chart(
queryset.values('contest__problem__order', 'problem__name').annotate(ac_rate=ac_rate)
.order_by('contest__problem__order').values_list('problem__name', 'ac_rate'),
),
'language_count': get_pie_chart(
queryset.values('language__name').annotate(count=Count('language__name'))
.filter(count__gt=0).order_by('-count').values_list('language__name', 'count'),
),
'language_ac_rate': get_bar_chart(
queryset.values('language__name').annotate(ac_rate=ac_rate)
.filter(ac_rate__gt=0).values_list('language__name', 'ac_rate'),
),
}
context['stats'] = mark_safe(json.dumps(stats))
return context
ContestRankingProfile = namedtuple(
'ContestRankingProfile',
'id user css_class username points cumtime organization participation '
'participation_rating problem_cells result_cell',
)
BestSolutionData = namedtuple('BestSolutionData', 'code points time state is_pretested')
def make_contest_ranking_profile(contest, participation, contest_problems):
user = participation.user
return ContestRankingProfile(
id=user.id,
user=user.user,
css_class=user.css_class,
username=user.username,
points=participation.score,
cumtime=participation.cumtime,
organization=user.organization,
participation_rating=participation.rating.rating if hasattr(participation, 'rating') else None,
problem_cells=[contest.format.display_user_problem(participation, contest_problem)
for contest_problem in contest_problems],
result_cell=contest.format.display_participation_result(participation),
participation=participation,
)
def base_contest_ranking_list(contest, problems, queryset):
return [make_contest_ranking_profile(contest, participation, problems) for participation in
queryset.select_related('user__user', 'rating').defer('user__about', 'user__organizations__about')]
def contest_ranking_list(contest, problems):
return base_contest_ranking_list(contest, problems, contest.users.filter(virtual=0, user__is_unlisted=False)
.prefetch_related('user__organizations')
.order_by('is_disqualified', '-score', 'cumtime'))
def get_contest_ranking_list(request, contest, participation=None, ranking_list=contest_ranking_list,
show_current_virtual=True, ranker=ranker):
problems = list(contest.contest_problems.select_related('problem').defer('problem__description').order_by('order'))
if contest.hide_scoreboard and contest.is_in_contest(request.user):
return ([(_('???'), make_contest_ranking_profile(contest, request.profile.current_contest, problems))],
problems)
users = ranker(ranking_list(contest, problems), key=attrgetter('points', 'cumtime'))
if show_current_virtual:
if participation is None and request.user.is_authenticated:
participation = request.profile.current_contest
if participation is None or participation.contest_id != contest.id:
participation = None
if participation is not None and participation.virtual:
users = chain([('-', make_contest_ranking_profile(contest, participation, problems))], users)
return users, problems
def contest_ranking_ajax(request, contest, participation=None):
contest, exists = _find_contest(request, contest)
if not exists:
return HttpResponseBadRequest('Invalid contest', content_type='text/plain')
if not contest.can_see_scoreboard(request.user):
raise Http404()
users, problems = get_contest_ranking_list(request, contest, participation)
return render(request, 'contest/ranking-table.html', {
'users': users,
'problems': problems,
'contest': contest,
'has_rating': contest.ratings.exists(),
})
class ContestRankingBase(ContestMixin, TitleMixin, DetailView):
template_name = 'contest/ranking.html'
tab = None
def get_title(self):
raise NotImplementedError()
def get_content_title(self):
return self.object.name
def get_ranking_list(self):
raise NotImplementedError()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if not self.object.can_see_scoreboard(self.request.user):
raise Http404()
users, problems = self.get_ranking_list()
context['users'] = users
context['problems'] = problems
context['last_msg'] = event.last()
context['tab'] = self.tab
return context
class ContestRanking(ContestRankingBase):
tab = 'ranking'
def get_title(self):
return _('%s Rankings') % self.object.name
def get_ranking_list(self):
return get_contest_ranking_list(self.request, self.object)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['has_rating'] = self.object.ratings.exists()
return context
class ContestParticipationList(LoginRequiredMixin, ContestRankingBase):
tab = 'participation'
def get_title(self):
if self.profile == self.request.profile:
return _('Your participation in %s') % self.object.name
return _("%s's participation in %s") % (self.profile.username, self.object.name)
def get_ranking_list(self):
queryset = self.object.users.filter(user=self.profile, virtual__gte=0).order_by('-virtual')
live_link = format_html('<a href="{2}#!{1}">{0}</a>', _('Live'), self.profile.username,
reverse('contest_ranking', args=[self.object.key]))
return get_contest_ranking_list(
self.request, self.object, show_current_virtual=False,
ranking_list=partial(base_contest_ranking_list, queryset=queryset),
ranker=lambda users, key: ((user.participation.virtual or live_link, user) for user in users))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['has_rating'] = False
context['now'] = timezone.now()
context['rank_header'] = _('Participation')
return context
def get(self, request, *args, **kwargs):
if 'user' in kwargs:
self.profile = get_object_or_404(Profile, user__username=kwargs['user'])
else:
self.profile = self.request.profile
return super().get(request, *args, **kwargs)
class ContestParticipationDisqualify(ContestMixin, SingleObjectMixin, View):
def get_object(self, queryset=None):
contest = super().get_object(queryset)
if not contest.is_editable_by(self.request.user):
raise Http404()
return contest
def post(self, request, *args, **kwargs):
self.object = self.get_object()
try:
participation = self.object.users.get(pk=request.POST.get('participation'))
except ObjectDoesNotExist:
pass
else:
participation.set_disqualified(not participation.is_disqualified)
return HttpResponseRedirect(reverse('contest_ranking', args=(self.object.key,)))
class ContestMossMixin(ContestMixin, PermissionRequiredMixin):
permission_required = 'judge.moss_contest'
def get_object(self, queryset=None):
contest = super().get_object(queryset)
if settings.MOSS_API_KEY is None:
raise Http404()
if not contest.is_editable_by(self.request.user):
raise Http404()
return contest
class ContestMossView(ContestMossMixin, TitleMixin, DetailView):
template_name = 'contest/moss.html'
def get_title(self):
return _('%s MOSS Results') % self.object.name
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
problems = list(map(attrgetter('problem'), self.object.contest_problems.order_by('order')
.select_related('problem')))
languages = list(map(itemgetter(0), ContestMoss.LANG_MAPPING))
results = ContestMoss.objects.filter(contest=self.object)
moss_results = defaultdict(list)
for result in results:
moss_results[result.problem].append(result)
for result_list in moss_results.values():
result_list.sort(key=lambda x: languages.index(x.language))
context['languages'] = languages
context['has_results'] = results.exists()
context['moss_results'] = [(problem, moss_results[problem]) for problem in problems]
return context
def post(self, request, *args, **kwargs):
self.object = self.get_object()
status = run_moss.delay(self.object.key)
return redirect_to_task_status(
status, message=_('Running MOSS for %s...') % (self.object.name,),
redirect=reverse('contest_moss', args=(self.object.key,)),
)
class ContestMossDelete(ContestMossMixin, SingleObjectMixin, View):
def post(self, request, *args, **kwargs):
self.object = self.get_object()
ContestMoss.objects.filter(contest=self.object).delete()
return HttpResponseRedirect(reverse('contest_moss', args=(self.object.key,)))
class ContestTagDetailAjax(DetailView):
model = ContestTag
slug_field = slug_url_kwarg = 'name'
context_object_name = 'tag'
template_name = 'contest/tag-ajax.html'
class ContestTagDetail(TitleMixin, ContestTagDetailAjax):
template_name = 'contest/tag.html'
def get_title(self):
return _('Contest tag: %s') % self.object.name

33
judge/views/error.py Normal file
View file

@ -0,0 +1,33 @@
import traceback
from django.shortcuts import render
from django.utils.translation import gettext as _
def error(request, context, status):
return render(request, 'error.html', context=context, status=status)
def error404(request, exception=None):
# TODO: "panic: go back"
return render(request, 'generic-message.html', {
'title': _('404 error'),
'message': _('Could not find page "%s"') % request.path,
}, status=404)
def error403(request, exception=None):
return error(request, {
'id': 'unauthorized_access',
'description': _('no permission for %s') % request.path,
'code': 403,
}, 403)
def error500(request):
return error(request, {
'id': 'invalid_state',
'description': _('corrupt page %s') % request.path,
'traceback': traceback.format_exc(),
'code': 500,
}, 500)

18
judge/views/language.py Normal file
View file

@ -0,0 +1,18 @@
from django.utils.translation import gettext_lazy
from django.views.generic import ListView
from judge.models import Language
from judge.utils.views import TitleMixin
class LanguageList(TitleMixin, ListView):
model = Language
context_object_name = 'languages'
template_name = 'status/language-list.html'
title = gettext_lazy('Runtimes')
def get_queryset(self):
queryset = super().get_queryset().prefetch_related('runtimeversion_set')
if not self.request.user.is_superuser and not self.request.user.is_staff:
queryset = queryset.filter(judges__online=True).distinct()
return queryset

14
judge/views/license.py Normal file
View file

@ -0,0 +1,14 @@
from django.views.generic import DetailView
from judge.models import License
from judge.utils.views import TitleMixin
class LicenseDetail(TitleMixin, DetailView):
model = License
slug_field = slug_url_kwarg = 'key'
context_object_name = 'license'
template_name = 'license.html'
def get_title(self):
return self.object.name

65
judge/views/mailgun.py Normal file
View file

@ -0,0 +1,65 @@
import hashlib
import hmac
import logging
from email.utils import parseaddr
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View
from registration.models import RegistrationProfile
from judge.utils.unicode import utf8bytes
logger = logging.getLogger('judge.mail.activate')
class MailgunActivationView(View):
if hasattr(settings, 'MAILGUN_ACCESS_KEY'):
def post(self, request, *args, **kwargs):
params = request.POST
timestamp = params.get('timestamp', '')
token = params.get('token', '')
signature = params.get('signature', '')
logger.debug('Received request: %s', params)
if signature != hmac.new(key=utf8bytes(settings.MAILGUN_ACCESS_KEY),
msg=utf8bytes('%s%s' % (timestamp, token)), digestmod=hashlib.sha256).hexdigest():
logger.info('Rejected request: signature: %s, timestamp: %s, token: %s', signature, timestamp, token)
raise PermissionDenied()
_, sender = parseaddr(params.get('from'))
if not sender:
logger.info('Rejected invalid sender: %s', params.get('from'))
return HttpResponse(status=406)
try:
user = User.objects.get(email__iexact=sender)
except (User.DoesNotExist, User.MultipleObjectsReturned):
logger.info('Rejected unknown sender: %s: %s', sender, params.get('from'))
return HttpResponse(status=406)
try:
registration = RegistrationProfile.objects.get(user=user)
except RegistrationProfile.DoesNotExist:
logger.info('Rejected sender without RegistrationProfile: %s: %s', sender, params.get('from'))
return HttpResponse(status=406)
if registration.activated:
logger.info('Rejected activated sender: %s: %s', sender, params.get('from'))
return HttpResponse(status=406)
key = registration.activation_key
if key in params.get('body-plain', '') or key in params.get('body-html', ''):
if RegistrationProfile.objects.activate_user(key, get_current_site(request)):
logger.info('Activated sender: %s: %s', sender, params.get('from'))
return HttpResponse('Activated', status=200)
logger.info('Failed to activate sender: %s: %s', sender, params.get('from'))
else:
logger.info('Activation key not found: %s: %s', sender, params.get('from'))
return HttpResponse(status=406)
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
return super(MailgunActivationView, self).dispatch(request, *args, **kwargs)

330
judge/views/organization.py Normal file
View file

@ -0,0 +1,330 @@
from django import forms
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import Count, Q
from django.forms import Form, modelformset_factory
from django.http import Http404, HttpResponsePermanentRedirect, HttpResponseRedirect
from django.urls import reverse
from django.utils.translation import gettext as _, gettext_lazy, ungettext
from django.views.generic import DetailView, FormView, ListView, UpdateView, View
from django.views.generic.detail import SingleObjectMixin, SingleObjectTemplateResponseMixin
from reversion import revisions
from judge.forms import EditOrganizationForm
from judge.models import Organization, OrganizationRequest, Profile
from judge.utils.ranker import ranker
from judge.utils.views import TitleMixin, generic_message
__all__ = ['OrganizationList', 'OrganizationHome', 'OrganizationUsers', 'OrganizationMembershipChange',
'JoinOrganization', 'LeaveOrganization', 'EditOrganization', 'RequestJoinOrganization',
'OrganizationRequestDetail', 'OrganizationRequestView', 'OrganizationRequestLog',
'KickUserWidgetView']
class OrganizationMixin(object):
context_object_name = 'organization'
model = Organization
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['logo_override_image'] = self.object.logo_override_image
return context
def dispatch(self, request, *args, **kwargs):
try:
return super(OrganizationMixin, self).dispatch(request, *args, **kwargs)
except Http404:
key = kwargs.get(self.slug_url_kwarg, None)
if key:
return generic_message(request, _('No such organization'),
_('Could not find an organization with the key "%s".') % key)
else:
return generic_message(request, _('No such organization'),
_('Could not find such organization.'))
def can_edit_organization(self, org=None):
if org is None:
org = self.object
if not self.request.user.is_authenticated:
return False
profile_id = self.request.profile.id
return org.admins.filter(id=profile_id).exists() or org.registrant_id == profile_id
class OrganizationDetailView(OrganizationMixin, DetailView):
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.slug != kwargs['slug']:
return HttpResponsePermanentRedirect(request.get_full_path().replace(kwargs['slug'], self.object.slug))
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
class OrganizationList(TitleMixin, ListView):
model = Organization
context_object_name = 'organizations'
template_name = 'organization/list.html'
title = gettext_lazy('Organizations')
def get_queryset(self):
return super(OrganizationList, self).get_queryset().annotate(member_count=Count('member'))
class OrganizationHome(OrganizationDetailView):
template_name = 'organization/home.html'
def get_context_data(self, **kwargs):
context = super(OrganizationHome, self).get_context_data(**kwargs)
context['title'] = self.object.name
context['can_edit'] = self.can_edit_organization()
return context
class OrganizationUsers(OrganizationDetailView):
template_name = 'organization/users.html'
def get_context_data(self, **kwargs):
context = super(OrganizationUsers, self).get_context_data(**kwargs)
context['title'] = _('%s Members') % self.object.name
context['users'] = \
ranker(self.object.members.filter(is_unlisted=False).order_by('-performance_points', '-problem_count')
.select_related('user').defer('about', 'user_script', 'notes'))
context['partial'] = True
context['is_admin'] = self.can_edit_organization()
context['kick_url'] = reverse('organization_user_kick', args=[self.object.id, self.object.slug])
return context
class OrganizationMembershipChange(LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View):
def post(self, request, *args, **kwargs):
org = self.get_object()
response = self.handle(request, org, request.profile)
if response is not None:
return response
return HttpResponseRedirect(org.get_absolute_url())
def handle(self, request, org, profile):
raise NotImplementedError()
class JoinOrganization(OrganizationMembershipChange):
def handle(self, request, org, profile):
if profile.organizations.filter(id=org.id).exists():
return generic_message(request, _('Joining organization'), _('You are already in the organization.'))
if not org.is_open:
return generic_message(request, _('Joining organization'), _('This organization is not open.'))
max_orgs = settings.DMOJ_USER_MAX_ORGANIZATION_COUNT
if profile.organizations.filter(is_open=True).count() >= max_orgs:
return generic_message(
request, _('Joining organization'),
_('You may not be part of more than {count} public organizations.').format(count=max_orgs),
)
profile.organizations.add(org)
profile.save()
cache.delete(make_template_fragment_key('org_member_count', (org.id,)))
class LeaveOrganization(OrganizationMembershipChange):
def handle(self, request, org, profile):
if not profile.organizations.filter(id=org.id).exists():
return generic_message(request, _('Leaving organization'), _('You are not in "%s".') % org.short_name)
profile.organizations.remove(org)
cache.delete(make_template_fragment_key('org_member_count', (org.id,)))
class OrganizationRequestForm(Form):
reason = forms.CharField(widget=forms.Textarea)
class RequestJoinOrganization(LoginRequiredMixin, SingleObjectMixin, FormView):
model = Organization
slug_field = 'key'
slug_url_kwarg = 'key'
template_name = 'organization/requests/request.html'
form_class = OrganizationRequestForm
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
return super(RequestJoinOrganization, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(RequestJoinOrganization, self).get_context_data(**kwargs)
if self.object.is_open:
raise Http404()
context['title'] = _('Request to join %s') % self.object.name
return context
def form_valid(self, form):
request = OrganizationRequest()
request.organization = self.get_object()
request.user = self.request.profile
request.reason = form.cleaned_data['reason']
request.state = 'P'
request.save()
return HttpResponseRedirect(reverse('request_organization_detail', args=(
request.organization.id, request.organization.slug, request.id,
)))
class OrganizationRequestDetail(LoginRequiredMixin, TitleMixin, DetailView):
model = OrganizationRequest
template_name = 'organization/requests/detail.html'
title = gettext_lazy('Join request detail')
pk_url_kwarg = 'rpk'
def get_object(self, queryset=None):
object = super(OrganizationRequestDetail, self).get_object(queryset)
profile = self.request.profile
if object.user_id != profile.id and not object.organization.admins.filter(id=profile.id).exists():
raise PermissionDenied()
return object
OrganizationRequestFormSet = modelformset_factory(OrganizationRequest, extra=0, fields=('state',), can_delete=True)
class OrganizationRequestBaseView(LoginRequiredMixin, SingleObjectTemplateResponseMixin, SingleObjectMixin, View):
model = Organization
slug_field = 'key'
slug_url_kwarg = 'key'
tab = None
def get_object(self, queryset=None):
organization = super(OrganizationRequestBaseView, self).get_object(queryset)
if not (organization.admins.filter(id=self.request.profile.id).exists() or
organization.registrant_id == self.request.profile.id):
raise PermissionDenied()
return organization
def get_context_data(self, **kwargs):
context = super(OrganizationRequestBaseView, self).get_context_data(**kwargs)
context['title'] = _('Managing join requests for %s') % self.object.name
context['tab'] = self.tab
return context
class OrganizationRequestView(OrganizationRequestBaseView):
template_name = 'organization/requests/pending.html'
tab = 'pending'
def get_context_data(self, **kwargs):
context = super(OrganizationRequestView, self).get_context_data(**kwargs)
context['formset'] = self.formset
return context
def get(self, request, *args, **kwargs):
self.object = self.get_object()
self.formset = OrganizationRequestFormSet(
queryset=OrganizationRequest.objects.filter(state='P', organization=self.object),
)
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
self.object = organization = self.get_object()
self.formset = formset = OrganizationRequestFormSet(request.POST, request.FILES)
if formset.is_valid():
if organization.slots is not None:
deleted_set = set(formset.deleted_forms)
to_approve = sum(form.cleaned_data['state'] == 'A' for form in formset.forms if form not in deleted_set)
can_add = organization.slots - organization.members.count()
if to_approve > can_add:
messages.error(request, _('Your organization can only receive %d more members. '
'You cannot approve %d users.') % (can_add, to_approve))
return self.render_to_response(self.get_context_data(object=organization))
approved, rejected = 0, 0
for obj in formset.save():
if obj.state == 'A':
obj.user.organizations.add(obj.organization)
approved += 1
elif obj.state == 'R':
rejected += 1
messages.success(request,
ungettext('Approved %d user.', 'Approved %d users.', approved) % approved + '\n' +
ungettext('Rejected %d user.', 'Rejected %d users.', rejected) % rejected)
cache.delete(make_template_fragment_key('org_member_count', (organization.id,)))
return HttpResponseRedirect(request.get_full_path())
return self.render_to_response(self.get_context_data(object=organization))
put = post
class OrganizationRequestLog(OrganizationRequestBaseView):
states = ('A', 'R')
tab = 'log'
template_name = 'organization/requests/log.html'
def get(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
def get_context_data(self, **kwargs):
context = super(OrganizationRequestLog, self).get_context_data(**kwargs)
context['requests'] = self.object.requests.filter(state__in=self.states)
return context
class EditOrganization(LoginRequiredMixin, TitleMixin, OrganizationMixin, UpdateView):
template_name = 'organization/edit.html'
model = Organization
form_class = EditOrganizationForm
def get_title(self):
return _('Editing %s') % self.object.name
def get_object(self, queryset=None):
object = super(EditOrganization, self).get_object()
if not self.can_edit_organization(object):
raise PermissionDenied()
return object
def get_form(self, form_class=None):
form = super(EditOrganization, self).get_form(form_class)
form.fields['admins'].queryset = \
Profile.objects.filter(Q(organizations=self.object) | Q(admin_of=self.object)).distinct()
return form
def form_valid(self, form):
with transaction.atomic(), revisions.create_revision():
revisions.set_comment(_('Edited from site'))
revisions.set_user(self.request.user)
return super(EditOrganization, self).form_valid(form)
def dispatch(self, request, *args, **kwargs):
try:
return super(EditOrganization, self).dispatch(request, *args, **kwargs)
except PermissionDenied:
return generic_message(request, _("Can't edit organization"),
_('You are not allowed to edit this organization.'), status=403)
class KickUserWidgetView(LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View):
def post(self, request, *args, **kwargs):
organization = self.get_object()
if not self.can_edit_organization(organization):
return generic_message(request, _("Can't edit organization"),
_('You are not allowed to kick people from this organization.'), status=403)
try:
user = Profile.objects.get(id=request.POST.get('user', None))
except Profile.DoesNotExist:
return generic_message(request, _("Can't kick user"),
_('The user you are trying to kick does not exist!'), status=400)
if not organization.members.filter(id=user.id).exists():
return generic_message(request, _("Can't kick user"),
_('The user you are trying to kick is not in organization: %s.') %
organization.name, status=400)
organization.members.remove(user)
return HttpResponseRedirect(organization.get_users_url())

50
judge/views/preview.py Normal file
View file

@ -0,0 +1,50 @@
from django.http import HttpResponseBadRequest
from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
class MarkdownPreviewView(TemplateResponseMixin, ContextMixin, View):
def post(self, request, *args, **kwargs):
try:
self.preview_data = data = request.POST['preview']
except KeyError:
return HttpResponseBadRequest('No preview data specified.')
return self.render_to_response(self.get_context_data(
preview_data=data,
))
class ProblemMarkdownPreviewView(MarkdownPreviewView):
template_name = 'problem/preview.html'
class BlogMarkdownPreviewView(MarkdownPreviewView):
template_name = 'blog/preview.html'
class ContestMarkdownPreviewView(MarkdownPreviewView):
template_name = 'contest/preview.html'
class CommentMarkdownPreviewView(MarkdownPreviewView):
template_name = 'comments/preview.html'
class ProfileMarkdownPreviewView(MarkdownPreviewView):
template_name = 'user/preview.html'
class OrganizationMarkdownPreviewView(MarkdownPreviewView):
template_name = 'organization/preview.html'
class SolutionMarkdownPreviewView(MarkdownPreviewView):
template_name = 'solution-preview.html'
class LicenseMarkdownPreviewView(MarkdownPreviewView):
template_name = 'license-preview.html'
class TicketMarkdownPreviewView(MarkdownPreviewView):
template_name = 'ticket/preview.html'

674
judge/views/problem.py Normal file
View file

@ -0,0 +1,674 @@
import logging
import os
import shutil
from datetime import timedelta
from operator import itemgetter
from random import randrange
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import transaction
from django.db.models import Count, F, Prefetch, Q
from django.db.utils import ProgrammingError
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.template.loader import get_template
from django.urls import reverse
from django.utils import timezone, translation
from django.utils.functional import cached_property
from django.utils.html import escape, format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _, gettext_lazy
from django.views.generic import ListView, View
from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin
from judge.comments import CommentedDetailView
from judge.forms import ProblemCloneForm, ProblemSubmitForm
from judge.models import ContestProblem, ContestSubmission, Judge, Language, Problem, ProblemGroup, \
ProblemTranslation, ProblemType, RuntimeVersion, Solution, Submission, SubmissionSource, \
TranslatedProblemForeignKeyQuerySet
from judge.pdf_problems import DefaultPdfMaker, HAS_PDF
from judge.utils.diggpaginator import DiggPaginator
from judge.utils.opengraph import generate_opengraph
from judge.utils.problems import contest_attempted_ids, contest_completed_ids, hot_problems, user_attempted_ids, \
user_completed_ids
from judge.utils.strings import safe_float_or_none, safe_int_or_none
from judge.utils.tickets import own_ticket_filter
from judge.utils.views import QueryStringSortMixin, SingleObjectFormView, TitleMixin, generic_message
def get_contest_problem(problem, profile):
try:
return problem.contests.get(contest_id=profile.current_contest.contest_id)
except ObjectDoesNotExist:
return None
def get_contest_submission_count(problem, profile, virtual):
return profile.current_contest.submissions.exclude(submission__status__in=['IE']) \
.filter(problem__problem__code=problem, participation__virtual=virtual).count()
class ProblemMixin(object):
model = Problem
slug_url_kwarg = 'problem'
slug_field = 'code'
def get_object(self, queryset=None):
problem = super(ProblemMixin, self).get_object(queryset)
if not problem.is_accessible_by(self.request.user):
raise Http404()
return problem
def no_such_problem(self):
code = self.kwargs.get(self.slug_url_kwarg, None)
return generic_message(self.request, _('No such problem'),
_('Could not find a problem with the code "%s".') % code, status=404)
def get(self, request, *args, **kwargs):
try:
return super(ProblemMixin, self).get(request, *args, **kwargs)
except Http404:
return self.no_such_problem()
class SolvedProblemMixin(object):
def get_completed_problems(self):
if self.in_contest:
return contest_completed_ids(self.profile.current_contest)
else:
return user_completed_ids(self.profile) if self.profile is not None else ()
def get_attempted_problems(self):
if self.in_contest:
return contest_attempted_ids(self.profile.current_contest)
else:
return user_attempted_ids(self.profile) if self.profile is not None else ()
@cached_property
def in_contest(self):
return self.profile is not None and self.profile.current_contest is not None
@cached_property
def contest(self):
return self.request.profile.current_contest.contest
@cached_property
def profile(self):
if not self.request.user.is_authenticated:
return None
return self.request.profile
class ProblemSolution(SolvedProblemMixin, ProblemMixin, TitleMixin, CommentedDetailView):
context_object_name = 'problem'
template_name = 'problem/editorial.html'
def get_title(self):
return _('Editorial for {0}').format(self.object.name)
def get_content_title(self):
return format_html(_(u'Editorial for <a href="{1}">{0}</a>'), self.object.name,
reverse('problem_detail', args=[self.object.code]))
def get_context_data(self, **kwargs):
context = super(ProblemSolution, self).get_context_data(**kwargs)
solution = get_object_or_404(Solution, problem=self.object)
if (not solution.is_public or solution.publish_on > timezone.now()) and \
not self.request.user.has_perm('judge.see_private_solution') or \
(self.request.user.is_authenticated and
self.request.profile.current_contest):
raise Http404()
context['solution'] = solution
context['has_solved_problem'] = self.object.id in self.get_completed_problems()
return context
def get_comment_page(self):
return 's:' + self.object.code
class ProblemRaw(ProblemMixin, TitleMixin, TemplateResponseMixin, SingleObjectMixin, View):
context_object_name = 'problem'
template_name = 'problem/raw.html'
def get_title(self):
return self.object.name
def get_context_data(self, **kwargs):
context = super(ProblemRaw, self).get_context_data(**kwargs)
context['problem_name'] = self.object.name
context['url'] = self.request.build_absolute_uri()
context['description'] = self.object.description
return context
def get(self, request, *args, **kwargs):
self.object = self.get_object()
with translation.override(settings.LANGUAGE_CODE):
return self.render_to_response(self.get_context_data(
object=self.object,
))
class ProblemDetail(ProblemMixin, SolvedProblemMixin, CommentedDetailView):
context_object_name = 'problem'
template_name = 'problem/problem.html'
def get_comment_page(self):
return 'p:%s' % self.object.code
def get_context_data(self, **kwargs):
context = super(ProblemDetail, self).get_context_data(**kwargs)
user = self.request.user
authed = user.is_authenticated
context['has_submissions'] = authed and Submission.objects.filter(user=user.profile,
problem=self.object).exists()
contest_problem = (None if not authed or user.profile.current_contest is None else
get_contest_problem(self.object, user.profile))
context['contest_problem'] = contest_problem
if contest_problem:
clarifications = self.object.clarifications
context['has_clarifications'] = clarifications.count() > 0
context['clarifications'] = clarifications.order_by('-date')
context['submission_limit'] = contest_problem.max_submissions
if contest_problem.max_submissions:
context['submissions_left'] = max(contest_problem.max_submissions -
get_contest_submission_count(self.object.code, user.profile,
user.profile.current_contest.virtual), 0)
context['available_judges'] = Judge.objects.filter(online=True, problems=self.object)
context['show_languages'] = self.object.allowed_languages.count() != Language.objects.count()
context['has_pdf_render'] = HAS_PDF
context['completed_problem_ids'] = self.get_completed_problems()
context['attempted_problems'] = self.get_attempted_problems()
can_edit = self.object.is_editable_by(user)
context['can_edit_problem'] = can_edit
if user.is_authenticated:
tickets = self.object.tickets
if not can_edit:
tickets = tickets.filter(own_ticket_filter(user.profile.id))
context['has_tickets'] = tickets.exists()
context['num_open_tickets'] = tickets.filter(is_open=True).values('id').distinct().count()
try:
context['editorial'] = Solution.objects.get(problem=self.object)
except ObjectDoesNotExist:
pass
try:
translation = self.object.translations.get(language=self.request.LANGUAGE_CODE)
except ProblemTranslation.DoesNotExist:
context['title'] = self.object.name
context['language'] = settings.LANGUAGE_CODE
context['description'] = self.object.description
context['translated'] = False
else:
context['title'] = translation.name
context['language'] = self.request.LANGUAGE_CODE
context['description'] = translation.description
context['translated'] = True
if not self.object.og_image or not self.object.summary:
metadata = generate_opengraph('generated-meta-problem:%s:%d' % (context['language'], self.object.id),
context['description'], 'problem')
context['meta_description'] = self.object.summary or metadata[0]
context['og_image'] = self.object.og_image or metadata[1]
return context
class LatexError(Exception):
pass
class ProblemPdfView(ProblemMixin, SingleObjectMixin, View):
logger = logging.getLogger('judge.problem.pdf')
languages = set(map(itemgetter(0), settings.LANGUAGES))
def get(self, request, *args, **kwargs):
if not HAS_PDF:
raise Http404()
language = kwargs.get('language', self.request.LANGUAGE_CODE)
if language not in self.languages:
raise Http404()
problem = self.get_object()
try:
trans = problem.translations.get(language=language)
except ProblemTranslation.DoesNotExist:
trans = None
cache = os.path.join(settings.DMOJ_PDF_PROBLEM_CACHE, '%s.%s.pdf' % (problem.code, language))
if not os.path.exists(cache):
self.logger.info('Rendering: %s.%s.pdf', problem.code, language)
with DefaultPdfMaker() as maker, translation.override(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': request.build_absolute_uri(),
'math_engine': maker.math_engine,
}).replace('"//', '"https://').replace("'//", "'https://")
maker.title = problem_name
assets = ['style.css', 'pygment-github.css']
if maker.math_engine == 'jax':
assets.append('mathjax_config.js')
for file in assets:
maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file))
maker.make()
if not maker.success:
self.logger.error('Failed to render PDF for %s', problem.code)
return HttpResponse(maker.log, status=500, content_type='text/plain')
shutil.move(maker.pdffile, cache)
response = HttpResponse()
if hasattr(settings, 'DMOJ_PDF_PROBLEM_INTERNAL') and \
request.META.get('SERVER_SOFTWARE', '').startswith('nginx/'):
response['X-Accel-Redirect'] = '%s/%s.%s.pdf' % (settings.DMOJ_PDF_PROBLEM_INTERNAL, problem.code, language)
else:
with open(cache, 'rb') as f:
response.content = f.read()
response['Content-Type'] = 'application/pdf'
response['Content-Disposition'] = 'inline; filename=%s.%s.pdf' % (problem.code, language)
return response
class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView):
model = Problem
title = gettext_lazy('Problems')
context_object_name = 'problems'
template_name = 'problem/list.html'
paginate_by = 50
sql_sort = frozenset(('points', 'ac_rate', 'user_count', 'code'))
manual_sort = frozenset(('name', 'group', 'solved', 'type'))
all_sorts = sql_sort | manual_sort
default_desc = frozenset(('points', 'ac_rate', 'user_count'))
default_sort = 'code'
def get_paginator(self, queryset, per_page, orphans=0,
allow_empty_first_page=True, **kwargs):
paginator = DiggPaginator(queryset, per_page, body=6, padding=2, orphans=orphans,
allow_empty_first_page=allow_empty_first_page, **kwargs)
if not self.in_contest:
# Get the number of pages and then add in this magic.
# noinspection PyStatementEffect
paginator.num_pages
queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE)
sort_key = self.order.lstrip('-')
if sort_key in self.sql_sort:
queryset = queryset.order_by(self.order)
elif sort_key == 'name':
queryset = queryset.order_by(self.order.replace('name', 'i18n_name'))
elif sort_key == 'group':
queryset = queryset.order_by(self.order + '__name')
elif sort_key == 'solved':
if self.request.user.is_authenticated:
profile = self.request.profile
solved = user_completed_ids(profile)
attempted = user_attempted_ids(profile)
def _solved_sort_order(problem):
if problem.id in solved:
return 1
if problem.id in attempted:
return 0
return -1
queryset = list(queryset)
queryset.sort(key=_solved_sort_order, reverse=self.order.startswith('-'))
elif sort_key == 'type':
if self.show_types:
queryset = list(queryset)
queryset.sort(key=lambda problem: problem.types_list[0] if problem.types_list else '',
reverse=self.order.startswith('-'))
paginator.object_list = queryset
return paginator
@cached_property
def profile(self):
if not self.request.user.is_authenticated:
return None
return self.request.profile
def get_contest_queryset(self):
queryset = self.profile.current_contest.contest.contest_problems.select_related('problem__group') \
.defer('problem__description').order_by('problem__code') \
.annotate(user_count=Count('submission__participation', distinct=True)) \
.order_by('order')
queryset = TranslatedProblemForeignKeyQuerySet.add_problem_i18n_name(queryset, 'i18n_name',
self.request.LANGUAGE_CODE,
'problem__name')
return [{
'id': p['problem_id'],
'code': p['problem__code'],
'name': p['problem__name'],
'i18n_name': p['i18n_name'],
'group': {'full_name': p['problem__group__full_name']},
'points': p['points'],
'partial': p['partial'],
'user_count': p['user_count'],
} for p in queryset.values('problem_id', 'problem__code', 'problem__name', 'i18n_name',
'problem__group__full_name', 'points', 'partial', 'user_count')]
def get_normal_queryset(self):
filter = Q(is_public=True)
if self.profile is not None:
filter |= Q(authors=self.profile)
filter |= Q(curators=self.profile)
filter |= Q(testers=self.profile)
queryset = Problem.objects.filter(filter).select_related('group').defer('description')
if not self.request.user.has_perm('see_organization_problem'):
filter = Q(is_organization_private=False)
if self.profile is not None:
filter |= Q(organizations__in=self.profile.organizations.all())
queryset = queryset.filter(filter)
if self.profile is not None and self.hide_solved:
queryset = queryset.exclude(id__in=Submission.objects.filter(user=self.profile, points=F('problem__points'))
.values_list('problem__id', flat=True))
if self.show_types:
queryset = queryset.prefetch_related('types')
if self.category is not None:
queryset = queryset.filter(group__id=self.category)
if self.selected_types:
queryset = queryset.filter(types__in=self.selected_types)
if 'search' in self.request.GET:
self.search_query = query = ' '.join(self.request.GET.getlist('search')).strip()
if query:
if settings.ENABLE_FTS and self.full_text:
queryset = queryset.search(query, queryset.BOOLEAN).extra(order_by=['-relevance'])
else:
queryset = queryset.filter(
Q(code__icontains=query) | Q(name__icontains=query) |
Q(translations__name__icontains=query, translations__language=self.request.LANGUAGE_CODE))
self.prepoint_queryset = queryset
if self.point_start is not None:
queryset = queryset.filter(points__gte=self.point_start)
if self.point_end is not None:
queryset = queryset.filter(points__lte=self.point_end)
return queryset.distinct()
def get_queryset(self):
if self.in_contest:
return self.get_contest_queryset()
else:
return self.get_normal_queryset()
def get_context_data(self, **kwargs):
context = super(ProblemList, self).get_context_data(**kwargs)
context['hide_solved'] = 0 if self.in_contest else int(self.hide_solved)
context['show_types'] = 0 if self.in_contest else int(self.show_types)
context['full_text'] = 0 if self.in_contest else int(self.full_text)
context['category'] = self.category
context['categories'] = ProblemGroup.objects.all()
if self.show_types:
context['selected_types'] = self.selected_types
context['problem_types'] = ProblemType.objects.all()
context['has_fts'] = settings.ENABLE_FTS
context['search_query'] = self.search_query
context['completed_problem_ids'] = self.get_completed_problems()
context['attempted_problems'] = self.get_attempted_problems()
context.update(self.get_sort_paginate_context())
if not self.in_contest:
context.update(self.get_sort_context())
context['hot_problems'] = hot_problems(timedelta(days=1), 7)
context['point_start'], context['point_end'], context['point_values'] = self.get_noui_slider_points()
else:
context['hot_problems'] = None
context['point_start'], context['point_end'], context['point_values'] = 0, 0, {}
context['hide_contest_scoreboard'] = self.contest.hide_scoreboard
return context
def get_noui_slider_points(self):
points = sorted(self.prepoint_queryset.values_list('points', flat=True).distinct())
if not points:
return 0, 0, {}
if len(points) == 1:
return points[0], points[0], {
'min': points[0] - 1,
'max': points[0] + 1,
}
start, end = points[0], points[-1]
if self.point_start is not None:
start = self.point_start
if self.point_end is not None:
end = self.point_end
points_map = {0.0: 'min', 1.0: 'max'}
size = len(points) - 1
return start, end, {points_map.get(i / size, '%.2f%%' % (100 * i / size,)): j for i, j in enumerate(points)}
def GET_with_session(self, request, key):
if not request.GET:
return request.session.get(key, False)
return request.GET.get(key, None) == '1'
def setup_problem_list(self, request):
self.hide_solved = self.GET_with_session(request, 'hide_solved')
self.show_types = self.GET_with_session(request, 'show_types')
self.full_text = self.GET_with_session(request, 'full_text')
self.search_query = None
self.category = None
self.selected_types = []
# This actually copies into the instance dictionary...
self.all_sorts = set(self.all_sorts)
if not self.show_types:
self.all_sorts.discard('type')
self.category = safe_int_or_none(request.GET.get('category'))
if 'type' in request.GET:
try:
self.selected_types = list(map(int, request.GET.getlist('type')))
except ValueError:
pass
self.point_start = safe_float_or_none(request.GET.get('point_start'))
self.point_end = safe_float_or_none(request.GET.get('point_end'))
def get(self, request, *args, **kwargs):
self.setup_problem_list(request)
try:
return super(ProblemList, self).get(request, *args, **kwargs)
except ProgrammingError as e:
return generic_message(request, 'FTS syntax error', e.args[1], status=400)
def post(self, request, *args, **kwargs):
to_update = ('hide_solved', 'show_types', 'full_text')
for key in to_update:
if key in request.GET:
val = request.GET.get(key) == '1'
request.session[key] = val
else:
request.session.pop(key, None)
return HttpResponseRedirect(request.get_full_path())
class LanguageTemplateAjax(View):
def get(self, request, *args, **kwargs):
try:
language = get_object_or_404(Language, id=int(request.GET.get('id', 0)))
except ValueError:
raise Http404()
return HttpResponse(language.template, content_type='text/plain')
class RandomProblem(ProblemList):
def get(self, request, *args, **kwargs):
self.setup_problem_list(request)
if self.in_contest:
raise Http404()
queryset = self.get_normal_queryset()
count = queryset.count()
if not count:
return HttpResponseRedirect('%s%s%s' % (reverse('problem_list'), request.META['QUERY_STRING'] and '?',
request.META['QUERY_STRING']))
return HttpResponseRedirect(queryset[randrange(count)].get_absolute_url())
user_logger = logging.getLogger('judge.user')
@login_required
def problem_submit(request, problem=None, submission=None):
if submission is not None and not request.user.has_perm('judge.resubmit_other') and \
get_object_or_404(Submission, id=int(submission)).user.user != request.user:
raise PermissionDenied()
profile = request.profile
if request.method == 'POST':
form = ProblemSubmitForm(request.POST, instance=Submission(user=profile))
if form.is_valid():
if (not request.user.has_perm('judge.spam_submission') and
Submission.objects.filter(user=profile, was_rejudged=False)
.exclude(status__in=['D', 'IE', 'CE', 'AB']).count() >= settings.DMOJ_SUBMISSION_LIMIT):
return HttpResponse('<h1>You submitted too many submissions.</h1>', status=429)
if not form.cleaned_data['problem'].allowed_languages.filter(
id=form.cleaned_data['language'].id).exists():
raise PermissionDenied()
if not form.cleaned_data['problem'].is_accessible_by(request.user):
user_logger.info('Naughty user %s wants to submit to %s without permission',
request.user.username, form.cleaned_data['problem'].code)
return HttpResponseForbidden('<h1>Do you want me to ban you?</h1>')
if not request.user.is_superuser and form.cleaned_data['problem'].banned_users.filter(
id=profile.id).exists():
return generic_message(request, _('Banned from submitting'),
_('You have been declared persona non grata for this problem. '
'You are permanently barred from submitting this problem.'))
with transaction.atomic():
if profile.current_contest is not None:
contest_id = profile.current_contest.contest_id
try:
contest_problem = form.cleaned_data['problem'].contests.get(contest_id=contest_id)
except ContestProblem.DoesNotExist:
model = form.save()
else:
max_subs = contest_problem.max_submissions
if max_subs and get_contest_submission_count(problem, profile,
profile.current_contest.virtual) >= max_subs:
return generic_message(request, _('Too many submissions'),
_('You have exceeded the submission limit for this problem.'))
model = form.save()
model.contest_object_id = contest_id
contest = ContestSubmission(submission=model, problem=contest_problem,
participation=profile.current_contest)
contest.save()
else:
model = form.save()
# Create the SubmissionSource object
source = SubmissionSource(submission=model, source=form.cleaned_data['source'])
source.save()
profile.update_contest()
# Save a query
model.source = source
model.judge(rejudge=False)
return HttpResponseRedirect(reverse('submission_status', args=[str(model.id)]))
else:
form_data = form.cleaned_data
if submission is not None:
sub = get_object_or_404(Submission, id=int(submission))
if 'problem' not in form_data:
return HttpResponseBadRequest()
else:
initial = {'language': profile.language}
if problem is not None:
initial['problem'] = get_object_or_404(Problem, code=problem)
problem_object = initial['problem']
if not problem_object.is_accessible_by(request.user):
raise Http404()
if submission is not None:
try:
sub = get_object_or_404(Submission.objects.select_related('source', 'language'), id=int(submission))
initial['source'] = sub.source.source
initial['language'] = sub.language
except ValueError:
raise Http404()
form = ProblemSubmitForm(initial=initial)
form_data = initial
if 'problem' in form_data:
form.fields['language'].queryset = (
form_data['problem'].usable_languages.order_by('name', 'key')
.prefetch_related(Prefetch('runtimeversion_set', RuntimeVersion.objects.order_by('priority')))
)
problem_object = form_data['problem']
if 'language' in form_data:
form.fields['source'].widget.mode = form_data['language'].ace
form.fields['source'].widget.theme = profile.ace_theme
if submission is not None:
default_lang = sub.language
else:
default_lang = request.profile.language
submission_limit = submissions_left = None
if profile.current_contest is not None:
try:
submission_limit = problem_object.contests.get(contest=profile.current_contest.contest).max_submissions
except ContestProblem.DoesNotExist:
pass
else:
if submission_limit:
submissions_left = submission_limit - get_contest_submission_count(problem, profile,
profile.current_contest.virtual)
return render(request, 'problem/submit.html', {
'form': form,
'title': _('Submit to %(problem)s') % {
'problem': problem_object.translated_name(request.LANGUAGE_CODE),
},
'content_title': mark_safe(escape(_('Submit to %(problem)s')) % {
'problem': format_html('<a href="{0}">{1}</a>',
reverse('problem_detail', args=[problem_object.code]),
problem_object.translated_name(request.LANGUAGE_CODE)),
}),
'langs': Language.objects.all(),
'no_judges': not form.fields['language'].queryset,
'submission_limit': submission_limit,
'submissions_left': submissions_left,
'ACE_URL': settings.ACE_URL,
'default_lang': default_lang,
})
class ProblemClone(ProblemMixin, PermissionRequiredMixin, TitleMixin, SingleObjectFormView):
title = _('Clone Problem')
template_name = 'problem/clone.html'
form_class = ProblemCloneForm
permission_required = 'judge.clone_problem'
def form_valid(self, form):
problem = self.object
languages = problem.allowed_languages.all()
language_limits = problem.language_limits.all()
types = problem.types.all()
problem.pk = None
problem.is_public = False
problem.ac_rate = 0
problem.user_count = 0
problem.code = form.cleaned_data['code']
problem.save()
problem.authors.add(self.request.profile)
problem.allowed_languages.set(languages)
problem.language_limits.set(language_limits)
problem.types.set(types)
return HttpResponseRedirect(reverse('admin:judge_problem_change', args=(problem.id,)))

241
judge/views/problem_data.py Normal file
View file

@ -0,0 +1,241 @@
import json
import mimetypes
import os
from itertools import chain
from zipfile import BadZipfile, ZipFile
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ValidationError
from django.forms import BaseModelFormSet, HiddenInput, ModelForm, NumberInput, Select, formset_factory
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils.html import escape, format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
from django.views.generic import DetailView
from judge.highlight_code import highlight_code
from judge.models import Problem, ProblemData, ProblemTestCase, Submission, problem_data_storage
from judge.utils.problem_data import ProblemDataCompiler
from judge.utils.unicode import utf8text
from judge.utils.views import TitleMixin
from judge.views.problem import ProblemMixin
mimetypes.init()
mimetypes.add_type('application/x-yaml', '.yml')
def checker_args_cleaner(self):
data = self.cleaned_data['checker_args']
if not data or data.isspace():
return ''
try:
if not isinstance(json.loads(data), dict):
raise ValidationError(_('Checker arguments must be a JSON object'))
except ValueError:
raise ValidationError(_('Checker arguments is invalid JSON'))
return data
class ProblemDataForm(ModelForm):
def clean_zipfile(self):
if hasattr(self, 'zip_valid') and not self.zip_valid:
raise ValidationError(_('Your zip file is invalid!'))
return self.cleaned_data['zipfile']
clean_checker_args = checker_args_cleaner
class Meta:
model = ProblemData
fields = ['zipfile', 'generator', 'output_limit', 'output_prefix', 'checker', 'checker_args']
widgets = {
'checker_args': HiddenInput,
}
class ProblemCaseForm(ModelForm):
clean_checker_args = checker_args_cleaner
class Meta:
model = ProblemTestCase
fields = ('order', 'type', 'input_file', 'output_file', 'points',
'is_pretest', 'output_limit', 'output_prefix', 'checker', 'checker_args', 'generator_args')
widgets = {
'generator_args': HiddenInput,
'type': Select(attrs={'style': 'width: 100%'}),
'points': NumberInput(attrs={'style': 'width: 4em'}),
'output_prefix': NumberInput(attrs={'style': 'width: 4.5em'}),
'output_limit': NumberInput(attrs={'style': 'width: 6em'}),
'checker_args': HiddenInput,
}
class ProblemCaseFormSet(formset_factory(ProblemCaseForm, formset=BaseModelFormSet, extra=1, max_num=1,
can_delete=True)):
model = ProblemTestCase
def __init__(self, *args, **kwargs):
self.valid_files = kwargs.pop('valid_files', None)
super(ProblemCaseFormSet, self).__init__(*args, **kwargs)
def _construct_form(self, i, **kwargs):
form = super(ProblemCaseFormSet, self)._construct_form(i, **kwargs)
form.valid_files = self.valid_files
return form
class ProblemManagerMixin(LoginRequiredMixin, ProblemMixin, DetailView):
def get_object(self, queryset=None):
problem = super(ProblemManagerMixin, self).get_object(queryset)
if problem.is_manually_managed:
raise Http404()
if self.request.user.is_superuser or problem.is_editable_by(self.request.user):
return problem
raise Http404()
class ProblemSubmissionDiff(TitleMixin, ProblemMixin, DetailView):
template_name = 'problem/submission-diff.html'
def get_title(self):
return _('Comparing submissions for {0}').format(self.object.name)
def get_content_title(self):
return format_html(_('Comparing submissions for <a href="{1}">{0}</a>'), self.object.name,
reverse('problem_detail', args=[self.object.code]))
def get_object(self, queryset=None):
problem = super(ProblemSubmissionDiff, self).get_object(queryset)
if self.request.user.is_superuser or problem.is_editable_by(self.request.user):
return problem
raise Http404()
def get_context_data(self, **kwargs):
context = super(ProblemSubmissionDiff, self).get_context_data(**kwargs)
try:
ids = self.request.GET.getlist('id')
subs = Submission.objects.filter(id__in=ids)
except ValueError:
raise Http404
if not subs:
raise Http404
context['submissions'] = subs
# If we have associated data we can do better than just guess
data = ProblemTestCase.objects.filter(dataset=self.object, type='C')
if data:
num_cases = data.count()
else:
num_cases = subs.first().test_cases.count()
context['num_cases'] = num_cases
return context
class ProblemDataView(TitleMixin, ProblemManagerMixin):
template_name = 'problem/data.html'
def get_title(self):
return _('Editing data for {0}').format(self.object.name)
def get_content_title(self):
return mark_safe(escape(_('Editing data for %s')) % (
format_html('<a href="{1}">{0}</a>', self.object.name,
reverse('problem_detail', args=[self.object.code]))))
def get_data_form(self, post=False):
return ProblemDataForm(data=self.request.POST if post else None, prefix='problem-data',
files=self.request.FILES if post else None,
instance=ProblemData.objects.get_or_create(problem=self.object)[0])
def get_case_formset(self, files, post=False):
return ProblemCaseFormSet(data=self.request.POST if post else None, prefix='cases', valid_files=files,
queryset=ProblemTestCase.objects.filter(dataset_id=self.object.pk).order_by('order'))
def get_valid_files(self, data, post=False):
try:
if post and 'problem-data-zipfile-clear' in self.request.POST:
return []
elif post and 'problem-data-zipfile' in self.request.FILES:
return ZipFile(self.request.FILES['problem-data-zipfile']).namelist()
elif data.zipfile:
return ZipFile(data.zipfile.path).namelist()
except BadZipfile:
return []
return []
def get_context_data(self, **kwargs):
context = super(ProblemDataView, self).get_context_data(**kwargs)
if 'data_form' not in context:
context['data_form'] = self.get_data_form()
valid_files = context['valid_files'] = self.get_valid_files(context['data_form'].instance)
context['data_form'].zip_valid = valid_files is not False
context['cases_formset'] = self.get_case_formset(valid_files)
context['valid_files_json'] = mark_safe(json.dumps(context['valid_files']))
context['valid_files'] = set(context['valid_files'])
context['all_case_forms'] = chain(context['cases_formset'], [context['cases_formset'].empty_form])
return context
def post(self, request, *args, **kwargs):
self.object = problem = self.get_object()
data_form = self.get_data_form(post=True)
valid_files = self.get_valid_files(data_form.instance, post=True)
data_form.zip_valid = valid_files is not False
cases_formset = self.get_case_formset(valid_files, post=True)
if data_form.is_valid() and cases_formset.is_valid():
data = data_form.save()
for case in cases_formset.save(commit=False):
case.dataset_id = problem.id
case.save()
for case in cases_formset.deleted_objects:
case.delete()
ProblemDataCompiler.generate(problem, data, problem.cases.order_by('order'), valid_files)
return HttpResponseRedirect(request.get_full_path())
return self.render_to_response(self.get_context_data(data_form=data_form, cases_formset=cases_formset,
valid_files=valid_files))
put = post
@login_required
def problem_data_file(request, problem, path):
object = get_object_or_404(Problem, code=problem)
if not object.is_editable_by(request.user):
raise Http404()
response = HttpResponse()
if hasattr(settings, 'DMOJ_PROBLEM_DATA_INTERNAL') and request.META.get('SERVER_SOFTWARE', '').startswith('nginx/'):
response['X-Accel-Redirect'] = '%s/%s/%s' % (settings.DMOJ_PROBLEM_DATA_INTERNAL, problem, path)
else:
try:
with problem_data_storage.open(os.path.join(problem, path), 'rb') as f:
response.content = f.read()
except IOError:
raise Http404()
response['Content-Type'] = 'application/octet-stream'
return response
@login_required
def problem_init_view(request, problem):
problem = get_object_or_404(Problem, code=problem)
if not request.user.is_superuser and not problem.is_editable_by(request.user):
raise Http404()
try:
with problem_data_storage.open(os.path.join(problem.code, 'init.yml'), 'rb') as f:
data = utf8text(f.read()).rstrip('\n')
except IOError:
raise Http404()
return render(request, 'problem/yaml.html', {
'raw_source': data, 'highlighted_source': highlight_code(data, 'yaml'),
'title': _('Generated init.yml for %s') % problem.name,
'content_title': mark_safe(escape(_('Generated init.yml for %s')) % (
format_html('<a href="{1}">{0}</a>', problem.name,
reverse('problem_detail', args=[problem.code])))),
})

View file

@ -0,0 +1,130 @@
from operator import itemgetter
from celery.result import AsyncResult
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
from django.urls import reverse
from django.utils.html import escape, format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _, ngettext
from django.views.generic import DetailView
from django.views.generic.detail import BaseDetailView
from judge.models import Language, Submission
from judge.tasks import apply_submission_filter, rejudge_problem_filter, rescore_problem
from judge.utils.celery import redirect_to_task_status
from judge.utils.views import TitleMixin
from judge.views.problem import ProblemMixin
class ManageProblemSubmissionMixin(ProblemMixin):
def get_object(self, queryset=None):
problem = super().get_object(queryset)
user = self.request.user
if not problem.is_subs_manageable_by(user):
raise Http404()
return problem
class ManageProblemSubmissionActionMixin(ManageProblemSubmissionMixin):
def perform_action(self):
raise NotImplementedError()
def get(self, request, *args, **kwargs):
raise Http404()
def post(self, request, *args, **kwargs):
try:
self.object = self.get_object()
except Http404:
return self.no_such_problem()
else:
return self.perform_action()
class ManageProblemSubmissionView(TitleMixin, ManageProblemSubmissionMixin, DetailView):
template_name = 'problem/manage_submission.html'
def get_title(self):
return _('Managing submissions for %s') % (self.object.name,)
def get_content_title(self):
return mark_safe(escape(_('Managing submissions for %s')) % (
format_html('<a href="{1}">{0}</a>', self.object.name,
reverse('problem_detail', args=[self.object.code]))))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['submission_count'] = self.object.submission_set.count()
context['languages'] = [(lang_id, short_name or key) for lang_id, key, short_name in
Language.objects.values_list('id', 'key', 'short_name')]
context['results'] = sorted(map(itemgetter(0), Submission.RESULT))
return context
class BaseRejudgeSubmissionsView(PermissionRequiredMixin, ManageProblemSubmissionActionMixin, BaseDetailView):
permission_required = 'judge.rejudge_submission_lot'
def perform_action(self):
if self.request.POST.get('use_range', 'off') == 'on':
try:
start = int(self.request.POST.get('start'))
end = int(self.request.POST.get('end'))
except (KeyError, ValueError):
return HttpResponseBadRequest()
id_range = (start, end)
else:
id_range = None
try:
languages = list(map(int, self.request.POST.getlist('language')))
except ValueError:
return HttpResponseBadRequest()
return self.generate_response(id_range, languages, self.request.POST.getlist('result'))
def generate_response(self, id_range, languages, results):
raise NotImplementedError()
class RejudgeSubmissionsView(BaseRejudgeSubmissionsView):
def generate_response(self, id_range, languages, results):
status = rejudge_problem_filter.delay(self.object.id, id_range, languages, results)
return redirect_to_task_status(
status, message=_('Rejudging selected submissions for %s...') % (self.object.name,),
redirect=reverse('problem_submissions_rejudge_success', args=[self.object.code, status.id]),
)
class PreviewRejudgeSubmissionsView(BaseRejudgeSubmissionsView):
def generate_response(self, id_range, languages, results):
queryset = apply_submission_filter(self.object.submission_set.all(), id_range, languages, results)
return HttpResponse(str(queryset.count()))
class RescoreAllSubmissionsView(ManageProblemSubmissionActionMixin, BaseDetailView):
def perform_action(self):
status = rescore_problem.delay(self.object.id)
return redirect_to_task_status(
status, message=_('Rescoring all submissions for %s...') % (self.object.name,),
redirect=reverse('problem_submissions_rescore_success', args=[self.object.code, status.id]),
)
def rejudge_success(request, problem, task_id):
count = AsyncResult(task_id).result
if not isinstance(count, int):
raise Http404()
messages.success(request, ngettext('Successfully scheduled %d submission for rejudging.',
'Successfully scheduled %d submissions for rejudging.', count) % (count,))
return HttpResponseRedirect(reverse('problem_manage_submissions', args=[problem]))
def rescore_success(request, problem, task_id):
count = AsyncResult(task_id).result
if not isinstance(count, int):
raise Http404()
messages.success(request, ngettext('%d submission were successfully rescored.',
'%d submissions were successfully rescored.', count) % (count,))
return HttpResponseRedirect(reverse('problem_manage_submissions', args=[problem]))

View file

@ -0,0 +1,89 @@
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext as _
from judge.models import Submission
from judge.utils.problems import get_result_data
from judge.utils.raw_sql import join_sql_subquery
from judge.views.submission import ForceContestMixin, ProblemSubmissions
__all__ = ['RankedSubmissions', 'ContestRankedSubmission']
class RankedSubmissions(ProblemSubmissions):
tab = 'best_submissions_list'
dynamic_update = False
def get_queryset(self):
if self.in_contest:
contest_join = '''INNER JOIN judge_contestsubmission AS cs ON (sub.id = cs.submission_id)
INNER JOIN judge_contestparticipation AS cp ON (cs.participation_id = cp.id)'''
points = 'cs.points'
constraint = 'AND cp.contest_id = %s'
else:
contest_join = ''
points = 'sub.points'
constraint = ''
queryset = super(RankedSubmissions, self).get_queryset().filter(user__is_unlisted=False)
join_sql_subquery(
queryset,
subquery='''
SELECT sub.id AS id
FROM (
SELECT sub.user_id AS uid, MAX(sub.points) AS points
FROM judge_submission AS sub {contest_join}
WHERE sub.problem_id = %s AND {points} > 0 {constraint}
GROUP BY sub.user_id
) AS highscore STRAIGHT_JOIN (
SELECT sub.user_id AS uid, sub.points, MIN(sub.time) as time
FROM judge_submission AS sub {contest_join}
WHERE sub.problem_id = %s AND {points} > 0 {constraint}
GROUP BY sub.user_id, {points}
) AS fastest ON (highscore.uid = fastest.uid AND highscore.points = fastest.points)
STRAIGHT_JOIN judge_submission AS sub
ON (sub.user_id = fastest.uid AND sub.time = fastest.time) {contest_join}
WHERE sub.problem_id = %s AND {points} > 0 {constraint}
GROUP BY sub.user_id
'''.format(points=points, contest_join=contest_join, constraint=constraint),
params=[self.problem.id, self.contest.id] * 3 if self.in_contest else [self.problem.id] * 3,
alias='best_subs', join_fields=[('id', 'id')],
)
if self.in_contest:
return queryset.order_by('-contest__points', 'time')
else:
return queryset.order_by('-points', 'time')
def get_title(self):
return _('Best solutions for %s') % self.problem_name
def get_content_title(self):
return format_html(_('Best solutions for <a href="{1}">{0}</a>'), self.problem_name,
reverse('problem_detail', args=[self.problem.code]))
def _get_result_data(self):
return get_result_data(super(RankedSubmissions, self).get_queryset().order_by())
class ContestRankedSubmission(ForceContestMixin, RankedSubmissions):
def get_title(self):
if self.problem.is_accessible_by(self.request.user):
return _('Best solutions for %(problem)s in %(contest)s') % {
'problem': self.problem_name, 'contest': self.contest.name,
}
return _('Best solutions for problem %(number)s in %(contest)s') % {
'number': self.get_problem_number(self.problem), 'contest': self.contest.name,
}
def get_content_title(self):
if self.problem.is_accessible_by(self.request.user):
return format_html(_('Best solutions for <a href="{1}">{0}</a> in <a href="{3}">{2}</a>'),
self.problem_name, reverse('problem_detail', args=[self.problem.code]),
self.contest.name, reverse('contest_view', args=[self.contest.key]))
return format_html(_('Best solutions for problem {0} in <a href="{2}">{1}</a>'),
self.get_problem_number(self.problem), self.contest.name,
reverse('contest_view', args=[self.contest.key]))
def _get_result_data(self):
return get_result_data(Submission.objects.filter(
problem_id=self.problem.id, contest__participation__contest_id=self.contest.id))

108
judge/views/register.py Normal file
View file

@ -0,0 +1,108 @@
# coding=utf-8
import re
from django import forms
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.auth.password_validation import get_default_password_validators
from django.forms import ChoiceField, ModelChoiceField
from django.shortcuts import render
from django.utils.translation import gettext, gettext_lazy as _
from registration.backends.default.views import (ActivationView as OldActivationView,
RegistrationView as OldRegistrationView)
from registration.forms import RegistrationForm
from sortedm2m.forms import SortedMultipleChoiceField
from judge.models import Language, Organization, Profile, TIMEZONE
from judge.utils.recaptcha import ReCaptchaField, ReCaptchaWidget
from judge.utils.subscription import Subscription, newsletter_id
from judge.widgets import Select2MultipleWidget, Select2Widget
valid_id = re.compile(r'^\w+$')
bad_mail_regex = list(map(re.compile, settings.BAD_MAIL_PROVIDER_REGEX))
class CustomRegistrationForm(RegistrationForm):
username = forms.RegexField(regex=r'^\w+$', max_length=30, label=_('Username'),
error_messages={'invalid': _('A username must contain letters, '
'numbers, or underscores')})
timezone = ChoiceField(label=_('Timezone'), choices=TIMEZONE,
widget=Select2Widget(attrs={'style': 'width:100%'}))
language = ModelChoiceField(queryset=Language.objects.all(), label=_('Preferred language'), empty_label=None,
widget=Select2Widget(attrs={'style': 'width:100%'}))
organizations = SortedMultipleChoiceField(queryset=Organization.objects.filter(is_open=True),
label=_('Organizations'), required=False,
widget=Select2MultipleWidget(attrs={'style': 'width:100%'}))
if newsletter_id is not None:
newsletter = forms.BooleanField(label=_('Subscribe to newsletter?'), initial=True, required=False)
if ReCaptchaField is not None:
captcha = ReCaptchaField(widget=ReCaptchaWidget())
def clean_email(self):
if User.objects.filter(email=self.cleaned_data['email']).exists():
raise forms.ValidationError(gettext('The email address "%s" is already taken. Only one registration '
'is allowed per address.') % self.cleaned_data['email'])
if '@' in self.cleaned_data['email']:
domain = self.cleaned_data['email'].split('@')[-1].lower()
if (domain in settings.BAD_MAIL_PROVIDERS or
any(regex.match(domain) for regex in bad_mail_regex)):
raise forms.ValidationError(gettext('Your email provider is not allowed due to history of abuse. '
'Please use a reputable email provider.'))
return self.cleaned_data['email']
class RegistrationView(OldRegistrationView):
title = _('Registration')
form_class = CustomRegistrationForm
template_name = 'registration/registration_form.html'
def get_context_data(self, **kwargs):
if 'title' not in kwargs:
kwargs['title'] = self.title
tzmap = settings.TIMEZONE_MAP
kwargs['TIMEZONE_MAP'] = tzmap or 'http://momentjs.com/static/img/world.png'
kwargs['TIMEZONE_BG'] = settings.TIMEZONE_BG if tzmap else '#4E7CAD'
kwargs['password_validators'] = get_default_password_validators()
kwargs['tos_url'] = settings.TERMS_OF_SERVICE_URL
return super(RegistrationView, self).get_context_data(**kwargs)
def register(self, form):
user = super(RegistrationView, self).register(form)
profile, _ = Profile.objects.get_or_create(user=user, defaults={
'language': Language.get_python3(),
})
cleaned_data = form.cleaned_data
profile.timezone = cleaned_data['timezone']
profile.language = cleaned_data['language']
profile.organizations.add(*cleaned_data['organizations'])
profile.save()
if newsletter_id is not None and cleaned_data['newsletter']:
Subscription(user=user, newsletter_id=newsletter_id, subscribed=True).save()
return user
def get_initial(self, *args, **kwargs):
initial = super(RegistrationView, self).get_initial(*args, **kwargs)
initial['timezone'] = settings.DEFAULT_USER_TIME_ZONE
initial['language'] = Language.objects.get(key=settings.DEFAULT_USER_LANGUAGE)
return initial
class ActivationView(OldActivationView):
title = _('Registration')
template_name = 'registration/activate.html'
def get_context_data(self, **kwargs):
if 'title' not in kwargs:
kwargs['title'] = self.title
return super(ActivationView, self).get_context_data(**kwargs)
def social_auth_error(request):
return render(request, 'generic-message.html', {
'title': gettext('Authentication failure'),
'message': request.GET.get('message'),
})

138
judge/views/select2.py Normal file
View file

@ -0,0 +1,138 @@
from django.db.models import F, Q
from django.http import Http404, JsonResponse
from django.shortcuts import get_object_or_404
from django.utils.encoding import smart_text
from django.views.generic.list import BaseListView
from judge.jinja2.gravatar import gravatar
from judge.models import Comment, Contest, Organization, Problem, Profile
def _get_user_queryset(term):
qs = Profile.objects
if term.endswith(' '):
qs = qs.filter(user__username=term.strip())
else:
qs = qs.filter(user__username__icontains=term)
return qs
class Select2View(BaseListView):
paginate_by = 20
def get(self, request, *args, **kwargs):
self.request = request
self.term = kwargs.get('term', request.GET.get('term', ''))
self.object_list = self.get_queryset()
context = self.get_context_data()
return JsonResponse({
'results': [
{
'text': smart_text(self.get_name(obj)),
'id': obj.pk,
} for obj in context['object_list']],
'more': context['page_obj'].has_next(),
})
def get_name(self, obj):
return str(obj)
class UserSelect2View(Select2View):
def get_queryset(self):
return _get_user_queryset(self.term).annotate(username=F('user__username')).only('id')
def get_name(self, obj):
return obj.username
class OrganizationSelect2View(Select2View):
def get_queryset(self):
return Organization.objects.filter(name__icontains=self.term)
class ProblemSelect2View(Select2View):
def get_queryset(self):
queryset = Problem.objects.filter(Q(code__icontains=self.term) | Q(name__icontains=self.term))
if not self.request.user.has_perm('judge.see_private_problem'):
filter = Q(is_public=True)
if self.request.user.is_authenticated:
filter |= Q(authors=self.request.profile) | Q(curators=self.request.profile)
queryset = queryset.filter(filter).distinct()
return queryset.distinct()
class ContestSelect2View(Select2View):
def get_queryset(self):
queryset = Contest.objects.filter(Q(key__icontains=self.term) | Q(name__icontains=self.term))
if not self.request.user.has_perm('judge.see_private_contest'):
queryset = queryset.filter(is_visible=True)
if not self.request.user.has_perm('judge.edit_all_contest'):
q = Q(is_private=False, is_organization_private=False)
if self.request.user.is_authenticated:
q |= Q(is_organization_private=True,
organizations__in=self.request.profile.organizations.all())
q |= Q(is_private=True, private_contestants=self.request.profile)
queryset = queryset.filter(q)
return queryset
class CommentSelect2View(Select2View):
def get_queryset(self):
return Comment.objects.filter(page__icontains=self.term)
class UserSearchSelect2View(BaseListView):
paginate_by = 20
def get_queryset(self):
return _get_user_queryset(self.term)
def get(self, request, *args, **kwargs):
self.request = request
self.kwargs = kwargs
self.term = kwargs.get('term', request.GET.get('term', ''))
self.gravatar_size = request.GET.get('gravatar_size', 128)
self.gravatar_default = request.GET.get('gravatar_default', None)
self.object_list = self.get_queryset().values_list('pk', 'user__username', 'user__email', 'display_rank')
context = self.get_context_data()
return JsonResponse({
'results': [
{
'text': username,
'id': username,
'gravatar_url': gravatar(email, self.gravatar_size, self.gravatar_default),
'display_rank': display_rank,
} for pk, username, email, display_rank in context['object_list']],
'more': context['page_obj'].has_next(),
})
def get_name(self, obj):
return str(obj)
class ContestUserSearchSelect2View(UserSearchSelect2View):
def get_queryset(self):
contest = get_object_or_404(Contest, key=self.kwargs['contest'])
if not contest.can_see_scoreboard(self.request.user) or \
contest.hide_scoreboard and contest.is_in_contest(self.request.user):
raise Http404()
return Profile.objects.filter(contest_history__contest=contest,
user__username__icontains=self.term).distinct()
class TicketUserSelect2View(UserSearchSelect2View):
def get_queryset(self):
return Profile.objects.filter(tickets__isnull=False,
user__username__icontains=self.term).distinct()
class AssigneeSelect2View(UserSearchSelect2View):
def get_queryset(self):
return Profile.objects.filter(assigned_tickets__isnull=False,
user__username__icontains=self.term).distinct()

68
judge/views/stats.py Normal file
View file

@ -0,0 +1,68 @@
from itertools import chain, repeat
from operator import itemgetter
from django.conf import settings
from django.db.models import Case, Count, FloatField, IntegerField, Value, When
from django.db.models.expressions import CombinedExpression
from django.http import JsonResponse
from django.shortcuts import render
from django.utils.translation import gettext as _
from judge.models import Language, Submission
from judge.utils.stats import chart_colors, get_bar_chart, get_pie_chart, highlight_colors
ac_count = Count(Case(When(submission__result='AC', then=Value(1)), output_field=IntegerField()))
def repeat_chain(iterable):
return chain.from_iterable(repeat(iterable))
def language_data(request, language_count=Language.objects.annotate(count=Count('submission'))):
languages = language_count.filter(count__gt=0).values('key', 'name', 'count').order_by('-count')
num_languages = min(len(languages), settings.DMOJ_STATS_LANGUAGE_THRESHOLD)
other_count = sum(map(itemgetter('count'), languages[num_languages:]))
return JsonResponse({
'labels': list(map(itemgetter('name'), languages[:num_languages])) + ['Other'],
'datasets': [
{
'backgroundColor': chart_colors[:num_languages] + ['#FDB45C'],
'highlightBackgroundColor': highlight_colors[:num_languages] + ['#FFC870'],
'data': list(map(itemgetter('count'), languages[:num_languages])) + [other_count],
},
],
}, safe=False)
def ac_language_data(request):
return language_data(request, Language.objects.annotate(count=ac_count))
def status_data(request, statuses=None):
if not statuses:
statuses = (Submission.objects.values('result').annotate(count=Count('result'))
.values('result', 'count').order_by('-count'))
data = []
for status in statuses:
res = status['result']
if not res:
continue
count = status['count']
data.append((str(Submission.USER_DISPLAY_CODES[res]), count))
return JsonResponse(get_pie_chart(data), safe=False)
def ac_rate(request):
rate = CombinedExpression(ac_count / Count('submission'), '*', Value(100.0), output_field=FloatField())
data = Language.objects.annotate(total=Count('submission'), ac_rate=rate).filter(total__gt=0) \
.order_by('total').values_list('name', 'ac_rate')
return JsonResponse(get_bar_chart(list(data)))
def language(request):
return render(request, 'stats/language.html', {
'title': _('Language statistics'), 'tab': 'language',
})

111
judge/views/status.py Normal file
View file

@ -0,0 +1,111 @@
from collections import defaultdict
from functools import partial
from django.shortcuts import render
from django.utils import six
from django.utils.translation import gettext as _
from packaging import version
from judge.models import Judge, Language, RuntimeVersion
__all__ = ['status_all', 'status_table']
def get_judges(request):
if request.user.is_superuser or request.user.is_staff:
return True, Judge.objects.order_by('-online', 'name')
else:
return False, Judge.objects.filter(online=True)
def status_all(request):
see_all, judges = get_judges(request)
return render(request, 'status/judge-status.html', {
'title': _('Status'),
'judges': judges,
'see_all_judges': see_all,
})
def status_table(request):
see_all, judges = get_judges(request)
return render(request, 'status/judge-status-table.html', {
'judges': judges,
'see_all_judges': see_all,
})
class LatestList(list):
__slots__ = ('versions', 'is_latest')
def compare_version_list(x, y):
if sorted(x.keys()) != sorted(y.keys()):
return False
for k in x.keys():
if len(x[k]) != len(y[k]):
return False
for a, b in zip(x[k], y[k]):
if a.name != b.name:
return False
if a.version != b.version:
return False
return True
def version_matrix(request):
matrix = defaultdict(partial(defaultdict, LatestList))
latest = defaultdict(list)
groups = defaultdict(list)
judges = {judge.id: judge.name for judge in Judge.objects.filter(online=True)}
languages = Language.objects.filter(judges__online=True).distinct()
for runtime in RuntimeVersion.objects.filter(judge__online=True).order_by('priority'):
matrix[runtime.judge_id][runtime.language_id].append(runtime)
for judge, data in six.iteritems(matrix):
name_tuple = judges[judge].rpartition('.')
groups[name_tuple[0] or name_tuple[-1]].append((judges[judge], data))
matrix = {}
for group, data in six.iteritems(groups):
if len(data) == 1:
judge, data = data[0]
matrix[judge] = data
continue
ds = list(range(len(data)))
size = [1] * len(data)
for i, (p, x) in enumerate(data):
if ds[i] != i:
continue
for j, (q, y) in enumerate(data):
if i != j and compare_version_list(x, y):
ds[j] = i
size[i] += 1
size[j] = 0
rep = max(range(len(data)), key=size.__getitem__)
matrix[group] = data[rep][1]
for i, (j, x) in enumerate(data):
if ds[i] != rep:
matrix[j] = x
for data in six.itervalues(matrix):
for language, versions in six.iteritems(data):
versions.versions = [version.parse(runtime.version) for runtime in versions]
if versions.versions > latest[language]:
latest[language] = versions.versions
for data in six.itervalues(matrix):
for language, versions in six.iteritems(data):
versions.is_latest = versions.versions == latest[language]
languages = sorted(languages, key=lambda lang: version.parse(lang.name))
return render(request, 'status/versions.html', {
'title': _('Version matrix'),
'judges': sorted(matrix.keys()),
'languages': languages,
'matrix': matrix,
})

525
judge/views/submission.py Normal file
View file

@ -0,0 +1,525 @@
import json
from operator import attrgetter
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.cache import cache
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied
from django.db.models import Prefetch, Q
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.html import escape, format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _, gettext_lazy
from django.views.decorators.http import require_POST
from django.views.generic import DetailView, ListView
from judge import event_poster as event
from judge.highlight_code import highlight_code
from judge.models import Contest, Language, Problem, ProblemTranslation, Profile, Submission
from judge.utils.problems import get_result_data, user_authored_ids, user_completed_ids, user_editable_ids
from judge.utils.raw_sql import use_straight_join
from judge.utils.views import DiggPaginatorMixin, TitleMixin
def submission_related(queryset):
return queryset.select_related('user__user', 'problem', 'language') \
.only('id', 'user__user__username', 'user__display_rank', 'user__rating', 'problem__name',
'problem__code', 'problem__is_public', 'language__short_name', 'language__key', 'date', 'time', 'memory',
'points', 'result', 'status', 'case_points', 'case_total', 'current_testcase', 'contest_object')
class SubmissionMixin(object):
model = Submission
context_object_name = 'submission'
pk_url_kwarg = 'submission'
class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, DetailView):
def get_object(self, queryset=None):
submission = super(SubmissionDetailBase, self).get_object(queryset)
profile = self.request.profile
problem = submission.problem
if self.request.user.has_perm('judge.view_all_submission'):
return submission
if submission.user_id == profile.id:
return submission
if problem.is_editor(profile):
return submission
if problem.is_public or problem.testers.filter(id=profile.id).exists():
if Submission.objects.filter(user_id=profile.id, result='AC', problem_id=problem.id,
points=problem.points).exists():
return submission
raise PermissionDenied()
def get_title(self):
submission = self.object
return _('Submission of %(problem)s by %(user)s') % {
'problem': submission.problem.translated_name(self.request.LANGUAGE_CODE),
'user': submission.user.user.username,
}
def get_content_title(self):
submission = self.object
return mark_safe(escape(_('Submission of %(problem)s by %(user)s')) % {
'problem': format_html('<a href="{0}">{1}</a>',
reverse('problem_detail', args=[submission.problem.code]),
submission.problem.translated_name(self.request.LANGUAGE_CODE)),
'user': format_html('<a href="{0}">{1}</a>',
reverse('user_page', args=[submission.user.user.username]),
submission.user.user.username),
})
class SubmissionSource(SubmissionDetailBase):
template_name = 'submission/source.html'
def get_queryset(self):
return super().get_queryset().select_related('source')
def get_context_data(self, **kwargs):
context = super(SubmissionSource, self).get_context_data(**kwargs)
submission = self.object
context['raw_source'] = submission.source.source.rstrip('\n')
context['highlighted_source'] = highlight_code(submission.source.source, submission.language.pygments)
return context
def make_batch(batch, cases):
result = {'id': batch, 'cases': cases}
if batch:
result['points'] = min(map(attrgetter('points'), cases))
result['total'] = max(map(attrgetter('total'), cases))
return result
def group_test_cases(cases):
result = []
buf = []
last = None
for case in cases:
if case.batch != last and buf:
result.append(make_batch(last, buf))
buf = []
buf.append(case)
last = case.batch
if buf:
result.append(make_batch(last, buf))
return result
class SubmissionStatus(SubmissionDetailBase):
template_name = 'submission/status.html'
def get_context_data(self, **kwargs):
context = super(SubmissionStatus, self).get_context_data(**kwargs)
submission = self.object
context['last_msg'] = event.last()
context['batches'] = group_test_cases(submission.test_cases.all())
context['time_limit'] = submission.problem.time_limit
try:
lang_limit = submission.problem.language_limits.get(language=submission.language)
except ObjectDoesNotExist:
pass
else:
context['time_limit'] = lang_limit.time_limit
return context
class SubmissionTestCaseQuery(SubmissionStatus):
template_name = 'submission/status-testcases.html'
def get(self, request, *args, **kwargs):
if 'id' not in request.GET or not request.GET['id'].isdigit():
return HttpResponseBadRequest()
self.kwargs[self.pk_url_kwarg] = kwargs[self.pk_url_kwarg] = int(request.GET['id'])
return super(SubmissionTestCaseQuery, self).get(request, *args, **kwargs)
class SubmissionSourceRaw(SubmissionSource):
def get(self, request, *args, **kwargs):
submission = self.get_object()
return HttpResponse(submission.source.source, content_type='text/plain')
@require_POST
def abort_submission(request, submission):
submission = get_object_or_404(Submission, id=int(submission))
if (not request.user.is_authenticated or (submission.was_rejudged or (request.profile != submission.user)) and
not request.user.has_perm('abort_any_submission')):
raise PermissionDenied()
submission.abort()
return HttpResponseRedirect(reverse('submission_status', args=(submission.id,)))
class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
model = Submission
paginate_by = 50
show_problem = True
title = gettext_lazy('All submissions')
content_title = gettext_lazy('All submissions')
tab = 'all_submissions_list'
template_name = 'submission/list.html'
context_object_name = 'submissions'
first_page_href = None
def get_result_data(self):
result = self._get_result_data()
for category in result['categories']:
category['name'] = _(category['name'])
return result
def _get_result_data(self):
return get_result_data(self.get_queryset().order_by())
def access_check(self, request):
pass
@cached_property
def in_contest(self):
return self.request.user.is_authenticated and self.request.profile.current_contest is not None
@cached_property
def contest(self):
return self.request.profile.current_contest.contest
def _get_queryset(self):
queryset = Submission.objects.all()
use_straight_join(queryset)
queryset = submission_related(queryset.order_by('-id'))
if self.show_problem:
queryset = queryset.prefetch_related(Prefetch('problem__translations',
queryset=ProblemTranslation.objects.filter(
language=self.request.LANGUAGE_CODE), to_attr='_trans'))
if self.in_contest:
queryset = queryset.filter(contest__participation__contest_id=self.contest.id)
if self.contest.hide_scoreboard and self.contest.is_in_contest(self.request.user):
queryset = queryset.filter(contest__participation__user=self.request.profile)
else:
queryset = queryset.select_related('contest_object').defer('contest_object__description')
# This is not technically correct since contest organizers *should* see these, but
# the join would be far too messy
if not self.request.user.has_perm('judge.see_private_contest'):
queryset = queryset.exclude(contest_object_id__in=Contest.objects.filter(hide_scoreboard=True))
if self.selected_languages:
queryset = queryset.filter(language_id__in=Language.objects.filter(key__in=self.selected_languages))
if self.selected_statuses:
queryset = queryset.filter(result__in=self.selected_statuses)
return queryset
def get_queryset(self):
queryset = self._get_queryset()
if not self.in_contest:
if not self.request.user.has_perm('judge.see_private_problem'):
queryset = queryset.filter(problem__is_public=True)
if not self.request.user.has_perm('judge.see_organization_problem'):
filter = Q(problem__is_organization_private=False)
if self.request.user.is_authenticated:
filter |= Q(problem__organizations__in=self.request.profile.organizations.all())
queryset = queryset.filter(filter)
return queryset
def get_my_submissions_page(self):
return None
def get_all_submissions_page(self):
return reverse('all_submissions')
def get_searchable_status_codes(self):
hidden_codes = ['SC']
if not self.request.user.is_superuser and not self.request.user.is_staff:
hidden_codes += ['IE']
return [(key, value) for key, value in Submission.RESULT if key not in hidden_codes]
def get_context_data(self, **kwargs):
context = super(SubmissionsListBase, self).get_context_data(**kwargs)
authenticated = self.request.user.is_authenticated
context['dynamic_update'] = False
context['show_problem'] = self.show_problem
context['completed_problem_ids'] = user_completed_ids(self.request.profile) if authenticated else []
context['authored_problem_ids'] = user_authored_ids(self.request.profile) if authenticated else []
context['editable_problem_ids'] = user_editable_ids(self.request.profile) if authenticated else []
context['all_languages'] = Language.objects.all().values_list('key', 'name')
context['selected_languages'] = self.selected_languages
context['all_statuses'] = self.get_searchable_status_codes()
context['selected_statuses'] = self.selected_statuses
context['results_json'] = mark_safe(json.dumps(self.get_result_data()))
context['results_colors_json'] = mark_safe(json.dumps(settings.DMOJ_STATS_SUBMISSION_RESULT_COLORS))
context['page_suffix'] = suffix = ('?' + self.request.GET.urlencode()) if self.request.GET else ''
context['first_page_href'] = (self.first_page_href or '.') + suffix
context['my_submissions_link'] = self.get_my_submissions_page()
context['all_submissions_link'] = self.get_all_submissions_page()
context['tab'] = self.tab
return context
def get(self, request, *args, **kwargs):
check = self.access_check(request)
if check is not None:
return check
self.selected_languages = set(request.GET.getlist('language'))
self.selected_statuses = set(request.GET.getlist('status'))
if 'results' in request.GET:
return JsonResponse(self.get_result_data())
return super(SubmissionsListBase, self).get(request, *args, **kwargs)
class UserMixin(object):
def get(self, request, *args, **kwargs):
if 'user' not in kwargs:
raise ImproperlyConfigured('Must pass a user')
self.profile = get_object_or_404(Profile, user__username=kwargs['user'])
self.username = kwargs['user']
return super(UserMixin, self).get(request, *args, **kwargs)
class ConditionalUserTabMixin(object):
def get_context_data(self, **kwargs):
context = super(ConditionalUserTabMixin, self).get_context_data(**kwargs)
if self.request.user.is_authenticated and self.request.profile == self.profile:
context['tab'] = 'my_submissions_tab'
else:
context['tab'] = 'user_submissions_tab'
context['tab_username'] = self.profile.user.username
return context
class AllUserSubmissions(ConditionalUserTabMixin, UserMixin, SubmissionsListBase):
def get_queryset(self):
return super(AllUserSubmissions, self).get_queryset().filter(user_id=self.profile.id)
def get_title(self):
if self.request.user.is_authenticated and self.request.profile == self.profile:
return _('All my submissions')
return _('All submissions by %s') % self.username
def get_content_title(self):
if self.request.user.is_authenticated and self.request.profile == self.profile:
return format_html('All my submissions')
return format_html('All submissions by <a href="{1}">{0}</a>', self.username,
reverse('user_page', args=[self.username]))
def get_my_submissions_page(self):
if self.request.user.is_authenticated:
return reverse('all_user_submissions', kwargs={'user': self.request.user.username})
def get_context_data(self, **kwargs):
context = super(AllUserSubmissions, self).get_context_data(**kwargs)
context['dynamic_update'] = context['page_obj'].number == 1
context['dynamic_user_id'] = self.profile.id
context['last_msg'] = event.last()
return context
class ProblemSubmissionsBase(SubmissionsListBase):
show_problem = False
dynamic_update = True
check_contest_in_access_check = True
def get_queryset(self):
if self.in_contest and not self.contest.contest_problems.filter(problem_id=self.problem.id).exists():
raise Http404()
return super(ProblemSubmissionsBase, self)._get_queryset().filter(problem_id=self.problem.id)
def get_title(self):
return _('All submissions for %s') % self.problem_name
def get_content_title(self):
return format_html('All submissions for <a href="{1}">{0}</a>', self.problem_name,
reverse('problem_detail', args=[self.problem.code]))
def access_check_contest(self, request):
if self.in_contest and not self.contest.can_see_scoreboard(request.user):
raise Http404()
def access_check(self, request):
if not self.problem.is_accessible_by(request.user):
raise Http404()
if self.check_contest_in_access_check:
self.access_check_contest(request)
def get(self, request, *args, **kwargs):
if 'problem' not in kwargs:
raise ImproperlyConfigured(_('Must pass a problem'))
self.problem = get_object_or_404(Problem, code=kwargs['problem'])
self.problem_name = self.problem.translated_name(self.request.LANGUAGE_CODE)
return super(ProblemSubmissionsBase, self).get(request, *args, **kwargs)
def get_all_submissions_page(self):
return reverse('chronological_submissions', kwargs={'problem': self.problem.code})
def get_context_data(self, **kwargs):
context = super(ProblemSubmissionsBase, self).get_context_data(**kwargs)
if self.dynamic_update:
context['dynamic_update'] = context['page_obj'].number == 1
context['dynamic_problem_id'] = self.problem.id
context['last_msg'] = event.last()
context['best_submissions_link'] = reverse('ranked_submissions', kwargs={'problem': self.problem.code})
return context
class ProblemSubmissions(ProblemSubmissionsBase):
def get_my_submissions_page(self):
if self.request.user.is_authenticated:
return reverse('user_submissions', kwargs={'problem': self.problem.code,
'user': self.request.user.username})
class UserProblemSubmissions(ConditionalUserTabMixin, UserMixin, ProblemSubmissions):
check_contest_in_access_check = False
@cached_property
def is_own(self):
return self.request.user.is_authenticated and self.request.profile == self.profile
def access_check(self, request):
super(UserProblemSubmissions, self).access_check(request)
if not self.is_own:
self.access_check_contest(request)
def get_queryset(self):
return super(UserProblemSubmissions, self).get_queryset().filter(user_id=self.profile.id)
def get_title(self):
if self.is_own:
return _("My submissions for %(problem)s") % {'problem': self.problem_name}
return _("%(user)s's submissions for %(problem)s") % {'user': self.username, 'problem': self.problem_name}
def get_content_title(self):
if self.request.user.is_authenticated and self.request.profile == self.profile:
return format_html('''My submissions for <a href="{3}">{2}</a>''',
self.username, reverse('user_page', args=[self.username]),
self.problem_name, reverse('problem_detail', args=[self.problem.code]))
return format_html('''<a href="{1}">{0}</a>'s submissions for <a href="{3}">{2}</a>''',
self.username, reverse('user_page', args=[self.username]),
self.problem_name, reverse('problem_detail', args=[self.problem.code]))
def get_context_data(self, **kwargs):
context = super(UserProblemSubmissions, self).get_context_data(**kwargs)
context['dynamic_user_id'] = self.profile.id
return context
def single_submission(request, submission_id, show_problem=True):
request.no_profile_update = True
authenticated = request.user.is_authenticated
submission = get_object_or_404(submission_related(Submission.objects.all()), id=int(submission_id))
if not submission.problem.is_accessible_by(request.user):
raise Http404()
return render(request, 'submission/row.html', {
'submission': submission,
'authored_problem_ids': user_authored_ids(request.profile) if authenticated else [],
'completed_problem_ids': user_completed_ids(request.profile) if authenticated else [],
'editable_problem_ids': user_editable_ids(request.profile) if authenticated else [],
'show_problem': show_problem,
'problem_name': show_problem and submission.problem.translated_name(request.LANGUAGE_CODE),
'profile_id': request.profile.id if authenticated else 0,
})
def single_submission_query(request):
request.no_profile_update = True
if 'id' not in request.GET or not request.GET['id'].isdigit():
return HttpResponseBadRequest()
try:
show_problem = int(request.GET.get('show_problem', '1'))
except ValueError:
return HttpResponseBadRequest()
return single_submission(request, int(request.GET['id']), bool(show_problem))
class AllSubmissions(SubmissionsListBase):
stats_update_interval = 3600
def get_my_submissions_page(self):
if self.request.user.is_authenticated:
return reverse('all_user_submissions', kwargs={'user': self.request.user.username})
def get_context_data(self, **kwargs):
context = super(AllSubmissions, self).get_context_data(**kwargs)
context['dynamic_update'] = context['page_obj'].number == 1
context['last_msg'] = event.last()
context['stats_update_interval'] = self.stats_update_interval
return context
def _get_result_data(self):
if self.in_contest or self.selected_languages or self.selected_statuses:
return super(AllSubmissions, self)._get_result_data()
key = 'global_submission_result_data'
result = cache.get(key)
if result:
return result
result = super(AllSubmissions, self)._get_result_data()
cache.set(key, result, self.stats_update_interval)
return result
class ForceContestMixin(object):
@property
def in_contest(self):
return True
@property
def contest(self):
return self._contest
def access_check(self, request):
super(ForceContestMixin, self).access_check(request)
if not request.user.has_perm('judge.see_private_contest'):
if not self.contest.is_visible:
raise Http404()
if self.contest.start_time is not None and self.contest.start_time > timezone.now():
raise Http404()
def get_problem_number(self, problem):
return self.contest.contest_problems.select_related('problem').get(problem=problem).order
def get(self, request, *args, **kwargs):
if 'contest' not in kwargs:
raise ImproperlyConfigured(_('Must pass a contest'))
self._contest = get_object_or_404(Contest, key=kwargs['contest'])
return super(ForceContestMixin, self).get(request, *args, **kwargs)
class UserContestSubmissions(ForceContestMixin, UserProblemSubmissions):
def get_title(self):
if self.problem.is_accessible_by(self.request.user):
return "%s's submissions for %s in %s" % (self.username, self.problem_name, self.contest.name)
return "%s's submissions for problem %s in %s" % (
self.username, self.get_problem_number(self.problem), self.contest.name)
def access_check(self, request):
super(UserContestSubmissions, self).access_check(request)
if not self.contest.users.filter(user_id=self.profile.id).exists():
raise Http404()
def get_content_title(self):
if self.problem.is_accessible_by(self.request.user):
return format_html(_('<a href="{1}">{0}</a>\'s submissions for '
'<a href="{3}">{2}</a> in <a href="{5}">{4}</a>'),
self.username, reverse('user_page', args=[self.username]),
self.problem_name, reverse('problem_detail', args=[self.problem.code]),
self.contest.name, reverse('contest_view', args=[self.contest.key]))
return format_html(_('<a href="{1}">{0}</a>\'s submissions for '
'problem {2} in <a href="{4}">{3}</a>'),
self.username, reverse('user_page', args=[self.username]),
self.get_problem_number(self.problem),
self.contest.name, reverse('contest_view', args=[self.contest.key]))

66
judge/views/tasks.py Normal file
View file

@ -0,0 +1,66 @@
import json
from functools import partial
from uuid import UUID
from celery.result import AsyncResult
from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
from django.shortcuts import render
from django.urls import reverse
from django.utils.http import is_safe_url
from judge.tasks import failure, progress, success
from judge.utils.celery import redirect_to_task_status
from judge.utils.views import short_circuit_middleware
def get_task_status(task_id):
result = AsyncResult(task_id)
info = result.result
if result.state == 'PROGRESS':
return {'code': 'PROGRESS', 'done': info['done'], 'total': info['total'], 'stage': info['stage']}
elif result.state == 'SUCCESS':
return {'code': 'SUCCESS'}
elif result.state == 'FAILURE':
return {'code': 'FAILURE', 'error': str(info)}
else:
return {'code': 'WORKING'}
def task_status(request, task_id):
try:
UUID(task_id)
except ValueError:
raise Http404()
redirect = request.GET.get('redirect')
if not is_safe_url(redirect, allowed_hosts={request.get_host()}):
redirect = None
status = get_task_status(task_id)
if status['code'] == 'SUCCESS' and redirect:
return HttpResponseRedirect(redirect)
return render(request, 'task_status.html', {
'task_id': task_id, 'task_status': json.dumps(status),
'message': request.GET.get('message', ''), 'redirect': redirect or '',
})
@short_circuit_middleware
def task_status_ajax(request):
if 'id' not in request.GET:
return HttpResponseBadRequest('Need to pass GET parameter "id"', content_type='text/plain')
return JsonResponse(get_task_status(request.GET['id']))
def demo_task(request, task, message):
if not request.user.is_superuser:
raise PermissionDenied()
result = task.delay()
return redirect_to_task_status(result, message=message, redirect=reverse('home'))
demo_success = partial(demo_task, task=success, message='Running example task that succeeds...')
demo_failure = partial(demo_task, task=failure, message='Running example task that fails...')
demo_progress = partial(demo_task, task=progress, message='Running example task that waits 10 seconds...')

326
judge/views/ticket.py Normal file
View file

@ -0,0 +1,326 @@
import json
from django import forms
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ImproperlyConfigured, PermissionDenied, ValidationError
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404
from django.template.defaultfilters import truncatechars
from django.template.loader import get_template
from django.urls import reverse, reverse_lazy
from django.utils.functional import cached_property
from django.utils.html import escape, format_html, linebreaks
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _, gettext_lazy
from django.views import View
from django.views.generic import ListView
from django.views.generic.detail import SingleObjectMixin
from judge import event_poster as event
from judge.models import Problem, Profile, Ticket, TicketMessage
from judge.utils.diggpaginator import DiggPaginator
from judge.utils.tickets import filter_visible_tickets, own_ticket_filter
from judge.utils.views import SingleObjectFormView, TitleMixin, paginate_query_context
from judge.views.problem import ProblemMixin
from judge.widgets import HeavyPreviewPageDownWidget
ticket_widget = (forms.Textarea() if HeavyPreviewPageDownWidget is None else
HeavyPreviewPageDownWidget(preview=reverse_lazy('ticket_preview'),
preview_timeout=1000, hide_preview_button=True))
class TicketForm(forms.Form):
title = forms.CharField(max_length=100, label=gettext_lazy('Ticket title'))
body = forms.CharField(widget=ticket_widget)
def __init__(self, request, *args, **kwargs):
self.request = request
super(TicketForm, self).__init__(*args, **kwargs)
self.fields['title'].widget.attrs.update({'placeholder': _('Ticket title')})
self.fields['body'].widget.attrs.update({'placeholder': _('Issue description')})
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.'))
return super(TicketForm, self).clean()
class NewTicketView(LoginRequiredMixin, SingleObjectFormView):
form_class = TicketForm
template_name = 'ticket/new.html'
def get_assignees(self):
return []
def get_form_kwargs(self):
kwargs = super(NewTicketView, self).get_form_kwargs()
kwargs['request'] = self.request
return kwargs
def form_valid(self, form):
ticket = Ticket(user=self.request.profile, title=form.cleaned_data['title'])
ticket.linked_item = self.object
ticket.save()
message = TicketMessage(ticket=ticket, user=ticket.user, body=form.cleaned_data['body'])
message.save()
ticket.assignees.set(self.get_assignees())
if event.real:
event.post('tickets', {
'type': 'new-ticket', 'id': ticket.id,
'message': message.id, 'user': ticket.user_id,
'assignees': list(ticket.assignees.values_list('id', flat=True)),
})
return HttpResponseRedirect(reverse('ticket', args=[ticket.id]))
class NewProblemTicketView(ProblemMixin, TitleMixin, NewTicketView):
template_name = 'ticket/new_problem.html'
def get_assignees(self):
return self.object.authors.all()
def get_title(self):
return _('New ticket for %s') % self.object.name
def get_content_title(self):
return mark_safe(escape(_('New ticket for %s')) %
format_html('<a href="{0}">{1}</a>', reverse('problem_detail', args=[self.object.code]),
self.object.translated_name(self.request.LANGUAGE_CODE)))
def form_valid(self, form):
if not self.object.is_accessible_by(self.request.user):
raise Http404()
return super().form_valid(form)
class TicketCommentForm(forms.Form):
body = forms.CharField(widget=ticket_widget)
class TicketMixin(object):
model = Ticket
def get_object(self, queryset=None):
ticket = super(TicketMixin, self).get_object(queryset)
profile_id = self.request.profile.id
if self.request.user.has_perm('judge.change_ticket'):
return ticket
if ticket.user_id == profile_id:
return ticket
if ticket.assignees.filter(id=profile_id).exists():
return ticket
linked = ticket.linked_item
if isinstance(linked, Problem) and linked.is_editable_by(self.request.user):
return ticket
raise PermissionDenied()
class TicketView(TitleMixin, LoginRequiredMixin, TicketMixin, SingleObjectFormView):
form_class = TicketCommentForm
template_name = 'ticket/ticket.html'
context_object_name = 'ticket'
def form_valid(self, form):
message = TicketMessage(user=self.request.profile,
body=form.cleaned_data['body'],
ticket=self.object)
message.save()
if event.real:
event.post('tickets', {
'type': 'ticket-message', 'id': self.object.id,
'message': message.id, 'user': self.object.user_id,
'assignees': list(self.object.assignees.values_list('id', flat=True)),
})
event.post('ticket-%d' % self.object.id, {
'type': 'ticket-message', 'message': message.id,
})
return HttpResponseRedirect('%s#message-%d' % (reverse('ticket', args=[self.object.id]), message.id))
def get_title(self):
return _('%(title)s - Ticket %(id)d') % {'title': self.object.title, 'id': self.object.id}
def get_context_data(self, **kwargs):
context = super(TicketView, self).get_context_data(**kwargs)
context['ticket_messages'] = self.object.messages.select_related('user__user')
context['assignees'] = self.object.assignees.select_related('user')
context['last_msg'] = event.last()
return context
class TicketStatusChangeView(LoginRequiredMixin, TicketMixin, SingleObjectMixin, View):
open = None
def post(self, request, *args, **kwargs):
if self.open is None:
raise ImproperlyConfigured('Need to define open')
ticket = self.get_object()
if ticket.is_open != self.open:
ticket.is_open = self.open
ticket.save()
if event.real:
event.post('tickets', {
'type': 'ticket-status', 'id': ticket.id,
'open': self.open, 'user': ticket.user_id,
'assignees': list(ticket.assignees.values_list('id', flat=True)),
'title': ticket.title,
})
event.post('ticket-%d' % ticket.id, {
'type': 'ticket-status', 'open': self.open,
})
return HttpResponse(status=204)
class TicketNotesForm(forms.Form):
notes = forms.CharField(widget=forms.Textarea(), required=False)
class TicketNotesEditView(LoginRequiredMixin, TicketMixin, SingleObjectFormView):
template_name = 'ticket/edit-notes.html'
form_class = TicketNotesForm
context_object_name = 'ticket'
def get_initial(self):
return {'notes': self.get_object().notes}
def form_valid(self, form):
ticket = self.get_object()
ticket.notes = notes = form.cleaned_data['notes']
ticket.save()
if notes:
return HttpResponse(linebreaks(notes, autoescape=True))
else:
return HttpResponse()
def form_invalid(self, form):
return HttpResponseBadRequest()
class TicketList(LoginRequiredMixin, ListView):
model = Ticket
template_name = 'ticket/list.html'
context_object_name = 'tickets'
paginate_by = 50
paginator_class = DiggPaginator
@cached_property
def user(self):
return self.request.user
@cached_property
def profile(self):
return self.user.profile
@cached_property
def can_edit_all(self):
return self.request.user.has_perm('judge.change_ticket')
@cached_property
def filter_users(self):
return self.request.GET.getlist('user')
@cached_property
def filter_assignees(self):
return self.request.GET.getlist('assignee')
def GET_with_session(self, key):
if not self.request.GET:
return self.request.session.get(key, False)
return self.request.GET.get(key, None) == '1'
def _get_queryset(self):
return Ticket.objects.select_related('user__user').prefetch_related('assignees__user').order_by('-id')
def get_queryset(self):
queryset = self._get_queryset()
if self.GET_with_session('own'):
queryset = queryset.filter(own_ticket_filter(self.profile.id))
elif not self.can_edit_all:
queryset = filter_visible_tickets(queryset, self.user, self.profile)
if self.filter_assignees:
queryset = queryset.filter(assignees__user__username__in=self.filter_assignees)
if self.filter_users:
queryset = queryset.filter(user__user__username__in=self.filter_users)
return queryset.distinct()
def get_context_data(self, **kwargs):
context = super(TicketList, self).get_context_data(**kwargs)
page = context['page_obj']
context['title'] = _('Tickets - Page %(number)d of %(total)d') % {
'number': page.number,
'total': page.paginator.num_pages,
}
context['can_edit_all'] = self.can_edit_all
context['filter_status'] = {
'own': self.GET_with_session('own'), 'user': self.filter_users, 'assignee': self.filter_assignees,
'user_id': json.dumps(list(Profile.objects.filter(user__username__in=self.filter_users)
.values_list('id', flat=True))),
'assignee_id': json.dumps(list(Profile.objects.filter(user__username__in=self.filter_assignees)
.values_list('id', flat=True))),
'own_id': self.profile.id if self.GET_with_session('own') else 'null',
}
context['last_msg'] = event.last()
context.update(paginate_query_context(self.request))
return context
def post(self, request, *args, **kwargs):
to_update = ('own',)
for key in to_update:
if key in request.GET:
val = request.GET.get(key) == '1'
request.session[key] = val
else:
request.session.pop(key, None)
return HttpResponseRedirect(request.get_full_path())
class ProblemTicketListView(TicketList):
def _get_queryset(self):
problem = get_object_or_404(Problem, code=self.kwargs.get('problem'))
if problem.is_editable_by(self.request.user):
return problem.tickets.order_by('-id')
elif problem.is_accessible_by(self.request.user):
return problem.tickets.filter(own_ticket_filter(self.profile.id)).order_by('-id')
raise Http404()
class TicketListDataAjax(TicketMixin, SingleObjectMixin, View):
def get(self, request, *args, **kwargs):
try:
self.kwargs['pk'] = request.GET['id']
except KeyError:
return HttpResponseBadRequest()
ticket = self.get_object()
message = ticket.messages.first()
return JsonResponse({
'row': get_template('ticket/row.html').render({'ticket': ticket}, request),
'notification': {
'title': _('New Ticket: %s') % ticket.title,
'body': '%s\n%s' % (_('#%(id)d, assigned to: %(users)s') % {
'id': ticket.id,
'users': (_(', ').join(ticket.assignees.values_list('user__username', flat=True)) or _('no one')),
}, truncatechars(message.body, 200)),
},
})
class TicketMessageDataAjax(TicketMixin, SingleObjectMixin, View):
def get(self, request, *args, **kwargs):
try:
message_id = request.GET['message']
except KeyError:
return HttpResponseBadRequest()
ticket = self.get_object()
try:
message = ticket.messages.get(id=message_id)
except TicketMessage.DoesNotExist:
return HttpResponseBadRequest()
return JsonResponse({
'message': get_template('ticket/message.html').render({'message': message}, request),
'notification': {
'title': _('New Ticket Message For: %s') % ticket.title,
'body': truncatechars(message.body, 200),
},
})

122
judge/views/totp.py Normal file
View file

@ -0,0 +1,122 @@
import base64
from io import BytesIO
import pyotp
import qrcode
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import SuccessURLAllowedHostsMixin
from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.urls import reverse
from django.utils.http import is_safe_url
from django.utils.translation import gettext as _
from django.views.generic import FormView
from judge.forms import TOTPForm
from judge.utils.views import TitleMixin
class TOTPView(TitleMixin, LoginRequiredMixin, FormView):
form_class = TOTPForm
def get_form_kwargs(self):
result = super(TOTPView, self).get_form_kwargs()
result['totp_key'] = self.profile.totp_key
return result
def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated:
self.profile = request.profile
if self.check_skip():
return self.next_page()
return super(TOTPView, self).dispatch(request, *args, **kwargs)
def check_skip(self):
raise NotImplementedError()
def next_page(self):
return HttpResponseRedirect(reverse('user_edit_profile'))
class TOTPEnableView(TOTPView):
title = _('Enable Two Factor Authentication')
template_name = 'registration/totp_enable.html'
def get(self, request, *args, **kwargs):
profile = self.profile
if not profile.totp_key:
profile.totp_key = pyotp.random_base32(length=32)
profile.save()
return self.render_to_response(self.get_context_data())
def check_skip(self):
return self.profile.is_totp_enabled
def post(self, request, *args, **kwargs):
if not self.profile.totp_key:
return HttpResponseBadRequest('No TOTP key generated on server side?')
return super(TOTPEnableView, self).post(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(TOTPEnableView, self).get_context_data(**kwargs)
context['totp_key'] = self.profile.totp_key
context['qr_code'] = self.render_qr_code(self.request.user.username, self.profile.totp_key)
return context
def form_valid(self, form):
self.profile.is_totp_enabled = True
self.profile.save()
# Make sure users don't get prompted to enter code right after enabling:
self.request.session['2fa_passed'] = True
return self.next_page()
@classmethod
def render_qr_code(cls, username, key):
totp = pyotp.TOTP(key)
uri = totp.provisioning_uri(username, settings.SITE_NAME)
qr = qrcode.QRCode(box_size=1)
qr.add_data(uri)
qr.make(fit=True)
image = qr.make_image(fill_color='black', back_color='white')
buf = BytesIO()
image.save(buf, format='PNG')
return 'data:image/png;base64,' + base64.b64encode(buf.getvalue()).decode('ascii')
class TOTPDisableView(TOTPView):
title = _('Disable Two Factor Authentication')
template_name = 'registration/totp_disable.html'
def check_skip(self):
if not self.profile.is_totp_enabled:
return True
return settings.DMOJ_REQUIRE_STAFF_2FA and self.request.user.is_staff
def form_valid(self, form):
self.profile.is_totp_enabled = False
self.profile.totp_key = None
self.profile.save()
return self.next_page()
class TOTPLoginView(SuccessURLAllowedHostsMixin, TOTPView):
title = _('Perform Two Factor Authentication')
template_name = 'registration/totp_auth.html'
def check_skip(self):
return not self.profile.is_totp_enabled or self.request.session.get('2fa_passed', False)
def next_page(self):
redirect_to = self.request.GET.get('next', '')
url_is_safe = is_safe_url(
url=redirect_to,
allowed_hosts=self.get_success_url_allowed_hosts(),
require_https=self.request.is_secure(),
)
return HttpResponseRedirect((redirect_to if url_is_safe else '') or reverse('user_page'))
def form_valid(self, form):
self.request.session['2fa_passed'] = True
return self.next_page()

324
judge/views/user.py Normal file
View file

@ -0,0 +1,324 @@
import itertools
import json
from datetime import datetime
from operator import itemgetter
from django.conf import settings
from django.contrib.auth import logout as auth_logout
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import Permission
from django.contrib.auth.views import redirect_to_login
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from django.db.models import Count, Max, Min
from django.http import Http404, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils import timezone
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _, gettext_lazy
from django.views.generic import DetailView, ListView, TemplateView
from reversion import revisions
from judge.forms import ProfileForm, newsletter_id
from judge.models import Profile, Rating, Submission
from judge.performance_points import get_pp_breakdown
from judge.ratings import rating_class, rating_progress
from judge.utils.problems import contest_completed_ids, user_completed_ids
from judge.utils.ranker import ranker
from judge.utils.subscription import Subscription
from judge.utils.unicode import utf8text
from judge.utils.views import DiggPaginatorMixin, QueryStringSortMixin, TitleMixin, generic_message
from .contests import ContestRanking
__all__ = ['UserPage', 'UserAboutPage', 'UserProblemsPage', 'users', 'edit_profile']
def remap_keys(iterable, mapping):
return [dict((mapping.get(k, k), v) for k, v in item.items()) for item in iterable]
class UserMixin(object):
model = Profile
slug_field = 'user__username'
slug_url_kwarg = 'user'
context_object_name = 'user'
def render_to_response(self, context, **response_kwargs):
return super(UserMixin, self).render_to_response(context, **response_kwargs)
class UserPage(TitleMixin, UserMixin, DetailView):
template_name = 'user/user-base.html'
def get_object(self, queryset=None):
if self.kwargs.get(self.slug_url_kwarg, None) is None:
return self.request.profile
return super(UserPage, self).get_object(queryset)
def dispatch(self, request, *args, **kwargs):
if self.kwargs.get(self.slug_url_kwarg, None) is None:
if not self.request.user.is_authenticated:
return redirect_to_login(self.request.get_full_path())
try:
return super(UserPage, self).dispatch(request, *args, **kwargs)
except Http404:
return generic_message(request, _('No such user'), _('No user handle "%s".') %
self.kwargs.get(self.slug_url_kwarg, None))
def get_title(self):
return (_('My account') if self.request.user == self.object.user else
_('User %s') % self.object.user.username)
# TODO: the same code exists in problem.py, maybe move to problems.py?
@cached_property
def profile(self):
if not self.request.user.is_authenticated:
return None
return self.request.profile
@cached_property
def in_contest(self):
return self.profile is not None and self.profile.current_contest is not None
def get_completed_problems(self):
if self.in_contest:
return contest_completed_ids(self.profile.current_contest)
else:
return user_completed_ids(self.profile) if self.profile is not None else ()
def get_context_data(self, **kwargs):
context = super(UserPage, self).get_context_data(**kwargs)
context['hide_solved'] = int(self.hide_solved)
context['authored'] = self.object.authored_problems.filter(is_public=True, is_organization_private=False) \
.order_by('code')
rating = self.object.ratings.order_by('-contest__end_time')[:1]
context['rating'] = rating[0] if rating else None
context['rank'] = Profile.objects.filter(
is_unlisted=False, performance_points__gt=self.object.performance_points,
).count() + 1
if rating:
context['rating_rank'] = Profile.objects.filter(
is_unlisted=False, rating__gt=self.object.rating,
).count() + 1
context['rated_users'] = Profile.objects.filter(is_unlisted=False, rating__isnull=False).count()
context.update(self.object.ratings.aggregate(min_rating=Min('rating'), max_rating=Max('rating'),
contests=Count('contest')))
return context
def get(self, request, *args, **kwargs):
self.hide_solved = request.GET.get('hide_solved') == '1' if 'hide_solved' in request.GET else False
return super(UserPage, self).get(request, *args, **kwargs)
EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
class UserAboutPage(UserPage):
template_name = 'user/user-about.html'
def get_context_data(self, **kwargs):
context = super(UserAboutPage, self).get_context_data(**kwargs)
ratings = context['ratings'] = self.object.ratings.order_by('-contest__end_time').select_related('contest') \
.defer('contest__description')
context['rating_data'] = mark_safe(json.dumps([{
'label': rating.contest.name,
'rating': rating.rating,
'ranking': rating.rank,
'link': reverse('contest_ranking', args=(rating.contest.key,)),
'timestamp': (rating.contest.end_time - EPOCH).total_seconds() * 1000,
'date': date_format(rating.contest.end_time, _('M j, Y, G:i')),
'class': rating_class(rating.rating),
'height': '%.3fem' % rating_progress(rating.rating),
} for rating in ratings]))
if ratings:
user_data = self.object.ratings.aggregate(Min('rating'), Max('rating'))
global_data = Rating.objects.aggregate(Min('rating'), Max('rating'))
min_ever, max_ever = global_data['rating__min'], global_data['rating__max']
min_user, max_user = user_data['rating__min'], user_data['rating__max']
delta = max_user - min_user
ratio = (max_ever - max_user) / (max_ever - min_ever) if max_ever != min_ever else 1.0
context['max_graph'] = max_user + ratio * delta
context['min_graph'] = min_user + ratio * delta - delta
return context
class UserProblemsPage(UserPage):
template_name = 'user/user-problems.html'
def get_context_data(self, **kwargs):
context = super(UserProblemsPage, self).get_context_data(**kwargs)
result = Submission.objects.filter(user=self.object, points__gt=0, problem__is_public=True,
problem__is_organization_private=False) \
.exclude(problem__in=self.get_completed_problems() if self.hide_solved else []) \
.values('problem__id', 'problem__code', 'problem__name', 'problem__points', 'problem__group__full_name') \
.distinct().annotate(points=Max('points')).order_by('problem__group__full_name', 'problem__code')
def process_group(group, problems_iter):
problems = list(problems_iter)
points = sum(map(itemgetter('points'), problems))
return {'name': group, 'problems': problems, 'points': points}
context['best_submissions'] = [
process_group(group, problems) for group, problems in itertools.groupby(
remap_keys(result, {
'problem__code': 'code', 'problem__name': 'name', 'problem__points': 'total',
'problem__group__full_name': 'group',
}), itemgetter('group'))
]
breakdown, has_more = get_pp_breakdown(self.object, start=0, end=10)
context['pp_breakdown'] = breakdown
context['pp_has_more'] = has_more
return context
class UserPerformancePointsAjax(UserProblemsPage):
template_name = 'user/pp-table-body.html'
def get_context_data(self, **kwargs):
context = super(UserPerformancePointsAjax, self).get_context_data(**kwargs)
try:
start = int(self.request.GET.get('start', 0))
end = int(self.request.GET.get('end', settings.DMOJ_PP_ENTRIES))
if start < 0 or end < 0 or start > end:
raise ValueError
except ValueError:
start, end = 0, 100
breakdown, self.has_more = get_pp_breakdown(self.object, start=start, end=end)
context['pp_breakdown'] = breakdown
return context
def get(self, request, *args, **kwargs):
httpresp = super(UserPerformancePointsAjax, self).get(request, *args, **kwargs)
httpresp.render()
return JsonResponse({
'results': utf8text(httpresp.content),
'has_more': self.has_more,
})
@login_required
def edit_profile(request):
profile = Profile.objects.get(user=request.user)
if profile.mute:
raise Http404()
if request.method == 'POST':
form = ProfileForm(request.POST, instance=profile, user=request.user)
if form.is_valid():
with transaction.atomic(), revisions.create_revision():
form.save()
revisions.set_user(request.user)
revisions.set_comment(_('Updated on site'))
if newsletter_id is not None:
try:
subscription = Subscription.objects.get(user=request.user, newsletter_id=newsletter_id)
except Subscription.DoesNotExist:
if form.cleaned_data['newsletter']:
Subscription(user=request.user, newsletter_id=newsletter_id, subscribed=True).save()
else:
if subscription.subscribed != form.cleaned_data['newsletter']:
subscription.update(('unsubscribe', 'subscribe')[form.cleaned_data['newsletter']])
perm = Permission.objects.get(codename='test_site', content_type=ContentType.objects.get_for_model(Profile))
if form.cleaned_data['test_site']:
request.user.user_permissions.add(perm)
else:
request.user.user_permissions.remove(perm)
return HttpResponseRedirect(request.path)
else:
form = ProfileForm(instance=profile, user=request.user)
if newsletter_id is not None:
try:
subscription = Subscription.objects.get(user=request.user, newsletter_id=newsletter_id)
except Subscription.DoesNotExist:
form.fields['newsletter'].initial = False
else:
form.fields['newsletter'].initial = subscription.subscribed
form.fields['test_site'].initial = request.user.has_perm('judge.test_site')
tzmap = settings.TIMEZONE_MAP
return render(request, 'user/edit-profile.html', {
'require_staff_2fa': settings.DMOJ_REQUIRE_STAFF_2FA,
'form': form, 'title': _('Edit profile'), 'profile': profile,
'has_math_config': bool(settings.MATHOID_URL),
'TIMEZONE_MAP': tzmap or 'http://momentjs.com/static/img/world.png',
'TIMEZONE_BG': settings.TIMEZONE_BG if tzmap else '#4E7CAD',
})
class UserList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ListView):
model = Profile
title = gettext_lazy('Leaderboard')
context_object_name = 'users'
template_name = 'user/list.html'
paginate_by = 100
all_sorts = frozenset(('points', 'problem_count', 'rating', 'performance_points'))
default_desc = all_sorts
default_sort = '-performance_points'
def get_queryset(self):
return (Profile.objects.filter(is_unlisted=False).order_by(self.order, 'id').select_related('user')
.only('display_rank', 'user__username', 'points', 'rating', 'performance_points',
'problem_count'))
def get_context_data(self, **kwargs):
context = super(UserList, self).get_context_data(**kwargs)
context['users'] = ranker(context['users'], rank=self.paginate_by * (context['page_obj'].number - 1))
context['first_page_href'] = '.'
context.update(self.get_sort_context())
context.update(self.get_sort_paginate_context())
return context
user_list_view = UserList.as_view()
class FixedContestRanking(ContestRanking):
contest = None
def get_object(self, queryset=None):
return self.contest
def users(request):
if request.user.is_authenticated:
participation = request.profile.current_contest
if participation is not None:
contest = participation.contest
return FixedContestRanking.as_view(contest=contest)(request, contest=contest.key)
return user_list_view(request)
def user_ranking_redirect(request):
try:
username = request.GET['handle']
except KeyError:
raise Http404()
user = get_object_or_404(Profile, user__username=username)
rank = Profile.objects.filter(is_unlisted=False, performance_points__gt=user.performance_points).count()
rank += Profile.objects.filter(
is_unlisted=False, performance_points__exact=user.performance_points, id__lt=user.id,
).count()
page = rank // UserList.paginate_by
return HttpResponseRedirect('%s%s#!%s' % (reverse('user_list'), '?page=%d' % (page + 1) if page else '', username))
class UserLogoutView(TitleMixin, TemplateView):
template_name = 'registration/logout.html'
title = 'You have been successfully logged out.'
def post(self, request, *args, **kwargs):
auth_logout(request)
return HttpResponseRedirect(request.get_full_path())

72
judge/views/widgets.py Normal file
View file

@ -0,0 +1,72 @@
import requests
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ImproperlyConfigured
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseRedirect
from django.utils.translation import gettext as _
from django.views.generic import View
from judge.models import Submission
__all__ = ['rejudge_submission', 'DetectTimezone']
@login_required
def rejudge_submission(request):
if request.method != 'POST' or not request.user.has_perm('judge.rejudge_submission') or \
not request.user.has_perm('judge.edit_own_problem'):
return HttpResponseForbidden()
if 'id' not in request.POST or not request.POST['id'].isdigit():
return HttpResponseBadRequest()
try:
submission = Submission.objects.get(id=request.POST['id'])
except Submission.DoesNotExist:
return HttpResponseBadRequest()
if not request.user.has_perm('judge.edit_all_problem') and \
not submission.problem.is_editor(request.profile):
return HttpResponseForbidden()
submission.judge(rejudge=True)
redirect = request.POST.get('path', None)
return HttpResponseRedirect(redirect) if redirect else HttpResponse('success', content_type='text/plain')
class DetectTimezone(View):
def askgeo(self, lat, long):
if not hasattr(settings, 'ASKGEO_ACCOUNT_ID') or not hasattr(settings, 'ASKGEO_ACCOUNT_API_KEY'):
raise ImproperlyConfigured()
data = requests.get('http://api.askgeo.com/v1/%s/%s/query.json?databases=TimeZone&points=%f,%f' %
(settings.ASKGEO_ACCOUNT_ID, settings.ASKGEO_ACCOUNT_API_KEY, lat, long)).json()
try:
return HttpResponse(data['data'][0]['TimeZone']['TimeZoneId'], content_type='text/plain')
except (IndexError, KeyError):
return HttpResponse(_('Invalid upstream data: %s') % data, content_type='text/plain', status=500)
def geonames(self, lat, long):
if not hasattr(settings, 'GEONAMES_USERNAME'):
raise ImproperlyConfigured()
data = requests.get('http://api.geonames.org/timezoneJSON?lat=%f&lng=%f&username=%s' %
(lat, long, settings.GEONAMES_USERNAME)).json()
try:
return HttpResponse(data['timezoneId'], content_type='text/plain')
except KeyError:
return HttpResponse(_('Invalid upstream data: %s') % data, content_type='text/plain', status=500)
def default(self, lat, long):
raise Http404()
def get(self, request, *args, **kwargs):
backend = settings.TIMEZONE_DETECT_BACKEND
try:
lat, long = float(request.GET['lat']), float(request.GET['long'])
except (ValueError, KeyError):
return HttpResponse(_('Bad latitude or longitude'), content_type='text/plain', status=404)
return {
'askgeo': self.askgeo,
'geonames': self.geonames,
}.get(backend, self.default)(lat, long)