Skip to content

Commit

Permalink
Replace old search function
Browse files Browse the repository at this point in the history
Using the existing filter mechanisms from django-filter
  • Loading branch information
rolandgeider committed Jul 12, 2024
1 parent ea614b1 commit e138bee
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 225 deletions.
79 changes: 75 additions & 4 deletions wger/nutrition/api/filtersets.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
# This file is part of wger Workout Manager.
#
# wger Workout Manager is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# wger Workout Manager is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Workout Manager. If not, see <http://www.gnu.org/licenses/>.

# Standard Library
import logging

# Django
from django.contrib.postgres.search import TrigramSimilarity

# Third Party
from django_filters import rest_framework as filters

Expand All @@ -6,6 +27,11 @@
Ingredient,
LogItem,
)
from wger.utils.db import is_postgres_db
from wger.utils.language import load_language


logger = logging.getLogger(__name__)


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


class IngredientFilterSet(filters.FilterSet):
code = filters.CharFilter(method='search_code')
name__search = filters.CharFilter(method='search_name_fulltext')
language__code = filters.CharFilter(method='search_languagecode')

def search_code(self, queryset, name, value):
"""
'exact' search for the barcode.
It this is not known locally, try fetching the result from OFF
"""

if not value:
return queryset

queryset = queryset.filter(code=value)
if queryset.count() == 0:
logger.debug('code not found locally, fetching code from off')
Ingredient.fetch_ingredient_from_off(value)

return queryset

def search_name_fulltext(self, queryset, name, value):
"""
Perform a fulltext search when postgres is available
"""

if is_postgres_db():
return (
queryset.annotate(similarity=TrigramSimilarity('name', value))
.filter(similarity__gt=0.15)
.order_by('-similarity', 'name')
)
else:
return queryset.filter(name__icontains=value)

def search_languagecode(self, queryset, name, value):
"""
Filter based on language codes, not IDs
Also accepts a comma separated list of codes. Unknown codes are ignored
"""

languages = [load_language(l) for l in value.split(',')]
if languages:
queryset = queryset.filter(language__in=languages)

return queryset

class Meta:
model = Ingredient
fields = {
Expand All @@ -29,7 +103,6 @@ class Meta:
'code': ['exact'],
'source_name': ['exact'],
'name': ['exact'],

'energy': ['exact'],
'protein': ['exact'],
'carbohydrates': ['exact'],
Expand All @@ -38,12 +111,10 @@ class Meta:
'fat_saturated': ['exact'],
'fiber': ['exact'],
'sodium': ['exact'],

'created': ['exact', 'gt', 'lt'],
'last_update': ['exact', 'gt', 'lt'],
'last_imported': ['exact', 'gt', 'lt'],

'language': ['exact'],
'language': ['exact', 'in'],
'license': ['exact'],
'license_author': ['exact'],
}
138 changes: 2 additions & 136 deletions wger/nutrition/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,12 @@

# Django
from django.conf import settings
from django.contrib.postgres.search import TrigramSimilarity
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page

# Third Party
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
OpenApiParameter,
extend_schema,
inline_serializer,
)
from easy_thumbnails.alias import aliases
from easy_thumbnails.files import get_thumbnailer
from rest_framework import viewsets
from rest_framework.decorators import (
action,
api_view,
)
from rest_framework.fields import (
CharField,
IntegerField,
)
from rest_framework.decorators import action
from rest_framework.response import Response

# wger
Expand Down Expand Up @@ -73,12 +57,6 @@
NutritionPlan,
WeightUnit,
)
from wger.utils.constants import (
ENGLISH_SHORT_NAME,
SEARCH_ALL_LANGUAGES,
)
from wger.utils.db import is_postgres_db
from wger.utils.language import load_language
from wger.utils.viewsets import WgerOwnerObjectModelViewSet


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

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

def get_queryset(self):
"""H"""
qs = Ingredient.objects.all()

code = self.request.query_params.get('code')
if not code:
return qs

qs = qs.filter(code=code)
if qs.count() == 0:
logger.debug('code not found locally, fetching code from off')
Ingredient.fetch_ingredient_from_off(code)

return qs

@action(detail=True)
def get_values(self, request, pk):
"""
Expand Down Expand Up @@ -168,104 +132,6 @@ class IngredientInfoViewSet(IngredientViewSet):
serializer_class = IngredientInfoSerializer


@extend_schema(
parameters=[
OpenApiParameter(
'term',
OpenApiTypes.STR,
OpenApiParameter.QUERY,
description='The name of the ingredient to search"',
required=True,
),
OpenApiParameter(
'language',
OpenApiTypes.STR,
OpenApiParameter.QUERY,
description='Comma separated list of language codes to search',
required=True,
),
],
responses={
200: inline_serializer(
name='IngredientSearchResponse',
fields={
'value': CharField(),
'data': inline_serializer(
name='IngredientSearchItemResponse',
fields={
'id': IntegerField(),
'name': CharField(),
'category': CharField(),
'image': CharField(),
'image_thumbnail': CharField(),
},
),
},
)
},
)
@api_view(['GET'])
def search(request):
"""
Searches for ingredients.
This format is currently used by the ingredient search autocompleter
"""
term = request.GET.get('term', None)
language_codes = request.GET.get('language', ENGLISH_SHORT_NAME)
results = []
response = {}

if not term:
return Response(response)

query = Ingredient.objects.all()

# Filter the appropriate languages
languages = [load_language(l) for l in language_codes.split(',')]
if language_codes != SEARCH_ALL_LANGUAGES:
query = query.filter(
language__in=languages,
)

query = query.only('name')

# Postgres uses a full-text search
if is_postgres_db():
query = (
query.annotate(similarity=TrigramSimilarity('name', term))
.filter(similarity__gt=0.15)
.order_by('-similarity', 'name')
)
else:
query = query.filter(name__icontains=term)

for ingredient in query[:150]:
if hasattr(ingredient, 'image'):
image_obj = ingredient.image
image = image_obj.image.url
t = get_thumbnailer(image_obj.image)
thumbnail = t.get_thumbnail(aliases.get('micro_cropped')).url
else:
ingredient.get_image(request)
image = None
thumbnail = None

ingredient_json = {
'value': ingredient.name,
'data': {
'id': ingredient.id,
'name': ingredient.name,
'image': image,
'image_thumbnail': thumbnail,
},
}
results.append(ingredient_json)
response['suggestions'] = results

return Response(response)


class ImageViewSet(viewsets.ReadOnlyModelViewSet):
"""
API endpoint for ingredient images
Expand Down
43 changes: 0 additions & 43 deletions wger/nutrition/tests/test_ingredient.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,49 +239,6 @@ def test_ingredient_detail_logged_out(self):
self.ingredient_detail(editor=False)


class IngredientSearchTestCase(WgerTestCase):
"""
Tests the ingredient search functions
"""

def search_ingredient(self, fail=True):
"""
Helper function
"""

kwargs = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
response = self.client.get(reverse('ingredient-search'), {'term': 'test'}, **kwargs)
self.assertEqual(response.status_code, 200)
result = json.loads(response.content.decode('utf8'))
self.assertEqual(len(result['suggestions']), 2)
self.assertEqual(result['suggestions'][0]['value'], 'Ingredient, test, 2, organic, raw')
self.assertEqual(result['suggestions'][0]['data']['id'], 2)
suggestion_0_name = 'Ingredient, test, 2, organic, raw'
self.assertEqual(result['suggestions'][0]['data']['name'], suggestion_0_name)
self.assertEqual(result['suggestions'][0]['data']['image'], None)
self.assertEqual(result['suggestions'][0]['data']['image_thumbnail'], None)
self.assertEqual(result['suggestions'][1]['value'], 'Test ingredient 1')
self.assertEqual(result['suggestions'][1]['data']['id'], 1)
self.assertEqual(result['suggestions'][1]['data']['name'], 'Test ingredient 1')
self.assertEqual(result['suggestions'][1]['data']['image'], None)
self.assertEqual(result['suggestions'][1]['data']['image_thumbnail'], None)

def test_search_ingredient_anonymous(self):
"""
Test searching for an ingredient by an anonymous user
"""

self.search_ingredient()

def test_search_ingredient_logged_in(self):
"""
Test searching for an ingredient by a logged-in user
"""

self.user_login('test')
self.search_ingredient()


class IngredientValuesTestCase(WgerTestCase):
"""
Tests the nutritional value calculator for an ingredient
Expand Down
Loading

0 comments on commit e138bee

Please sign in to comment.