NDOJ/judge/views/comment.py

299 lines
9.3 KiB
Python
Raw Normal View History

2023-05-22 13:52:18 +00:00
from django.conf import settings
2020-01-21 06:35:58 +00:00
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
2023-05-22 13:52:18 +00:00
from django.contrib.auth.context_processors import PermWrapper
2020-01-21 06:35:58 +00:00
from django.core.exceptions import PermissionDenied
from django.db import IntegrityError, transaction
2023-05-22 13:52:18 +00:00
from django.db.models import Q, F, Count, FilteredRelation
from django.db.models.functions import Coalesce
from django.db.models.expressions import F, Value
2020-01-21 06:35:58 +00:00
from django.forms.models import ModelForm
2022-05-14 17:57:27 +00:00
from django.http import (
Http404,
HttpResponse,
HttpResponseBadRequest,
HttpResponseForbidden,
)
2023-09-28 23:23:39 +00:00
from django.shortcuts import get_object_or_404, render
2020-01-21 06:35:58 +00:00
from django.utils.translation import gettext as _
from django.views.decorators.http import require_POST
from django.views.generic import DetailView, UpdateView
2022-07-15 06:00:34 +00:00
from django.urls import reverse_lazy
2020-01-21 06:35:58 +00:00
from reversion import revisions
from reversion.models import Version
from judge.dblock import LockModel
2023-05-22 13:52:18 +00:00
from judge.models import Comment, CommentVote, Notification, BlogPost
2020-01-21 06:35:58 +00:00
from judge.utils.views import TitleMixin
2022-07-15 06:00:34 +00:00
from judge.widgets import MathJaxPagedownWidget, HeavyPreviewPageDownWidget
2020-07-03 02:50:31 +00:00
from judge.comments import add_mention_notifications, del_mention_notifications
2020-01-21 06:35:58 +00:00
2023-04-11 00:44:33 +00:00
import json
2022-05-14 17:57:27 +00:00
__all__ = [
"upvote_comment",
"downvote_comment",
"CommentEditAjax",
"CommentContent",
"CommentEdit",
]
2020-01-21 06:35:58 +00:00
@login_required
def vote_comment(request, delta):
if abs(delta) != 1:
2022-05-14 17:57:27 +00:00
return HttpResponseBadRequest(
_("Messing around, are we?"), content_type="text/plain"
)
2020-01-21 06:35:58 +00:00
2022-05-14 17:57:27 +00:00
if request.method != "POST":
2020-01-21 06:35:58 +00:00
return HttpResponseForbidden()
2022-05-14 17:57:27 +00:00
if "id" not in request.POST:
2020-01-21 06:35:58 +00:00
return HttpResponseBadRequest()
2022-05-14 17:57:27 +00:00
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",
)
2020-01-21 06:35:58 +00:00
try:
2022-05-14 17:57:27 +00:00
comment_id = int(request.POST["id"])
2020-01-21 06:35:58 +00:00
except ValueError:
return HttpResponseBadRequest()
else:
if not Comment.objects.filter(id=comment_id).exists():
raise Http404()
vote = CommentVote()
vote.comment_id = comment_id
vote.voter = request.profile
vote.score = delta
while True:
try:
vote.save()
except IntegrityError:
with LockModel(write=(CommentVote,)):
try:
2022-05-14 17:57:27 +00:00
vote = CommentVote.objects.get(
comment_id=comment_id, voter=request.profile
)
2020-01-21 06:35:58 +00:00
except CommentVote.DoesNotExist:
# We must continue racing in case this is exploited to manipulate votes.
continue
if -vote.score != delta:
2022-05-14 17:57:27 +00:00
return HttpResponseBadRequest(
_("You already voted."), content_type="text/plain"
)
2020-01-21 06:35:58 +00:00
vote.delete()
2022-05-14 17:57:27 +00:00
Comment.objects.filter(id=comment_id).update(score=F("score") - vote.score)
2020-01-21 06:35:58 +00:00
else:
2022-05-14 17:57:27 +00:00
Comment.objects.filter(id=comment_id).update(score=F("score") + delta)
2020-01-21 06:35:58 +00:00
break
2022-05-14 17:57:27 +00:00
return HttpResponse("success", content_type="text/plain")
2020-01-21 06:35:58 +00:00
def upvote_comment(request):
return vote_comment(request, 1)
2023-05-22 16:11:40 +00:00
2020-01-21 06:35:58 +00:00
def downvote_comment(request):
return vote_comment(request, -1)
2023-05-22 16:11:40 +00:00
2023-05-22 13:52:18 +00:00
def get_comments(request, limit=10):
try:
comment_id = int(request.GET["id"])
2023-05-22 16:11:40 +00:00
parent_none = int(request.GET["parent_none"])
2023-05-22 13:52:18 +00:00
except ValueError:
return HttpResponseBadRequest()
else:
if comment_id and not Comment.objects.filter(id=comment_id).exists():
raise Http404()
2023-05-22 16:11:40 +00:00
2023-05-22 13:52:18 +00:00
offset = 0
if "offset" in request.GET:
offset = int(request.GET["offset"])
2023-07-06 15:39:16 +00:00
2023-05-22 16:11:40 +00:00
target_comment = -1
if "target_comment" in request.GET:
target_comment = int(request.GET["target_comment"])
2023-07-06 15:39:16 +00:00
2023-05-22 13:52:18 +00:00
comment_root_id = 0
2023-07-06 15:39:16 +00:00
2023-05-22 16:11:40 +00:00
if comment_id:
2023-05-22 13:52:18 +00:00
comment_obj = Comment.objects.get(pk=comment_id)
comment_root_id = comment_obj.id
else:
comment_obj = None
2023-07-06 15:39:16 +00:00
2023-05-22 13:52:18 +00:00
queryset = comment_obj.linked_object.comments
2023-05-22 16:11:40 +00:00
if parent_none:
2023-05-22 13:52:18 +00:00
queryset = queryset.filter(parent=None, hidden=False)
2023-05-22 16:11:40 +00:00
queryset = queryset.exclude(pk=target_comment)
2023-05-22 13:52:18 +00:00
else:
queryset = queryset.filter(parent=comment_obj, hidden=False)
comment_count = len(queryset)
queryset = (
2023-05-22 16:11:40 +00:00
queryset.select_related("author__user")
.defer("author__about")
.annotate(
2023-07-06 15:39:16 +00:00
count_replies=Count("replies", distinct=True),
2023-05-22 16:11:40 +00:00
revisions=Count("versions", distinct=True),
2023-07-06 15:39:16 +00:00
)[offset : offset + limit]
2023-05-22 16:11:40 +00:00
)
2023-09-29 05:37:28 +00:00
profile = None
2023-05-22 13:52:18 +00:00
if request.user.is_authenticated:
profile = request.profile
queryset = queryset.annotate(
2023-07-06 15:39:16 +00:00
my_vote=FilteredRelation("votes", condition=Q(votes__voter_id=profile.id)),
2023-05-22 13:52:18 +00:00
).annotate(vote_score=Coalesce(F("my_vote__score"), Value(0)))
2023-05-22 16:11:40 +00:00
new_offset = offset + min(len(queryset), limit)
2023-09-28 23:23:39 +00:00
return render(
request,
2023-07-06 15:39:16 +00:00
"comments/content-list.html",
2023-05-22 13:52:18 +00:00
{
2023-09-28 23:23:39 +00:00
"profile": profile,
2023-07-06 15:39:16 +00:00
"comment_root_id": comment_root_id,
"comment_list": queryset,
"vote_hide_threshold": settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD,
2023-05-22 13:52:18 +00:00
"perms": PermWrapper(request.user),
2023-07-06 15:39:16 +00:00
"offset": new_offset,
2023-05-22 13:52:18 +00:00
"limit": limit,
"comment_count": comment_count,
2023-05-22 16:11:40 +00:00
"comment_parent_none": parent_none,
"target_comment": target_comment,
2023-07-06 15:39:16 +00:00
"comment_more": comment_count - new_offset,
},
2023-05-22 13:52:18 +00:00
)
2023-05-22 16:11:40 +00:00
2023-05-22 13:52:18 +00:00
def get_show_more(request):
return get_comments(request)
2023-05-22 16:11:40 +00:00
2023-05-22 13:52:18 +00:00
def get_replies(request):
return get_comments(request)
2020-01-21 06:35:58 +00:00
2023-05-22 16:11:40 +00:00
2020-01-21 06:35:58 +00:00
class CommentMixin(object):
model = Comment
2022-05-14 17:57:27 +00:00
pk_url_kwarg = "id"
context_object_name = "comment"
2020-01-21 06:35:58 +00:00
class CommentRevisionAjax(CommentMixin, DetailView):
2022-05-14 17:57:27 +00:00
template_name = "comments/revision-ajax.html"
2020-01-21 06:35:58 +00:00
def get_context_data(self, **kwargs):
context = super(CommentRevisionAjax, self).get_context_data(**kwargs)
2022-05-14 17:57:27 +00:00
revisions = Version.objects.get_for_object(self.object).order_by("-revision")
2020-01-21 06:35:58 +00:00
try:
2022-05-14 17:57:27 +00:00
wanted = min(
max(int(self.request.GET.get("revision", 0)), 0), len(revisions) - 1
)
2020-01-21 06:35:58 +00:00
except ValueError:
raise Http404
2023-04-11 00:44:33 +00:00
revision = revisions[wanted]
data = json.loads(revision.serialized_data)
try:
context["body"] = data[0]["fields"]["body"]
except Exception:
context["body"] = ""
2020-01-21 06:35:58 +00:00
return context
def get_object(self, queryset=None):
comment = super(CommentRevisionAjax, self).get_object(queryset)
2022-05-14 17:57:27 +00:00
if comment.hidden and not self.request.user.has_perm("judge.change_comment"):
2020-01-21 06:35:58 +00:00
raise Http404()
return comment
class CommentEditForm(ModelForm):
class Meta:
model = Comment
2022-05-14 17:57:27 +00:00
fields = ["body"]
2022-07-15 06:00:34 +00:00
widgets = {
"body": HeavyPreviewPageDownWidget(
2022-12-23 08:20:53 +00:00
id="id-edit-comment-body",
preview=reverse_lazy("comment_preview"),
preview_timeout=1000,
hide_preview_button=True,
2022-07-15 06:00:34 +00:00
),
}
2020-01-21 06:35:58 +00:00
class CommentEditAjax(LoginRequiredMixin, CommentMixin, UpdateView):
2022-05-14 17:57:27 +00:00
template_name = "comments/edit-ajax.html"
2020-01-21 06:35:58 +00:00
form_class = CommentEditForm
def form_valid(self, form):
2020-07-03 02:50:31 +00:00
# update notifications
comment = form.instance
del_mention_notifications(comment)
add_mention_notifications(comment)
2020-01-21 06:35:58 +00:00
with transaction.atomic(), revisions.create_revision():
2022-05-14 17:57:27 +00:00
revisions.set_comment(_("Edited from site"))
2020-01-21 06:35:58 +00:00
revisions.set_user(self.request.user)
return super(CommentEditAjax, self).form_valid(form)
def get_success_url(self):
return self.object.get_absolute_url()
def get_object(self, queryset=None):
comment = super(CommentEditAjax, self).get_object(queryset)
2022-05-14 17:57:27 +00:00
if self.request.user.has_perm("judge.change_comment"):
2020-01-21 06:35:58 +00:00
return comment
profile = self.request.profile
if profile != comment.author or profile.mute or comment.hidden:
raise Http404()
return comment
class CommentEdit(TitleMixin, CommentEditAjax):
2022-05-14 17:57:27 +00:00
template_name = "comments/edit.html"
2020-01-21 06:35:58 +00:00
def get_title(self):
2022-05-14 17:57:27 +00:00
return _("Editing comment")
2020-01-21 06:35:58 +00:00
class CommentContent(CommentMixin, DetailView):
2022-05-14 17:57:27 +00:00
template_name = "comments/content.html"
2020-01-21 06:35:58 +00:00
class CommentVotesAjax(PermissionRequiredMixin, CommentMixin, DetailView):
2022-05-14 17:57:27 +00:00
template_name = "comments/votes.html"
permission_required = "judge.change_commentvote"
2020-01-21 06:35:58 +00:00
def get_context_data(self, **kwargs):
context = super(CommentVotesAjax, self).get_context_data(**kwargs)
2022-05-14 17:57:27 +00:00
context["votes"] = self.object.votes.select_related("voter__user").only(
"id", "voter__display_rank", "voter__user__username", "score"
)
2020-01-21 06:35:58 +00:00
return context
@require_POST
def comment_hide(request):
2022-05-14 17:57:27 +00:00
if not request.user.has_perm("judge.change_comment"):
2020-01-21 06:35:58 +00:00
raise PermissionDenied()
try:
2022-05-14 17:57:27 +00:00
comment_id = int(request.POST["id"])
2020-01-21 06:35:58 +00:00
except ValueError:
return HttpResponseBadRequest()
comment = get_object_or_404(Comment, id=comment_id)
comment.get_descendants(include_self=True).update(hidden=True)
2022-05-14 17:57:27 +00:00
return HttpResponse("ok")