Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace ingredient search function #1724

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 75 additions & 1 deletion 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 @@ -40,7 +114,7 @@ class Meta:
'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
Loading