Cloned DMOJ
This commit is contained in:
parent
f623974b58
commit
49dc9ff10c
513 changed files with 132349 additions and 39 deletions
10
judge/views/__init__.py
Normal file
10
judge/views/__init__.py
Normal 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)
|
2
judge/views/api/__init__.py
Normal file
2
judge/views/api/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
from .api_v1 import *
|
||||
from .api_v2 import *
|
173
judge/views/api/api_v1.py
Normal file
173
judge/views/api/api_v1.py
Normal 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
122
judge/views/api/api_v2.py
Normal 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
126
judge/views/blog.py
Normal 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
170
judge/views/comment.py
Normal 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
793
judge/views/contests.py
Normal 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
33
judge/views/error.py
Normal 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
18
judge/views/language.py
Normal 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
14
judge/views/license.py
Normal 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
65
judge/views/mailgun.py
Normal 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
330
judge/views/organization.py
Normal 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
50
judge/views/preview.py
Normal 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
674
judge/views/problem.py
Normal 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
241
judge/views/problem_data.py
Normal 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])))),
|
||||
})
|
130
judge/views/problem_manage.py
Normal file
130
judge/views/problem_manage.py
Normal 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]))
|
89
judge/views/ranked_submission.py
Normal file
89
judge/views/ranked_submission.py
Normal 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
108
judge/views/register.py
Normal 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
138
judge/views/select2.py
Normal 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
68
judge/views/stats.py
Normal 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
111
judge/views/status.py
Normal 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
525
judge/views/submission.py
Normal 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
66
judge/views/tasks.py
Normal 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
326
judge/views/ticket.py
Normal 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
122
judge/views/totp.py
Normal 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
324
judge/views/user.py
Normal 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
72
judge/views/widgets.py
Normal 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)
|
Loading…
Add table
Add a link
Reference in a new issue