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

@ -162,6 +162,7 @@ else:
'children': [
'judge.ProblemGroup',
'judge.ProblemType',
'judge.ProblemPointsVote',
],
},
{

View file

@ -139,6 +139,8 @@ urlpatterns = [
url(r'^/tickets$', ticket.ProblemTicketListView.as_view(), name='problem_ticket_list'),
url(r'^/tickets/new$', ticket.NewProblemTicketView.as_view(), name='new_problem_ticket'),
url(r'^/vote$', problem.Vote.as_view(), name='vote'),
url(r'^/manage/submission', include([
url('^$', problem_manage.ManageProblemSubmissionView.as_view(), name='problem_manage_submissions'),
url('^/action$', problem_manage.ActionSubmissionsView.as_view(), name='problem_submissions_action'),

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
@ -162,3 +162,9 @@ class ContestCloneForm(Form):
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']

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,9 +26,9 @@ 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, \
ProblemGroup, ProblemTranslation, ProblemType, ProblemPointsVote, RuntimeVersion, Solution, Submission, SubmissionSource, \
TranslatedProblemForeignKeyQuerySet, Organization
from judge.pdf_problems import DefaultPdfMaker, HAS_PDF
from judge.utils.diggpaginator import DiggPaginator
@ -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'

View file

@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: lqdoj2\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-01-10 18:14+0700\n"
"POT-Creation-Date: 2022-03-10 12:30+0700\n"
"PO-Revision-Date: 2021-07-20 03:44\n"
"Last-Translator: Icyene\n"
"Language-Team: Vietnamese\n"
@ -20,7 +20,7 @@ msgstr ""
#: chat_box/models.py:23 chat_box/models.py:41 chat_box/models.py:47
#: judge/admin/interface.py:110 judge/models/contest.py:403
#: judge/models/contest.py:528 judge/models/profile.py:215
#: judge/models/contest.py:528 judge/models/profile.py:220
msgid "user"
msgstr "người dùng"
@ -36,71 +36,71 @@ msgstr "nội dung bình luận"
msgid "last seen"
msgstr "xem lần cuối"
#: chat_box/views.py:29 templates/chat/chat.html:4 templates/chat/chat.html:541
#: chat_box/views.py:29 templates/chat/chat.html:4 templates/chat/chat.html:548
msgid "Chat Box"
msgstr "Chat Box"
#: dmoj/settings.py:358
#: dmoj/settings.py:359
msgid "German"
msgstr ""
#: dmoj/settings.py:359
#: dmoj/settings.py:360
msgid "English"
msgstr ""
#: dmoj/settings.py:360
#: dmoj/settings.py:361
msgid "Spanish"
msgstr ""
#: dmoj/settings.py:361
#: dmoj/settings.py:362
msgid "French"
msgstr ""
#: dmoj/settings.py:362
#: dmoj/settings.py:363
msgid "Croatian"
msgstr ""
#: dmoj/settings.py:363
#: dmoj/settings.py:364
msgid "Hungarian"
msgstr ""
#: dmoj/settings.py:364
#: dmoj/settings.py:365
msgid "Japanese"
msgstr ""
#: dmoj/settings.py:365
#: dmoj/settings.py:366
msgid "Korean"
msgstr ""
#: dmoj/settings.py:366
#: dmoj/settings.py:367
msgid "Brazilian Portuguese"
msgstr ""
#: dmoj/settings.py:367
#: dmoj/settings.py:368
msgid "Romanian"
msgstr ""
#: dmoj/settings.py:368
#: dmoj/settings.py:369
msgid "Russian"
msgstr ""
#: dmoj/settings.py:369
#: dmoj/settings.py:370
msgid "Serbian (Latin)"
msgstr ""
#: dmoj/settings.py:370
#: dmoj/settings.py:371
msgid "Turkish"
msgstr ""
#: dmoj/settings.py:371
#: dmoj/settings.py:372
msgid "Vietnamese"
msgstr "Tiếng Việt"
#: dmoj/settings.py:372
#: dmoj/settings.py:373
msgid "Simplified Chinese"
msgstr ""
#: dmoj/settings.py:373
#: dmoj/settings.py:374
msgid "Traditional Chinese"
msgstr ""
@ -108,7 +108,7 @@ msgstr ""
msgid "Login"
msgstr "Đăng nhập"
#: dmoj/urls.py:106 templates/base.html:249
#: dmoj/urls.py:106 templates/base.html:251
msgid "Home"
msgstr "Trang chủ"
@ -217,7 +217,7 @@ msgstr "Tính toán lại kết quả"
msgid "username"
msgstr "tên đăng nhập"
#: judge/admin/contest.py:320 templates/base.html:337
#: judge/admin/contest.py:320 templates/base.html:339
msgid "virtual"
msgstr "ảo"
@ -587,9 +587,9 @@ msgstr "người bình luận"
msgid "associated page"
msgstr "trang tương ứng"
#: judge/models/comment.py:46
#: judge/models/comment.py:46 judge/models/problem.py:492
msgid "votes"
msgstr ""
msgstr "bình chọn"
#: judge/models/comment.py:48
msgid "hide the comment"
@ -607,7 +607,7 @@ msgstr "bình luận"
msgid "comments"
msgstr ""
#: judge/models/comment.py:137 judge/models/problem.py:443
#: judge/models/comment.py:137 judge/models/problem.py:462
#, python-format
msgid "Editorial for %s"
msgstr ""
@ -725,7 +725,7 @@ msgstr ""
msgid "description"
msgstr "mô tả"
#: judge/models/contest.py:72 judge/models/problem.py:390
#: judge/models/contest.py:72 judge/models/problem.py:409
#: judge/models/runtime.py:138
msgid "problems"
msgstr "bài tập"
@ -739,7 +739,7 @@ msgid "end time"
msgstr "thời gian kết thúc"
#: judge/models/contest.py:75 judge/models/problem.py:118
#: judge/models/problem.py:414
#: judge/models/problem.py:433
msgid "time limit"
msgstr "giới hạn thời gian"
@ -1037,8 +1037,8 @@ msgid "contest participations"
msgstr "lần tham gia kỳ thi"
#: judge/models/contest.py:491 judge/models/contest.py:513
#: judge/models/contest.py:554 judge/models/problem.py:389
#: judge/models/problem.py:394 judge/models/problem.py:412
#: judge/models/contest.py:554 judge/models/problem.py:408
#: judge/models/problem.py:413 judge/models/problem.py:431
#: judge/models/problem_data.py:40
msgid "problem"
msgstr "bài tập"
@ -1176,7 +1176,7 @@ msgstr "mục cha"
msgid "post title"
msgstr "tiêu đề bài đăng"
#: judge/models/interface.py:67 judge/models/problem.py:432
#: judge/models/interface.py:67 judge/models/problem.py:451
msgid "authors"
msgstr "tác giả"
@ -1184,7 +1184,7 @@ msgstr "tác giả"
msgid "slug"
msgstr "slug"
#: judge/models/interface.py:69 judge/models/problem.py:430
#: judge/models/interface.py:69 judge/models/problem.py:449
msgid "public visibility"
msgstr "khả năng hiển thị công khai"
@ -1380,7 +1380,7 @@ msgid ""
"are supported."
msgstr ""
#: judge/models/problem.py:123 judge/models/problem.py:417
#: judge/models/problem.py:123 judge/models/problem.py:436
msgid "memory limit"
msgstr ""
@ -1453,67 +1453,85 @@ msgstr ""
msgid "If private, only these organizations may see the problem."
msgstr ""
#: judge/models/problem.py:395 judge/models/problem.py:413
#: judge/models/problem.py:414 judge/models/problem.py:432
#: judge/models/runtime.py:111
msgid "language"
msgstr ""
#: judge/models/problem.py:396
#: judge/models/problem.py:415
msgid "translated name"
msgstr ""
#: judge/models/problem.py:397
#: judge/models/problem.py:416
msgid "translated description"
msgstr ""
#: judge/models/problem.py:401
#: judge/models/problem.py:420
msgid "problem translation"
msgstr ""
#: judge/models/problem.py:402
#: judge/models/problem.py:421
msgid "problem translations"
msgstr ""
#: judge/models/problem.py:406
#: judge/models/problem.py:425
msgid "clarified problem"
msgstr ""
#: judge/models/problem.py:407
#: judge/models/problem.py:426
msgid "clarification body"
msgstr ""
#: judge/models/problem.py:408
#: judge/models/problem.py:427
msgid "clarification timestamp"
msgstr ""
#: judge/models/problem.py:423
#: judge/models/problem.py:442
msgid "language-specific resource limit"
msgstr ""
#: judge/models/problem.py:424
#: judge/models/problem.py:443
msgid "language-specific resource limits"
msgstr ""
#: judge/models/problem.py:428
#: judge/models/problem.py:447
msgid "associated problem"
msgstr ""
#: judge/models/problem.py:431
#: judge/models/problem.py:450
msgid "publish date"
msgstr ""
#: judge/models/problem.py:433
#: judge/models/problem.py:452
msgid "editorial content"
msgstr "nội dung lời giải"
#: judge/models/problem.py:449
#: judge/models/problem.py:468
msgid "solution"
msgstr "lời giải"
#: judge/models/problem.py:450
#: judge/models/problem.py:469
msgid "solutions"
msgstr "lời giải"
#: judge/models/problem.py:474
#, fuzzy
#| msgid "point value"
msgid "proposed point value"
msgstr "điểm"
#: judge/models/problem.py:475
msgid "The amount of points you think this problem deserves."
msgstr ""
#: judge/models/problem.py:485
msgid "The time this vote was cast"
msgstr ""
#: judge/models/problem.py:491
msgid "vote"
msgstr ""
#: judge/models/problem_data.py:26
msgid "Standard"
msgstr "Tiêu chuẩn"
@ -1707,7 +1725,7 @@ msgid ""
msgstr "Ảnh này sẽ thay thế logo mặc định khi ở trong tổ chức."
#: judge/models/profile.py:76 judge/models/profile.py:93
#: judge/models/profile.py:216
#: judge/models/profile.py:221
msgid "organization"
msgstr ""
@ -1756,78 +1774,88 @@ msgid "User will not be ranked."
msgstr ""
#: judge/models/profile.py:102
#, fuzzy
#| msgid "Banned from joining"
msgid "banned from voting"
msgstr "Bị cấm tham gia"
#: judge/models/profile.py:103
msgid "User will not be able to vote on problems' point values."
msgstr ""
#: judge/models/profile.py:107
msgid "user script"
msgstr ""
#: judge/models/profile.py:103
#: judge/models/profile.py:108
msgid "User-defined JavaScript for site customization."
msgstr ""
#: judge/models/profile.py:104
#: judge/models/profile.py:109
msgid "current contest"
msgstr "kỳ thi hiện tại"
#: judge/models/profile.py:106
#: judge/models/profile.py:111
msgid "math engine"
msgstr ""
#: judge/models/profile.py:108
#: judge/models/profile.py:113
msgid "the rendering engine used to render math"
msgstr ""
#: judge/models/profile.py:109
#: judge/models/profile.py:114
msgid "2FA enabled"
msgstr ""
#: judge/models/profile.py:110
#: judge/models/profile.py:115
msgid "check to enable TOTP-based two factor authentication"
msgstr "đánh dấu để sử dụng TOTP-based two factor authentication"
#: judge/models/profile.py:111
#: judge/models/profile.py:116
msgid "TOTP key"
msgstr "mã TOTP"
#: judge/models/profile.py:112
#: judge/models/profile.py:117
msgid "32 character base32-encoded key for TOTP"
msgstr ""
#: judge/models/profile.py:114
#: judge/models/profile.py:119
msgid "TOTP key must be empty or base32"
msgstr ""
#: judge/models/profile.py:115
#: judge/models/profile.py:120
msgid "internal notes"
msgstr "ghi chú nội bộ"
#: judge/models/profile.py:116
#: judge/models/profile.py:121
msgid "Notes for administrators regarding this user."
msgstr "Ghi chú riêng cho quản trị viên."
#: judge/models/profile.py:210
#: judge/models/profile.py:215
msgid "user profile"
msgstr "thông tin người dùng"
#: judge/models/profile.py:211
#: judge/models/profile.py:216
msgid "user profiles"
msgstr "thông tin người dùng"
#: judge/models/profile.py:218
#: judge/models/profile.py:223
msgid "request time"
msgstr "thời gian đăng ký"
#: judge/models/profile.py:219
#: judge/models/profile.py:224
msgid "state"
msgstr "trạng thái"
#: judge/models/profile.py:224
#: judge/models/profile.py:229
msgid "reason"
msgstr "lý do"
#: judge/models/profile.py:227
#: judge/models/profile.py:232
msgid "organization join request"
msgstr "đơn đăng ký tham gia"
#: judge/models/profile.py:228
#: judge/models/profile.py:233
msgid "organization join requests"
msgstr "đơn đăng ký tham gia"
@ -2666,46 +2694,46 @@ msgstr "Hướng dẫn cho {0}"
msgid "Editorial for <a href=\"{1}\">{0}</a>"
msgstr "Hướng dẫn cho <a href=\"{1}\">{0}</a>"
#: judge/views/problem.py:227
#: judge/views/problem.py:284
#, python-brace-format
msgid "Disscuss {0}"
msgstr ""
#: judge/views/problem.py:230
#: judge/views/problem.py:287
#, python-brace-format
msgid "Discuss <a href=\"{1}\">{0}</a>"
msgstr "Thảo luận <a href=\"{1}\">{0}</a>"
#: judge/views/problem.py:298 templates/contest/contest.html:79
#: judge/views/problem.py:355 templates/contest/contest.html:79
#: templates/user/user-about.html:28 templates/user/user-tabs.html:5
#: templates/user/users-table.html:29
msgid "Problems"
msgstr "Bài tập"
#: judge/views/problem.py:598
#: judge/views/problem.py:655
msgid "Banned from submitting"
msgstr "Bị cấm nộp bài"
#: judge/views/problem.py:599
#: judge/views/problem.py:656
msgid ""
"You have been declared persona non grata for this problem. You are "
"permanently barred from submitting this problem."
msgstr "Bạn đã bị cấm nộp bài này."
#: judge/views/problem.py:613
#: judge/views/problem.py:670
msgid "Too many submissions"
msgstr "Quá nhiều lần nộp"
#: judge/views/problem.py:614
#: judge/views/problem.py:671
msgid "You have exceeded the submission limit for this problem."
msgstr "Bạn đã vượt quá số lần nộp cho bài này."
#: judge/views/problem.py:674 judge/views/problem.py:677
#: judge/views/problem.py:731 judge/views/problem.py:734
#, python-format
msgid "Submit to %(problem)s"
msgstr "Nộp bài cho %(problem)s"
#: judge/views/problem.py:692
#: judge/views/problem.py:749
msgid "Clone Problem"
msgstr "Nhân bản bài tập"
@ -2992,7 +3020,7 @@ msgid "Updated on site"
msgstr "Được cập nhật trên web"
#: judge/views/user.py:326 templates/admin/auth/user/change_form.html:14
#: templates/admin/auth/user/change_form.html:17 templates/base.html:297
#: templates/admin/auth/user/change_form.html:17 templates/base.html:299
#: templates/user/user-tabs.html:10
msgid "Edit profile"
msgstr "Chỉnh sửa thông tin"
@ -3177,15 +3205,22 @@ msgstr "Ngắt kết nối"
msgid "Terminate"
msgstr "Dừng"
#: templates/admin/judge/problem/change_form.html:14
#: templates/admin/judge/problem/change_form.html:15
msgid "View Submissions"
msgstr "Xem Bài Nộp"
#: templates/admin/judge/problem/change_form.html:17
#: templates/admin/judge/problem/change_form.html:18
#: templates/user/user-base.html:112
msgid "View submissions"
msgstr "Xem bài nộp"
#: templates/admin/judge/problem/change_form.html:19
#: templates/admin/judge/problem/change_form.html:22
#, fuzzy
#| msgid "View on site"
msgid "View votes"
msgstr "Xem trên trang"
#: templates/admin/judge/profile/change_form.html:14
#: templates/admin/judge/profile/change_form.html:17
msgid "Edit user"
@ -3197,20 +3232,20 @@ msgstr "Chỉnh sửa thông tin"
msgid "Rejudge"
msgstr "Chấm lại"
#: templates/base.html:266 templates/chat/chat.html:566
#: templates/base.html:268 templates/chat/chat.html:573
msgid "Chat"
msgstr "Chat"
#: templates/base.html:272
#: templates/base.html:274
msgid "Notification"
msgstr "Thông báo"
#: templates/base.html:289
#: templates/base.html:291
#, python-format
msgid "Hello, <b>%(username)s</b>."
msgstr "Xin chào, <b>%(username)s</b>."
#: templates/base.html:295 templates/chat/chat.html:20
#: templates/base.html:297 templates/chat/chat.html:20
#: templates/comments/list.html:89 templates/contest/contest-list-tabs.html:24
#: templates/contest/ranking-table.html:53
#: templates/problem/problem-list-tabs.html:6
@ -3219,36 +3254,36 @@ msgstr "Xin chào, <b>%(username)s</b>."
msgid "Admin"
msgstr ""
#: templates/base.html:304
#: templates/base.html:306
msgid "Log out"
msgstr "Đăng xuất"
#: templates/base.html:313
#: templates/base.html:315
#: templates/registration/password_reset_complete.html:4
msgid "Log in"
msgstr "Đăng nhập"
#: templates/base.html:314 templates/registration/registration_form.html:177
#: templates/base.html:316 templates/registration/registration_form.html:177
msgid "or"
msgstr "hoặc"
#: templates/base.html:315
#: templates/base.html:317
msgid "Sign up"
msgstr "Đăng ký"
#: templates/base.html:331
#: templates/base.html:333
msgid "spectating"
msgstr "đang theo dõi"
#: templates/base.html:343
#: templates/base.html:345
msgid "Compete"
msgstr "Thi"
#: templates/base.html:345
#: templates/base.html:347
msgid "General"
msgstr "Chung"
#: templates/base.html:352
#: templates/base.html:354
msgid "This site works best with JavaScript enabled."
msgstr ""
@ -3292,7 +3327,7 @@ msgid "News"
msgstr "Tin tức"
#: templates/blog/list.html:115 templates/problem/list.html:347
#: templates/problem/problem.html:364
#: templates/problem/problem.html:370
msgid "Clarifications"
msgstr "Thông báo"
@ -3301,7 +3336,7 @@ msgid "Add"
msgstr "Thêm mới"
#: templates/blog/list.html:140 templates/problem/list.html:369
#: templates/problem/problem.html:375
#: templates/problem/problem.html:381
msgid "No clarifications have been made at this time."
msgstr "Không có thông báo nào."
@ -3349,24 +3384,24 @@ msgstr "Thành viên khác"
msgid "New message(s)"
msgstr "Tin nhắn mới"
#: templates/chat/chat.html:507 templates/chat/chat.html:588
#: templates/chat/chat.html:514 templates/chat/chat.html:595
#: templates/user/base-users.html:14 templates/user/base-users.html:80
msgid "Search by handle..."
msgstr "Tìm kiếm theo tên..."
#: templates/chat/chat.html:568 templates/chat/chat.html:575
#: templates/chat/chat.html:575 templates/chat/chat.html:582
msgid "Online Users"
msgstr "Trực tuyến"
#: templates/chat/chat.html:576
#: templates/chat/chat.html:583
msgid "Refresh"
msgstr "Làm mới"
#: templates/chat/chat.html:609
#: templates/chat/chat.html:616
msgid "Emoji"
msgstr ""
#: templates/chat/chat.html:610
#: templates/chat/chat.html:617
msgid "Enter your message"
msgstr "Nhập tin nhắn"
@ -3563,8 +3598,8 @@ msgstr "Lịch"
msgid "Info"
msgstr "Thông tin"
#: templates/contest/contest-tabs.html:6 templates/stats/base.html:9
#: templates/submission/list.html:339
#: templates/contest/contest-tabs.html:6 templates/problem/voting-stats.html:26
#: templates/stats/base.html:9 templates/submission/list.html:339
msgid "Statistics"
msgstr "Thống kê"
@ -3845,27 +3880,27 @@ msgstr "Bạn có chắc muốn hủy kết quả này?"
msgid "Are you sure you want to un-disqualify this participation?"
msgstr "Bạn có chắc muốn khôi phục kết quả này?"
#: templates/contest/ranking.html:415
#: templates/contest/ranking.html:446
msgid "View user participation"
msgstr "Xem các lần tham gia"
#: templates/contest/ranking.html:419
#: templates/contest/ranking.html:450
msgid "Show organizations"
msgstr "Hiển thị tổ chức"
#: templates/contest/ranking.html:423
#: templates/contest/ranking.html:454
msgid "Show full name"
msgstr "Hiển thị họ tên"
#: templates/contest/ranking.html:426
#: templates/contest/ranking.html:457
msgid "Show friends only"
msgstr "Chỉ hiển thị bạn bè"
#: templates/contest/ranking.html:429
#: templates/contest/ranking.html:460
msgid "Total score only"
msgstr "Chỉ hiển thị tổng điểm"
#: templates/contest/ranking.html:431
#: templates/contest/ranking.html:462
msgid "Show virtual participation"
msgstr "Hiển thị tham gia ảo"
@ -4415,16 +4450,16 @@ msgstr[0] "Máy chấm:"
msgid "none available"
msgstr "không có sẵn"
#: templates/problem/problem.html:328
#: templates/problem/problem.html:331
#, python-format
msgid "This problem has %(length)s clarification(s)"
msgstr "Bài này có %(length)s thông báo"
#: templates/problem/problem.html:353
#: templates/problem/problem.html:359
msgid "Request clarification"
msgstr "Yêu cầu làm rõ đề"
#: templates/problem/problem.html:355
#: templates/problem/problem.html:361
msgid "Report an issue"
msgstr "Báo cáo một vấn đề"
@ -4522,6 +4557,50 @@ msgstr "Không có máy chấm có thể chấm bài này."
msgid "Submit!"
msgstr "Nộp bài!"
#: templates/problem/voting-controls.html:53
msgid "Edit difficulty"
msgstr "Thay đổi độ khó"
#: templates/problem/voting-controls.html:61
msgid "Vote difficulty"
msgstr "Bình chọn độ khó"
#: templates/problem/voting-form.html:21
msgid "How difficult is this problem?"
msgstr "Bạn thấy độ khó bài này thế nào?"
#: templates/problem/voting-form.html:35
msgid "This helps us improve the site"
msgstr "Bình chọn giúp admin cải thiện bài tập."
#: templates/problem/voting-form.html:38
msgid "Easy"
msgstr "Dễ"
#: templates/problem/voting-form.html:39
msgid "Hard"
msgstr "Khó"
#: templates/problem/voting-stats.html:29
msgid "Voting Statistics"
msgstr "Thống kê"
#: templates/problem/voting-stats.html:32
msgid "No Votes Available!"
msgstr "Không có bình chọn nào!"
#: templates/problem/voting-stats.html:35
msgid "Median:"
msgstr "Trung vị:"
#: templates/problem/voting-stats.html:37
msgid "Mean:"
msgstr "Trung bình:"
#: templates/problem/voting-stats.html:39 templates/submission/list.html:345
msgid "Total:"
msgstr "Tổng:"
#: templates/registration/activate.html:3
#, python-format
msgid "%(key)s is an invalid activation key."
@ -4779,10 +4858,6 @@ msgstr "Lọc theo kết quả..."
msgid "Filter by language..."
msgstr "Lọc theo ngôn ngữ..."
#: templates/submission/list.html:345
msgid "Total:"
msgstr "Tổng:"
#: templates/submission/list.html:355
msgid "You were disconnected. Refresh to show latest updates."
msgstr "Bạn bị ngắt kết nối. Hãy làm mới để xem cập nhật mới nhất."

View file

@ -5,6 +5,7 @@
<script>
django.jQuery(function ($) {
$('.submissions-link').appendTo('div#bottombar').show();
$('.votes-link').appendTo('div#bottombar').show();
});
</script>
{% endblock extrahead %}
@ -15,6 +16,10 @@
href="{% url 'admin:judge_submission_changelist' %}?problem__code={{ original.code }}">
<i class="fa fa-lg fa-search-plus"></i>
<span class="text">{% trans "View submissions" %}</span>
<a style="display: none" title="{{ _('View votes') }}" class="button votes-link"
href="{% url 'admin:judge_problempointsvote_changelist' %}?problem__code={{ original.code }}">
<i class="fa fa-lg fa-envelope"></i>
<span class="text">{{ _('View votes') }}</span>
</a>
{% endif %}
{% endblock %}

View file

@ -321,6 +321,9 @@
{% endblock %}
{% block description %}
{% if can_vote and not vote %}
{% include 'problem/voting-form.html' %}
{% endif %}
{% if contest_problem and contest_problem.contest.use_clarifications and has_clarifications %}
<div id="clarification_header_container">
<i class="fa fa-question-circle"></i>
@ -346,6 +349,9 @@
{% endblock %}
{% block post_description_end %}
{% if can_vote %}
{% include 'problem/voting-controls.html' %}
{% endif %}
{% if request.user.is_authenticated and not request.profile.mute %}
<a href="{{ url('new_problem_ticket', problem.code) }}" class="clarify">
<i class="fa fa-flag" style="margin-right:0.5em"></i>

View file

@ -0,0 +1,73 @@
<style>
.featherlight .featherlight-inner{
display: block !important;
}
.featherlight-content {
overflow: inherit !important;
}
.stars-container {
display: inline-block;
vertical-align: top;
}
.stars-container .star {
display: inline-block;
padding: 2px;
color: orange;
cursor: pointer;
font-size: 30px;
}
.stars-container .star:before {
content:"\f006";
}
.filled-star:before, .star:hover:before {
content:"\f005" !important;
}
</style>
{% if can_vote %}
<div class="vote-form" id="id_vote_form_box" style="display: none">
{% include 'problem/voting-form.html' %}
</div>
<span>
<a href="#" class="form-button" id="id_vote_button" data-featherlight="#id_vote_form_box"></a>
{% if request.user.is_superuser %}
- {% include 'problem/voting-stats.html' %}
{% endif %}
</span>
<script src="{{ static('libs/featherlight/featherlight.min.js') }}" type="text/javascript"></script>
<script>
let voted_points = null;
{% if vote is not none %}
let has_voted = true;
voted_points = {{ vote.points }};
{% else %}
let has_voted = false;
{% endif %}
function voteUpdate(){
$('#id_has_voted_prompt').show();
$('#id_current_vote_value').prop('innerText', voted_points);
$('#id_has_not_voted_prompt').hide();
$('#id_vote_button').prop('innerText', `{{ _('Edit difficulty') }} (` + voted_points + ')');
$('#id_points_error_box').hide();
has_voted = true;
}
function deleteVoteUpdate(){
$('#id_has_voted_prompt').hide();
$('#id_has_not_voted_prompt').show();
$('#id_vote_button').prop('innerText', `{{ _('Vote difficulty') }}`);
$('#id_points_error_box').hide();
has_voted = false;
}
if (has_voted) voteUpdate();
else deleteVoteUpdate();
$('#id_vote_form').on('submit', function (e) {
e.preventDefault();
});
</script>
{% endif %}

View file

@ -0,0 +1,93 @@
<style>
.vote_form {
border: 3px solid blue;
border-radius: 5px;
width: fit-content;
margin-left: auto;
margin-right: auto;
padding: 0.2em 1em;
background: #ebf0ff;
color: #2e69ff;
}
</style>
<form id="id_vote_form" class="vote_form">
{% csrf_token %}
<input id="id_points" class="vote-form-value" type="hidden" step="1"
min="{{ min_possible_vote }}" max="{{ max_possible_vote }}" name="points">
<table>
<tbody>
<tr>
<td style="padding-top: 1em; padding-right: 3em">
<span><b>{{_('How difficult is this problem?')}}</b></span>
</td>
<td>
<div class="vote-rating">
<span class="stars-container">
{% for i in range(1, (max_possible_vote - min_possible_vote) // 100 + 2) %}
<span id="star{{i}}" star-score="{{i * 100}}" class="fa star"></span>
{% endfor %}
</span>
</div>
</td>
</tr>
<tr>
<td>
<span>{{_('This helps us improve the site')}}</span>
</td>
<td>
<span style="padding-left: 0.2em"><b>{{_('Easy')}}</b></span>
<span style="float: right; padding-right: 0.2em"><b>{{_('Hard')}}</b></span>
</td>
</tr>
</tbody>
</table>
<div id="id_points_error_box">
<span id="id_points_error" class="voting-form-error"></span>
</div>
<div>
<input type="hidden" id="id_vote_form_submit_button">
</div>
</form>
<script>
$('.star').hover(
(e) => $(e.target).prevAll().addClass('filled-star'),
(e) => $(e.target).prevAll().removeClass('filled-star')
);
$('.star').on('click', function() {
$("#id_points").val($(this).attr('star-score'));
$('#id_vote_form_submit_button').click();
$('#id_vote_form').fadeOut(500);
})
$('#id_vote_form_submit_button').on('click', function (e) {
e.preventDefault();
$.ajax({
url: '{{ url('vote', object.code) }}',
type: 'POST',
data: $('#id_vote_form').serialize(),
success: function (data) {
{% if request.user.is_superuser %}
updateUserVote(voted_points, data.points);
{% endif %}
voted_points = data.points;
voteUpdate();
// Forms are auto disabled to prevent resubmission, but we need to allow resubmission here.
$('#id_vote_form_submit_button').removeAttr('disabled');
var current = $.featherlight.current();
current.close();
},
error: function (data) {
let errors = data.responseJSON;
if(errors === undefined) {
alert('Unable to delete vote: ' + data.responsetext);
}
if('points' in errors){
$('#id_points_error_box').show();
$('#id_points_error').prop('textContent', errors.points[0]);
} else {
$('#id_points_error_box').hide();
}
}
});
});
</script>

View file

@ -0,0 +1,146 @@
<style>
.vote-stats-background {
background-color: rgb(255,255,255);
padding: 20px;
border-radius: 25px;
justify-content: center;
align-items: center;
margin: auto;
text-align: center;
}
.canvas {
border: 1px;
border: solid;
border: #000000;
}
.vote-stats-value {
margin-right: 1em;
}
.vote-stats-info {
font-weight: 500;
}
</style>
<a href="#" class="form-button" id="id_vote_stats_button">{{ _('Statistics') }}</a>
<div class="vote-stats-background" id="id_vote_stats" style="display: none;">
<script src={{ static('libs/chart.js/Chart.js') }}></script>
<span class="vote-stats-info">{{ _('Voting Statistics') }}</span>
<br/>
<canvas class="canvas" id="id_vote_chart"></canvas>
<span id="id_no_votes_error" class="vote-stats-info no_votes_error">{{ _('No Votes Available!') }}</span>
<br/>
<div id="id_has_votes_footer" class="has_votes_footer">
<span class="vote-stats-info">{{ _('Median:') }}</span>
<span class="vote-stats-value median_vote" id="id_median_vote"></span>
<span class="vote-stats-info">{{ _('Mean:') }}</span>
<span class="vote-stats-value mean_vote" id="id_mean_vote"></span>
<span class="vote-stats-info">{{ _('Total:') }}</span>
<span class="vote-stats-value total_vote" id="id_num_of_votes"></span>
</div>
</div>
<script>
let voteChart = null;
let data = {{ all_votes }};
function reload_vote_graph() {
if (voteChart !== null) voteChart.destroy();
if (data.length === 0) {
$('.canvas').hide();
$('.has_votes_footer').hide();
$('.no_votes_error').show();
} else {
$('.canvas').show();
$('.has_votes_footer').show();
$('.no_votes_error').hide();
data.sort(function(a, b){return a - b});
// Give the graph some padding on both sides.
let min_points = {{ min_possible_vote }};
let max_points = {{ max_possible_vote }};
let xlabels = [];
let voteFreq = [];
for (let i = min_points; i <= max_points; i += 100) {
xlabels.push(i);
voteFreq.push(0);
}
let max_number_of_votes = 0;
let total_votes = 0;
let mean = 0;
for (let i = 0; i < data.length; i++) {
// Assume the data is valid.
voteFreq[(data[i] - min_points) / 100]++;
max_number_of_votes = Math.max(max_number_of_votes, voteFreq[(data[i] - min_points) / 100]);
mean += data[i];
total_votes++;
}
mean = mean / total_votes;
let half = Math.floor(total_votes / 2);
let median = data[half];
if (total_votes % 2 === 0) {
median = (median + data[half - 1]) / 2;
}
$('.mean_vote').prop('innerText', mean.toFixed(2));
$('.median_vote').prop('innerText', median.toFixed(2));
$('.total_vote').prop('innerText', total_votes);
const voteData = {
labels: xlabels,
datasets: [{
data: voteFreq,
backgroundColor: 'pink',
}]
};
const voteDataConfig = {
type: 'bar',
data: voteData,
options: {
legend: {
display: false
},
responsive: true,
scales: {
yAxes: [{
ticks: {
precision: 0,
suggestedMax: Math.ceil(max_number_of_votes * 1.2),
beginAtZero: true,
}
}],
xAxes: [{
ticks: {
beginAtZero: false,
}
}]
}
}
};
voteChart = new Chart($('.featherlight-inner .canvas'), voteDataConfig);
}
}
function updateUserVote(prev_voted_points, voted_points) {
let index = data.indexOf(prev_voted_points);
if (index > -1) {
data.splice(index, 1);
}
data.push(voted_points);
}
function deleteUserVote(prev_voted_points) {
data.splice(data.indexOf(prev_voted_points), 1);
}
$(function() {
$('#id_vote_stats_button').featherlight('#id_vote_stats', {
afterOpen: reload_vote_graph,
})
});
</script>