Skip to content

Add school year field to contest model and filter for contest selection #491

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion oioioi/contests/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ class ContestLinkInline(admin.TabularInline):

class ContestAdmin(admin.ModelAdmin):
inlines = [RoundInline, AttachmentInline, ContestLinkInline]
readonly_fields = ['creation_date']
readonly_fields = ['creation_date', 'school_year']
prepopulated_fields = {'id': ('name',)}
list_display = ['name', 'id', 'creation_date']
list_display_links = ['id', 'name']
Expand All @@ -218,6 +218,7 @@ def get_fields(self, request, obj=None):
fields = [
'name',
'id',
'school_year',
'controller_name',
'default_submissions_limit',
'contact_email'
Expand Down
20 changes: 19 additions & 1 deletion oioioi/contests/forms.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json

from django import forms
from django.core.validators import RegexValidator
from django.contrib.admin import widgets
from django.contrib.auth.models import User
from django.forms import ValidationError
Expand Down Expand Up @@ -31,7 +32,7 @@ class Meta(object):
# form should not be on the 'name' field, otherwise the 'id' field,
# as prepopulated with 'name' in ContestAdmin model, is cleared by
# javascript with prepopulated fields functionality.
fields = ['controller_name', 'name', 'id']
fields = ['controller_name', 'name', 'id', 'school_year']

start_date = forms.SplitDateTimeField(
label=_("Start date"), widget=widgets.AdminSplitDateTime()
Expand All @@ -43,6 +44,23 @@ class Meta(object):
required=False, label=_("Results date"), widget=widgets.AdminSplitDateTime()
)

def validate_years(year):
year1 = int(year[:4])
year2 = int(year[5:])
if year1+1 != year2:
raise ValidationError("The selected years must be consecutive.")

school_year = forms.CharField(
required=False, label=_("School year"), validators=[
RegexValidator(
regex=r'^[0-9]{4}[/][0-9]{4}$',
message="Enter a valid school year in the format 2021/2022.",
code="invalid_school_year",
),
validate_years,
]
)

def _generate_default_dates(self):
now = timezone.now()
self.initial['start_date'] = now
Expand Down
18 changes: 18 additions & 0 deletions oioioi/contests/migrations/0020_contest_school_year.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.19 on 2025-04-09 14:28

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('contests', '0019_submissionmessage'),
]

operations = [
migrations.AddField(
model_name='contest',
name='school_year',
field=models.CharField(default='', max_length=10, verbose_name='school year'),
),
]
3 changes: 3 additions & 0 deletions oioioi/contests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ class Contest(models.Model):
verbose_name=_("is archived"),
default=False
)
school_year = models.CharField(
max_length=10, verbose_name=_("school year"), default=""
)

# Part of szkopul backporting.
# This is a hack for situation where contest controller is empty,
Expand Down
29 changes: 27 additions & 2 deletions oioioi/contests/templates/contests/select_contest.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% extends "base-with-menu.html" %}
{% load i18n %}
{% load i18n pagination_tags %}

{% block title %}{% trans "Select contest" %}{% endblock %}

Expand All @@ -8,7 +8,31 @@
<article>
<h1>{% trans "Select contest" %}</h1>

<div class="table-responsive-md">
<div style="width: 20%; margin-bottom: 1rem;">
<form method="GET" action="{% url 'filter_contests' filter_value='PLACEHOLDER' %}" id="filter_form">
<div class="input-group">
<input type="text" id="filter_input" class="form-control search-query" style="width: 20%;" placeholder="Search" name="filter_field" value="{{ filter }}">
<span class="input-group-btn">
<button type="submit" class="btn btn-outline-secondary " name="submit_button"> <i class="fa-solid fa-magnifying-glass"> </i> </button>
</span>
</div>
</form>
<script>
document.getElementById('filter_form').addEventListener('submit', function(event) {
event.preventDefault();
let filterValue = document.getElementById('filter_input').value;
let actionUrl = "";
if(filterValue == "") {
actionUrl = "{% url 'select_contest'%}";
} else {
actionUrl = "{% url 'filter_contests' filter_value='PLACEHOLDER' %}".replace('PLACEHOLDER', encodeURIComponent(filterValue));
}
window.location.href = actionUrl;
});
</script>
</div>
{% autopaginate contests contests_on_page %}
<div class="table-responsive-md">
<table class="table">
<thead>
<tr>
Expand All @@ -34,6 +58,7 @@ <h1>{% trans "Select contest" %}</h1>
</tbody>
</table>
</div>
{% paginate %}
</article>

{% endblock %}
27 changes: 27 additions & 0 deletions oioioi/contests/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -4499,3 +4499,30 @@ def test_score_badge(self):
self.assertIn('badge-success', self._get_badge_for_problem(response.content, 'zad1'))
self.assertIn('badge-warning', self._get_badge_for_problem(response.content, 'zad2'))
self.assertIn('badge-danger', self._get_badge_for_problem(response.content, 'zad3'))

class TestContestListFiltering(TestCase):
fixtures = [
'test_contest',
'test_extra_contests',
]

def setUp(self):
self.c = Contest.objects.get(id='c')
self.c1 = Contest.objects.get(id='c1')
self.c2 = Contest.objects.get(id='c2')

return super().setUp()

def test_simple_filter(self):
self.url = reverse('filter_contests', kwargs={'filter_value':'test'})
response = self.client.get(self.url, follow=True)
self.assertContains(response, self.c.name)
self.assertContains(response, self.c1.name)
self.assertContains(response, self.c2.name)

def extra_filter(self):
self.url = reverse('filter_contests', kwargs={'filter_value':'ExTrA'})
response = self.client.get(self.url, follow=True)
self.assertNotContains(response, self.c.name)
self.assertContains(response, self.c1.name)
self.assertContains(response, self.c2.name)
5 changes: 5 additions & 0 deletions oioioi/contests/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ def glob_namespaced_patterns(namespace):
views.reattach_problem_confirm_view,
name='reattach_problem_confirm',
),
re_path(
r'^contest/query/(?P<filter_value>.+)/$',
views.filter_contests_view,
name='filter_contests',
),
]

if settings.USE_API:
Expand Down
18 changes: 13 additions & 5 deletions oioioi/contests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,10 +397,8 @@ def used_controllers():
by contests on this instance.
"""
return Contest.objects.values_list('controller_name', flat=True).distinct()


@request_cached
def visible_contests(request):

def visible_contests_query(request):
"""Returns materialized set of contests visible to the logged in user."""
if request.GET.get('living', 'safely') == 'dangerously':
visible_query = Contest.objects.none()
Expand All @@ -423,8 +421,18 @@ def visible_contests(request):
visible_query |= Q(
controller_name=controller_name
) & controller.registration_controller().visible_contests_query(request)
return set(Contest.objects.filter(visible_query).distinct())
return Contest.objects.filter(visible_query).distinct()

@request_cached
def visible_contests(request):
contests = visible_contests_query(request)
return set(contests)

@request_cached_complex
def visible_contests_queryset(request, filter_value):
contests = visible_contests_query(request)
contests = contests.filter(Q(name__icontains=filter_value) | Q(id__icontains=filter_value) | Q(school_year=filter_value))
return set(contests)

@request_cached
def administered_contests(request):
Expand Down
14 changes: 14 additions & 0 deletions oioioi/contests/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
is_contest_basicadmin,
is_contest_observer,
visible_contests,
visible_contests_queryset,
visible_problem_instances,
visible_rounds,
get_files_message,
Expand Down Expand Up @@ -91,6 +92,7 @@ def select_contest_view(request):
contests = sorted(contests, key=lambda x: x.creation_date, reverse=True)
context = {
'contests': contests,
'contests_on_page': getattr(settings, "CONTESTS_ON_PAGE", 20)
}
return TemplateResponse(
request, 'contests/select_contest.html', context
Expand Down Expand Up @@ -838,3 +840,15 @@ def unarchive_contest(request):
contest.is_archived = False
contest.save()
return redirect('default_contest_view', contest_id=contest.id)

def filter_contests_view(request, filter_value=""):
contests = visible_contests_queryset(request, filter_value)
contests = sorted(contests, key=lambda x: x.creation_date, reverse=True)

context = {
'contests' : contests,
'contests_on_page' : getattr(settings, 'CONTESTS_ON_PAGE', 20),
}
return TemplateResponse(
request, 'contests/select_contest.html', context
)
1 change: 1 addition & 0 deletions oioioi/default_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@
PAGINATION_DEFAULT_MARGIN = 1
FILES_ON_PAGE = 100
PROBLEMS_ON_PAGE = 100
CONTESTS_ON_PAGE = 20
QUESTIONS_ON_PAGE = 30
SUBMISSIONS_ON_PAGE = 100
PARTICIPANTS_ON_PAGE = 100
Expand Down
1 change: 1 addition & 0 deletions oioioi/deployment/settings.py.template
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ FILETRACKER_CACHE_ROOT = '__DIR__/cache'
# PAGINATION_DEFAULT_MARGIN = 1
# FILES_ON_PAGE = 100
# PROBLEMS_ON_PAGE = 100
# CONTESTS_ON_PAGE = 20
# QUESTIONS_ON_PAGE = 30
# SUBMISSIONS_ON_PAGE = 100
# PARTICIPANTS_ON_PAGE = 100
Expand Down