Add problem vote
This commit is contained in:
parent
68c6f13926
commit
2e3a45168e
17 changed files with 685 additions and 122 deletions
|
@ -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>
|
||||
|
|
73
templates/problem/voting-controls.html
Normal file
73
templates/problem/voting-controls.html
Normal 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 %}
|
93
templates/problem/voting-form.html
Normal file
93
templates/problem/voting-form.html
Normal 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>
|
146
templates/problem/voting-stats.html
Normal file
146
templates/problem/voting-stats.html
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue