Skip to content

Commit e138bee

Browse files
committed
Replace old search function
Using the existing filter mechanisms from django-filter
1 parent ea614b1 commit e138bee

File tree

5 files changed

+102
-225
lines changed

5 files changed

+102
-225
lines changed

wger/nutrition/api/filtersets.py

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
# This file is part of wger Workout Manager.
2+
#
3+
# wger Workout Manager is free software: you can redistribute it and/or modify
4+
# it under the terms of the GNU Affero General Public License as published by
5+
# the Free Software Foundation, either version 3 of the License, or
6+
# (at your option) any later version.
7+
#
8+
# wger Workout Manager is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU Affero General Public License
14+
# along with Workout Manager. If not, see <http://www.gnu.org/licenses/>.
15+
16+
# Standard Library
17+
import logging
18+
19+
# Django
20+
from django.contrib.postgres.search import TrigramSimilarity
21+
122
# Third Party
223
from django_filters import rest_framework as filters
324

@@ -6,6 +27,11 @@
627
Ingredient,
728
LogItem,
829
)
30+
from wger.utils.db import is_postgres_db
31+
from wger.utils.language import load_language
32+
33+
34+
logger = logging.getLogger(__name__)
935

1036

1137
class LogItemFilterSet(filters.FilterSet):
@@ -21,6 +47,54 @@ class Meta:
2147

2248

2349
class IngredientFilterSet(filters.FilterSet):
50+
code = filters.CharFilter(method='search_code')
51+
name__search = filters.CharFilter(method='search_name_fulltext')
52+
language__code = filters.CharFilter(method='search_languagecode')
53+
54+
def search_code(self, queryset, name, value):
55+
"""
56+
'exact' search for the barcode.
57+
58+
It this is not known locally, try fetching the result from OFF
59+
"""
60+
61+
if not value:
62+
return queryset
63+
64+
queryset = queryset.filter(code=value)
65+
if queryset.count() == 0:
66+
logger.debug('code not found locally, fetching code from off')
67+
Ingredient.fetch_ingredient_from_off(value)
68+
69+
return queryset
70+
71+
def search_name_fulltext(self, queryset, name, value):
72+
"""
73+
Perform a fulltext search when postgres is available
74+
"""
75+
76+
if is_postgres_db():
77+
return (
78+
queryset.annotate(similarity=TrigramSimilarity('name', value))
79+
.filter(similarity__gt=0.15)
80+
.order_by('-similarity', 'name')
81+
)
82+
else:
83+
return queryset.filter(name__icontains=value)
84+
85+
def search_languagecode(self, queryset, name, value):
86+
"""
87+
Filter based on language codes, not IDs
88+
89+
Also accepts a comma separated list of codes. Unknown codes are ignored
90+
"""
91+
92+
languages = [load_language(l) for l in value.split(',')]
93+
if languages:
94+
queryset = queryset.filter(language__in=languages)
95+
96+
return queryset
97+
2498
class Meta:
2599
model = Ingredient
26100
fields = {
@@ -29,7 +103,6 @@ class Meta:
29103
'code': ['exact'],
30104
'source_name': ['exact'],
31105
'name': ['exact'],
32-
33106
'energy': ['exact'],
34107
'protein': ['exact'],
35108
'carbohydrates': ['exact'],
@@ -38,12 +111,10 @@ class Meta:
38111
'fat_saturated': ['exact'],
39112
'fiber': ['exact'],
40113
'sodium': ['exact'],
41-
42114
'created': ['exact', 'gt', 'lt'],
43115
'last_update': ['exact', 'gt', 'lt'],
44116
'last_imported': ['exact', 'gt', 'lt'],
45-
46-
'language': ['exact'],
117+
'language': ['exact', 'in'],
47118
'license': ['exact'],
48119
'license_author': ['exact'],
49120
}

wger/nutrition/api/views.py

Lines changed: 2 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,12 @@
2020

2121
# Django
2222
from django.conf import settings
23-
from django.contrib.postgres.search import TrigramSimilarity
2423
from django.utils.decorators import method_decorator
2524
from django.views.decorators.cache import cache_page
2625

2726
# Third Party
28-
from drf_spectacular.types import OpenApiTypes
29-
from drf_spectacular.utils import (
30-
OpenApiParameter,
31-
extend_schema,
32-
inline_serializer,
33-
)
34-
from easy_thumbnails.alias import aliases
35-
from easy_thumbnails.files import get_thumbnailer
3627
from rest_framework import viewsets
37-
from rest_framework.decorators import (
38-
action,
39-
api_view,
40-
)
41-
from rest_framework.fields import (
42-
CharField,
43-
IntegerField,
44-
)
28+
from rest_framework.decorators import action
4529
from rest_framework.response import Response
4630

4731
# wger
@@ -73,12 +57,6 @@
7357
NutritionPlan,
7458
WeightUnit,
7559
)
76-
from wger.utils.constants import (
77-
ENGLISH_SHORT_NAME,
78-
SEARCH_ALL_LANGUAGES,
79-
)
80-
from wger.utils.db import is_postgres_db
81-
from wger.utils.language import load_language
8260
from wger.utils.viewsets import WgerOwnerObjectModelViewSet
8361

8462

@@ -94,26 +72,12 @@ class IngredientViewSet(viewsets.ReadOnlyModelViewSet):
9472
serializer_class = IngredientSerializer
9573
ordering_fields = '__all__'
9674
filterset_class = IngredientFilterSet
75+
queryset = Ingredient.objects.all()
9776

9877
@method_decorator(cache_page(settings.WGER_SETTINGS['INGREDIENT_CACHE_TTL']))
9978
def list(self, request, *args, **kwargs):
10079
return super().list(request, *args, **kwargs)
10180

102-
def get_queryset(self):
103-
"""H"""
104-
qs = Ingredient.objects.all()
105-
106-
code = self.request.query_params.get('code')
107-
if not code:
108-
return qs
109-
110-
qs = qs.filter(code=code)
111-
if qs.count() == 0:
112-
logger.debug('code not found locally, fetching code from off')
113-
Ingredient.fetch_ingredient_from_off(code)
114-
115-
return qs
116-
11781
@action(detail=True)
11882
def get_values(self, request, pk):
11983
"""
@@ -168,104 +132,6 @@ class IngredientInfoViewSet(IngredientViewSet):
168132
serializer_class = IngredientInfoSerializer
169133

170134

171-
@extend_schema(
172-
parameters=[
173-
OpenApiParameter(
174-
'term',
175-
OpenApiTypes.STR,
176-
OpenApiParameter.QUERY,
177-
description='The name of the ingredient to search"',
178-
required=True,
179-
),
180-
OpenApiParameter(
181-
'language',
182-
OpenApiTypes.STR,
183-
OpenApiParameter.QUERY,
184-
description='Comma separated list of language codes to search',
185-
required=True,
186-
),
187-
],
188-
responses={
189-
200: inline_serializer(
190-
name='IngredientSearchResponse',
191-
fields={
192-
'value': CharField(),
193-
'data': inline_serializer(
194-
name='IngredientSearchItemResponse',
195-
fields={
196-
'id': IntegerField(),
197-
'name': CharField(),
198-
'category': CharField(),
199-
'image': CharField(),
200-
'image_thumbnail': CharField(),
201-
},
202-
),
203-
},
204-
)
205-
},
206-
)
207-
@api_view(['GET'])
208-
def search(request):
209-
"""
210-
Searches for ingredients.
211-
212-
This format is currently used by the ingredient search autocompleter
213-
"""
214-
term = request.GET.get('term', None)
215-
language_codes = request.GET.get('language', ENGLISH_SHORT_NAME)
216-
results = []
217-
response = {}
218-
219-
if not term:
220-
return Response(response)
221-
222-
query = Ingredient.objects.all()
223-
224-
# Filter the appropriate languages
225-
languages = [load_language(l) for l in language_codes.split(',')]
226-
if language_codes != SEARCH_ALL_LANGUAGES:
227-
query = query.filter(
228-
language__in=languages,
229-
)
230-
231-
query = query.only('name')
232-
233-
# Postgres uses a full-text search
234-
if is_postgres_db():
235-
query = (
236-
query.annotate(similarity=TrigramSimilarity('name', term))
237-
.filter(similarity__gt=0.15)
238-
.order_by('-similarity', 'name')
239-
)
240-
else:
241-
query = query.filter(name__icontains=term)
242-
243-
for ingredient in query[:150]:
244-
if hasattr(ingredient, 'image'):
245-
image_obj = ingredient.image
246-
image = image_obj.image.url
247-
t = get_thumbnailer(image_obj.image)
248-
thumbnail = t.get_thumbnail(aliases.get('micro_cropped')).url
249-
else:
250-
ingredient.get_image(request)
251-
image = None
252-
thumbnail = None
253-
254-
ingredient_json = {
255-
'value': ingredient.name,
256-
'data': {
257-
'id': ingredient.id,
258-
'name': ingredient.name,
259-
'image': image,
260-
'image_thumbnail': thumbnail,
261-
},
262-
}
263-
results.append(ingredient_json)
264-
response['suggestions'] = results
265-
266-
return Response(response)
267-
268-
269135
class ImageViewSet(viewsets.ReadOnlyModelViewSet):
270136
"""
271137
API endpoint for ingredient images

wger/nutrition/tests/test_ingredient.py

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -239,49 +239,6 @@ def test_ingredient_detail_logged_out(self):
239239
self.ingredient_detail(editor=False)
240240

241241

242-
class IngredientSearchTestCase(WgerTestCase):
243-
"""
244-
Tests the ingredient search functions
245-
"""
246-
247-
def search_ingredient(self, fail=True):
248-
"""
249-
Helper function
250-
"""
251-
252-
kwargs = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
253-
response = self.client.get(reverse('ingredient-search'), {'term': 'test'}, **kwargs)
254-
self.assertEqual(response.status_code, 200)
255-
result = json.loads(response.content.decode('utf8'))
256-
self.assertEqual(len(result['suggestions']), 2)
257-
self.assertEqual(result['suggestions'][0]['value'], 'Ingredient, test, 2, organic, raw')
258-
self.assertEqual(result['suggestions'][0]['data']['id'], 2)
259-
suggestion_0_name = 'Ingredient, test, 2, organic, raw'
260-
self.assertEqual(result['suggestions'][0]['data']['name'], suggestion_0_name)
261-
self.assertEqual(result['suggestions'][0]['data']['image'], None)
262-
self.assertEqual(result['suggestions'][0]['data']['image_thumbnail'], None)
263-
self.assertEqual(result['suggestions'][1]['value'], 'Test ingredient 1')
264-
self.assertEqual(result['suggestions'][1]['data']['id'], 1)
265-
self.assertEqual(result['suggestions'][1]['data']['name'], 'Test ingredient 1')
266-
self.assertEqual(result['suggestions'][1]['data']['image'], None)
267-
self.assertEqual(result['suggestions'][1]['data']['image_thumbnail'], None)
268-
269-
def test_search_ingredient_anonymous(self):
270-
"""
271-
Test searching for an ingredient by an anonymous user
272-
"""
273-
274-
self.search_ingredient()
275-
276-
def test_search_ingredient_logged_in(self):
277-
"""
278-
Test searching for an ingredient by a logged-in user
279-
"""
280-
281-
self.user_login('test')
282-
self.search_ingredient()
283-
284-
285242
class IngredientValuesTestCase(WgerTestCase):
286243
"""
287244
Tests the nutritional value calculator for an ingredient

0 commit comments

Comments
 (0)