Add live contest notification
This commit is contained in:
parent
054da6dc0d
commit
ce5ea027d2
11 changed files with 229 additions and 5 deletions
|
@ -225,6 +225,8 @@ urlpatterns = [
|
||||||
url(r'^/participation/disqualify$', contests.ContestParticipationDisqualify.as_view(),
|
url(r'^/participation/disqualify$', contests.ContestParticipationDisqualify.as_view(),
|
||||||
name='contest_participation_disqualify'),
|
name='contest_participation_disqualify'),
|
||||||
|
|
||||||
|
url(r'^/clarification$', contests.NewContestClarificationView.as_view(), name='new_contest_clarification'),
|
||||||
|
|
||||||
url(r'^/$', lambda _, contest: HttpResponsePermanentRedirect(reverse('contest_view', args=[contest]))),
|
url(r'^/$', lambda _, contest: HttpResponsePermanentRedirect(reverse('contest_view', args=[contest]))),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,9 @@ from django.db.models.functions import Coalesce
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from judge import event_poster as event
|
||||||
from judge.fulltext import SearchQuerySet
|
from judge.fulltext import SearchQuerySet
|
||||||
from judge.models.profile import Organization, Profile
|
from judge.models.profile import Organization, Profile
|
||||||
from judge.models.runtime import Language
|
from judge.models.runtime import Language
|
||||||
|
@ -407,6 +409,26 @@ class ProblemClarification(models.Model):
|
||||||
description = models.TextField(verbose_name=_('clarification body'))
|
description = models.TextField(verbose_name=_('clarification body'))
|
||||||
date = models.DateTimeField(verbose_name=_('clarification timestamp'), auto_now_add=True)
|
date = models.DateTimeField(verbose_name=_('clarification timestamp'), auto_now_add=True)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
super(ProblemClarification, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
if event.real:
|
||||||
|
from judge.models import ContestProblem
|
||||||
|
|
||||||
|
now = timezone.now()
|
||||||
|
# List all ongoing contests containing this problem
|
||||||
|
contest_problems = ContestProblem.objects.filter(
|
||||||
|
contest__start_time__lte=now,
|
||||||
|
contest__end_time__gt=now,
|
||||||
|
problem=self.problem).values_list('order', 'contest')
|
||||||
|
|
||||||
|
for order, contest_id in contest_problems.iterator():
|
||||||
|
event.post('contest_clarification_' + str(contest_id), {
|
||||||
|
'problem_label': order,
|
||||||
|
'problem_name': self.problem.name,
|
||||||
|
'problem_code': self.problem.code,
|
||||||
|
'body': self.description
|
||||||
|
})
|
||||||
|
|
||||||
class LanguageLimit(models.Model):
|
class LanguageLimit(models.Model):
|
||||||
problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='language_limits', on_delete=CASCADE)
|
problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='language_limits', on_delete=CASCADE)
|
||||||
|
|
|
@ -104,6 +104,12 @@ class PostList(ListView):
|
||||||
context['open_tickets'] = filter_visible_tickets(tickets, self.request.user, profile)[:10]
|
context['open_tickets'] = filter_visible_tickets(tickets, self.request.user, profile)[:10]
|
||||||
else:
|
else:
|
||||||
context['open_tickets'] = []
|
context['open_tickets'] = []
|
||||||
|
|
||||||
|
if self.request.in_contest:
|
||||||
|
if self.request.user.is_superuser or \
|
||||||
|
self.request.profile in self.request.participation.contest.authors.all() or \
|
||||||
|
self.request.profile in self.request.participation.contest.curators.all():
|
||||||
|
context['can_edit_contest'] = True
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -18,10 +18,10 @@ from django.db.models.expressions import CombinedExpression
|
||||||
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
|
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
from django.template.defaultfilters import date as date_filter
|
from django.template.defaultfilters import date as date_filter
|
||||||
from django.urls import reverse
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html, escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware
|
||||||
from django.utils.translation import gettext as _, gettext_lazy
|
from django.utils.translation import gettext as _, gettext_lazy
|
||||||
|
@ -32,7 +32,7 @@ from judge import event_poster as event
|
||||||
from judge.comments import CommentedDetailView
|
from judge.comments import CommentedDetailView
|
||||||
from judge.forms import ContestCloneForm
|
from judge.forms import ContestCloneForm
|
||||||
from judge.models import Contest, ContestMoss, ContestParticipation, ContestProblem, ContestTag, \
|
from judge.models import Contest, ContestMoss, ContestParticipation, ContestProblem, ContestTag, \
|
||||||
Organization, Problem, Profile, Submission
|
Organization, Problem, Profile, Submission, ProblemClarification
|
||||||
from judge.tasks import run_moss
|
from judge.tasks import run_moss
|
||||||
from judge.utils.celery import redirect_to_task_status
|
from judge.utils.celery import redirect_to_task_status
|
||||||
from judge.utils.opengraph import generate_opengraph
|
from judge.utils.opengraph import generate_opengraph
|
||||||
|
@ -40,11 +40,12 @@ from judge.utils.problems import _get_result_data
|
||||||
from judge.utils.ranker import ranker
|
from judge.utils.ranker import ranker
|
||||||
from judge.utils.stats import get_bar_chart, get_pie_chart, get_histogram
|
from judge.utils.stats import get_bar_chart, get_pie_chart, get_histogram
|
||||||
from judge.utils.views import DiggPaginatorMixin, SingleObjectFormView, TitleMixin, generic_message
|
from judge.utils.views import DiggPaginatorMixin, SingleObjectFormView, TitleMixin, generic_message
|
||||||
|
from judge.widgets import HeavyPreviewPageDownWidget
|
||||||
|
|
||||||
__all__ = ['ContestList', 'ContestDetail', 'ContestRanking', 'ContestJoin', 'ContestLeave', 'ContestCalendar',
|
__all__ = ['ContestList', 'ContestDetail', 'ContestRanking', 'ContestJoin', 'ContestLeave', 'ContestCalendar',
|
||||||
'ContestClone', 'ContestStats', 'ContestMossView', 'ContestMossDelete', 'contest_ranking_ajax',
|
'ContestClone', 'ContestStats', 'ContestMossView', 'ContestMossDelete', 'contest_ranking_ajax',
|
||||||
'ContestParticipationList', 'ContestParticipationDisqualify', 'get_contest_ranking_list',
|
'ContestParticipationList', 'ContestParticipationDisqualify', 'get_contest_ranking_list',
|
||||||
'base_contest_ranking_list']
|
'base_contest_ranking_list', 'ContestClarificationView']
|
||||||
|
|
||||||
|
|
||||||
def _find_contest(request, key, private_check=True):
|
def _find_contest(request, key, private_check=True):
|
||||||
|
@ -854,3 +855,64 @@ class ContestTagDetail(TitleMixin, ContestTagDetailAjax):
|
||||||
|
|
||||||
def get_title(self):
|
def get_title(self):
|
||||||
return _('Contest tag: %s') % self.object.name
|
return _('Contest tag: %s') % self.object.name
|
||||||
|
|
||||||
|
|
||||||
|
class ProblemClarificationForm(forms.Form):
|
||||||
|
body = forms.CharField(widget=HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'),
|
||||||
|
preview_timeout=1000, hide_preview_button=True))
|
||||||
|
|
||||||
|
def __init__(self, request, *args, **kwargs):
|
||||||
|
self.request = request
|
||||||
|
super(ProblemClarificationForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields['body'].widget.attrs.update({'placeholder': _('Issue description')})
|
||||||
|
|
||||||
|
|
||||||
|
class NewContestClarificationView(ContestMixin, TitleMixin, SingleObjectFormView):
|
||||||
|
form_class = ProblemClarificationForm
|
||||||
|
template_name = 'contest/clarification.html'
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super(NewContestClarificationView, self).get_form_kwargs()
|
||||||
|
kwargs['request'] = self.request
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def is_accessible(self):
|
||||||
|
if not self.request.user.is_authenticated:
|
||||||
|
return False
|
||||||
|
if not self.request.in_contest:
|
||||||
|
return False
|
||||||
|
if not self.request.participation.contest == self.get_object():
|
||||||
|
return False
|
||||||
|
return self.request.user.is_superuser or \
|
||||||
|
self.request.profile in self.request.participation.contest.authors.all() or \
|
||||||
|
self.request.profile in self.request.participation.contest.curators.all()
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
if not self.is_accessible():
|
||||||
|
raise Http404()
|
||||||
|
return super().get(self, request, *args, **kwargs)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
problem_code = self.request.POST['problem']
|
||||||
|
description = form.cleaned_data['body']
|
||||||
|
|
||||||
|
clarification = ProblemClarification(description=description)
|
||||||
|
clarification.problem = Problem.objects.get(code=problem_code)
|
||||||
|
clarification.save()
|
||||||
|
|
||||||
|
link = reverse('home')
|
||||||
|
return HttpResponseRedirect(link)
|
||||||
|
|
||||||
|
def get_title(self):
|
||||||
|
return "New clarification for %s" % self.object.name
|
||||||
|
|
||||||
|
def get_content_title(self):
|
||||||
|
return mark_safe(escape(_('New clarification for %s')) %
|
||||||
|
format_html('<a href="{0}">{1}</a>', reverse('problem_detail', args=[self.object.key]),
|
||||||
|
self.object.name))
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(NewContestClarificationView, self).get_context_data(**kwargs)
|
||||||
|
context['problems'] = ContestProblem.objects.filter(contest=self.object)\
|
||||||
|
.order_by('order')
|
||||||
|
return context
|
|
@ -25,6 +25,7 @@ from django.views.generic import ListView, View
|
||||||
from django.views.generic.base import TemplateResponseMixin
|
from django.views.generic.base import TemplateResponseMixin
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
|
|
||||||
|
from judge import event_poster as event
|
||||||
from judge.comments import CommentedDetailView
|
from judge.comments import CommentedDetailView
|
||||||
from judge.forms import ProblemCloneForm, ProblemSubmitForm
|
from judge.forms import ProblemCloneForm, ProblemSubmitForm
|
||||||
from judge.models import ContestProblem, ContestSubmission, Judge, Language, Problem, ProblemGroup, \
|
from judge.models import ContestProblem, ContestSubmission, Judge, Language, Problem, ProblemGroup, \
|
||||||
|
@ -170,8 +171,10 @@ class ProblemDetail(ProblemMixin, SolvedProblemMixin, CommentedDetailView):
|
||||||
contest_problem = (None if not authed or user.profile.current_contest is None else
|
contest_problem = (None if not authed or user.profile.current_contest is None else
|
||||||
get_contest_problem(self.object, user.profile))
|
get_contest_problem(self.object, user.profile))
|
||||||
context['contest_problem'] = contest_problem
|
context['contest_problem'] = contest_problem
|
||||||
|
|
||||||
if contest_problem:
|
if contest_problem:
|
||||||
clarifications = self.object.clarifications
|
clarifications = self.object.clarifications
|
||||||
|
context['last_msg'] = event.last()
|
||||||
context['has_clarifications'] = clarifications.count() > 0
|
context['has_clarifications'] = clarifications.count() > 0
|
||||||
context['clarifications'] = clarifications.order_by('-date')
|
context['clarifications'] = clarifications.order_by('-date')
|
||||||
context['submission_limit'] = contest_problem.max_submissions
|
context['submission_limit'] = contest_problem.max_submissions
|
||||||
|
@ -434,6 +437,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
|
||||||
context['hot_problems'] = hot_problems(timedelta(days=1), 7)
|
context['hot_problems'] = hot_problems(timedelta(days=1), 7)
|
||||||
context['point_start'], context['point_end'], context['point_values'] = self.get_noui_slider_points()
|
context['point_start'], context['point_end'], context['point_values'] = self.get_noui_slider_points()
|
||||||
else:
|
else:
|
||||||
|
context['last_msg'] = event.last()
|
||||||
context['hot_problems'] = None
|
context['hot_problems'] = None
|
||||||
context['point_start'], context['point_end'], context['point_values'] = 0, 0, {}
|
context['point_start'], context['point_end'], context['point_values'] = 0, 0, {}
|
||||||
context['hide_contest_scoreboard'] = self.contest.scoreboard_visibility in \
|
context['hide_contest_scoreboard'] = self.contest.scoreboard_visibility in \
|
||||||
|
|
BIN
logo.png
Normal file
BIN
logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
|
@ -322,6 +322,10 @@ window.register_notify = function (type, options) {
|
||||||
status_change();
|
status_change();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.notify_clarification = function(msg) {
|
||||||
|
var message = `Problem ${msg.problem_label} (${msg.problem_name}):\n` + msg.body;
|
||||||
|
alert(message);
|
||||||
|
}
|
||||||
|
|
||||||
$(function () {
|
$(function () {
|
||||||
// Close dismissable boxes
|
// Close dismissable boxes
|
||||||
|
|
|
@ -48,6 +48,14 @@
|
||||||
h3 a {
|
h3 a {
|
||||||
color: lightcyan;
|
color: lightcyan;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#add-clarification {
|
||||||
|
float: left;
|
||||||
|
color: chartreuse;
|
||||||
|
}
|
||||||
|
#add-clarification:hover {
|
||||||
|
color: cyan;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -104,7 +112,16 @@
|
||||||
<div class="blog-sidebar">
|
<div class="blog-sidebar">
|
||||||
{% if request.in_contest and request.participation.contest.use_clarifications %}
|
{% if request.in_contest and request.participation.contest.use_clarifications %}
|
||||||
<div class="blog-sidebox sidebox">
|
<div class="blog-sidebox sidebox">
|
||||||
<h3>{{ _('Clarifications') }} <i class="fa fa-question-circle"></i></h3>
|
<h3>{{ _('Clarifications') }}
|
||||||
|
<i class="fa fa-question-circle"></i>
|
||||||
|
{% if can_edit_contest %}
|
||||||
|
<a href="{{url('new_contest_clarification', request.participation.contest.key)}}"
|
||||||
|
class="fa fa-plus-circle"
|
||||||
|
id="add-clarification"
|
||||||
|
title="{{_('Add')}}">
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</h3>
|
||||||
<div class="sidebox-content">
|
<div class="sidebox-content">
|
||||||
{% if has_clarifications %}
|
{% if has_clarifications %}
|
||||||
<ul>
|
<ul>
|
||||||
|
|
54
templates/contest/clarification.html
Normal file
54
templates/contest/clarification.html
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block media %}
|
||||||
|
{{ form.media.css }}
|
||||||
|
<style>
|
||||||
|
form#clarification-form {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 750px;
|
||||||
|
padding-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#id_title {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
form#clarification-form .submit {
|
||||||
|
margin: 10px 0 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js_media %}
|
||||||
|
{{ form.media.js }}
|
||||||
|
<script>
|
||||||
|
$(function() {
|
||||||
|
$('#problem-select').select2({width: '40em'});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<form id="clarification-form" action="" method="POST" class="form-area">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% if form.body.errors %}
|
||||||
|
<div class="form-errors">
|
||||||
|
{{ form.body.errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<h4>
|
||||||
|
<span>{{_('Problem')}}: </span>
|
||||||
|
<select name="problem" id="problem-select">
|
||||||
|
{% for problem in problems %}
|
||||||
|
<option value="{{ problem.problem.code }}" class="point-dropdown">
|
||||||
|
{{ problem.order }}. {{problem.problem.name}}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</h4>
|
||||||
|
<div class="body-block">{{ form.body }}</div>
|
||||||
|
<button type="submit" class="submit">{{ _('Create') }}</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -132,6 +132,21 @@
|
||||||
</script>
|
</script>
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
{% if request.in_contest %}
|
{% if request.in_contest %}
|
||||||
|
{% if last_msg %}
|
||||||
|
<script type="text/javascript" src="{{ static('event.js') }}"></script>
|
||||||
|
<script>
|
||||||
|
function setup_event_contest(last_msg) {
|
||||||
|
var channel = ['contest_clarification_' + {{request.participation.contest.id}}]
|
||||||
|
return new EventReceiver(
|
||||||
|
"{{ EVENT_DAEMON_LOCATION }}", "{{ EVENT_DAEMON_POLL_LOCATION }}",
|
||||||
|
channel, last_msg, function (message) {
|
||||||
|
notify_clarification(message);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setup_event_contest();
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
<script src="{{ static('libs/tablesorter.js') }}" type="text/javascript"></script>
|
<script src="{{ static('libs/tablesorter.js') }}" type="text/javascript"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
|
@ -47,11 +47,36 @@
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#clarification_header {
|
||||||
|
color: red;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#clarification_header:hover {
|
||||||
|
color: orange;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content_js_media %}
|
{% block content_js_media %}
|
||||||
{% include "comments/media-js.html" %}
|
{% include "comments/media-js.html" %}
|
||||||
|
{% if request.in_contest %}
|
||||||
|
{% if last_msg %}
|
||||||
|
<script type="text/javascript" src="{{ static('event.js') }}"></script>
|
||||||
|
<script>
|
||||||
|
function setup_event_contest(last_msg) {
|
||||||
|
var channel = ['contest_clarification_' + {{request.participation.contest.id}}]
|
||||||
|
return new EventReceiver(
|
||||||
|
"{{ EVENT_DAEMON_LOCATION }}", "{{ EVENT_DAEMON_POLL_LOCATION }}",
|
||||||
|
channel, last_msg, function (message) {
|
||||||
|
notify_clarification(message);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setup_event_contest();
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(function() {
|
$(function() {
|
||||||
$('#pdf_button').click(async function(e) {
|
$('#pdf_button').click(async function(e) {
|
||||||
|
@ -61,6 +86,10 @@
|
||||||
}
|
}
|
||||||
frames['raw_problem'].print();
|
frames['raw_problem'].print();
|
||||||
});
|
});
|
||||||
|
$('#clarification_header').on('click', function() {
|
||||||
|
$('#clarification_header_container').hide();
|
||||||
|
window.scrollTo(0, document.body.scrollHeight);
|
||||||
|
})
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -299,6 +328,15 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block description %}
|
{% block description %}
|
||||||
|
{% if contest_problem and contest_problem.contest.use_clarifications and has_clarifications %}
|
||||||
|
<div id="clarification_header_container">
|
||||||
|
<i class="fa fa-question-circle"></i>
|
||||||
|
<a id="clarification_header">
|
||||||
|
{{ _('This problem has %d clarification(s)' % clarifications|length) }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% cache 86400 'problem_html' problem.id MATH_ENGINE LANGUAGE_CODE %}
|
{% cache 86400 'problem_html' problem.id MATH_ENGINE LANGUAGE_CODE %}
|
||||||
{{ description|markdown("problem", MATH_ENGINE)|reference|str|safe }}
|
{{ description|markdown("problem", MATH_ENGINE)|reference|str|safe }}
|
||||||
{% endcache %}
|
{% endcache %}
|
||||||
|
|
Loading…
Reference in a new issue