diff --git a/dmoj/urls.py b/dmoj/urls.py index e022a40..353fdcb 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -59,6 +59,7 @@ from judge.views import ( totp, user, volunteer, + pagevote, widgets, 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/downvote/$", comment.downvote_comment, name="comment_downvote"), url(r"^comments/hide/$", comment.comment_hide, name="comment_hide"), diff --git a/judge/migrations/0137_auto_20221116_2201.py b/judge/migrations/0137_auto_20221116_2201.py new file mode 100644 index 0000000..b615e83 --- /dev/null +++ b/judge/migrations/0137_auto_20221116_2201.py @@ -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')}, + }, + ), + ] diff --git a/judge/models/__init__.py b/judge/models/__init__.py index f23645c..aac74f0 100644 --- a/judge/models/__init__.py +++ b/judge/models/__init__.py @@ -54,6 +54,7 @@ from judge.models.submission import ( ) from judge.models.ticket import Ticket, TicketMessage from judge.models.volunteer import VolunteerProblemVote +from judge.models.pagevote import PageVote, PageVoteVoter revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"]) revisions.register(Problem, follow=["language_limits"]) @@ -76,5 +77,6 @@ revisions.register(ProblemData) revisions.register(ProblemTestCase) revisions.register(ContestParticipation) revisions.register(Rating) +revisions.register(PageVoteVoter) revisions.register(VolunteerProblemVote) del revisions diff --git a/judge/models/pagevote.py b/judge/models/pagevote.py new file mode 100644 index 0000000..ca13be7 --- /dev/null +++ b/judge/models/pagevote.py @@ -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") \ No newline at end of file diff --git a/judge/views/blog.py b/judge/views/blog.py index e49b98e..4936bf9 100644 --- a/judge/views/blog.py +++ b/judge/views/blog.py @@ -7,6 +7,7 @@ from django.utils.translation import ugettext as _ from django.views.generic import ListView from judge.comments import CommentedDetailView +from judge.views.pagevote import PageVoteDetailView from judge.models import ( BlogPost, Comment, @@ -190,7 +191,7 @@ class CommentFeed(FeedView): return context -class PostView(TitleMixin, CommentedDetailView): +class PostView(TitleMixin, CommentedDetailView, PageVoteDetailView): model = BlogPost pk_url_kwarg = "id" context_object_name = "post" diff --git a/judge/views/pagevote.py b/judge/views/pagevote.py new file mode 100644 index 0000000..2764514 --- /dev/null +++ b/judge/views/pagevote.py @@ -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 + + + diff --git a/templates/blog/blog.html b/templates/blog/blog.html index f4d0635..48e6804 100644 --- a/templates/blog/blog.html +++ b/templates/blog/blog.html @@ -2,10 +2,12 @@ {% block js_media %} {% include "comments/media-js.html" %} + {% include "pagevotes/media-js.html" %} {% endblock %} {% block media %} {% include "comments/media-css.html" %} + {% include "pagevotes/media-css.html" %} {% endblock %} {% block title_row %} @@ -46,6 +48,7 @@ {{ post_to_facebook(request, post, '') }} {{ post_to_twitter(request, SITE_NAME + ':', post, '') }} + {% include "pagevotes/list.html" %} {% include "comments/list.html" %} {% endblock %} diff --git a/templates/pagevotes/list.html b/templates/pagevotes/list.html new file mode 100644 index 0000000..c9b5e91 --- /dev/null +++ b/templates/pagevotes/list.html @@ -0,0 +1,22 @@ +{% set logged_in = request.user.is_authenticated %} +{% set profile = request.profile if logged_in else None %} +
\ No newline at end of file diff --git a/templates/pagevotes/media-css.html b/templates/pagevotes/media-css.html new file mode 100644 index 0000000..19bf6c0 --- /dev/null +++ b/templates/pagevotes/media-css.html @@ -0,0 +1,19 @@ + diff --git a/templates/pagevotes/media-js.html b/templates/pagevotes/media-js.html new file mode 100644 index 0000000..cb36c55 --- /dev/null +++ b/templates/pagevotes/media-js.html @@ -0,0 +1,54 @@ + +{% compress js %} + +{% endcompress %} \ No newline at end of file