Add problem vote
This commit is contained in:
parent
68c6f13926
commit
2e3a45168e
17 changed files with 685 additions and 122 deletions
|
@ -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)
|
||||
|
|
|
@ -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',)
|
|
@ -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',
|
||||
|
|
|
@ -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']
|
34
judge/migrations/0120_auto_20220306_1124.py
Normal file
34
judge/migrations/0120_auto_20220306_1124.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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}'
|
|
@ -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.'))
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue