Skip to content

Commit 971f22e

Browse files
steewootwalen
authored andcommitted
Autocomplete for the contest search bar (#518)
* Autocomplete contest search * Bug fix * Add tests * Contest visibility bug fix * fixing some code conflicts with szkopul patches * fixing some code conflicts with szkopul patches part2 --------- Co-authored-by: Tomasz Waleń <[email protected]>
1 parent 8f7747c commit 971f22e

File tree

7 files changed

+375
-7
lines changed

7 files changed

+375
-7
lines changed
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
[
2+
{
3+
"pk": "cs1",
4+
"model": "contests.contest",
5+
"fields": {
6+
"name": "AAAcontest",
7+
"controller_name": "oioioi.programs.controllers.ProgrammingContestController",
8+
"creation_date": "2011-07-31T20:27:58.768Z",
9+
"is_archived": "False"
10+
}
11+
},
12+
{
13+
"pk": 1,
14+
"model": "contests.round",
15+
"fields": {
16+
"name": "Round 1",
17+
"contest": "cs1",
18+
"start_date": "2011-07-31T20:27:58.768Z",
19+
"results_date": "2012-07-31T20:27:58.768Z"
20+
}
21+
},
22+
{
23+
"pk": "cs2",
24+
"model": "contests.contest",
25+
"fields": {
26+
"name": "ABAcontest",
27+
"controller_name": "oioioi.programs.controllers.ProgrammingContestController",
28+
"creation_date": "2011-07-31T20:27:58.768Z",
29+
"is_archived": "False"
30+
}
31+
},
32+
{
33+
"pk": 1,
34+
"model": "contests.round",
35+
"fields": {
36+
"name": "Round 1",
37+
"contest": "cs2",
38+
"start_date": "2011-07-31T20:27:58.768Z",
39+
"results_date": "2012-07-31T20:27:58.768Z"
40+
}
41+
},
42+
{
43+
"pk": "cs3",
44+
"model": "contests.contest",
45+
"fields": {
46+
"name": "ACAcontest",
47+
"controller_name": "oioioi.programs.controllers.ProgrammingContestController",
48+
"creation_date": "2011-07-31T20:27:58.768Z",
49+
"is_archived": "False"
50+
}
51+
},
52+
{
53+
"pk": 1,
54+
"model": "contests.round",
55+
"fields": {
56+
"name": "Round 1",
57+
"contest": "cs3",
58+
"start_date": "2011-07-31T20:27:58.768Z",
59+
"results_date": "2012-07-31T20:27:58.768Z"
60+
}
61+
},
62+
{
63+
"pk": "cs4",
64+
"model": "contests.contest",
65+
"fields": {
66+
"name": "AA contest",
67+
"controller_name": "oioioi.programs.controllers.ProgrammingContestController",
68+
"creation_date": "2011-07-31T20:27:58.768Z",
69+
"is_archived": "False"
70+
}
71+
},
72+
{
73+
"pk": 1,
74+
"model": "contests.round",
75+
"fields": {
76+
"name": "Round 1",
77+
"contest": "cs4",
78+
"start_date": "2011-07-31T20:27:58.768Z",
79+
"results_date": "2012-07-31T20:27:58.768Z"
80+
}
81+
},
82+
{
83+
"pk": "cs5",
84+
"model": "contests.contest",
85+
"fields": {
86+
"name": "BA contest",
87+
"controller_name": "oioioi.programs.controllers.ProgrammingContestController",
88+
"creation_date": "2011-07-31T20:27:58.768Z",
89+
"is_archived": "False"
90+
}
91+
},
92+
{
93+
"pk": 1,
94+
"model": "contests.round",
95+
"fields": {
96+
"name": "Round 1",
97+
"contest": "cs5",
98+
"start_date": "2011-07-31T20:27:58.768Z",
99+
"results_date": "2012-07-31T20:27:58.768Z"
100+
}
101+
},
102+
{
103+
"pk": "cs6",
104+
"model": "contests.contest",
105+
"fields": {
106+
"name": "CA contest",
107+
"controller_name": "oioioi.programs.controllers.ProgrammingContestController",
108+
"creation_date": "2011-07-31T20:27:58.768Z",
109+
"is_archived": "False"
110+
}
111+
},
112+
{
113+
"pk": 1,
114+
"model": "contests.round",
115+
"fields": {
116+
"name": "Round 1",
117+
"contest": "cs6",
118+
"start_date": "2011-07-31T20:27:58.768Z",
119+
"results_date": "2012-07-31T20:27:58.768Z"
120+
}
121+
},
122+
{
123+
"pk": "cs7",
124+
"model": "contests.contest",
125+
"fields": {
126+
"name": "DA contest",
127+
"controller_name": "oioioi.programs.controllers.ProgrammingContestController",
128+
"creation_date": "2011-07-31T20:27:58.768Z",
129+
"is_archived": "False"
130+
}
131+
},
132+
{
133+
"pk": 1,
134+
"model": "contests.round",
135+
"fields": {
136+
"name": "Round 1",
137+
"contest": "cs7",
138+
"start_date": "2011-07-31T20:27:58.768Z",
139+
"results_date": "2012-07-31T20:27:58.768Z"
140+
}
141+
},
142+
{
143+
"pk": "cs8",
144+
"model": "contests.contest",
145+
"fields": {
146+
"name": "EA contest",
147+
"controller_name": "oioioi.programs.controllers.ProgrammingContestController",
148+
"creation_date": "2011-07-31T20:27:58.768Z",
149+
"is_archived": "False"
150+
}
151+
},
152+
{
153+
"pk": 1,
154+
"model": "contests.round",
155+
"fields": {
156+
"name": "Round 1",
157+
"contest": "cs8",
158+
"start_date": "2011-07-31T20:27:58.768Z",
159+
"results_date": "2012-07-31T20:27:58.768Z"
160+
}
161+
},
162+
{
163+
"pk": "cs9",
164+
"model": "contests.contest",
165+
"fields": {
166+
"name": "FA contest",
167+
"controller_name": "oioioi.programs.controllers.ProgrammingContestController",
168+
"creation_date": "2011-07-31T20:27:58.768Z",
169+
"is_archived": "False"
170+
}
171+
},
172+
{
173+
"pk": 1,
174+
"model": "contests.round",
175+
"fields": {
176+
"name": "Round 1",
177+
"contest": "cs9",
178+
"start_date": "2011-07-31T20:27:58.768Z",
179+
"results_date": "2012-07-31T20:27:58.768Z"
180+
}
181+
},
182+
{
183+
"pk": "cs10",
184+
"model": "contests.contest",
185+
"fields": {
186+
"name": "Archived contest",
187+
"controller_name": "oioioi.programs.controllers.ProgrammingContestController",
188+
"creation_date": "2011-07-31T20:27:58.768Z",
189+
"is_archived": "True"
190+
}
191+
},
192+
{
193+
"pk": 1,
194+
"model": "contests.round",
195+
"fields": {
196+
"name": "Round 1",
197+
"contest": "cs10",
198+
"start_date": "2011-07-31T20:27:58.768Z",
199+
"results_date": "2012-07-31T20:27:58.768Z"
200+
}
201+
}
202+
]
203+
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
function init_search_selection(id) {
2+
$(function(){
3+
const input = $('#' + id);
4+
5+
const source_default = function(query, process) {
6+
$.getJSON(input.data("hintsUrl"), {q: query}, process);
7+
};
8+
9+
input.typeahead({
10+
source: source_default,
11+
minLength: 2,
12+
fitToElement: true,
13+
autoSelect: false,
14+
followLinkOnSelect: true,
15+
itemLink: function(item) {
16+
return item.url;
17+
},
18+
matcher: function(item) {
19+
if(!input.val()) {
20+
return false;
21+
}
22+
return true;
23+
},
24+
updater: function(item) {
25+
const typeahead = input.data('typeahead');
26+
let result = item.search_name || item.name;
27+
28+
return result;
29+
},
30+
});
31+
});
32+
}

oioioi/contests/templates/contests/select_contest.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ <h1>{% trans "Select contest" %}</h1>
1111
<div style="width: 20%; margin-bottom: 1rem;">
1212
<form method="GET" action="{% url 'filter_contests' filter_value='PLACEHOLDER' %}" id="filter_form">
1313
<div class="input-group">
14-
<input type="text" id="filter_input" class="form-control search-query" style="width: 20%;" placeholder="Search" name="filter_field" value="{{ filter }}">
14+
<input type="text"
15+
id="filter_input"
16+
class="form-control search-query"
17+
autocomplete="off"
18+
data-hints-url="{% url 'get_contest_hints' %}"
19+
style="width: 20%;" placeholder="Search" name="q" value="{{ filter }}">
20+
21+
<script>init_search_selection('filter_input');</script>
1522
<span class="input-group-btn">
1623
<button type="submit" class="btn btn-outline-secondary " name="submit_button"> <i class="fa-solid fa-magnifying-glass"> </i> </button>
1724
</span>

oioioi/contests/tests/tests.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import pytest
1010
import pytz
1111

12+
import urllib.parse
13+
1214
from django.conf import settings
1315
from django.contrib.admin.utils import quote
1416
from django.contrib.auth.models import AnonymousUser, User
@@ -4960,6 +4962,62 @@ def extra_filter(self):
49604962
self.assertContains(response, self.c1.name)
49614963
self.assertContains(response, self.c2.name)
49624964

4965+
class TestContestSearchHints(TestCase):
4966+
fixtures = [
4967+
'test_contest_search',
4968+
'test_contest',
4969+
'test_extra_contests',
4970+
]
4971+
url = reverse('get_contest_hints')
4972+
4973+
allowed_values = [
4974+
'AAAcontest',
4975+
'ABAcontest',
4976+
'ACAcontest',
4977+
'AA contest',
4978+
'BA contest',
4979+
'CA contest',
4980+
'DA contest',
4981+
'EA contest',
4982+
'Extra test contest 1',
4983+
'Extra test contest 2',
4984+
'FA contest',
4985+
'Test contest',
4986+
'Archived contest',
4987+
]
4988+
4989+
def get_query_url(self, parameter):
4990+
return self.url + '?' + urllib.parse.urlencode(parameter)
4991+
4992+
def assert_contains_only(self, response, allowed_values):
4993+
for contest in self.allowed_values:
4994+
if contest in allowed_values:
4995+
self.assertContains(response, contest)
4996+
else:
4997+
self.assertNotContains(response, contest)
4998+
4999+
def test_contest_search_basic(self):
5000+
self.client.get('/c/c1/')
5001+
5002+
response = self.client.get(self.get_query_url({'q' : 'XX'}), follow=True)
5003+
self.assertEqual(response.status_code, 200)
5004+
self.assert_contains_only(response, [])
5005+
5006+
response = self.client.get(self.get_query_url({'q' : 'AA'}), follow=True)
5007+
self.assertEqual(response.status_code, 200)
5008+
self.assert_contains_only(response, ['AAAcontest', 'AA contest'])
5009+
5010+
response = self.client.get(self.get_query_url({'q' : 'Archived'}), follow=True)
5011+
self.assertEqual(response.status_code, 200)
5012+
self.assert_contains_only(response, [])
5013+
5014+
response = self.client.get(self.get_query_url({'q' : 'DA'}), follow=True)
5015+
self.assertEqual(response.status_code, 200)
5016+
self.assert_contains_only(response, ['DA contest'])
5017+
5018+
response = self.client.get(self.get_query_url({'q' : 'Extra test'}), follow=True)
5019+
self.assertEqual(response.status_code, 200)
5020+
self.assert_contains_only(response, ['Extra test contest 1', 'Extra test contest 2',])
49635021

49645022
class TestProblemsLimits(TestCase):
49655023
fixtures = [

oioioi/contests/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,11 @@ def glob_namespaced_patterns(namespace):
207207
views.filter_contests_view,
208208
name='filter_contests',
209209
),
210+
re_path(
211+
r'^get_contest_hints/$',
212+
views.get_contest_hints_view,
213+
name='get_contest_hints',
214+
),
210215
]
211216

212217
if settings.USE_API:

oioioi/contests/utils.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -451,18 +451,56 @@ def visible_contests_query(request):
451451
) & controller.registration_controller().visible_contests_query(request)
452452
return Contest.objects.filter(visible_query).distinct()
453453

454+
455+
def visible_contests_as_django_queryset(request):
456+
"""Returns query set of contests visible to the logged in user."""
457+
if request.GET.get('living', 'safely') == 'dangerously':
458+
visible_query = Contest.objects.none()
459+
for controller_name in used_controllers():
460+
controller_class = import_string(controller_name)
461+
# HACK: we pass None contest just to call visible_contests_query.
462+
# This is a workaround for mixins not taking classmethods very well.
463+
controller = controller_class(None)
464+
subquery = Contest.objects.filter(controller_name=controller_name).filter(
465+
controller.registration_controller().visible_contests_query(request)
466+
)
467+
visible_query = visible_query.union(subquery, all=False)
468+
return visible_query
469+
visible_query = Q_always_false()
470+
for controller_name in used_controllers():
471+
controller_class = import_string(controller_name)
472+
# HACK: we pass None contest just to call visible_contests_query.
473+
# This is a workaround for mixins not taking classmethods very well.
474+
controller = controller_class(None)
475+
visible_query |= Q(
476+
controller_name=controller_name
477+
) & controller.registration_controller().visible_contests_query(request)
478+
return Contest.objects.filter(visible_query).distinct()
479+
480+
454481
@request_cached
455482
def visible_contests(request):
456-
contests = visible_contests_query(request)
483+
contests = visible_contests_as_django_queryset(request)
457484
return set(contests)
458485

486+
459487
@request_cached_complex
460488
def visible_contests_queryset(request, filter_value=None):
461-
contests = visible_contests_query(request)
489+
contests = visible_contests_as_django_queryset(request)
462490
if filter_value is not None:
463-
contests = contests.filter(Q(name__icontains=filter_value) | Q(id__icontains=filter_value) | Q(school_year=filter_value))
491+
contests = contests.filter(Q(name__icontains=filter_value) | Q(id__icontains=filter_value) | Q(school_year=filter_value))
464492
return set(contests)
465493

494+
495+
@request_cached_complex
496+
def visible_filtered_contests_as_django_queryset(request, filter_value=None):
497+
"""TODO: remove code duplication visible_contests_queryset/visible_contests_query"""
498+
contests = visible_contests_as_django_queryset(request)
499+
if filter_value is not None:
500+
contests = contests.filter(Q(name__icontains=filter_value) | Q(id__icontains=filter_value) | Q(school_year=filter_value))
501+
return contests
502+
503+
466504
# why is there no `can_admin_contest_query`?
467505
@request_cached
468506
def administered_contests(request):

0 commit comments

Comments
 (0)