add blog vote
This commit is contained in:
parent
2a7a33fe1a
commit
c1cf8bc0e4
8 changed files with 242 additions and 7 deletions
|
@ -738,6 +738,8 @@ urlpatterns = [
|
|||
),
|
||||
),
|
||||
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"^license/(?P<key>[-\w.]+)$", license.LicenseDetail.as_view(), name="license"),
|
||||
url(
|
||||
|
|
38
judge/migrations/0136_auto_20221030_0804.py
Normal file
38
judge/migrations/0136_auto_20221030_0804.py
Normal file
File diff suppressed because one or more lines are too long
|
@ -17,7 +17,7 @@ from judge.models.contest import (
|
|||
Rating,
|
||||
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.problem import (
|
||||
LanguageLimit,
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import re
|
||||
from tabnanny import verbose
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import CASCADE
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
@ -82,6 +84,7 @@ class BlogPost(models.Model):
|
|||
og_image = models.CharField(
|
||||
verbose_name=_("openGraph image"), default="", max_length=150, blank=True
|
||||
)
|
||||
score = models.IntegerField(verbose_name=_("vote"), default=0)
|
||||
organizations = models.ManyToManyField(
|
||||
Organization,
|
||||
blank=True,
|
||||
|
@ -125,7 +128,25 @@ class BlogPost(models.Model):
|
|||
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:
|
||||
permissions = (("edit_all_post", _("Edit all posts")),)
|
||||
verbose_name = _("blog post")
|
||||
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")
|
|
@ -1,14 +1,26 @@
|
|||
from django.db.models import Count, Max, Q
|
||||
from django.http import Http404
|
||||
from django.contrib.auth.decorators import login_required
|
||||
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.utils import timezone
|
||||
from django.utils.functional import lazy
|
||||
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.dblock import LockModel
|
||||
from judge.models import (
|
||||
BlogPost,
|
||||
BlogVote,
|
||||
Comment,
|
||||
Contest,
|
||||
Language,
|
||||
|
@ -18,6 +30,7 @@ from judge.models import (
|
|||
Submission,
|
||||
Ticket,
|
||||
)
|
||||
from judge.utils.raw_sql import RawSQLColumn, unique_together_left_join
|
||||
from judge.models.profile import Organization, OrganizationProfile
|
||||
from judge.utils.cachedict import CacheDict
|
||||
from judge.utils.diggpaginator import DiggPaginator
|
||||
|
@ -227,3 +240,72 @@ class PostView(TitleMixin, CommentedDetailView):
|
|||
if not post.can_see(self.request.user):
|
||||
raise Http404()
|
||||
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)
|
||||
|
||||
|
|
|
@ -2,10 +2,12 @@
|
|||
|
||||
{% block js_media %}
|
||||
{% include "comments/media-js.html" %}
|
||||
{% include "blog/media-js.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block media %}
|
||||
{% include "comments/media-css.html" %}
|
||||
{% include "blog/media-css.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block title_row %}
|
||||
|
@ -15,8 +17,30 @@
|
|||
{% endblock %}
|
||||
|
||||
{% 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-title">{{ title }}</div>
|
||||
<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>
|
||||
<div class="time">
|
||||
{% with authors=post.authors.all() %}
|
||||
{% if authors %}
|
||||
|
|
|
@ -27,4 +27,18 @@
|
|||
.recently-attempted h4, .recommended-problems h4 {
|
||||
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>
|
54
templates/blog/media-js.html
Normal file
54
templates/blog/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 = $('.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 %}
|
Loading…
Add table
Add a link
Reference in a new issue