From b467e63d35736f85122edb8093153a4375e96994 Mon Sep 17 00:00:00 2001 From: steewoo Date: Thu, 15 May 2025 21:08:33 +0200 Subject: [PATCH 1/4] Autocomplete contest search --- .../contests/static/common/contest_hints.js | 33 +++++++++++++++++++ .../templates/contests/select_contest.html | 9 ++++- oioioi/contests/urls.py | 5 +++ oioioi/contests/views.py | 33 ++++++++++++++++++- 4 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 oioioi/contests/static/common/contest_hints.js diff --git a/oioioi/contests/static/common/contest_hints.js b/oioioi/contests/static/common/contest_hints.js new file mode 100644 index 000000000..c3162a8cf --- /dev/null +++ b/oioioi/contests/static/common/contest_hints.js @@ -0,0 +1,33 @@ +function init_search_selection(id) { + $(function(){ + const input = $('#' + id); + + // The default source - returns hints for contests + const source_default = function(query, process) { + $.getJSON(input.data("hintsUrl"), {q: query}, process); + }; + + input.typeahead({ + source: source_default, + minLength: 2, + fitToElement: true, + autoSelect: false, + followLinkOnSelect: true, + itemLink: function(item) { + return item.url; + }, + matcher: function(item) { + if(!input.val()) { + return false; + } + return true; + }, + updater: function(item) { + const typeahead = input.data('typeahead'); + let result = item.search_name || item.name; + + return result; + }, + }); + }); +} diff --git a/oioioi/contests/templates/contests/select_contest.html b/oioioi/contests/templates/contests/select_contest.html index 9440d467c..8be577170 100644 --- a/oioioi/contests/templates/contests/select_contest.html +++ b/oioioi/contests/templates/contests/select_contest.html @@ -11,7 +11,14 @@

{% trans "Select contest" %}

- + + + diff --git a/oioioi/contests/urls.py b/oioioi/contests/urls.py index 173145c86..2aacc0e90 100644 --- a/oioioi/contests/urls.py +++ b/oioioi/contests/urls.py @@ -198,6 +198,11 @@ def glob_namespaced_patterns(namespace): views.filter_contests_view, name='filter_contests', ), + re_path( + r'^get_search_hints/(?Ppublic|my|all)/$', + views.get_search_hints_view, + name='get_search_hints', + ), ] if settings.USE_API: diff --git a/oioioi/contests/views.py b/oioioi/contests/views.py index e553b37a7..9cfc81810 100755 --- a/oioioi/contests/views.py +++ b/oioioi/contests/views.py @@ -19,6 +19,7 @@ from oioioi.base.main_page import register_main_page_view from oioioi.base.menu import menu_registry from oioioi.base.permissions import enforce_condition, not_anonymous +from oioioi.base.utils import jsonify from oioioi.base.utils.redirect import safe_redirect from oioioi.base.utils.user_selection import get_user_hints_view from oioioi.contests.attachment_registration import attachment_registry @@ -851,4 +852,34 @@ def filter_contests_view(request, filter_value=""): } return TemplateResponse( request, 'contests/select_contest.html', context - ) \ No newline at end of file + ) + +def get_contest_hints(query): + contests = Contest.objects.filter( + Q(name__icontains=query) | Q(id__icontains=query) | Q(is_archived=False) + ).distinct() + + return [ + { + 'trigger': 'problem', + 'name': contest.name, + 'url': reverse('filter_contests', kwargs={'filter_value': contest.name}) + } + for contest in contests[: getattr(settings, 'NUM_HINTS', 10)] + ] + +@jsonify +def get_search_hints_view(request, view_type): + # Function works analogously to the auto-completion function implemented in the problemset + + print(view_type) + + if view_type == 'all' and request.user.is_superuser: + raise PermissionDenied + + query = request.GET.get('q', '') + + result = [] + result.extend(list(get_contest_hints(query))) + + return result \ No newline at end of file From d408ace7eb25de0b0c380fa7b4ffbfdf245da823 Mon Sep 17 00:00:00 2001 From: steewoo Date: Thu, 15 May 2025 21:40:34 +0200 Subject: [PATCH 2/4] Bug fix --- oioioi/contests/templates/contests/select_contest.html | 2 +- oioioi/contests/urls.py | 6 +++--- oioioi/contests/views.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/oioioi/contests/templates/contests/select_contest.html b/oioioi/contests/templates/contests/select_contest.html index 8be577170..82d00b71c 100644 --- a/oioioi/contests/templates/contests/select_contest.html +++ b/oioioi/contests/templates/contests/select_contest.html @@ -15,7 +15,7 @@

{% trans "Select contest" %}

id="filter_input" class="form-control search-query" autocomplete="off" - data-hints-url="{% url 'get_search_hints' view_type='all' %}" + data-hints-url="{% url 'get_contest_hints' view_type='all' %}" style="width: 20%;" placeholder="Search" name="q" value="{{ filter }}"> diff --git a/oioioi/contests/urls.py b/oioioi/contests/urls.py index 2aacc0e90..4574ecf40 100644 --- a/oioioi/contests/urls.py +++ b/oioioi/contests/urls.py @@ -199,9 +199,9 @@ def glob_namespaced_patterns(namespace): name='filter_contests', ), re_path( - r'^get_search_hints/(?Ppublic|my|all)/$', - views.get_search_hints_view, - name='get_search_hints', + r'^get_contest_hints/(?Ppublic|my|all)/$', + views.get_contest_hints_view, + name='get_contest_hints', ), ] diff --git a/oioioi/contests/views.py b/oioioi/contests/views.py index 9cfc81810..b678e0505 100755 --- a/oioioi/contests/views.py +++ b/oioioi/contests/views.py @@ -869,7 +869,7 @@ def get_contest_hints(query): ] @jsonify -def get_search_hints_view(request, view_type): +def get_contest_hints_view(request, view_type): # Function works analogously to the auto-completion function implemented in the problemset print(view_type) From 67d4967fd00f594d5e5e42bbf15bf08be48e699d Mon Sep 17 00:00:00 2001 From: steewoo Date: Tue, 27 May 2025 15:41:24 +0200 Subject: [PATCH 3/4] Add tests --- .../fixtures/test_contest_search.json | 203 ++++++++++++++++++ .../contests/static/common/contest_hints.js | 1 - .../templates/contests/select_contest.html | 2 +- oioioi/contests/tests/tests.py | 59 +++++ oioioi/contests/urls.py | 2 +- oioioi/contests/views.py | 10 +- 6 files changed, 266 insertions(+), 11 deletions(-) create mode 100644 oioioi/contests/fixtures/test_contest_search.json diff --git a/oioioi/contests/fixtures/test_contest_search.json b/oioioi/contests/fixtures/test_contest_search.json new file mode 100644 index 000000000..7761c138b --- /dev/null +++ b/oioioi/contests/fixtures/test_contest_search.json @@ -0,0 +1,203 @@ +[ + { + "pk": "cs1", + "model": "contests.contest", + "fields": { + "name": "AAAcontest", + "controller_name": "oioioi.programs.controllers.ProgrammingContestController", + "creation_date": "2011-07-31T20:27:58.768Z", + "is_archived": "False" + } + }, + { + "pk": 1, + "model": "contests.round", + "fields": { + "name": "Round 1", + "contest": "cs1", + "start_date": "2011-07-31T20:27:58.768Z", + "results_date": "2012-07-31T20:27:58.768Z" + } + }, + { + "pk": "cs2", + "model": "contests.contest", + "fields": { + "name": "ABAcontest", + "controller_name": "oioioi.programs.controllers.ProgrammingContestController", + "creation_date": "2011-07-31T20:27:58.768Z", + "is_archived": "False" + } + }, + { + "pk": 1, + "model": "contests.round", + "fields": { + "name": "Round 1", + "contest": "cs2", + "start_date": "2011-07-31T20:27:58.768Z", + "results_date": "2012-07-31T20:27:58.768Z" + } + }, + { + "pk": "cs3", + "model": "contests.contest", + "fields": { + "name": "ACAcontest", + "controller_name": "oioioi.programs.controllers.ProgrammingContestController", + "creation_date": "2011-07-31T20:27:58.768Z", + "is_archived": "False" + } + }, + { + "pk": 1, + "model": "contests.round", + "fields": { + "name": "Round 1", + "contest": "cs3", + "start_date": "2011-07-31T20:27:58.768Z", + "results_date": "2012-07-31T20:27:58.768Z" + } + }, + { + "pk": "cs4", + "model": "contests.contest", + "fields": { + "name": "AA contest", + "controller_name": "oioioi.programs.controllers.ProgrammingContestController", + "creation_date": "2011-07-31T20:27:58.768Z", + "is_archived": "False" + } + }, + { + "pk": 1, + "model": "contests.round", + "fields": { + "name": "Round 1", + "contest": "cs4", + "start_date": "2011-07-31T20:27:58.768Z", + "results_date": "2012-07-31T20:27:58.768Z" + } + }, + { + "pk": "cs5", + "model": "contests.contest", + "fields": { + "name": "BA contest", + "controller_name": "oioioi.programs.controllers.ProgrammingContestController", + "creation_date": "2011-07-31T20:27:58.768Z", + "is_archived": "False" + } + }, + { + "pk": 1, + "model": "contests.round", + "fields": { + "name": "Round 1", + "contest": "cs5", + "start_date": "2011-07-31T20:27:58.768Z", + "results_date": "2012-07-31T20:27:58.768Z" + } + }, + { + "pk": "cs6", + "model": "contests.contest", + "fields": { + "name": "CA contest", + "controller_name": "oioioi.programs.controllers.ProgrammingContestController", + "creation_date": "2011-07-31T20:27:58.768Z", + "is_archived": "False" + } + }, + { + "pk": 1, + "model": "contests.round", + "fields": { + "name": "Round 1", + "contest": "cs6", + "start_date": "2011-07-31T20:27:58.768Z", + "results_date": "2012-07-31T20:27:58.768Z" + } + }, + { + "pk": "cs7", + "model": "contests.contest", + "fields": { + "name": "DA contest", + "controller_name": "oioioi.programs.controllers.ProgrammingContestController", + "creation_date": "2011-07-31T20:27:58.768Z", + "is_archived": "False" + } + }, + { + "pk": 1, + "model": "contests.round", + "fields": { + "name": "Round 1", + "contest": "cs7", + "start_date": "2011-07-31T20:27:58.768Z", + "results_date": "2012-07-31T20:27:58.768Z" + } + }, + { + "pk": "cs8", + "model": "contests.contest", + "fields": { + "name": "EA contest", + "controller_name": "oioioi.programs.controllers.ProgrammingContestController", + "creation_date": "2011-07-31T20:27:58.768Z", + "is_archived": "False" + } + }, + { + "pk": 1, + "model": "contests.round", + "fields": { + "name": "Round 1", + "contest": "cs8", + "start_date": "2011-07-31T20:27:58.768Z", + "results_date": "2012-07-31T20:27:58.768Z" + } + }, + { + "pk": "cs9", + "model": "contests.contest", + "fields": { + "name": "FA contest", + "controller_name": "oioioi.programs.controllers.ProgrammingContestController", + "creation_date": "2011-07-31T20:27:58.768Z", + "is_archived": "False" + } + }, + { + "pk": 1, + "model": "contests.round", + "fields": { + "name": "Round 1", + "contest": "cs9", + "start_date": "2011-07-31T20:27:58.768Z", + "results_date": "2012-07-31T20:27:58.768Z" + } + }, + { + "pk": "cs10", + "model": "contests.contest", + "fields": { + "name": "Archived contest", + "controller_name": "oioioi.programs.controllers.ProgrammingContestController", + "creation_date": "2011-07-31T20:27:58.768Z", + "is_archived": "True" + } + }, + { + "pk": 1, + "model": "contests.round", + "fields": { + "name": "Round 1", + "contest": "cs10", + "start_date": "2011-07-31T20:27:58.768Z", + "results_date": "2012-07-31T20:27:58.768Z" + } + } + ] + \ No newline at end of file diff --git a/oioioi/contests/static/common/contest_hints.js b/oioioi/contests/static/common/contest_hints.js index c3162a8cf..9d2eb1274 100644 --- a/oioioi/contests/static/common/contest_hints.js +++ b/oioioi/contests/static/common/contest_hints.js @@ -2,7 +2,6 @@ function init_search_selection(id) { $(function(){ const input = $('#' + id); - // The default source - returns hints for contests const source_default = function(query, process) { $.getJSON(input.data("hintsUrl"), {q: query}, process); }; diff --git a/oioioi/contests/templates/contests/select_contest.html b/oioioi/contests/templates/contests/select_contest.html index 82d00b71c..a46ca26c2 100644 --- a/oioioi/contests/templates/contests/select_contest.html +++ b/oioioi/contests/templates/contests/select_contest.html @@ -15,7 +15,7 @@

{% trans "Select contest" %}

id="filter_input" class="form-control search-query" autocomplete="off" - data-hints-url="{% url 'get_contest_hints' view_type='all' %}" + data-hints-url="{% url 'get_contest_hints' %}" style="width: 20%;" placeholder="Search" name="q" value="{{ filter }}"> diff --git a/oioioi/contests/tests/tests.py b/oioioi/contests/tests/tests.py index 9f6f950bb..7a6ab635c 100755 --- a/oioioi/contests/tests/tests.py +++ b/oioioi/contests/tests/tests.py @@ -9,6 +9,8 @@ import pytest import pytz +import urllib.parse + from django.conf import settings from django.contrib.admin.utils import quote from django.contrib.auth.models import AnonymousUser, User @@ -4526,3 +4528,60 @@ def extra_filter(self): self.assertNotContains(response, self.c.name) self.assertContains(response, self.c1.name) self.assertContains(response, self.c2.name) + +class TestContestSearchHints(TestCase): + fixtures = [ + 'test_contest_search', + 'test_contest', + 'test_extra_contests', + ] + url = reverse('get_contest_hints') + + allowed_values = [ + 'AAAcontest', + 'ABAcontest', + 'ACAcontest', + 'AA contest', + 'BA contest', + 'CA contest', + 'DA contest', + 'EA contest', + 'Extra test contest 1', + 'Extra test contest 2', + 'FA contest', + 'Test contest', + 'Archived contest', + ] + + def get_query_url(self, parameter): + return self.url + '?' + urllib.parse.urlencode(parameter) + + def assert_contains_only(self, response, allowed_values): + for contest in self.allowed_values: + if contest in allowed_values: + self.assertContains(response, contest) + else: + self.assertNotContains(response, contest) + + def test_contest_search_basic(self): + self.client.get('/c/c1/') + + response = self.client.get(self.get_query_url({'q' : 'XX'}), follow=True) + self.assertEqual(response.status_code, 200) + self.assert_contains_only(response, []) + + response = self.client.get(self.get_query_url({'q' : 'AA'}), follow=True) + self.assertEqual(response.status_code, 200) + self.assert_contains_only(response, ['AAAcontest', 'AA contest']) + + response = self.client.get(self.get_query_url({'q' : 'Archived'}), follow=True) + self.assertEqual(response.status_code, 200) + self.assert_contains_only(response, []) + + response = self.client.get(self.get_query_url({'q' : 'DA'}), follow=True) + self.assertEqual(response.status_code, 200) + self.assert_contains_only(response, ['DA contest']) + + response = self.client.get(self.get_query_url({'q' : 'Extra test'}), follow=True) + self.assertEqual(response.status_code, 200) + self.assert_contains_only(response, ['Extra test contest 1', 'Extra test contest 2',]) diff --git a/oioioi/contests/urls.py b/oioioi/contests/urls.py index 4574ecf40..708e6944a 100644 --- a/oioioi/contests/urls.py +++ b/oioioi/contests/urls.py @@ -199,7 +199,7 @@ def glob_namespaced_patterns(namespace): name='filter_contests', ), re_path( - r'^get_contest_hints/(?Ppublic|my|all)/$', + r'^get_contest_hints/$', views.get_contest_hints_view, name='get_contest_hints', ), diff --git a/oioioi/contests/views.py b/oioioi/contests/views.py index b678e0505..f281974cf 100755 --- a/oioioi/contests/views.py +++ b/oioioi/contests/views.py @@ -856,9 +856,8 @@ def filter_contests_view(request, filter_value=""): def get_contest_hints(query): contests = Contest.objects.filter( - Q(name__icontains=query) | Q(id__icontains=query) | Q(is_archived=False) + (Q(name__icontains=query) | Q(id__icontains=query)) & Q(is_archived=False) ).distinct() - return [ { 'trigger': 'problem', @@ -869,14 +868,9 @@ def get_contest_hints(query): ] @jsonify -def get_contest_hints_view(request, view_type): +def get_contest_hints_view(request): # Function works analogously to the auto-completion function implemented in the problemset - print(view_type) - - if view_type == 'all' and request.user.is_superuser: - raise PermissionDenied - query = request.GET.get('q', '') result = [] From 4a6b5a0343e7fa2e5737898914a986fbcd09e388 Mon Sep 17 00:00:00 2001 From: steewoo Date: Tue, 27 May 2025 16:30:59 +0200 Subject: [PATCH 4/4] Contest visibility bug fix --- oioioi/contests/utils.py | 2 +- oioioi/contests/views.py | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/oioioi/contests/utils.py b/oioioi/contests/utils.py index 0447a239c..f876e34c3 100755 --- a/oioioi/contests/utils.py +++ b/oioioi/contests/utils.py @@ -432,7 +432,7 @@ def visible_contests(request): 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) + return contests @request_cached def administered_contests(request): diff --git a/oioioi/contests/views.py b/oioioi/contests/views.py index f281974cf..3c9e95f7a 100755 --- a/oioioi/contests/views.py +++ b/oioioi/contests/views.py @@ -843,7 +843,7 @@ def unarchive_contest(request): return redirect('default_contest_view', contest_id=contest.id) def filter_contests_view(request, filter_value=""): - contests = visible_contests_queryset(request, filter_value) + contests = set(visible_contests_queryset(request, filter_value)) contests = sorted(contests, key=lambda x: x.creation_date, reverse=True) context = { @@ -854,10 +854,9 @@ def filter_contests_view(request, filter_value=""): request, 'contests/select_contest.html', context ) -def get_contest_hints(query): - contests = Contest.objects.filter( - (Q(name__icontains=query) | Q(id__icontains=query)) & Q(is_archived=False) - ).distinct() +def get_contest_hints(request, query): + contests = visible_contests_queryset(request, query) + contests = contests.filter(Q(is_archived=False)).distinct() return [ { 'trigger': 'problem', @@ -874,6 +873,6 @@ def get_contest_hints_view(request): query = request.GET.get('q', '') result = [] - result.extend(list(get_contest_hints(query))) + result.extend(list(get_contest_hints(request, query))) return result \ No newline at end of file