Add problem volunteer

This commit is contained in:
cuom1999 2022-05-02 21:44:14 -05:00
parent e51129d36f
commit e70618ed19
15 changed files with 396 additions and 5 deletions

View file

@ -20,7 +20,7 @@ from judge.sitemap import BlogPostSitemap, ContestSitemap, HomePageSitemap, Orga
SolutionSitemap, UrlSitemap, UserSitemap
from judge.views import TitledTemplateView, about, api, blog, comment, contests, language, license, mailgun, \
notification, organization, preview, problem, problem_manage, ranked_submission, register, stats, status, submission, tasks, \
ticket, totp, user, widgets
ticket, totp, user, volunteer, widgets, internal
from judge.views.problem_data import ProblemDataView, ProblemSubmissionDiff, \
problem_data_file, problem_init_view, ProblemZipUploadView
from judge.views.register import ActivationView, RegistrationView
@ -118,6 +118,7 @@ urlpatterns = [
url(r'^problems/random/$', problem.RandomProblem.as_view(), name='problem_random'),
url(r'^problems/feed/', paged_list_view(problem.ProblemFeed, 'problem_feed', feed_type='for_you')),
url(r'^problems/feed/new/', paged_list_view(problem.ProblemFeed, 'problem_feed_new', feed_type='new')),
url(r'^problems/feed/volunteer/', paged_list_view(problem.ProblemFeed, 'problem_feed_volunteer', feed_type='volunteer')),
url(r'^problem/(?P<problem>[^/]+)', include([
url(r'^$', problem.ProblemDetail.as_view(), name='problem_detail'),
@ -396,6 +397,10 @@ urlpatterns = [
url(r'^get_unread_boxes$', chat.get_unread_boxes, name='get_unread_boxes'),
])),
url(r'^internal/', include([
url(r'^problem$', internal.InternalProblem.as_view(), name='internal_problem'),
])),
url(r'^notifications/',
login_required(notification.NotificationList.as_view()),
name='notification'),
@ -406,6 +411,10 @@ urlpatterns = [
url(r'submit/$', user.import_users_submit, name='import_users_submit'),
url(r'sample/$', user.sample_import_users, name='import_users_sample')
])),
url(r'^volunteer/', include([
url(r'^problem/vote$', volunteer.vote_problem, name='volunteer_problem_vote'),
])),
]
favicon_paths = ['apple-touch-icon-180x180.png', 'apple-touch-icon-114x114.png', 'android-chrome-72x72.png',

View file

@ -11,9 +11,11 @@ from judge.admin.runtime import JudgeAdmin, LanguageAdmin
from judge.admin.submission import SubmissionAdmin
from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin
from judge.admin.ticket import TicketAdmin
from judge.admin.volunteer import VolunteerProblemVoteAdmin
from judge.models import BlogPost, Comment, CommentLock, Contest, ContestParticipation, \
ContestTag, Judge, Language, License, MiscConfig, NavigationBar, Organization, \
OrganizationRequest, Problem, ProblemGroup, ProblemPointsVote, ProblemType, Profile, Submission, Ticket
OrganizationRequest, Problem, ProblemGroup, ProblemPointsVote, ProblemType, Profile, Submission, Ticket, \
VolunteerProblemVote
admin.site.register(BlogPost, BlogPostAdmin)
@ -37,3 +39,4 @@ admin.site.register(ProblemType, ProblemTypeAdmin)
admin.site.register(Profile, ProfileAdmin)
admin.site.register(Submission, SubmissionAdmin)
admin.site.register(Ticket, TicketAdmin)
admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin)

18
judge/admin/volunteer.py Normal file
View file

@ -0,0 +1,18 @@
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from django.utils.translation import gettext, gettext_lazy as _, ungettext
from judge.models import VolunteerProblemVote
class VolunteerProblemVoteAdmin(admin.ModelAdmin):
fields = ('voter', 'problem', 'time', 'thinking_points', 'knowledge_points', 'feedback')
readonly_fields = ('time', 'problem', 'voter')
list_display = ('voter', 'problem_link', 'time', 'thinking_points', 'knowledge_points', 'feedback')
date_hierarchy = 'time'
def problem_link(self, obj):
url = reverse('admin:judge_problem_change', args=(obj.problem.id,))
return format_html(f"<a href='{url}'>{obj.problem.code}</a>")
problem_link.short_description = _('Problem')
problem_link.admin_order_field = 'problem__code'

View file

@ -0,0 +1,36 @@
# Generated by Django 2.2.25 on 2022-05-02 16:56
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('judge', '0122_auto_20220425_1202'),
]
operations = [
migrations.AlterModelOptions(
name='problem',
options={'permissions': (('see_private_problem', 'See hidden problems'), ('edit_own_problem', 'Edit own problems'), ('edit_all_problem', 'Edit all problems'), ('edit_public_problem', 'Edit all public problems'), ('clone_problem', 'Clone problem'), ('change_public_visibility', 'Change is_public field'), ('change_manually_managed', 'Change is_manually_managed field'), ('see_organization_problem', 'See organization-private problems'), ('suggest_problem_changes', 'Suggest changes to problem')), 'verbose_name': 'problem', 'verbose_name_plural': 'problems'},
),
migrations.CreateModel(
name='VolunteerProblemVote',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time', models.DateTimeField(auto_now_add=True)),
('knowledge_points', models.PositiveIntegerField(help_text='Points awarded by knowledge difficulty', verbose_name='knowledge points')),
('thinking_points', models.PositiveIntegerField(help_text='Points awarded by thinking difficulty', verbose_name='thinking points')),
('feedback', models.TextField(blank=True, verbose_name='feedback')),
('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='volunteer_user_votes', to='judge.Problem')),
('types', models.ManyToManyField(help_text="The type of problem, as shown on the problem's page.", to='judge.ProblemType', verbose_name='problem types')),
('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='volunteer_problem_votes', to='judge.Profile')),
],
options={
'verbose_name': 'volunteer vote',
'verbose_name_plural': 'volunteer votes',
'unique_together': {('voter', 'problem')},
},
),
]

View file

@ -14,6 +14,7 @@ from judge.models.profile import Organization, OrganizationRequest, Profile, Fri
from judge.models.runtime import Judge, Language, RuntimeVersion
from judge.models.submission import SUBMISSION_RESULT, Submission, SubmissionSource, SubmissionTestCase
from judge.models.ticket import Ticket, TicketMessage
from judge.models.volunteer import VolunteerProblemVote
revisions.register(Profile, exclude=['points', 'last_access', 'ip', 'rating'])
revisions.register(Problem, follow=['language_limits'])

View file

@ -377,6 +377,7 @@ class Problem(models.Model):
save.alters_data = True
def can_vote(self, request):
return False
user = request.user
if not user.is_authenticated:
return False

28
judge/models/volunteer.py Normal file
View file

@ -0,0 +1,28 @@
from django.db import models
from django.db.models import CASCADE
from django.utils.translation import gettext_lazy as _
from judge.models import Profile, Problem, ProblemType
__all__ = ['VolunteerProblemVote']
class VolunteerProblemVote(models.Model):
voter = models.ForeignKey(Profile, related_name='volunteer_problem_votes', on_delete=CASCADE)
problem = models.ForeignKey(Problem, related_name='volunteer_user_votes', on_delete=CASCADE)
time = models.DateTimeField(auto_now_add=True)
knowledge_points = models.PositiveIntegerField(verbose_name=_('knowledge points'),
help_text=_('Points awarded by knowledge difficulty'))
thinking_points = models.PositiveIntegerField(verbose_name=_('thinking points'),
help_text=_('Points awarded by thinking difficulty'))
types = models.ManyToManyField(ProblemType, verbose_name=_('problem types'),
help_text=_('The type of problem, '
"as shown on the problem's page."))
feedback = models.TextField(verbose_name=_('feedback'), blank=True)
class Meta:
verbose_name = _('volunteer vote')
verbose_name_plural = _('volunteer votes')
unique_together = ['voter', 'problem']
def __str__(self):
return f'{self.voter} for {self.problem.code}'

35
judge/views/internal.py Normal file
View file

@ -0,0 +1,35 @@
from django.views.generic import ListView
from django.utils.translation import gettext as _, gettext_lazy
from django.db.models import Count
from django.http import HttpResponseForbidden
from judge.utils.diggpaginator import DiggPaginator
from judge.models import VolunteerProblemVote, Problem
class InternalProblem(ListView):
model = Problem
title = _('Internal problems')
template_name = 'internal/base.html'
paginate_by = 100
context_object_name = 'problems'
def get_paginator(self, queryset, per_page, orphans=0,
allow_empty_first_page=True, **kwargs):
return DiggPaginator(queryset, per_page, body=6, padding=2, orphans=orphans,
allow_empty_first_page=allow_empty_first_page, **kwargs)
def get_queryset(self):
queryset = Problem.objects.annotate(vote_count=Count('volunteer_user_votes')) \
.filter(vote_count__gte=1).order_by('-vote_count')
return queryset
def get_context_data(self, **kwargs):
context = super(InternalProblem, self).get_context_data(**kwargs)
context['page_type'] = 'problem'
context['title'] = self.title
return context
def get(self, request, *args, **kwargs):
if request.user.is_superuser:
return super(InternalProblem, self).get(request, *args, **kwargs)
return HttpResponseForbidden()

View file

@ -30,7 +30,7 @@ from judge.comments import CommentedDetailView
from judge.forms import ProblemCloneForm, ProblemSubmitForm, ProblemPointsVoteForm
from judge.models import ContestProblem, ContestSubmission, Judge, Language, Problem, ProblemClarification, \
ProblemGroup, ProblemTranslation, ProblemType, ProblemPointsVote, RuntimeVersion, Solution, Submission, SubmissionSource, \
TranslatedProblemForeignKeyQuerySet, Organization
TranslatedProblemForeignKeyQuerySet, Organization , VolunteerProblemVote
from judge.pdf_problems import DefaultPdfMaker, HAS_PDF
from judge.utils.diggpaginator import DiggPaginator
from judge.utils.opengraph import generate_opengraph
@ -594,6 +594,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
cf_logger = logging.getLogger('judge.ml.collab_filter')
class ProblemFeed(ProblemList):
model = Problem
context_object_name = 'problems'
@ -640,6 +641,9 @@ class ProblemFeed(ProblemList):
if self.feed_type == 'new':
return queryset.order_by('-date')
elif user and self.feed_type == 'volunteer':
voted_problems = user.volunteer_problem_votes.values_list('problem', flat=True)
return queryset.exclude(id__in=voted_problems).order_by('?')
if not settings.ML_OUTPUT_PATH or not user:
return queryset.order_by('?')

33
judge/views/volunteer.py Normal file
View file

@ -0,0 +1,33 @@
from django.http import HttpResponseBadRequest, JsonResponse
from django.db import transaction
from judge.models import VolunteerProblemVote, Problem, ProblemType
def vote_problem(request):
if not request.user or not request.user.has_perm('judge.suggest_problem_changes'):
return HttpResponseBadRequest()
if not request.method == 'POST':
return HttpResponseBadRequest()
try:
types_id = request.POST.getlist('types[]')
types = ProblemType.objects.filter(id__in=types_id)
problem = Problem.objects.get(code=request.POST['problem'])
knowledge_points = request.POST['knowledge_points']
thinking_points = request.POST['thinking_points']
feedback = request.POST['feedback']
except Exception as e:
return HttpResponseBadRequest()
with transaction.atomic():
vote, _ = VolunteerProblemVote.objects.get_or_create(
voter=request.profile,
problem=problem,
defaults={'knowledge_points': 0, 'thinking_points': 0},
)
vote.knowledge_points = knowledge_points
vote.thinking_points = thinking_points
vote.feedback = feedback
vote.types.set(types)
vote.save()
return JsonResponse({})

View file

@ -260,6 +260,9 @@
{% if request.user.is_staff or request.user.is_superuser %}
<li><a href="{{ url('admin:index') }}">{{ _('Admin') }}</a></li>
{% endif %}
{% if request.user.is_superuser %}
<li><a href="{{ url('internal_problem') }}">{{ _('Internal') }}</a></li>
{% endif %}
<li><a href="{{ url('user_edit_profile') }}">{{ _('Edit profile') }}</a></li>
{% if request.user.is_impersonate %}
<li><a href="{{ url('impersonate-stop') }}">Stop impersonating</a></li>

View file

@ -214,7 +214,6 @@
{% block body %}
<div class="content-description">
<form id="filter-form">
<input id="search-contest" type="text" name="contest" value="{{ contest_query or '' }}"
placeholder="{{ _('Search contests...') }}">

View file

@ -0,0 +1,92 @@
{% extends "three-column-content.html" %}
{% block three_col_media %}
<style>
.middle-content {
max-width: 50%;
}
ol {
padding-left: 1em;
}
</style>
{% endblock %}
{% block three_col_js %}
<script type="text/javascript">
$(function () {
$('.vote-detail').each(function() {
$(this).on('click', function() {
var pid = $(this).attr('pid');
$('.detail').hide();
$('#detail-'+pid).show();
})
})
});
</script>
{% endblock %}
{% block left_sidebar %}
<div class="left-sidebar">
{{ make_tab_item('problem', 'fa fa-list', url('internal_problem'), _('Problem')) }}
</div>
{% endblock %}
{% block middle_content %}
<table class="table">
<thead>
<tr>
<th>{{_('Problem')}}</th>
<th>{{_('Code')}}</th>
<th>{{_('Vote count')}}</th>
</tr>
</thead>
<tbody>
{% for problem in problems %}
<tr>
<td><a href="{{url('problem_detail', problem.code)}}">{{problem.name}}</a></td>
<td><a href="{{url('admin:judge_problem_change', problem.id)}}">{{problem.code}}</a></td>
<td><a href="#" class="vote-detail" pid="{{problem.id}}">{{problem.vote_count}}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% if page_obj.num_pages > 1 %}
<div style="margin-top:10px;">{% include "list-pages.html" %}</div>
{% endif %}
{% endblock %}
{% block right_sidebar %}
<div style="display: block; width: 100%">
<div><a href="{{url('admin:judge_volunteerproblemvote_changelist')}}">{{_('Admin')}}</a></div>
{% for problem in problems %}
<div class="detail" id="detail-{{problem.id}}" style="display: none;">
<h3>{{_('Votes for problem') }} {{problem.name}}</h3>
<ol>
{% for vote in problem.volunteer_user_votes.order_by('id') %}
<li>
<h4> {{link_user(vote.voter)}} </h4>
<table class="table">
<tbody>
<tr>
<td style="width:10%">{{_('Knowledge')}}</td>
<td>{{vote.knowledge_points}}</td>
</tr>
<tr>
<td>{{_('Thinking')}}</td>
<td>{{vote.thinking_points}}</td>
</tr>
<tr>
<td>{{_('Types')}}</td>
<td>{{vote.types.all() | join(', ')}}</td>
</tr>
<tr>
<td>{{_('Feedback')}}</td>
<td>{{vote.feedback}}</td>
</tr>
</tbody>
</table>
</li>
{% endfor %}
</ol>
</div>
{% endfor %}
{% endblock %}

View file

@ -31,5 +31,68 @@
{% cache 86400 'problem_html' problem.id MATH_ENGINE LANGUAGE_CODE %}
{{ problem.description|markdown("problem", MATH_ENGINE)|reference|str|safe }}
{% endcache %}
{% if feed_type=='volunteer' and request.user.has_perm('judge.suggest_problem_changes') %}
<hr>
<center><h3>{{_('Volunteer form')}}</h3></center>
<br>
<button class="edit-btn" id="edit-{{problem.id}}" pid="{{problem.id}}" style="float: right">{{_('Edit')}}</button>
<form class="volunteer-form" id="form-{{problem.id}}" pid="{{problem.id}}" style="display: none;" method="POST">
<input type="submit" class="volunteer-submit-btn" id="submit-{{problem.id}}" pid="{{problem.id}}" pcode="{{problem.code}}" style="float: right" value="{{_('Submit')}}">
<table class="table">
<thead>
<tr>
<th>
{{_('Field')}}
</th>
<th>
{{_('Value')}}
</th>
</tr>
</thead>
<tbody>
<tr>
<td width="30%">
<label for="knowledge_point-{{problem.id}}"><i>{{ _('Knowledge point') }}</i></label>
</td>
<td>
<input id="knowledge_point-{{problem.id}}" type="number" class="point-input" required>
</td>
</tr>
<tr>
<td width="30%">
<label for="thinking_point-{{problem.id}}"><i>{{ _('Thinking point') }}</i></label>
</td>
<td>
<input id="thinking_point-{{problem.id}}" type="number" class="point-input" required>
</td>
</tr>
<tr>
<td width="30%">
<label for="types"><i>{{ _('Problem types') }}</i></label>
</td>
<td>
<select id="volunteer-types-{{problem.id}}" name="types" multiple>
{% for type in problem_types %}
<option value="{{ type.id }}"{% if type in problem.types.all() %} selected{% endif %}>
{{ type.full_name }}
</option>
{% endfor %}
</select>
</td>
</tr>
<tr>
<td width="30%">
<label for="feedback"><i>{{ _('Feedback') }}</i></label>
</td>
<td>
<textarea id="feedback-{{problem.id}}" rows="2" style="width: 100%" placeholder="{{_('Any additional note here')}}"></textarea>
</td>
</tr>
</tbody>
</table>
</form>
<center id="thank-{{problem.id}}" style="display: none; margin-top: 3em"></center>
{% endif %}
</div>
</div>

View file

@ -37,6 +37,14 @@
width: 99%;
margin-left: 0;
}
.volunteer-types {
width: 100%;
}
.point-input {
height: 2em;
padding-top: 4px;
}
</style>
{% endif %}
{% endblock %}
@ -151,6 +159,59 @@
$end.prop('disabled', end === point_values.max).val(end);
});
}
{% if feed_type=='volunteer' and request.user.has_perm('judge.suggest_problem_changes') %}
$(".edit-btn").on('click', function() {
var pid = $(this).attr('pid');
$('#volunteer-types-' + pid).css({'width': '100%'});
$('#volunteer-types-' + pid).select2({multiple: 1, placeholder: '{{ _('Add types...') }}'})
.css({'visibility': 'visible'});
$('#form-' + pid).show();
$('#submit-' + pid).show();
$(this).hide();
});
let isChecking = false;
$(".volunteer-submit-btn").on('click', function(e) {
var pid = $(this).attr('pid');
var pcode = $(this).attr('pcode');
var $form = $('#form-' + pid);
if (!$form[0].checkValidity()) {
if (isChecking) return;
isChecking = true;
// The form won't actually submit;
$(this).click();
}
else {
isChecking = false;
}
if (isChecking) return;
e.preventDefault();
$('#volunteer-types-' + pid).select2({multiple: 1, placeholder: '{{ _('Add types...') }}'})
.css({'visibility': 'visible'});
$('#form-' + pid).hide();
$('#edit-' + pid).show();
$('#thank-' + pid).show();
$(this).hide();
var data = {
problem: pcode,
types: $('#volunteer-types-' + pid).val(),
knowledge_points: $('#knowledge_point-' + pid).val(),
thinking_points: $('#thinking_point-' + pid).val(),
feedback: $('#feedback-' + pid).val(),
};
$.post("{{url('volunteer_problem_vote')}}", data)
.fail(function() {
$('#thank-' + pid).html("{{_('Fail to vote!')}}");
})
.done(function() {
$('#thank-' + pid).html("{{_('Successful vote! Thank you!')}}");
});
});
{% endif %}
});
</script>
{% endcompress %}
@ -381,6 +442,11 @@
<a href="{{url('problem_feed_new')}}" class="problem-feed-option-item {{'active' if feed_type=='new'}}">
{{_('NEW')}}
</a>
{% if request.user.has_perm('judge.suggest_problem_changes') %}
<a href="{{url('problem_feed_volunteer')}}" class="problem-feed-option-item {{'active' if feed_type=='volunteer'}}">
{{_('VOLUNTEER')}}
</a>
{% endif %}
</div>
{% for problem in problems %}
{% include "problem/feed.html" %}