Add problem vote

This commit is contained in:
cuom1999 2022-03-09 23:38:29 -06:00
parent 68c6f13926
commit 2e3a45168e
17 changed files with 685 additions and 122 deletions

View file

@ -5,7 +5,7 @@ from judge.admin.comments import CommentAdmin
from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin
from judge.admin.interface import BlogPostAdmin, LicenseAdmin, LogEntryAdmin, NavigationBarAdmin
from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin
from judge.admin.problem import ProblemAdmin
from judge.admin.problem import ProblemAdmin, ProblemPointsVoteAdmin
from judge.admin.profile import ProfileAdmin
from judge.admin.runtime import JudgeAdmin, LanguageAdmin
from judge.admin.submission import SubmissionAdmin
@ -13,7 +13,7 @@ from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin
from judge.admin.ticket import TicketAdmin
from judge.models import BlogPost, Comment, CommentLock, Contest, ContestParticipation, \
ContestTag, Judge, Language, License, MiscConfig, NavigationBar, Organization, \
OrganizationRequest, Problem, ProblemGroup, ProblemType, Profile, Submission, Ticket
OrganizationRequest, Problem, ProblemGroup, ProblemPointsVote, ProblemType, Profile, Submission, Ticket
admin.site.register(BlogPost, BlogPostAdmin)
admin.site.register(Comment, CommentAdmin)
@ -31,6 +31,7 @@ admin.site.register(Organization, OrganizationAdmin)
admin.site.register(OrganizationRequest, OrganizationRequestAdmin)
admin.site.register(Problem, ProblemAdmin)
admin.site.register(ProblemGroup, ProblemGroupAdmin)
admin.site.register(ProblemPointsVote, ProblemPointsVoteAdmin)
admin.site.register(ProblemType, ProblemTypeAdmin)
admin.site.register(Profile, ProfileAdmin)
admin.site.register(Submission, SubmissionAdmin)

View file

@ -236,3 +236,17 @@ class ProblemAdmin(VersionAdmin):
if form.cleaned_data.get('change_message'):
return form.cleaned_data['change_message']
return super(ProblemAdmin, self).construct_change_message(request, form, *args, **kwargs)
class ProblemPointsVoteAdmin(admin.ModelAdmin):
list_display = ('points', 'voter', 'problem', 'vote_time')
search_fields = ('voter', 'problem')
readonly_fields = ('voter', 'problem', 'vote_time')
def has_change_permission(self, request, obj=None):
if obj is None:
return request.user.has_perm('judge.edit_own_problem')
return obj.problem.is_editable_by(request.user)
def lookup_allowed(self, key, value):
return super().lookup_allowed(key, value) or key in ('problem__code',)

View file

@ -45,7 +45,7 @@ class TimezoneFilter(admin.SimpleListFilter):
class ProfileAdmin(VersionAdmin):
fields = ('user', 'display_rank', 'about', 'organizations', 'timezone', 'language', 'ace_theme',
'math_engine', 'last_access', 'ip', 'mute', 'is_unlisted', 'notes', 'is_totp_enabled', 'user_script',
'math_engine', 'last_access', 'ip', 'mute', 'is_unlisted', 'is_banned_problem_voting', 'notes', 'is_totp_enabled', 'user_script',
'current_contest')
readonly_fields = ('user',)
list_display = ('admin_user_admin', 'email', 'is_totp_enabled', 'timezone_full',

View file

@ -12,7 +12,7 @@ from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django_ace import AceWidget
from judge.models import Contest, Language, Organization, PrivateMessage, Problem, Profile, Submission
from judge.models import Contest, Language, Organization, PrivateMessage, Problem, ProblemPointsVote, Profile, Submission
from judge.utils.subscription import newsletter_id
from judge.widgets import HeavyPreviewPageDownWidget, MathJaxPagedownWidget, PagedownWidget, Select2MultipleWidget, \
Select2Widget
@ -161,4 +161,10 @@ class ContestCloneForm(Form):
key = self.cleaned_data['key']
if Contest.objects.filter(key=key).exists():
raise ValidationError(_('Contest with key already exists.'))
return key
return key
class ProblemPointsVoteForm(ModelForm):
class Meta:
model = ProblemPointsVote
fields = ['points']

View file

@ -0,0 +1,34 @@
# Generated by Django 2.2.25 on 2022-03-06 04:24
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('judge', '0119_auto_20220306_0512'),
]
operations = [
migrations.AddField(
model_name='profile',
name='is_banned_problem_voting',
field=models.BooleanField(default=False, help_text="User will not be able to vote on problems' point values.", verbose_name='banned from voting'),
),
migrations.CreateModel(
name='ProblemPointsVote',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('points', models.IntegerField(help_text='The amount of points you think this problem deserves.', validators=[django.core.validators.MinValueValidator(100), django.core.validators.MaxValueValidator(600)], verbose_name='proposed point value')),
('vote_time', models.DateTimeField(auto_now_add=True, verbose_name='The time this vote was cast')),
('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='problem_points_votes', to='judge.Problem')),
('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='problem_points_votes', to='judge.Profile')),
],
options={
'verbose_name': 'vote',
'verbose_name_plural': 'votes',
},
),
]

View file

@ -7,7 +7,7 @@ from judge.models.contest import Contest, ContestMoss, ContestParticipation, Con
from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex
from judge.models.message import PrivateMessage, PrivateMessageThread
from judge.models.problem import LanguageLimit, License, Problem, ProblemClarification, ProblemGroup, \
ProblemTranslation, ProblemType, Solution, TranslatedProblemForeignKeyQuerySet, TranslatedProblemQuerySet
ProblemTranslation, ProblemType, Solution, TranslatedProblemForeignKeyQuerySet, TranslatedProblemQuerySet, ProblemPointsVote
from judge.models.problem_data import CHECKERS, ProblemData, ProblemTestCase, problem_data_storage, \
problem_directory_file
from judge.models.profile import Organization, OrganizationRequest, Profile, Friend

View file

@ -375,6 +375,25 @@ class Problem(models.Model):
save.alters_data = True
def can_vote(self, user):
if not user.is_authenticated:
return False
# If the user is in contest, nothing should be shown.
if user.profile.current_contest:
return False
# If the user is not allowed to vote
if user.profile.is_unlisted or user.profile.is_banned_problem_voting:
return False
# If the user is banned from submitting to the problem.
if self.banned_users.filter(pk=user.pk).exists():
return False
# If the user has a full AC submission to the problem (solved the problem).
return self.submission_set.filter(user=user.profile, result='AC', points=F('problem__points')).exists()
class Meta:
permissions = (
('see_private_problem', 'See hidden problems'),
@ -448,3 +467,29 @@ class Solution(models.Model):
)
verbose_name = _('solution')
verbose_name_plural = _('solutions')
class ProblemPointsVote(models.Model):
points = models.IntegerField(
verbose_name=_('proposed point value'),
help_text=_('The amount of points you think this problem deserves.'),
validators=[
MinValueValidator(100),
MaxValueValidator(600),
],
)
voter = models.ForeignKey(Profile, related_name='problem_points_votes', on_delete=CASCADE, db_index=True)
problem = models.ForeignKey(Problem, related_name='problem_points_votes', on_delete=CASCADE, db_index=True)
vote_time = models.DateTimeField(
verbose_name=_('The time this vote was cast'),
auto_now_add=True,
blank=True,
)
class Meta:
verbose_name = _('vote')
verbose_name_plural = _('votes')
def __str__(self):
return f'{self.voter}: {self.points} for {self.problem.code}'

View file

@ -98,6 +98,11 @@ class Profile(models.Model):
default=False)
is_unlisted = models.BooleanField(verbose_name=_('unlisted user'), help_text=_('User will not be ranked.'),
default=False)
is_banned_problem_voting = models.BooleanField(
verbose_name=_('banned from voting'),
help_text=_("User will not be able to vote on problems' point values."),
default=False,
)
rating = models.IntegerField(null=True, default=None)
user_script = models.TextField(verbose_name=_('user script'), default='', blank=True, max_length=65536,
help_text=_('User-defined JavaScript for site customization.'))

View file

@ -12,7 +12,7 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import transaction
from django.db.models import Count, F, Prefetch, Q, Sum, Case, When, IntegerField
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, render
from django.template.loader import get_template
from django.urls import reverse
@ -26,10 +26,10 @@ from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin
from judge.comments import CommentedDetailView
from judge.forms import ProblemCloneForm, ProblemSubmitForm
from judge.forms import ProblemCloneForm, ProblemSubmitForm, ProblemPointsVoteForm
from judge.models import ContestProblem, ContestSubmission, Judge, Language, Problem, ProblemClarification, \
ProblemGroup, ProblemTranslation, ProblemType, RuntimeVersion, Solution, Submission, SubmissionSource, \
TranslatedProblemForeignKeyQuerySet, Organization
ProblemGroup, ProblemTranslation, ProblemType, ProblemPointsVote, RuntimeVersion, Solution, Submission, SubmissionSource, \
TranslatedProblemForeignKeyQuerySet, Organization
from judge.pdf_problems import DefaultPdfMaker, HAS_PDF
from judge.utils.diggpaginator import DiggPaginator
from judge.utils.opengraph import generate_opengraph
@ -216,8 +216,65 @@ class ProblemDetail(ProblemMixin, SolvedProblemMixin, DetailView):
context['description'], 'problem')
context['meta_description'] = self.object.summary or metadata[0]
context['og_image'] = self.object.og_image or metadata[1]
context['can_vote'] = self.object.can_vote(user)
if context['can_vote']:
try:
context['vote'] = ProblemPointsVote.objects.get(voter=user.profile, problem=self.object)
except ObjectDoesNotExist:
context['vote'] = None
else:
context['vote'] = None
all_votes = list(self.object.problem_points_votes.order_by('points').values_list('points', flat=True))
context['has_votes'] = len(all_votes) > 0
# If the user is not currently in contest.
if not user.is_authenticated or user.profile.current_contest is None:
context['all_votes'] = all_votes
context['max_possible_vote'] = 600
context['min_possible_vote'] = 100
return context
class DeleteVote(ProblemMixin, SingleObjectMixin, View):
def get(self, request, *args, **kwargs):
return HttpResponseForbidden(status=405, content_type='text/plain')
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if not request.user.is_authenticated:
return HttpResponseForbidden('Not signed in.', content_type='text/plain')
elif self.object.can_vote(request.user):
ProblemPointsVote.objects.filter(voter=request.profile, problem=self.object).delete()
return HttpResponse('success', content_type='text/plain')
else:
return HttpResponseForbidden('Not allowed to delete votes on this problem.', content_type='text/plain')
class Vote(ProblemMixin, SingleObjectMixin, View):
def get(self, request, *args, **kwargs):
return HttpResponseForbidden(status=405, content_type='text/plain')
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if not self.object.can_vote(request.user): # Not allowed to vote for some reason.
return HttpResponseForbidden('Not allowed to vote on this problem.', content_type='text/plain')
form = ProblemPointsVoteForm(request.POST)
if form.is_valid():
with transaction.atomic():
# Delete any pre existing votes.
ProblemPointsVote.objects.filter(voter=request.profile, problem=self.object).delete()
vote = form.save(commit=False)
vote.voter = request.profile
vote.problem = self.object
vote.save()
return JsonResponse({'points': vote.points})
else:
return JsonResponse(form.errors, status=400)
class ProblemComments(ProblemMixin, TitleMixin, CommentedDetailView):
context_object_name = 'problem'