Add problem vote

This commit is contained in:
cuom1999 2022-03-09 23:38:29 -06:00
parent 68c6f13926
commit 2e3a45168e
17 changed files with 685 additions and 122 deletions

View file

@ -164,7 +164,7 @@
{% 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 not contest_problem or not contest_problem.contest.use_clarifications %}
<hr>
<div><a href="{{ url('problem_comments', problem.code) }}">{{ _('Discuss') }}</a></div>
@ -321,6 +321,9 @@
{% endblock %}
{% block description %}
{% if can_vote and not vote %}
{% include 'problem/voting-form.html' %}
{% endif %}
{% if contest_problem and contest_problem.contest.use_clarifications and has_clarifications %}
<div id="clarification_header_container">
<i class="fa fa-question-circle"></i>
@ -346,6 +349,9 @@
{% endblock %}
{% block post_description_end %}
{% if can_vote %}
{% include 'problem/voting-controls.html' %}
{% endif %}
{% if request.user.is_authenticated and not request.profile.mute %}
<a href="{{ url('new_problem_ticket', problem.code) }}" class="clarify">
<i class="fa fa-flag" style="margin-right:0.5em"></i>

View file

@ -0,0 +1,73 @@
<style>
.featherlight .featherlight-inner{
display: block !important;
}
.featherlight-content {
overflow: inherit !important;
}
.stars-container {
display: inline-block;
vertical-align: top;
}
.stars-container .star {
display: inline-block;
padding: 2px;
color: orange;
cursor: pointer;
font-size: 30px;
}
.stars-container .star:before {
content:"\f006";
}
.filled-star:before, .star:hover:before {
content:"\f005" !important;
}
</style>
{% if can_vote %}
<div class="vote-form" id="id_vote_form_box" style="display: none">
{% include 'problem/voting-form.html' %}
</div>
<span>
<a href="#" class="form-button" id="id_vote_button" data-featherlight="#id_vote_form_box"></a>
{% if request.user.is_superuser %}
- {% include 'problem/voting-stats.html' %}
{% endif %}
</span>
<script src="{{ static('libs/featherlight/featherlight.min.js') }}" type="text/javascript"></script>
<script>
let voted_points = null;
{% if vote is not none %}
let has_voted = true;
voted_points = {{ vote.points }};
{% else %}
let has_voted = false;
{% endif %}
function voteUpdate(){
$('#id_has_voted_prompt').show();
$('#id_current_vote_value').prop('innerText', voted_points);
$('#id_has_not_voted_prompt').hide();
$('#id_vote_button').prop('innerText', `{{ _('Edit difficulty') }} (` + voted_points + ')');
$('#id_points_error_box').hide();
has_voted = true;
}
function deleteVoteUpdate(){
$('#id_has_voted_prompt').hide();
$('#id_has_not_voted_prompt').show();
$('#id_vote_button').prop('innerText', `{{ _('Vote difficulty') }}`);
$('#id_points_error_box').hide();
has_voted = false;
}
if (has_voted) voteUpdate();
else deleteVoteUpdate();
$('#id_vote_form').on('submit', function (e) {
e.preventDefault();
});
</script>
{% endif %}

View file

@ -0,0 +1,93 @@
<style>
.vote_form {
border: 3px solid blue;
border-radius: 5px;
width: fit-content;
margin-left: auto;
margin-right: auto;
padding: 0.2em 1em;
background: #ebf0ff;
color: #2e69ff;
}
</style>
<form id="id_vote_form" class="vote_form">
{% csrf_token %}
<input id="id_points" class="vote-form-value" type="hidden" step="1"
min="{{ min_possible_vote }}" max="{{ max_possible_vote }}" name="points">
<table>
<tbody>
<tr>
<td style="padding-top: 1em; padding-right: 3em">
<span><b>{{_('How difficult is this problem?')}}</b></span>
</td>
<td>
<div class="vote-rating">
<span class="stars-container">
{% for i in range(1, (max_possible_vote - min_possible_vote) // 100 + 2) %}
<span id="star{{i}}" star-score="{{i * 100}}" class="fa star"></span>
{% endfor %}
</span>
</div>
</td>
</tr>
<tr>
<td>
<span>{{_('This helps us improve the site')}}</span>
</td>
<td>
<span style="padding-left: 0.2em"><b>{{_('Easy')}}</b></span>
<span style="float: right; padding-right: 0.2em"><b>{{_('Hard')}}</b></span>
</td>
</tr>
</tbody>
</table>
<div id="id_points_error_box">
<span id="id_points_error" class="voting-form-error"></span>
</div>
<div>
<input type="hidden" id="id_vote_form_submit_button">
</div>
</form>
<script>
$('.star').hover(
(e) => $(e.target).prevAll().addClass('filled-star'),
(e) => $(e.target).prevAll().removeClass('filled-star')
);
$('.star').on('click', function() {
$("#id_points").val($(this).attr('star-score'));
$('#id_vote_form_submit_button').click();
$('#id_vote_form').fadeOut(500);
})
$('#id_vote_form_submit_button').on('click', function (e) {
e.preventDefault();
$.ajax({
url: '{{ url('vote', object.code) }}',
type: 'POST',
data: $('#id_vote_form').serialize(),
success: function (data) {
{% if request.user.is_superuser %}
updateUserVote(voted_points, data.points);
{% endif %}
voted_points = data.points;
voteUpdate();
// Forms are auto disabled to prevent resubmission, but we need to allow resubmission here.
$('#id_vote_form_submit_button').removeAttr('disabled');
var current = $.featherlight.current();
current.close();
},
error: function (data) {
let errors = data.responseJSON;
if(errors === undefined) {
alert('Unable to delete vote: ' + data.responsetext);
}
if('points' in errors){
$('#id_points_error_box').show();
$('#id_points_error').prop('textContent', errors.points[0]);
} else {
$('#id_points_error_box').hide();
}
}
});
});
</script>

View file

@ -0,0 +1,146 @@
<style>
.vote-stats-background {
background-color: rgb(255,255,255);
padding: 20px;
border-radius: 25px;
justify-content: center;
align-items: center;
margin: auto;
text-align: center;
}
.canvas {
border: 1px;
border: solid;
border: #000000;
}
.vote-stats-value {
margin-right: 1em;
}
.vote-stats-info {
font-weight: 500;
}
</style>
<a href="#" class="form-button" id="id_vote_stats_button">{{ _('Statistics') }}</a>
<div class="vote-stats-background" id="id_vote_stats" style="display: none;">
<script src={{ static('libs/chart.js/Chart.js') }}></script>
<span class="vote-stats-info">{{ _('Voting Statistics') }}</span>
<br/>
<canvas class="canvas" id="id_vote_chart"></canvas>
<span id="id_no_votes_error" class="vote-stats-info no_votes_error">{{ _('No Votes Available!') }}</span>
<br/>
<div id="id_has_votes_footer" class="has_votes_footer">
<span class="vote-stats-info">{{ _('Median:') }}</span>
<span class="vote-stats-value median_vote" id="id_median_vote"></span>
<span class="vote-stats-info">{{ _('Mean:') }}</span>
<span class="vote-stats-value mean_vote" id="id_mean_vote"></span>
<span class="vote-stats-info">{{ _('Total:') }}</span>
<span class="vote-stats-value total_vote" id="id_num_of_votes"></span>
</div>
</div>
<script>
let voteChart = null;
let data = {{ all_votes }};
function reload_vote_graph() {
if (voteChart !== null) voteChart.destroy();
if (data.length === 0) {
$('.canvas').hide();
$('.has_votes_footer').hide();
$('.no_votes_error').show();
} else {
$('.canvas').show();
$('.has_votes_footer').show();
$('.no_votes_error').hide();
data.sort(function(a, b){return a - b});
// Give the graph some padding on both sides.
let min_points = {{ min_possible_vote }};
let max_points = {{ max_possible_vote }};
let xlabels = [];
let voteFreq = [];
for (let i = min_points; i <= max_points; i += 100) {
xlabels.push(i);
voteFreq.push(0);
}
let max_number_of_votes = 0;
let total_votes = 0;
let mean = 0;
for (let i = 0; i < data.length; i++) {
// Assume the data is valid.
voteFreq[(data[i] - min_points) / 100]++;
max_number_of_votes = Math.max(max_number_of_votes, voteFreq[(data[i] - min_points) / 100]);
mean += data[i];
total_votes++;
}
mean = mean / total_votes;
let half = Math.floor(total_votes / 2);
let median = data[half];
if (total_votes % 2 === 0) {
median = (median + data[half - 1]) / 2;
}
$('.mean_vote').prop('innerText', mean.toFixed(2));
$('.median_vote').prop('innerText', median.toFixed(2));
$('.total_vote').prop('innerText', total_votes);
const voteData = {
labels: xlabels,
datasets: [{
data: voteFreq,
backgroundColor: 'pink',
}]
};
const voteDataConfig = {
type: 'bar',
data: voteData,
options: {
legend: {
display: false
},
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($('.featherlight-inner .canvas'), voteDataConfig);
}
}
function updateUserVote(prev_voted_points, voted_points) {
let index = data.indexOf(prev_voted_points);
if (index > -1) {
data.splice(index, 1);
}
data.push(voted_points);
}
function deleteUserVote(prev_voted_points) {
data.splice(data.indexOf(prev_voted_points), 1);
}
$(function() {
$('#id_vote_stats_button').featherlight('#id_vote_stats', {
afterOpen: reload_vote_graph,
})
});
</script>