Skip to content

Commit

Permalink
:monkachrist:
Browse files Browse the repository at this point in the history
  • Loading branch information
LakshyRRPS authored and lakshy-gupta committed Apr 19, 2021
1 parent 201ccf7 commit df78858
Show file tree
Hide file tree
Showing 16 changed files with 852 additions and 10 deletions.
6 changes: 6 additions & 0 deletions dmoj/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
DMOJ_PROBLEM_MIN_MEMORY_LIMIT = 0 # kilobytes
DMOJ_PROBLEM_MAX_MEMORY_LIMIT = 1048576 # kilobytes
DMOJ_PROBLEM_MIN_PROBLEM_POINTS = 0
DMOJ_PROBLEM_MIN_USER_POINTS_VOTE = 0
DMOJ_PROBLEM_MAX_USER_POINTS_VOTE = 50
DMOJ_PROBLEM_HOT_PROBLEM_COUNT = 7
DMOJ_RATING_COLORS = True
DMOJ_EMAIL_THROTTLING = (10, 60)
Expand Down Expand Up @@ -181,6 +183,10 @@
'judge.Judge',
],
},
{
'model': 'judge.ProblemPointsVote',
'icon': 'fa-envelope',
},
{
'model': 'judge.Contest',
'icon': 'fa-bar-chart',
Expand Down
4 changes: 3 additions & 1 deletion judge/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
from judge.admin.submission import SubmissionAdmin
from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin
from judge.admin.ticket import TicketAdmin
from judge.admin.vote import VoteAdmin
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)
Expand All @@ -38,3 +39,4 @@
admin.site.register(Profile, ProfileAdmin)
admin.site.register(Submission, SubmissionAdmin)
admin.site.register(Ticket, TicketAdmin)
admin.site.register(ProblemPointsVote, VoteAdmin)
9 changes: 6 additions & 3 deletions judge/admin/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,12 @@ def queryset(self, request, queryset):


class ProfileAdmin(NoBatchDeleteMixin, 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',
'current_contest')
fields = (
'user', 'display_rank', 'about', 'organizations', 'timezone',
'language', 'ace_theme', 'math_engine', 'last_access',
'ip', 'mute', 'is_unlisted', 'is_banned_from_voting_problem_points',
'notes', 'is_totp_enabled', 'user_script', 'current_contest',
)
readonly_fields = ('user',)
list_display = ('admin_user_admin', 'email', 'is_totp_enabled', 'timezone_full',
'date_joined', 'last_access', 'ip', 'show_public')
Expand Down
18 changes: 18 additions & 0 deletions judge/admin/vote.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

from django.contrib import admin


class VoteAdmin(admin.ModelAdmin):
list_display = ('points', 'voter', 'problem', 'note')
search_fields = ('voter', 'problem')

# if the user has edit all problem or edit own problem perms, so curators authors and superusers
def has_change_permission(self, request, obj=None):
if not request.user.has_perm('judge.edit_own_problem'):
return False
if request.user.has_perm('judge.edit_all_problem') or obj is None:
return True
return obj.problem.is_editor(request.profile)

def lookup_allowed(self, key, value):
return super(VoteAdmin, self).lookup_allowed(key, value) or key in ('problem__code',)
9 changes: 8 additions & 1 deletion judge/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def get(self, request, *args, **kwargs):
self.object = self.get_object()
return self.render_to_response(self.get_context_data(
object=self.object,
comment_form=CommentForm(request, initial={'page': self.get_comment_page(), 'parent': None}),
comment_request=request,
))

def get_context_data(self, **kwargs):
Expand All @@ -119,4 +119,11 @@ def get_context_data(self, **kwargs):
context['comment_list'] = queryset
context['vote_hide_threshold'] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD

# if we have to create a new comment form, the form's 'request' is guaranteed to have been given by GET or POST
if 'comment_form' not in context:
context['comment_form'] = CommentForm(
context['comment_request'],
initial={'page': self.get_comment_page(), 'parent': None},
)

return context
10 changes: 8 additions & 2 deletions judge/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
from django.utils.translation import gettext_lazy as _

from django_ace import AceWidget
from judge.models import Contest, Language, Organization, PrivateMessage, Problem, Profile, Submission, \
WebAuthnCredential
from judge.models import Contest, Language, Organization, PrivateMessage, Problem, ProblemPointsVote, Profile, \
Submission, WebAuthnCredential
from judge.utils.subscription import newsletter_id
from judge.widgets import HeavyPreviewPageDownWidget, MathJaxPagedownWidget, PagedownWidget, Select2MultipleWidget, \
Select2Widget
Expand Down Expand Up @@ -277,3 +277,9 @@ def clean_key(self):
if Contest.objects.filter(key=key).exists():
raise ValidationError(_('Contest with key already exists.'))
return key


class ProblemPointsVoteForm(ModelForm):
class Meta:
model = ProblemPointsVote
fields = ['points', 'note']
34 changes: 34 additions & 0 deletions judge/migrations/0116_add_voting_functionality.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 2.2.19 on 2021-04-19 01:20

import django.core.validators
import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('judge', '0115_contest_scoreboard_visibility'),
]

operations = [
migrations.AddField(
model_name='profile',
name='is_banned_from_voting_problem_points',
field=models.BooleanField(default=False, help_text="User will not be able to vote on problems' points 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.FloatField(help_text='The amount of points you think this problem deserves.', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50)], verbose_name='points')),
('note', models.TextField(blank=True, default=' ', help_text='Justification for problem points value.', max_length=2048, verbose_name='note')),
('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',
},
),
]
1 change: 1 addition & 0 deletions judge/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
ProblemTranslation, ProblemType, Solution, TranslatedProblemForeignKeyQuerySet, TranslatedProblemQuerySet
from judge.models.problem_data import CHECKERS, ProblemData, ProblemTestCase, problem_data_storage, \
problem_directory_file
from judge.models.problem_points_vote import ProblemPointsVote
from judge.models.profile import Organization, OrganizationRequest, Profile, WebAuthnCredential
from judge.models.runtime import Judge, Language, RuntimeVersion
from judge.models.submission import SUBMISSION_RESULT, Submission, SubmissionSource, SubmissionTestCase
Expand Down
38 changes: 38 additions & 0 deletions judge/models/problem_points_vote.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from django.conf import settings
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import CASCADE
from django.utils.translation import gettext as _

from judge.models.problem import Problem
from judge.models.profile import Profile


class ProblemPointsVote(models.Model):
points = models.FloatField( # How much this vote is worth
verbose_name=_('points'),
help_text=_('The amount of points you think this problem deserves.'),
validators=[
MinValueValidator(settings.DMOJ_PROBLEM_MIN_USER_POINTS_VOTE),
MaxValueValidator(settings.DMOJ_PROBLEM_MAX_USER_POINTS_VOTE),
],
)
# who voted
voter = models.ForeignKey(Profile, related_name='problem_points_votes', on_delete=CASCADE, db_index=True)
# what problem is this vote for
problem = models.ForeignKey(Problem, related_name='problem_points_votes', on_delete=CASCADE, db_index=True)
note = models.TextField( # note to go along with vote
verbose_name=_('note'),
help_text=_('Justification for problem points value.'),
max_length=2048,
blank=True,
default=' ',
)

# The name that shows up on the sidebar instead of the model class name
class Meta:
verbose_name = _('Vote')
verbose_name_plural = _('Votes')

def __str__(self):
return f'{self.voter}: {self.points} for {self.problem.code} - "{self.note}"'
6 changes: 6 additions & 0 deletions judge/models/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ class Profile(models.Model):
default=False)
is_unlisted = models.BooleanField(verbose_name=_('unlisted user'), help_text=_('User will not be ranked.'),
default=False)
# field for whether or not users are able to vote on problems
is_banned_from_voting_problem_points = models.BooleanField(
verbose_name=_('banned from voting'),
help_text=_('User will not be able to vote on problems\' points 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.'))
Expand Down
128 changes: 125 additions & 3 deletions judge/views/problem.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import math
import os
import shutil
from datetime import timedelta
Expand All @@ -7,7 +8,7 @@

from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError
from django.db import transaction
from django.db.models import Count, F, Prefetch, Q
from django.db.utils import ProgrammingError
Expand All @@ -25,8 +26,8 @@
from django.views.generic.detail import SingleObjectMixin

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, \
TranslatedProblemForeignKeyQuerySet
from judge.pdf_problems import DefaultPdfMaker, HAS_PDF
Expand Down Expand Up @@ -157,6 +158,18 @@ class ProblemDetail(ProblemMixin, SolvedProblemMixin, CommentedDetailView):
def get_comment_page(self):
return 'p:%s' % self.object.code

def can_vote(self, user, problem):
if not user.is_authenticated: # reject anons
return False
banned = user.profile.is_banned_from_voting_problem_points # banned from voting site wide
in_contest = user.profile.current_contest is not None # whether or not they're in contest
# already ac'd this q, not in contest, and also not banned
ac = Submission.objects.filter(user=user.profile, problem=problem, result='AC').exists()
return ac and not in_contest and not banned

def default_note(self):
return _('A short justification for this problem\'s points value.')

def get_context_data(self, **kwargs):
context = super(ProblemDetail, self).get_context_data(**kwargs)
user = self.request.user
Expand Down Expand Up @@ -213,8 +226,117 @@ def get_context_data(self, **kwargs):
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.can_vote(user, self.object) # if this problem is votable by this user
# the vote this user has already cast on this problem
if context['can_vote']:
vote = ProblemPointsVote.objects.filter(voter=user.profile, problem=self.object)
# whether or not they've already voted
context['has_voted'] = context['can_vote'] and vote.exists()
if context['has_voted']:
context['voted_points'] = vote.first().points # the previous vote's points
context['voted_note'] = vote.first().note

if 'problem_points_vote_form' not in context:
context['problem_points_vote_form'] = ProblemPointsVoteForm({})

if 'has_errors' not in context:
context['has_errors'] = False

if 'points_placeholder' not in context: # placeholder for the points
if context['has_voted']: # if voted, the vote
context['points_placeholder'] = context['voted_points']
else: # otherwise a *nice* default
context['points_placeholder'] = 69.42069

if 'note_placeholder' not in context:
if context['has_voted']:
context['note_placeholder'] = context['voted_note']
else:
context['note_placeholder'] = self.default_note()

all_votes = sorted([v.points for v in ProblemPointsVote.objects.filter(problem=self.object)])
context['has_votes'] = len(all_votes) > 0
if context['has_votes']:
context['mean_vote'] = sum(all_votes) / len(all_votes)
context['num_votes'] = len(all_votes)
context['min_vote'] = all_votes[0]
context['max_vote'] = all_votes[-1]

# provides index and value of the median of some range of the data
def median(left_index, right_index, data):
size = right_index - left_index + 1
index = left_index + (size - 1) / 2
value = None
if size % 2 == 1:
value = data[int(index)]
else:
value = (data[math.floor(index)] + data[math.ceil(index)]) / 2
return index, value

median_data = median(0, len(all_votes) - 1, all_votes)
context['median_vote'] = median_data[1]
median_index = median_data[0]

context['enough_data_for_plot'] = len(all_votes) > 2
if context['enough_data_for_plot']:
# box and whisker plot data
q1 = median(0, math.ceil(median_index - 1), all_votes) # first quartile
q3 = median(math.floor(median_index + 1), len(all_votes) - 1, all_votes) # second quartile
context['first_quartile'] = q1[1]
context['third_quartile'] = q3[1]

context['in_contest'] = contest_problem is not None

if context['can_vote']:
context['all_votes'] = all_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

def post(self, request, *args, **kwargs):
self.object = self.get_object()

if 'vote_confirmation' in request.POST: # deal with request as problem points vote
if not self.can_vote(request.user, self.object): # not allowed to vote for some reason
return HttpResponseForbidden()
else:
form = ProblemPointsVoteForm(request.POST)
try:
if form.is_valid():
# delete any pre existing votes (will be replaced by new one)
ProblemPointsVote.objects.filter(voter=request.user.profile, problem=self.object).delete()
vote = form.save(commit=False)
vote.voter = request.user.profile
vote.problem = self.object
if vote.note == self.default_note() or vote.note.strip() == '':
vote.note = ' ' # correct to blank
vote.save()
return self.get(request, *args, **kwargs)
else:
raise ValidationError # go to invalid case
except ValidationError:
context = self.get_context_data(
object=self.object,
comment_request=request, # comment needs this to initialize
problem_points_vote_form=form, # extra context for the form re-rendering
has_errors=True,
points_placeholder=request.POST['points'],
note_placeholder=request.POST['note'],
)
return self.render_to_response(context)

elif 'delete_confirmation' in request.POST:
if not request.user.is_authenticated: # un authed person tries to find a way to delete
return HttpResponseForbidden()
# delete anything that matches (if nothing matches it doesn't matter)
ProblemPointsVote.objects.filter(voter=request.user.profile, problem=self.object).delete()
return self.get(request, *args, **kwargs)
else: # forward to next level of post request (comment post request as of writing)
return super(ProblemDetail, self).post(request, *args, **kwargs)


class LatexError(Exception):
pass
Expand Down
5 changes: 5 additions & 0 deletions templates/admin/judge/problem/change_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,10 @@
<i class="fa fa-lg fa-search-plus"></i>
<span class="text">{% trans "View submissions" %}</span>
</a>
<a style="display: none" title="{% trans "View votes" %}" class="button submissions-link"
href="{% url 'admin:judge_problempointsvote_changelist' %}?problem__code={{ original.code }}">
<i class="fa fa-lg fa-search-plus"></i>
<span class="text">{% trans "View votes" %}</span>
</a>
{% endif %}
{% endblock %}
1 change: 1 addition & 0 deletions templates/comments/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ <h3>{{ _('New comment') }}</h3>
</div>
<hr>
<input style="float:right" type="submit" value="{{ _('Post!') }}" class="button">
<input type="hidden" name="post_confirmation"/>
</form>
{% endif %}
</div>
Expand Down
Loading

0 comments on commit df78858

Please sign in to comment.