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"^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(
|
||||||
|
|
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,
|
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,
|
||||||
|
|
|
@ -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")
|
|
@ -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
|
||||||
|
@ -219,7 +232,7 @@ class PostView(TitleMixin, CommentedDetailView):
|
||||||
for post_org in self.object.organizations.all():
|
for post_org in self.object.organizations.all():
|
||||||
if post_org in self.request.profile.organizations.all():
|
if post_org in self.request.profile.organizations.all():
|
||||||
context["valid_org_to_show_edit"].append(post_org)
|
context["valid_org_to_show_edit"].append(post_org)
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
||||||
<div class="post-full">
|
{% set logged_in = request.user.is_authenticated %}
|
||||||
<div class="post-title">{{ title }}</div>
|
{% set profile = request.profile if logged_in else None %}
|
||||||
|
<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>
|
||||||
<div class="time">
|
<div class="time">
|
||||||
{% with authors=post.authors.all() %}
|
{% with authors=post.authors.all() %}
|
||||||
{% if authors %}
|
{% if authors %}
|
||||||
|
|
|
@ -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>
|
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