add blog vote

This commit is contained in:
Zhao-Linux 2022-10-30 10:46:22 +07:00
parent 2a7a33fe1a
commit c1cf8bc0e4
8 changed files with 242 additions and 7 deletions

View file

@ -738,6 +738,8 @@ urlpatterns = [
), ),
), ),
url(r"^blog/", paged_list_view(blog.PostList, "blog_post_list")), url(r"^blog/", paged_list_view(blog.PostList, "blog_post_list")),
url(r"^post/upvote/$", blog.upvote_blog, name="blog_upvote"),
url(r"^post/downvote/$", blog.downvote_blog, name="blog_downvote"),
url(r"^post/(?P<id>\d+)-(?P<slug>.*)$", blog.PostView.as_view(), name="blog_post"), url(r"^post/(?P<id>\d+)-(?P<slug>.*)$", blog.PostView.as_view(), name="blog_post"),
url(r"^license/(?P<key>[-\w.]+)$", license.LicenseDetail.as_view(), name="license"), url(r"^license/(?P<key>[-\w.]+)$", license.LicenseDetail.as_view(), name="license"),
url( url(

File diff suppressed because one or more lines are too long

View file

@ -17,7 +17,7 @@ from judge.models.contest import (
Rating, Rating,
ContestProblemClarification, ContestProblemClarification,
) )
from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex from judge.models.interface import BlogPost, BlogVote, MiscConfig, NavigationBar, validate_regex
from judge.models.message import PrivateMessage, PrivateMessageThread from judge.models.message import PrivateMessage, PrivateMessageThread
from judge.models.problem import ( from judge.models.problem import (
LanguageLimit, LanguageLimit,

View file

@ -1,7 +1,9 @@
import re import re
from tabnanny import verbose
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import CASCADE
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -82,6 +84,7 @@ class BlogPost(models.Model):
og_image = models.CharField( og_image = models.CharField(
verbose_name=_("openGraph image"), default="", max_length=150, blank=True verbose_name=_("openGraph image"), default="", max_length=150, blank=True
) )
score = models.IntegerField(verbose_name=_("vote"), default=0)
organizations = models.ManyToManyField( organizations = models.ManyToManyField(
Organization, Organization,
blank=True, blank=True,
@ -125,7 +128,25 @@ class BlogPost(models.Model):
and self.authors.filter(id=user.profile.id).exists() and self.authors.filter(id=user.profile.id).exists()
) )
def vote_score(self, user):
blogvote = BlogVote.objects.filter(blog=self, voter=user)
if blogvote.exists():
return blogvote.first().score
else:
return 0
class Meta: class Meta:
permissions = (("edit_all_post", _("Edit all posts")),) permissions = (("edit_all_post", _("Edit all posts")),)
verbose_name = _("blog post") verbose_name = _("blog post")
verbose_name_plural = _("blog posts") verbose_name_plural = _("blog posts")
class BlogVote(models.Model):
voter = models.ForeignKey(Profile, related_name="voted_blogs", on_delete=CASCADE)
blog = models.ForeignKey(BlogPost, related_name="votes", on_delete=CASCADE)
score = models.IntegerField()
class Meta:
unique_together = ["voter", "blog"]
verbose_name = _("blog vote")
verbose_name_plural = _("blog votes")

View file

@ -1,14 +1,26 @@
from django.db.models import Count, Max, Q from django.contrib.auth.decorators import login_required
from django.http import Http404 from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.db import IntegrityError, transaction
from django.db.models import Count, Max, Q, F
from django.db.models.expressions import F, Value
from django.db.models.functions import Coalesce
from django.http import (
Http404,
HttpResponse,
HttpResponseBadRequest,
HttpResponseForbidden,
)
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.functional import lazy from django.utils.functional import lazy
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic import ListView from django.views.generic import ListView, DetailView
from judge.comments import CommentedDetailView from judge.comments import CommentedDetailView
from judge.dblock import LockModel
from judge.models import ( from judge.models import (
BlogPost, BlogPost,
BlogVote,
Comment, Comment,
Contest, Contest,
Language, Language,
@ -18,6 +30,7 @@ from judge.models import (
Submission, Submission,
Ticket, Ticket,
) )
from judge.utils.raw_sql import RawSQLColumn, unique_together_left_join
from judge.models.profile import Organization, OrganizationProfile from judge.models.profile import Organization, OrganizationProfile
from judge.utils.cachedict import CacheDict from judge.utils.cachedict import CacheDict
from judge.utils.diggpaginator import DiggPaginator from judge.utils.diggpaginator import DiggPaginator
@ -227,3 +240,72 @@ class PostView(TitleMixin, CommentedDetailView):
if not post.can_see(self.request.user): if not post.can_see(self.request.user):
raise Http404() raise Http404()
return post return post
@login_required
def vote_blog(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:
blog_id = int(request.POST["id"])
except ValueError:
return HttpResponseBadRequest()
else:
if not BlogPost.objects.filter(id=blog_id).exists():
raise Http404()
vote = BlogVote()
vote.blog_id = blog_id
vote.voter = request.profile
vote.score = delta
while True:
try:
vote.save()
except IntegrityError:
with LockModel(write=(BlogVote,)):
try:
vote = BlogVote.objects.get(
blog_id=blog_id, voter=request.profile
)
except BlogVote.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()
BlogPost.objects.filter(id=blog_id).update(score=F("score") - vote.score)
else:
BlogPost.objects.filter(id=blog_id).update(score=F("score") + delta)
break
return HttpResponse("success", content_type="text/plain")
def upvote_blog(request):
return vote_blog(request, 1)
def downvote_blog(request):
return vote_blog(request, -1)

View file

@ -2,10 +2,12 @@
{% block js_media %} {% block js_media %}
{% include "comments/media-js.html" %} {% include "comments/media-js.html" %}
{% include "blog/media-js.html" %}
{% endblock %} {% endblock %}
{% block media %} {% block media %}
{% include "comments/media-css.html" %} {% include "comments/media-css.html" %}
{% include "blog/media-css.html" %}
{% endblock %} {% endblock %}
{% block title_row %} {% block title_row %}
@ -15,8 +17,30 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
{% set logged_in = request.user.is_authenticated %}
{% set profile = request.profile if logged_in else None %}
<div class="post-full"> <div class="post-full">
<div style="display: flex;">
<div class="blog-vote" style="margin-right: 5px;">
{% if logged_in %}
<a href="javascript:blog_upvote({{ post.id }})"
class="upvote-link fa fa-chevron-up fa-fw{% if post.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="post-score"> {{ post.score }} </div>
{% if logged_in %}
<a href="javascript:blog_downvote({{ post.id }})"
class="downvote-link fa fa-chevron-down fa-fw{% if post.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 class="post-title">{{ title }}</div> <div class="post-title">{{ title }}</div>
</div>
<div class="time"> <div class="time">
{% with authors=post.authors.all() %} {% with authors=post.authors.all() %}
{% if authors %} {% if authors %}

View file

@ -27,4 +27,18 @@
.recently-attempted h4, .recommended-problems h4 { .recently-attempted h4, .recommended-problems h4 {
font-weight: 500; font-weight: 500;
} }
.post-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> </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 = $('.post-full' + ' .post-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 = $('.post-full');
return {
upvote: $post.find('.upvote-link').first(),
downvote: $post.find('.downvote-link').first()
};
};
window.blog_upvote = function (id) {
ajax_vote('{{ url('blog_upvote') }}', id, 1, function () {
var $votes = get_$votes();
if ($votes.downvote.hasClass('voted'))
$votes.downvote.removeClass('voted');
else
$votes.upvote.addClass('voted');
});
};
window.blog_downvote = function (id) {
ajax_vote('{{ url('blog_downvote') }}', id, -1, function () {
var $votes = get_$votes();
if ($votes.upvote.hasClass('voted'))
$votes.upvote.removeClass('voted');
else
$votes.downvote.addClass('voted');
});
};
});
</script>
{% endcompress %}