From 8f259d1d9734e83e709e2c06b632c403f2a8ea31 Mon Sep 17 00:00:00 2001 From: int-y1 Date: Mon, 12 Sep 2022 23:22:45 -0400 Subject: [PATCH] Add view for problem point voting --- dmoj/urls.py | 4 ++ judge/forms.py | 11 ++++- judge/views/problem.py | 92 ++++++++++++++++++++++++++++++++++++++++-- robots.txt | 3 ++ 4 files changed, 105 insertions(+), 5 deletions(-) diff --git a/dmoj/urls.py b/dmoj/urls.py index de6d0dc50..a9932faa4 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -123,6 +123,10 @@ urlpatterns = [ path('/tickets', ticket.ProblemTicketListView.as_view(), name='problem_ticket_list'), path('/tickets/new', ticket.NewProblemTicketView.as_view(), name='new_problem_ticket'), + path('/vote', problem.ProblemVote.as_view(), name='problem_vote'), + path('/vote/delete', problem.DeleteProblemVote.as_view(), name='delete_problem_vote'), + path('/vote/stats', problem.ProblemVoteStats.as_view(), name='problem_vote_stats'), + path('/manage/submission', include([ path('', problem_manage.ManageProblemSubmissionView.as_view(), name='problem_manage_submissions'), path('/rejudge', problem_manage.RejudgeSubmissionsView.as_view(), name='problem_submissions_rejudge'), diff --git a/judge/forms.py b/judge/forms.py index 2d5ee5011..5741a5962 100644 --- a/judge/forms.py +++ b/judge/forms.py @@ -15,7 +15,8 @@ from django.utils.text import format_lazy from django.utils.translation import gettext_lazy as _, ngettext_lazy from django_ace import AceWidget -from judge.models import Contest, Language, Organization, Problem, Profile, Submission, WebAuthnCredential +from judge.models import Contest, Language, Organization, Problem, ProblemPointsVote, Profile, Submission, \ + WebAuthnCredential from judge.utils.subscription import newsletter_id from judge.widgets import HeavyPreviewPageDownWidget, Select2MultipleWidget, Select2Widget @@ -284,3 +285,11 @@ class ContestCloneForm(Form): if Contest.objects.filter(key=key).exists(): raise ValidationError(_('Contest with key already exists.')) return key + + +class ProblemPointsVoteForm(ModelForm): + note = CharField(max_length=8192, required=False) + + class Meta: + model = ProblemPointsVote + fields = ['points', 'note'] diff --git a/judge/views/problem.py b/judge/views/problem.py index 6206fa443..5104cd5ff 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -5,6 +5,7 @@ import shutil from datetime import timedelta from operator import itemgetter from random import randrange +from statistics import mean, median from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin @@ -13,7 +14,7 @@ from django.db import transaction from django.db.models import BooleanField, Case, CharField, Count, F, FilteredRelation, Prefetch, Q, When from django.db.models.functions import Coalesce from django.db.utils import ProgrammingError -from django.http import Http404, HttpResponse, HttpResponseForbidden, HttpResponseRedirect +from django.http import Http404, HttpResponse, HttpResponseForbidden, HttpResponseRedirect, JsonResponse from django.shortcuts import get_object_or_404 from django.template.loader import get_template from django.urls import reverse @@ -22,13 +23,13 @@ from django.utils.functional import cached_property from django.utils.html import escape, format_html from django.utils.safestring import mark_safe from django.utils.translation import gettext as _, gettext_lazy -from django.views.generic import ListView, View +from django.views.generic import DetailView, ListView, View from django.views.generic.detail import SingleObjectMixin from reversion import revisions from judge.comments import CommentedDetailView -from judge.forms import ProblemCloneForm, ProblemSubmitForm -from judge.models import ContestSubmission, Judge, Language, Problem, ProblemGroup, \ +from judge.forms import ProblemCloneForm, ProblemPointsVoteForm, ProblemSubmitForm +from judge.models import ContestSubmission, Judge, Language, Problem, ProblemGroup, ProblemPointsVote, \ ProblemTranslation, ProblemType, RuntimeVersion, Solution, Submission, SubmissionSource from judge.pdf_problems import DefaultPdfMaker, HAS_PDF from judge.utils.diggpaginator import DiggPaginator @@ -201,6 +202,89 @@ class ProblemDetail(ProblemMixin, SolvedProblemMixin, CommentedDetailView): context['description'], 'problem') context['meta_description'] = self.object.summary or metadata[0] context['og_image'] = self.object.og_image or metadata[1] + + context['vote_perm'] = self.object.vote_permission_for_user(user) + if context['vote_perm'].can_vote(): + try: + context['vote'] = ProblemPointsVote.objects.get(voter=user.profile, problem=self.object) + except ObjectDoesNotExist: + context['vote'] = None + else: + context['vote'] = None + + return context + + +class ProblemVote(ProblemMixin, DetailView): + context_object_name = 'problem' + template_name = 'problem/vote-ajax.html' + + def get_context_data(self, **kwargs): + if not self.object.vote_permission_for_user(self.request.user).can_vote(): + raise Http404() + + context = super().get_context_data(**kwargs) + + try: + context['vote'] = ProblemPointsVote.objects.get(voter=self.request.profile, problem=self.object) + except ObjectDoesNotExist: + context['vote'] = None + + context['max_possible_vote'] = settings.DMOJ_PROBLEM_MAX_USER_POINTS_VOTE + context['min_possible_vote'] = settings.DMOJ_PROBLEM_MIN_USER_POINTS_VOTE + return context + + def post(self, request, *args, **kwargs): + problem = self.get_object() + if not problem.vote_permission_for_user(request.user).can_vote(): + return JsonResponse({'message': _('Not allowed to vote on this problem.')}, status=403) + + form = ProblemPointsVoteForm(request.POST) + if not form.is_valid(): + return JsonResponse(form.errors, status=400) + + with transaction.atomic(): + # Delete any pre-existing votes. + ProblemPointsVote.objects.filter(voter=request.profile, problem=problem).delete() + vote = form.save(commit=False) + vote.voter = request.profile + vote.problem = problem + vote.save() + + return JsonResponse({'points': vote.points}) + + +class DeleteProblemVote(ProblemMixin, SingleObjectMixin, View): + http_method_names = ['options', 'post'] # This disables GET requests, even though ProblemMixin.get exists. + + def post(self, request, *args, **kwargs): + problem = self.get_object() + if not problem.vote_permission_for_user(request.user).can_vote(): + return JsonResponse({'message': _('Not allowed to delete votes on this problem.')}, status=403) + + ProblemPointsVote.objects.filter(voter=request.profile, problem=problem).delete() + return JsonResponse({'message': _('success')}) + + +class ProblemVoteStats(ProblemMixin, DetailView): + context_object_name = 'problem' + template_name = 'problem/vote-stats-ajax.html' + + def get_context_data(self, **kwargs): + if not self.object.vote_permission_for_user(self.request.user).can_view(): + raise Http404() + + context = super().get_context_data(**kwargs) + + votes = list(self.object.problem_points_votes.order_by('points').values_list('points', flat=True)) + context['votes'] = votes + + if votes: + context['mean'] = mean(votes) + context['median'] = median(votes) + + context['max_possible_vote'] = settings.DMOJ_PROBLEM_MAX_USER_POINTS_VOTE + context['min_possible_vote'] = settings.DMOJ_PROBLEM_MIN_USER_POINTS_VOTE return context diff --git a/robots.txt b/robots.txt index a12ad11ea..5bc2eec15 100644 --- a/robots.txt +++ b/robots.txt @@ -36,6 +36,9 @@ Disallow: /problem/*/submissions Disallow: /problem/*/submit Disallow: /problem/*/test_data Disallow: /problem/*/tickets +Disallow: /problem/*/vote +Disallow: /problem/*/vote/delete +Disallow: /problem/*/vote/stats Disallow: /src Disallow: /stats Disallow: /submission