Add HTML/CSS for problem point voting

This commit is contained in:
int-y1 2022-09-12 23:23:31 -04:00 committed by Guanzhong Chen
parent 8f259d1d97
commit a93756d8a1
6 changed files with 230 additions and 0 deletions

91
resources/problem-vote.js Normal file
View File

@ -0,0 +1,91 @@
var voteChart = null;
function init_problem_vote_form() {
$('#delete-problem-vote-form').on('submit', function (e) {
e.preventDefault();
$.ajax({
url: $('#delete-problem-vote-form').prop('action'),
type: 'POST',
data: $('#delete-problem-vote-form').serialize(),
success: function () {
$('#problem-vote-button').text(gettext('Vote on problem points'));
$.featherlight.close();
},
error: function (data) {
var msg = 'responseJSON' in data ? data.responseJSON.message : data.statusText;
alert(interpolate(gettext('Unable to delete vote: %s'), [msg]));
}
});
});
$('#problem-vote-form').on('submit', function (e) {
e.preventDefault();
$.ajax({
url: $('#problem-vote-form').prop('action'),
type: 'POST',
data: $('#problem-vote-form').serialize(),
success: function (data) {
$('#problem-vote-button').text(interpolate(gettext('Edit points vote (%s)'), [data.points]));
$.featherlight.close();
},
error: function (data) {
var errors = 'responseJSON' in data ? data.responseJSON : {'message': data.statusText};
if ('message' in errors) {
alert(interpolate(gettext('Unable to cast vote: %s'), [errors.message]));
}
$('#points-error').text('points' in errors ? errors.points[0] : '');
$('#note-error').text('note' in errors ? errors.note[0] : '');
}
});
});
}
function reload_problem_vote_graph(data, min_possible_vote, max_possible_vote) {
if (voteChart !== null) voteChart.destroy();
// Give the graph some padding on both sides.
var min_points = Math.max(data[0] - 2, min_possible_vote);
var max_points = Math.min(data[data.length - 1] + 2, max_possible_vote);
var xlabels = [];
var voteFreq = [];
for (var i = min_points; i <= max_points; i++) {
xlabels.push(i);
voteFreq.push(0);
}
data.forEach(function (x) { voteFreq[x - min_points]++; });
var max_number_of_votes = Math.max.apply(null, voteFreq);
var voteData = {
labels: xlabels,
datasets: [{
label: gettext('Number of votes for this point value'),
data: voteFreq,
borderColor: 'red',
backgroundColor: 'pink',
}],
};
var voteDataConfig = {
type: 'bar',
data: voteData,
options: {
responsive: true,
scales: {
yAxes: [{
ticks: {
precision: 0,
suggestedMax: Math.ceil(max_number_of_votes * 1.2),
beginAtZero: true,
}
}],
xAxes: [{
ticks: {
beginAtZero: false,
}
}],
},
},
};
voteChart = new Chart($('#problem-vote-chart').get(0), voteDataConfig);
}

View File

@ -0,0 +1,54 @@
.problem-vote-container {
margin: 1em;
min-width: 25em;
}
.problem-vote-form-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
border-bottom: 1px solid #aaa;
}
.problem-vote-form-title {
font-size: 2em;
margin-right: 0.5em;
}
.problem-vote-date {
font-style: italic;
}
.problem-vote-form-info {
font-size: 1.2em;
margin-right: 0.5em;
}
#problem-vote-form textarea {
margin-top: 0.5em;
width: 100%;
font-size: 14px;
}
.problem-voting-form-error {
font-size: 1.2em;
color: red;
}
.problem-vote-submits {
display: flex;
justify-content: space-between;
flex-direction: row-reverse;
}
.problem-vote-stats-bar {
font-size: 1.2em;
font-weight: 500;
margin: 0.6em 0;
display: flex;
justify-content: space-around;
}
.problem-vote-stats-bar span {
margin: 0 0.6em;
}

View File

@ -4,6 +4,7 @@
@import "status";
@import "blog";
@import "problem";
@import "problem-vote";
@import "ranks";
@import "users";
@import "content-description";

View File

@ -46,6 +46,11 @@
{% block content_js_media %}
{% include "comments/media-js.html" %}
{% if vote_perm.can_view() %}
<script src="{{ static('libs/chart.js/Chart.js') }}" type="text/javascript"></script>
<script src="{{ static('problem-vote.js') }}" type="text/javascript"></script>
{% endif %}
{% endblock %}
{% block title_row %}
@ -126,6 +131,21 @@
{% endif %}
<div><a href="{{ url('chronological_submissions', problem.code) }}">{{ _('All submissions') }}</a></div>
<div><a href="{{ url('ranked_submissions', problem.code) }}">{{ _('Best submissions') }}</a></div>
{% if vote_perm.can_view() %}
<hr style="padding-top: 0.1em">
{% if vote_perm.can_vote() %}
<div><a href="#" data-featherlight="{{ url('problem_vote', problem.code) }}" id="problem-vote-button">
{%- if vote is none -%}
{{ _('Vote on problem points') }}
{%- else -%}
{{ _('Edit points vote (%(points)d)', points=vote.points) }}
{%- endif -%}
</a></div>
{% endif %}
<div><a href="#" data-featherlight="{{ url('problem_vote_stats', problem.code) }}">{{ _('Voting statistics') }}</a></div>
{% endif %}
{% if (editorial and editorial.is_accessible_by(request.user)) and not request.in_contest %}
<hr>
<div><a href="{{ url('problem_editorial', problem.code) }}">{{ _('Read editorial') }}</a></div>

View File

@ -0,0 +1,42 @@
<div class="problem-vote-container">
<div class="problem-vote-form-header">
{% if vote %}
<span class="problem-vote-form-title">{{ _('Change vote') }}</span>
<span class="problem-vote-date">
{%- with vote_date=vote.vote_time|date(_('N j, Y, g:i a')) -%}
{{ _('Last voted on %(date)s', date=vote_date) }}
{%- endwith -%}
</span>
{% else %}
<span class="problem-vote-form-title">{{ _('Cast vote') }}</span>
{% endif %}
</div>
<form id="delete-problem-vote-form" action="{{ url('delete_problem_vote', problem.code) }}" method="post">
{% csrf_token %}
</form>
<br>
<form id="problem-vote-form" action="{{ url('problem_vote', problem.code) }}" method="post">
{% csrf_token %}
<span class="problem-vote-form-info">{{ _('Points:') }}</span>
<input type="number" step="1" placeholder="{{ problem.points|floatformat(0) }}" value="{% if vote %}{{ vote.points }}{% endif %}"
min="{{ min_possible_vote }}" max="{{ max_possible_vote }}" name="points" required>
<div id="points-error" class="problem-voting-form-error"></div>
<br>
<span class="problem-vote-form-info">{{ _('Justification:') }}</span>
<br>
<textarea name="note" rows="12" placeholder="{{ _("A short justification for your vote to this problem's point value (optional).") }}">
{%- if vote %}{{ vote.note }}{% endif -%}
</textarea>
<div id="note-error" class="problem-voting-form-error"></div>
<br>
<div class="problem-vote-submits">
<input type="submit" value="{{ _('Vote!') }}">
{% if vote %}
<input type="submit" value="{{ _('Delete vote') }}" form="delete-problem-vote-form">
{% endif %}
</div>
</form>
<script type="text/javascript">
init_problem_vote_form();
</script>
</div>

View File

@ -0,0 +1,22 @@
<div class="problem-vote-container">
<div class="problem-vote-form-header">
<span class="problem-vote-form-title">{{ _('Voting statistics') }}</span>
</div>
{% if not votes %}
<div class="problem-vote-stats-bar">
<span>{{ _('No votes available!') }}</span>
</div>
{% else %}
<canvas id="problem-vote-chart"></canvas>
<script type="text/javascript">
reload_problem_vote_graph({{ votes }}, {{ min_possible_vote }}, {{ max_possible_vote }});
</script>
<div class="problem-vote-stats-bar">
{% with median_str=median|floatformat(1), mean_str=mean|floatformat(1), vote_count=votes|length %}
<span>{{ _('Median vote: %(val)s', val=median_str) }}</span>
<span>{{ _('Mean vote: %(val)s', val=mean_str) }}</span>
<span>{{ _('Number of votes: %(val)d', val=vote_count) }}</span>
{% endwith %}
</div>
{% endif %}
</div>