add pages vote

This commit is contained in:
Zhao-Linux 2022-11-16 22:43:03 +07:00
parent 3ee2f2afb0
commit d86f3d8f3e
10 changed files with 310 additions and 1 deletions

View file

@ -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"),

View 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')},
},
),
]

View file

@ -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
View 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")

View file

@ -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
View 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

View file

@ -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 %}

View 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>

View 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>

View 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 %}