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 new file mode 100644 index 000000000..9d2eb1274 --- /dev/null +++ b/oioioi/contests/static/common/contest_hints.js @@ -0,0 +1,32 @@ +function init_search_selection(id) { + $(function(){ + const input = $('#' + id); + + 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..a46ca26c2 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/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 173145c86..708e6944a 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_contest_hints/$', + views.get_contest_hints_view, + name='get_contest_hints', + ), ] if settings.USE_API: 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 e553b37a7..3c9e95f7a 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 @@ -842,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 = { @@ -851,4 +852,27 @@ 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(request, query): + contests = visible_contests_queryset(request, query) + contests = contests.filter(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_contest_hints_view(request): + # Function works analogously to the auto-completion function implemented in the problemset + + query = request.GET.get('q', '') + + result = [] + result.extend(list(get_contest_hints(request, query))) + + return result \ No newline at end of file