Skip to content

Commit

Permalink
Added filtering.
Browse files Browse the repository at this point in the history
  • Loading branch information
EwokJedi committed Oct 31, 2022
1 parent 51c9662 commit 82afad9
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 20 deletions.
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ RUN python -m venv /py && \
--no-create-home \
django-user && \
mkdir -p /vol/web/media && \
mkdir -p /vol/web/static && \
chown -R django-user:django-user /vol && \
chmod -R 755 /vol
mkdir -p /vol/web/static
# chown -R django-user:django-user /vol && \
# chmod -R 755 /vol

ENV PATH="/py/bin:$PATH"

Expand Down
10 changes: 5 additions & 5 deletions app/core/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Database models
Database models.
"""
import uuid
import os
Expand All @@ -15,8 +15,8 @@

def recipe_image_file_path(instance, filename):
"""Generate file path for new recipe image."""
ext = os.path.splittext(filename)[1]
filename = f'{uuid.uuid64}{ext}'
ext = os.path.splitext(filename)[1]
filename = f'{uuid.uuid4()}{ext}'

return os.path.join('uploads', 'recipe', filename)

Expand All @@ -39,7 +39,7 @@ def create_superuser(self, email, password):
user = self.create_user(email, password)
user.is_staff = True
user.is_superuser = True
user.save(using=self.db)
user.save(using=self._db)

return user

Expand Down Expand Up @@ -75,7 +75,7 @@ def __str__(self):


class Tag(models.Model):
"""Tag for filtering recipes"""
"""Tag for filtering recipes."""
name = models.CharField(max_length=255)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
Expand Down
2 changes: 1 addition & 1 deletion app/core/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def test_create_ingredient(self):

self.assertEqual(str(ingredient), ingredient.name)

@patch('core.models.uuid.uuid64')
@patch('core.models.uuid.uuid4')
def test_recipe_file_name_uuid(self, mock_uuid):
"""Test generating image path."""
uuid = 'test-uuid'
Expand Down
48 changes: 47 additions & 1 deletion app/recipe/tests/test_ingredients_api.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
"""
Tests for the ingredients API.
"""
from decimal import Decimal

from django.contrib.auth import get_user_model
from django.urls import reverse
from django.test import TestCase

from rest_framework import status
from rest_framework.test import APIClient

from core.models import Ingredient
from core.models import (
Ingredient,
Recipe,
)

from recipe.serializers import IngredientSerializer

Expand Down Expand Up @@ -94,3 +99,44 @@ def test_delete_ingredient(self):
ingredients = Ingredient.objects.filter(user=self.user)
self.assertFalse(ingredients.exists())

def test_filter_ingredients_assigned_to_recipes(self):
"""Test listing ingredients by those assigned to recipes."""
in1 = Ingredient.objects.create(user=self.user, name='Apples')
in2 = Ingredient.objects.create(user=self.user, name='Turkey')
recipe = Recipe.objects.create(
title='Apple Crumble',
time_minutes=5,
price=Decimal('4.50'),
user=self.user
)
recipe.ingredients.add(in1)

res = self.client.get(INGREDIENTS_URL, {'assigned_only':1})

s1 = IngredientSerializer(in1)
s2 = IngredientSerializer(in2)
self.assertIn(s1.data, res.data)
self.assertNotIn(s2.data, res.data)

def test_filtered_ingredients_unique(self):
"""Test filtered ingredients returns a unique list."""
ing = Ingredient.objects.create(user=self.user, name='Eggs')
Ingredient.objects.create(user=self.user, name='Lentils')
recipe1 = Recipe.objects.create(
title='Eggs Benedict',
time_minutes=60,
price=Decimal('7.00'),
user=self.user,
)
recipe2 = Recipe.objects.create(
title='Herb Eggs',
time_minutes=20,
price=Decimal('4.00'),
user=self.user,
)
recipe1.ingredients.add(ing)
recipe2.ingredients.add(ing)

res = self.client.get(INGREDIENTS_URL, {'assigned_only': 1})

self.assertEqual(len(res.data), 1)
51 changes: 46 additions & 5 deletions app/recipe/tests/test_recipe_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def image_upload_url(recipe_id):
return reverse('recipe:recipe-upload-image', args=[recipe_id])

def create_recipe(user, **params):
"""Create and return a smaple recipe"""
"""Create and return a sample recipe"""
default = {
'title': 'Sample recipe title',
'time_minutes': 22,
Expand Down Expand Up @@ -114,7 +114,7 @@ def test_get_recipe_detail(self):
self.assertEqual(res.data, serializer.data)

def test_create_recipe(self):
"""Test create a recipe"""
"""Test creating a recipe"""
payload = {
'title': 'Sample recipe',
'time_minutes': 30,
Expand Down Expand Up @@ -301,7 +301,7 @@ def create_recipe_with_new_ingredient(self):
}
res = self.client.post(RECIPES_URL, payload, format='json')

self.assertEqual(res.status_code, status=HTTP_201_CREATED)
self.assertEqual(res.status_code, status.HTTP_201_CREATED)
recipes = Recipe.objects.filter(user=self.user)
self.assertEqual(recipes.count(), 1)
recipe = recipes[0]
Expand Down Expand Up @@ -376,6 +376,47 @@ def test_clear_recipe_ingredients(self):
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(recipe.ingredients.count(), 0)

def test_filter_by_tags(self):
"""Filterting recipes by tags."""
r1 = create_recipe(user=self.user, title='Thai Veg Curry')
r2 = create_recipe(user=self.user, title='Tahini')
tag1 = Tag.objects.create(user=self.user, name='Vegan')
tag2 = Tag.objects.create(user=self.user, name='Vegetarian')
r1.tags.add(tag1)
r2.tags.add(tag2)
r3 = create_recipe(user=self.user, title='Fish and chips')

params = {'tags': f'{tag1.id},{tag2.id}'}
res = self.client.get(RECIPES_URL, params)

s1 = RecipeSerializer(r1)
s2 = RecipeSerializer(r2)
s3 = RecipeSerializer(r3)
self.assertIn(s1.data, res.data)
self.assertIn(s2.data, res.data)
self.assertNotIn(s3.data, res.data)

def test_filter_by_ingredients(self):
"""Test filtering recipes by ingredients."""
r1 = create_recipe(user=self.user, title='Posh Beans on Toast')
r2 = create_recipe(user=self.user, title='Chicken Cacciatore')
in1 = Ingredient.objects.create(user=self.user, name='Feta Cheese')
in2 = Ingredient.objects.create(user=self.user, name='Chicken')
r1.ingredients.add(in1)
r2.ingredients.add(in2)
r3 = create_recipe(user=self.user, title='Red Lentil Daal')

params = {'ingredients': f'{in1.id},{in2.id}'}
res = self.client.get(RECIPES_URL, params)

s1 = RecipeSerializer(r1)
s2 = RecipeSerializer(r2)
s3 = RecipeSerializer(r3)
self.assertIn(s1.data, res.data)
self.assertIn(s2.data, res.data)
self.assertNotIn(s3.data, res.data)


class ImageUploadTests(TestCase):
"""Tests for the image upload API."""

Expand All @@ -391,7 +432,7 @@ def setUp(self):
def tearDown(self):
self.recipe.image.delete()

def test_upload_imag(self):
def test_upload_image(self):
"""Test uploading image to a recipe."""
url = image_upload_url(self.recipe.id)
with tempfile.NamedTemporaryFile(suffix='.jpg') as image_file:
Expand All @@ -407,7 +448,7 @@ def test_upload_imag(self):
self.assertTrue(os.path.exists(self.recipe.image.path))

def test_upload_image_bad_request(self):
"""Test uploading invalid image."""
"""Test uploading an invalid image."""
url = image_upload_url(self.recipe.id)
payload = {'image': 'notanimage'}
res = self.client.post(url, payload, format='multipart')
Expand Down
48 changes: 47 additions & 1 deletion app/recipe/tests/test_tags_api.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
"""
Tests for the tags API.
"""
from decimal import Decimal

from django.contrib.auth import get_user_model
from django.urls import reverse
from django.test import TestCase

from rest_framework import status
from rest_framework.test import APIClient

from core.models import Tag
from core.models import (
Tag,
Recipe,
)

from recipe.serializers import TagSerializer

Expand Down Expand Up @@ -93,3 +98,44 @@ def test_delete_tag(self):
self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT)
tags = Tag.objects.filter(user=self.user)
self.assertFalse(tags.exists())

def test_filter_tags_assigned_to_recipes(self):
"""Test listing tags to those assigned to recipes."""
tag1 = Tag.objects.create(user=self.user, name='Breakfast')
tag2 = Tag.objects.create(user=self.user, name='Lunch')
recipe = Recipe.objects.create(
title='Green Eggs on Toast',
time_minutes=10,
price=Decimal('2.50'),
user=self.user,
)
recipe.tags.add(tag1)

res = self.client.get(TAGS_URL, {'assigned_only': 1})

s1 = TagSerializer(tag1)
s2 = TagSerializer(tag2)
self.assertIn(s1.data, res.data)
self.assertNotIn(s2.data, res.data)

def test_filtered_tags_unique(self):
tag = Tag.objects.create(user=self.user, name='Breakfast')
Tag.objects.create(user=self.user, name='Dinner')
recipe1 = Recipe.objects.create(
title='Pancakes',
time_minutes=5,
price=Decimal('5.00'),
user=self.user,
)
recipe2 = Recipe.objects.create(
title='Porridge',
time_minutes=3,
price=Decimal('2.00'),
user=self.user,
)
recipe1.tags.add(tag)
recipe2.tags.add(tag)

res = self.client.get(TAGS_URL, {'assigned_only': 1})

self.assertEqual(len(res.data), 1)
66 changes: 62 additions & 4 deletions app/recipe/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
"""
Views for the recipe APIs.
"""
from this import d
from drf_spectacular.utils import (
extend_schema_view,
extend_schema,
OpenApiParameter,
OpenApiTypes,
)
from rest_framework import (
viewsets,
mixins,
Expand All @@ -19,17 +26,49 @@
from recipe import serializers



@extend_schema_view(
list=extend_schema(
parameters=[
OpenApiParameter(
'tags',
OpenApiTypes.STR,
description='Comma separated list of tag IDs to filter',
),
OpenApiParameter(
'ingredients',
OpenApiTypes.STR,
description='Comma separated list of ingredient IDs to filter',
)
]
)
)
class RecipeViewSet(viewsets.ModelViewSet):
"""View for manage recipe APIs."""
serializer_class = serializers.RecipeDetailSerializer
queryset = Recipe.objects.all()
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]

def _params_to_ints(self, qs):
"""Convert a list of strings to integers."""
return[int(str_id) for str_id in qs.split(',')]


def get_queryset(self):
"""Retrieve recipes for authenticated user."""
return self.queryset.filter(user=self.request.user).order_by('-id')
tags = self.request.query_params.get('tags')
ingredients = self.request.query_params.get('ingredients')
queryset = self.queryset
if tags:
tag_ids = self._params_to_ints(tags)
queryset = queryset.filter(tags__id__in=tag_ids)
if ingredients:
ingredient_ids = self._params_to_ints(ingredients)
queryset = queryset.filter(ingredients__id__in=ingredient_ids)

return queryset.filter(
user=self.request.user
).order_by('-id').distinct()

def get_serializer_class(self):
"""Return serializer class for request."""
Expand All @@ -55,7 +94,17 @@ def upload_image(self, request, pk=None):
return Response(serializer.data, status=status.HTTP_200_OK)

return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

@extend_schema_view(
list=extend_schema(
parameters=[
OpenApiParameter(
'assigned_only',
OpenApiTypes.INT, enum=[0, 1],
description='Filter by items assigned to recipes.',
)
]
)
)
class BaseRecipeAttrViewSet(
mixins.DestroyModelMixin,
mixins.UpdateModelMixin,
Expand All @@ -67,7 +116,16 @@ class BaseRecipeAttrViewSet(
permission_classes = [IsAuthenticated]
def get_queryset(self):
"""Filter queryset to authenticated user."""
return self.queryset.filter(user=self.request.user).order_by('-name')
assigned_only = bool(
int(self.request.query_params.get('assigned_only', 0))
)
queryset = self.queryset
if assigned_only:
queryset = queryset.filter(recipe__isnull=False)

return queryset.filter(
user=self.request.user
).order_by('-name').distinct()

class TagViewSet(BaseRecipeAttrViewSet):
"""Manage tags in the database."""
Expand Down

0 comments on commit 82afad9

Please sign in to comment.