Skip to content
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

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
26 changes: 25 additions & 1 deletion oioioi/contests/templates/contests/select_contest.html
Original file line number Diff line number Diff line change
Expand Up @@ -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">
{% csrf_token %}
<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"> <svg class="svg-inline--fa fa-magnifying-glass" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="magnifying-glass" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" data-fa-i2svg=""><path fill="currentColor" d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"></path></svg> </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>
<div class="table-responsive-md">
<table class="table">
<thead>
<tr>
Expand Down
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
12 changes: 12 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 @@ -838,3 +839,14 @@ 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,
}
return TemplateResponse(
request, 'contests/select_contest.html', context
)