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 SolutionSitemap, UrlSitemap, UserSitemap
from judge.views import TitledTemplateView, about, api, blog, comment, contests, language, license, mailgun, \ 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, \ 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, \ from judge.views.problem_data import ProblemDataView, ProblemSubmissionDiff, \
problem_data_file, problem_init_view, ProblemZipUploadView problem_data_file, problem_init_view, ProblemZipUploadView
from judge.views.register import ActivationView, RegistrationView 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/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/', 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/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/(?P<problem>[^/]+)', include([
url(r'^$', problem.ProblemDetail.as_view(), name='problem_detail'), 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'^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/', url(r'^notifications/',
login_required(notification.NotificationList.as_view()), login_required(notification.NotificationList.as_view()),
name='notification'), name='notification'),
@ -406,6 +411,10 @@ urlpatterns = [
url(r'submit/$', user.import_users_submit, name='import_users_submit'), url(r'submit/$', user.import_users_submit, name='import_users_submit'),
url(r'sample/$', user.sample_import_users, name='import_users_sample') 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', 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.submission import SubmissionAdmin
from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin
from judge.admin.ticket import TicketAdmin from judge.admin.ticket import TicketAdmin
from judge.admin.volunteer import VolunteerProblemVoteAdmin
from judge.models import BlogPost, Comment, CommentLock, Contest, ContestParticipation, \ from judge.models import BlogPost, Comment, CommentLock, Contest, ContestParticipation, \
ContestTag, Judge, Language, License, MiscConfig, NavigationBar, Organization, \ 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) admin.site.register(BlogPost, BlogPostAdmin)
@ -37,3 +39,4 @@ admin.site.register(ProblemType, ProblemTypeAdmin)
admin.site.register(Profile, ProfileAdmin) admin.site.register(Profile, ProfileAdmin)
admin.site.register(Submission, SubmissionAdmin) admin.site.register(Submission, SubmissionAdmin)
admin.site.register(Ticket, TicketAdmin) 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.runtime import Judge, Language, RuntimeVersion
from judge.models.submission import SUBMISSION_RESULT, Submission, SubmissionSource, SubmissionTestCase from judge.models.submission import SUBMISSION_RESULT, Submission, SubmissionSource, SubmissionTestCase
from judge.models.ticket import Ticket, TicketMessage from judge.models.ticket import Ticket, TicketMessage
from judge.models.volunteer import VolunteerProblemVote
revisions.register(Profile, exclude=['points', 'last_access', 'ip', 'rating']) revisions.register(Profile, exclude=['points', 'last_access', 'ip', 'rating'])
revisions.register(Problem, follow=['language_limits']) revisions.register(Problem, follow=['language_limits'])

View file

@ -377,6 +377,7 @@ class Problem(models.Model):
save.alters_data = True save.alters_data = True
def can_vote(self, request): def can_vote(self, request):
return False
user = request.user user = request.user
if not user.is_authenticated: if not user.is_authenticated:
return False 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.forms import ProblemCloneForm, ProblemSubmitForm, ProblemPointsVoteForm
from judge.models import ContestProblem, ContestSubmission, Judge, Language, Problem, ProblemClarification, \ from judge.models import ContestProblem, ContestSubmission, Judge, Language, Problem, ProblemClarification, \
ProblemGroup, ProblemTranslation, ProblemType, ProblemPointsVote, RuntimeVersion, Solution, Submission, SubmissionSource, \ ProblemGroup, ProblemTranslation, ProblemType, ProblemPointsVote, RuntimeVersion, Solution, Submission, SubmissionSource, \
TranslatedProblemForeignKeyQuerySet, Organization TranslatedProblemForeignKeyQuerySet, Organization , VolunteerProblemVote
from judge.pdf_problems import DefaultPdfMaker, HAS_PDF from judge.pdf_problems import DefaultPdfMaker, HAS_PDF
from judge.utils.diggpaginator import DiggPaginator from judge.utils.diggpaginator import DiggPaginator
from judge.utils.opengraph import generate_opengraph 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') cf_logger = logging.getLogger('judge.ml.collab_filter')
class ProblemFeed(ProblemList): class ProblemFeed(ProblemList):
model = Problem model = Problem
context_object_name = 'problems' context_object_name = 'problems'
@ -640,6 +641,9 @@ class ProblemFeed(ProblemList):
if self.feed_type == 'new': if self.feed_type == 'new':
return queryset.order_by('-date') 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: if not settings.ML_OUTPUT_PATH or not user:
return queryset.order_by('?') 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 %} {% if request.user.is_staff or request.user.is_superuser %}
<li><a href="{{ url('admin:index') }}">{{ _('Admin') }}</a></li> <li><a href="{{ url('admin:index') }}">{{ _('Admin') }}</a></li>
{% endif %} {% 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> <li><a href="{{ url('user_edit_profile') }}">{{ _('Edit profile') }}</a></li>
{% if request.user.is_impersonate %} {% if request.user.is_impersonate %}
<li><a href="{{ url('impersonate-stop') }}">Stop impersonating</a></li> <li><a href="{{ url('impersonate-stop') }}">Stop impersonating</a></li>

View file

@ -214,7 +214,6 @@
{% block body %} {% block body %}
<div class="content-description"> <div class="content-description">
<form id="filter-form"> <form id="filter-form">
<input id="search-contest" type="text" name="contest" value="{{ contest_query or '' }}" <input id="search-contest" type="text" name="contest" value="{{ contest_query or '' }}"
placeholder="{{ _('Search contests...') }}"> 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 %} {% cache 86400 'problem_html' problem.id MATH_ENGINE LANGUAGE_CODE %}
{{ problem.description|markdown("problem", MATH_ENGINE)|reference|str|safe }} {{ problem.description|markdown("problem", MATH_ENGINE)|reference|str|safe }}
{% endcache %} {% 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>
</div> </div>

View file

@ -36,7 +36,15 @@
#content { #content {
width: 99%; width: 99%;
margin-left: 0; margin-left: 0;
} }
.volunteer-types {
width: 100%;
}
.point-input {
height: 2em;
padding-top: 4px;
}
</style> </style>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
@ -151,6 +159,59 @@
$end.prop('disabled', end === point_values.max).val(end); $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> </script>
{% endcompress %} {% endcompress %}
@ -381,6 +442,11 @@
<a href="{{url('problem_feed_new')}}" class="problem-feed-option-item {{'active' if feed_type=='new'}}"> <a href="{{url('problem_feed_new')}}" class="problem-feed-option-item {{'active' if feed_type=='new'}}">
{{_('NEW')}} {{_('NEW')}}
</a> </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> </div>
{% for problem in problems %} {% for problem in problems %}
{% include "problem/feed.html" %} {% include "problem/feed.html" %}