commit
f9e9df6056
10 changed files with 310 additions and 1 deletions
|
@ -59,6 +59,7 @@ from judge.views import (
|
||||||
totp,
|
totp,
|
||||||
user,
|
user,
|
||||||
volunteer,
|
volunteer,
|
||||||
|
pagevote,
|
||||||
widgets,
|
widgets,
|
||||||
internal,
|
internal,
|
||||||
)
|
)
|
||||||
|
@ -445,6 +446,8 @@ urlpatterns = [
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
url(r"^pagevotes/upvote/$", pagevote.upvote_page, name="pagevote_upvote"),
|
||||||
|
url(r"^pagevotes/downvote/$", pagevote.downvote_page, name="pagevote_downvote"),
|
||||||
url(r"^comments/upvote/$", comment.upvote_comment, name="comment_upvote"),
|
url(r"^comments/upvote/$", comment.upvote_comment, name="comment_upvote"),
|
||||||
url(r"^comments/downvote/$", comment.downvote_comment, name="comment_downvote"),
|
url(r"^comments/downvote/$", comment.downvote_comment, name="comment_downvote"),
|
||||||
url(r"^comments/hide/$", comment.comment_hide, name="comment_hide"),
|
url(r"^comments/hide/$", comment.comment_hide, name="comment_hide"),
|
||||||
|
|
45
judge/migrations/0137_auto_20221116_2201.py
Normal file
45
judge/migrations/0137_auto_20221116_2201.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# Generated by Django 3.2.16 on 2022-11-16 15:01
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('judge', '0136_alter_profile_timezone'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PageVote',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('page', models.CharField(db_index=True, max_length=30, verbose_name='associated page')),
|
||||||
|
('score', models.IntegerField(default=0, verbose_name='votes')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'pagevote',
|
||||||
|
'verbose_name_plural': 'pagevotes',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='problemtranslation',
|
||||||
|
name='language',
|
||||||
|
field=models.CharField(choices=[('vi', 'Vietnamese'), ('en', 'English')], max_length=7, verbose_name='language'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PageVoteVoter',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('score', models.IntegerField()),
|
||||||
|
('pagevote', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='judge.pagevote')),
|
||||||
|
('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='voted_page', to='judge.profile')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'pagevote vote',
|
||||||
|
'verbose_name_plural': 'pagevote votes',
|
||||||
|
'unique_together': {('voter', 'pagevote')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -54,6 +54,7 @@ from judge.models.submission import (
|
||||||
)
|
)
|
||||||
from judge.models.ticket import Ticket, TicketMessage
|
from judge.models.ticket import Ticket, TicketMessage
|
||||||
from judge.models.volunteer import VolunteerProblemVote
|
from judge.models.volunteer import VolunteerProblemVote
|
||||||
|
from judge.models.pagevote import PageVote, PageVoteVoter
|
||||||
|
|
||||||
revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"])
|
revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"])
|
||||||
revisions.register(Problem, follow=["language_limits"])
|
revisions.register(Problem, follow=["language_limits"])
|
||||||
|
@ -76,5 +77,6 @@ revisions.register(ProblemData)
|
||||||
revisions.register(ProblemTestCase)
|
revisions.register(ProblemTestCase)
|
||||||
revisions.register(ContestParticipation)
|
revisions.register(ContestParticipation)
|
||||||
revisions.register(Rating)
|
revisions.register(Rating)
|
||||||
|
revisions.register(PageVoteVoter)
|
||||||
revisions.register(VolunteerProblemVote)
|
revisions.register(VolunteerProblemVote)
|
||||||
del revisions
|
del revisions
|
||||||
|
|
40
judge/models/pagevote.py
Normal file
40
judge/models/pagevote.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models import CASCADE
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from judge.models import Profile
|
||||||
|
|
||||||
|
__all__ = ["PageVote", "PageVoteVoter"]
|
||||||
|
|
||||||
|
|
||||||
|
class PageVote(models.Model):
|
||||||
|
page = models.CharField(
|
||||||
|
max_length=30,
|
||||||
|
verbose_name=_("associated page"),
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
score = models.IntegerField(verbose_name=_("votes"), default=0)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("pagevote")
|
||||||
|
verbose_name_plural = _("pagevotes")
|
||||||
|
|
||||||
|
def vote_score(self, user):
|
||||||
|
page_vote = PageVoteVoter.objects.filter(pagevote=self, voter=user)
|
||||||
|
if page_vote.exists():
|
||||||
|
return page_vote.first().score
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"pagevote for {self.page}"
|
||||||
|
|
||||||
|
class PageVoteVoter(models.Model):
|
||||||
|
voter = models.ForeignKey(Profile, related_name="voted_page", on_delete=CASCADE)
|
||||||
|
pagevote = models.ForeignKey(PageVote, related_name="votes", on_delete=CASCADE)
|
||||||
|
score = models.IntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ["voter", "pagevote"]
|
||||||
|
verbose_name = _("pagevote vote")
|
||||||
|
verbose_name_plural = _("pagevote votes")
|
|
@ -7,6 +7,7 @@ from django.utils.translation import ugettext as _
|
||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
|
|
||||||
from judge.comments import CommentedDetailView
|
from judge.comments import CommentedDetailView
|
||||||
|
from judge.views.pagevote import PageVoteDetailView
|
||||||
from judge.models import (
|
from judge.models import (
|
||||||
BlogPost,
|
BlogPost,
|
||||||
Comment,
|
Comment,
|
||||||
|
@ -190,7 +191,7 @@ class CommentFeed(FeedView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class PostView(TitleMixin, CommentedDetailView):
|
class PostView(TitleMixin, CommentedDetailView, PageVoteDetailView):
|
||||||
model = BlogPost
|
model = BlogPost
|
||||||
pk_url_kwarg = "id"
|
pk_url_kwarg = "id"
|
||||||
context_object_name = "post"
|
context_object_name = "post"
|
||||||
|
|
120
judge/views/pagevote.py
Normal file
120
judge/views/pagevote.py
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.db import IntegrityError
|
||||||
|
from django.db.models import F
|
||||||
|
from django.http import (
|
||||||
|
Http404,
|
||||||
|
HttpResponse,
|
||||||
|
HttpResponseBadRequest,
|
||||||
|
HttpResponseForbidden,
|
||||||
|
)
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from judge.models.pagevote import PageVote, PageVoteVoter
|
||||||
|
from django.views.generic.base import TemplateResponseMixin
|
||||||
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
|
|
||||||
|
from judge.dblock import LockModel
|
||||||
|
from django.views.generic import View
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"upvote_page",
|
||||||
|
"downvote_page",
|
||||||
|
]
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def vote_page(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:
|
||||||
|
pagevote_id = int(request.POST["id"])
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
else:
|
||||||
|
if not PageVote.objects.filter(id=pagevote_id).exists():
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
|
vote = PageVoteVoter()
|
||||||
|
vote.pagevote_id = pagevote_id
|
||||||
|
vote.voter = request.profile
|
||||||
|
vote.score = delta
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
vote.save()
|
||||||
|
except IntegrityError:
|
||||||
|
with LockModel(write=(PageVoteVoter,)):
|
||||||
|
try:
|
||||||
|
vote = PageVoteVoter.objects.get(
|
||||||
|
pagevote_id=pagevote_id, voter=request.profile
|
||||||
|
)
|
||||||
|
except PageVoteVoter.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()
|
||||||
|
PageVote.objects.filter(id=pagevote_id).update(score=F("score") - vote.score)
|
||||||
|
else:
|
||||||
|
PageVote.objects.filter(id=pagevote_id).update(score=F("score") + delta)
|
||||||
|
break
|
||||||
|
return HttpResponse("success", content_type="text/plain")
|
||||||
|
|
||||||
|
|
||||||
|
def upvote_page(request):
|
||||||
|
return vote_page(request, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def downvote_page(request):
|
||||||
|
return vote_page(request, -1)
|
||||||
|
|
||||||
|
class PageVoteDetailView(TemplateResponseMixin, SingleObjectMixin, View):
|
||||||
|
pagevote_page = None
|
||||||
|
|
||||||
|
def get_pagevote_page(self):
|
||||||
|
if self.pagevote_page is None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
return self.pagevote_page
|
||||||
|
|
||||||
|
# def get(self, request, *args, **kwargs):
|
||||||
|
# self.object = self.get_object()
|
||||||
|
# return self.render_to_response(
|
||||||
|
# self.get_context_data(
|
||||||
|
# object=self.object,
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(PageVoteDetailView, self).get_context_data(**kwargs)
|
||||||
|
queryset = PageVote.objects.filter(page=self.get_comment_page())
|
||||||
|
if (queryset.exists() == False):
|
||||||
|
pagevote = PageVote(
|
||||||
|
page=self.get_comment_page(), score=0
|
||||||
|
)
|
||||||
|
pagevote.save()
|
||||||
|
context["pagevote"] = queryset.first()
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,12 @@
|
||||||
|
|
||||||
{% block js_media %}
|
{% block js_media %}
|
||||||
{% include "comments/media-js.html" %}
|
{% include "comments/media-js.html" %}
|
||||||
|
{% include "pagevotes/media-js.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block media %}
|
{% block media %}
|
||||||
{% include "comments/media-css.html" %}
|
{% include "comments/media-css.html" %}
|
||||||
|
{% include "pagevotes/media-css.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block title_row %}
|
{% block title_row %}
|
||||||
|
@ -46,6 +48,7 @@
|
||||||
{{ post_to_facebook(request, post, '<i class="fa fa-facebook-official"></i>') }}
|
{{ post_to_facebook(request, post, '<i class="fa fa-facebook-official"></i>') }}
|
||||||
{{ post_to_twitter(request, SITE_NAME + ':', post, '<i class="fa fa-twitter"></i>') }}
|
{{ post_to_twitter(request, SITE_NAME + ':', post, '<i class="fa fa-twitter"></i>') }}
|
||||||
</span>
|
</span>
|
||||||
|
{% include "pagevotes/list.html" %}
|
||||||
{% include "comments/list.html" %}
|
{% include "comments/list.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
22
templates/pagevotes/list.html
Normal file
22
templates/pagevotes/list.html
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{% set logged_in = request.user.is_authenticated %}
|
||||||
|
{% set profile = request.profile if logged_in else None %}
|
||||||
|
<div class="page-vote">
|
||||||
|
<div class="page-vote-full" style="margin-right: 5px;">
|
||||||
|
{% if logged_in %}
|
||||||
|
<a href="javascript:pagevote_upvote({{ pagevote.id }})"
|
||||||
|
class="upvote-link fa fa-chevron-up fa-fw{% if pagevote.vote_score(request.profile) == 1 %} voted{% endif %}"></a>
|
||||||
|
{% else %}
|
||||||
|
<a href="javascript:alert('{{ _('Please log in to vote')|escapejs }}')" title="{{ _('Please log in to vote') }}"
|
||||||
|
class="upvote-link fa fa-chevron-up fa-fw"></a>
|
||||||
|
{% endif %}
|
||||||
|
<br>
|
||||||
|
<div class="pagevote-score"> {{ pagevote.score }} </div>
|
||||||
|
{% if logged_in %}
|
||||||
|
<a href="javascript:pagevote_downvote({{ pagevote.id }})"
|
||||||
|
class="downvote-link fa fa-chevron-down fa-fw{% if pagevote.vote_score(request.profile) == -1 %} voted{% endif %}"></a>
|
||||||
|
{% else %}
|
||||||
|
<a href="javascript:alert('{{ _('Please log in to vote')|escapejs }}')" title="{{ _('Please log in to vote') }}"
|
||||||
|
class="downvote-link fa fa-chevron-down fa-fw"></a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
19
templates/pagevotes/media-css.html
Normal file
19
templates/pagevotes/media-css.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<style>
|
||||||
|
.page-vote {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.pagevote-score {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upvote-link, .downvote-link {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voted {
|
||||||
|
text-shadow: 0 0 4px black, 0 0 9px blue;
|
||||||
|
}
|
||||||
|
</style>
|
54
templates/pagevotes/media-js.html
Normal file
54
templates/pagevotes/media-js.html
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<script src="{{ static('libs/featherlight/featherlight.min.js') }}" type="text/javascript"></script>
|
||||||
|
{% compress js %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(document).ready(function () {
|
||||||
|
function ajax_vote(url, id, delta, on_success) {
|
||||||
|
return $.ajax({
|
||||||
|
url: url,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
id: id
|
||||||
|
},
|
||||||
|
success: function (data, textStatus, jqXHR) {
|
||||||
|
var score = $('.page-vote-full' + ' .pagevote-score').first();
|
||||||
|
score.text(parseInt(score.text()) + delta);
|
||||||
|
if (typeof on_success !== 'undefined')
|
||||||
|
on_success();
|
||||||
|
},
|
||||||
|
error: function (data, textStatus, jqXHR) {
|
||||||
|
alert('Could not vote: ' + data.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var get_$votes = function () {
|
||||||
|
var $post = $('.page-vote-full');
|
||||||
|
return {
|
||||||
|
upvote: $post.find('.upvote-link').first(),
|
||||||
|
downvote: $post.find('.downvote-link').first()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
window.pagevote_upvote = function (id) {
|
||||||
|
ajax_vote('{{ url('pagevote_upvote') }}', id, 1, function () {
|
||||||
|
var $votes = get_$votes();
|
||||||
|
if ($votes.downvote.hasClass('voted'))
|
||||||
|
$votes.downvote.removeClass('voted');
|
||||||
|
else
|
||||||
|
$votes.upvote.addClass('voted');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.pagevote_downvote = function (id) {
|
||||||
|
ajax_vote('{{ url('pagevote_downvote') }}', id, -1, function () {
|
||||||
|
var $votes = get_$votes();
|
||||||
|
if ($votes.upvote.hasClass('voted'))
|
||||||
|
$votes.upvote.removeClass('voted');
|
||||||
|
else
|
||||||
|
$votes.downvote.addClass('voted');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endcompress %}
|
Loading…
Reference in a new issue