Skip to content

Commit

Permalink
added filtering to recipes
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewKassab committed Sep 11, 2024
1 parent b28c852 commit 4bce68e
Show file tree
Hide file tree
Showing 14 changed files with 306 additions and 24 deletions.
10 changes: 7 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ EXPOSE 8000
ARG DEV=false
RUN python -m venv /py && \
/py/bin/pip install --upgrade pip && \
apk add --update --no-cache postgresql-client && \
apk add --update --no-cache postgresql-client jpeg-dev && \
apk add --update --no-cache --virtual .tmp-build-deps \
build-base postgresql-dev musl-dev && \
build-base postgresql-dev musl-dev zlib zlib-dev && \
/py/bin/pip install -r /tmp/requirements.txt && \
if [ $DEV = "true" ]; \
then /py/bin/pip install -r /tmp/requirements.dev.txt ; \
Expand All @@ -24,7 +24,11 @@ RUN python -m venv /py && \
adduser \
--disabled-password \
--no-create-home \
django-user
django-user && \
mkdir -p /vol/web/media && \
mkdir -p /vol/web/static && \
chown -R django-user:django-user /vol && \
chmod -R 755 /vol

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

Expand Down
15 changes: 8 additions & 7 deletions app/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,11 @@

USE_TZ = True

STATIC_URL = '/static/static/'
MEDIA_URL = '/static/media/'

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/

STATIC_URL = '/static/'

# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
MEDIA_ROOT = '/vol/web/media'
STATIC_ROOT = '/vol/web/static'

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

Expand All @@ -139,3 +136,7 @@
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}

SPECTACULAR_SETTINGS = {
'COMPONENT_SPLIT_REQUEST': True,
}
12 changes: 12 additions & 0 deletions app/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
)
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
from django.conf import settings

urlpatterns = [
path('admin/', admin.site.urls),
Expand All @@ -31,3 +33,13 @@
path('api/user/', include('user.urls')),
path('api/recipe/', include('recipe.urls'))
]

if settings.DEBUG:
urlpatterns += static(
settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT
)
urlpatterns += static(
settings.STATIC_URL,
document_root=settings.STATIC_ROOT
)
19 changes: 19 additions & 0 deletions app/core/migrations/0007_recipe_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 3.2.25 on 2024-09-06 00:30

import core.models
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('core', '0006_ingredient_name'),
]

operations = [
migrations.AddField(
model_name='recipe',
name='image',
field=models.ImageField(null=True, upload_to=core.models.recipe_image_file_path),
),
]
12 changes: 12 additions & 0 deletions app/core/models.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
"""
Database models.
"""
import uuid
import os

from django.conf import settings
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin


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

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


class UserManager(BaseUserManager):
"""Manager for users."""

Expand Down Expand Up @@ -53,6 +64,7 @@ class Recipe(models.Model):
link = models.CharField(max_length=255, blank=True)
tags = models.ManyToManyField('Tag')
ingredients = models.ManyToManyField('Ingredient')
image = models.ImageField(null=True, upload_to=recipe_image_file_path)

def __str__(self):
return self.title
Expand Down
9 changes: 9 additions & 0 deletions app/core/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Tests for models
"""
from unittest.mock import patch
from decimal import Decimal

from django.test import TestCase
Expand Down Expand Up @@ -82,4 +83,12 @@ def test_create_ingredient(self):

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

@patch('core.models.uuid.uuid4')
def test_recipe_file_name_uuid(self, mock_uuid):
"""Test generating image path."""
uuid = 'test-uuid'
mock_uuid.return_value = uuid
file_path = models.recipe_image_file_path(None, 'example.jpg')

self.assertEqual(file_path, f'uploads/recipe/{uuid}.jpg')

23 changes: 16 additions & 7 deletions app/recipe/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,21 @@
from core.models import Recipe, Tag, Ingredient


class BaseIdNameSerializerMeta:
fields = ['id', 'name']
read_only_fields = ['id']


class TagSerializer(serializers.ModelSerializer):

class Meta:
class Meta(BaseIdNameSerializerMeta):
model = Tag
fields = ['id', 'name']
read_only_fields = ['id']


class IngredientSerializer(serializers.ModelSerializer):

class Meta:
class Meta(BaseIdNameSerializerMeta):
model = Ingredient
fields = ['id', 'name']
read_only_fields = ['id']


class RecipeSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -82,5 +83,13 @@ def update(self, instance, validated_data):
class RecipeDetailSerializer(RecipeSerializer):

class Meta(RecipeSerializer.Meta):
fields = RecipeSerializer.Meta.fields + ['description']
fields = RecipeSerializer.Meta.fields + ['description', 'image']


class RecipeImageSerializer(serializers.ModelSerializer):

class Meta:
model = Recipe
fields = ['id', 'image']
read_only_fields = ['id']
extra_kwargs = {'image': {'required': 'True'}}
58 changes: 55 additions & 3 deletions app/recipe/tests/base_test_name_user_model_api.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
from abc import ABC

from decimal import Decimal

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

from rest_framework import status
from rest_framework.test import APIClient

from core.models import Recipe, Ingredient, Tag


RELATED_FIELD_MAP = {
Ingredient: 'ingredients',
Tag: 'tags',
}


def create_user(email='[email protected]', password='testpass123'):
return get_user_model().objects.create(email=email, password=password)
Expand Down Expand Up @@ -54,14 +64,14 @@ def test_retrieve_model(self):
def test_model_limited_to_user(self):
user2 = create_user(email='[email protected]')
self.model.objects.create(user=user2, name="Meat")
tag = self.model.objects.create(user=self.user, name="Fruity")
item = self.model.objects.create(user=self.user, name="Fruity")

res = self.client.get(self.list_url)

self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(len(res.data), 1)
self.assertEqual(res.data[0]['name'], tag.name)
self.assertEqual(res.data[0]['id'], tag.id)
self.assertEqual(res.data[0]['name'], item.name)
self.assertEqual(res.data[0]['id'], item.id)

def test_update_model(self):
model= self.model.objects.create(user=self.user, name="Name")
Expand All @@ -85,3 +95,45 @@ def test_delete_model(self):
self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT)
models = self.model.objects.filter(user=self.user)
self.assertFalse(models.exists())

def test_filter_model_assigned_to_recipes(self):
obj1 = self.model.objects.create(user=self.user, name="Object 1")
obj2 = self.model.objects.create(user=self.user, name="Object 2")
recipe = Recipe.objects.create(
title='Apple Crumble',
time_minutes=5,
price=Decimal('4.50'),
user=self.user,
)
getattr(recipe, RELATED_FIELD_MAP.get(self.model)).add(obj1)

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

s1 = self.serializer(obj1)
s2 = self.serializer(obj2)
self.assertIn(s1.data, res.data)
self.assertNotIn(s2.data, res.data)

def test_filtered_objects_unique(self):
obj = self.model.objects.create(user=self.user, name="Object")
self.model.objects.create(user=self.user, name="Different Object")
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
)

getattr(recipe1, RELATED_FIELD_MAP.get(self.model)).add(obj)
getattr(recipe2, RELATED_FIELD_MAP.get(self.model)).add(obj)

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

self.assertEqual(len(res.data), 1)

File renamed without changes.
89 changes: 89 additions & 0 deletions app/recipe/tests/test_recipe_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
Tests for recipe APIs.
"""
from decimal import Decimal
import tempfile
import os

from PIL import Image

from django.contrib.auth import get_user_model
from django.test import TestCase
Expand Down Expand Up @@ -30,6 +34,11 @@ def detail_url(recipe_id):
return reverse('recipe:recipe-detail', args=[recipe_id])


def image_upload_url(recipe_id):
"""Create and return an image upload URL"""
return reverse('recipe:recipe-upload-image', args=[recipe_id])


def create_recipe(user, **params):
"""Create and return a same recipe"""
defaults = {
Expand Down Expand Up @@ -373,3 +382,83 @@ 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):
r1 = create_recipe(user=self.user, title = 'Thai vegetable curry')
r2 = create_recipe(user=self.user, title='Aubergine with Tahini')
tag1 = Tag.objects.create(user=self.user, name='Vegan')
tag2 = Tag.objects.create(user=self.user, name='Vegeterian')
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=params)

s1 = RecipeSerializer(r1)
s2 = RecipeSerializer(r2)
s3 = RecipeSerializer(r3)

self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertIn(s1.data, res.data)
self.assertIn(s2.data, res.data)
self.assertNotIn(s3.data, res.data)

def test_filter_by_ingredients(self):
r1 = create_recipe(user=self.user, title = 'Thai vegetable curry')
r2 = create_recipe(user=self.user, title='Aubergine with Tahini')
i1 = Ingredient.objects.create(user=self.user, name='Potato')
i2 = Ingredient.objects.create(user=self.user, name='Coconut Milk')
r1.ingredients.add(i1)
r2.ingredients.add(i2)
r3 = create_recipe(user=self.user, title='Fish and Chips')

params = {'ingredients': f'{i1.id},{i2.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."""

def setUp(self):
self.client = APIClient()
self.user = get_user_model().objects.create_user(
'[email protected]',
'password123',
)
self.client.force_authenticate(self.user)
self.recipe = create_recipe(user=self.user)

def tearDown(self):
self.recipe.image.delete()

def test_upload_image(self):
"""Test uploading an image to a recipe."""
url = image_upload_url(self.recipe.id)
with tempfile.NamedTemporaryFile(suffix='.jpg') as image_file:
img = Image.new('RGB', (10, 10))
img.save(image_file, format='JPEG')
image_file.seek(0)
payload = {'image': image_file}
res = self.client.post(url, payload, format='multipart')

self.recipe.refresh_from_db()
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertIn('image', res.data)
self.assertTrue(os.path.exists(self.recipe.image.path))

def test_upload_image_bad_request(self):
url = image_upload_url(self.recipe.id)
payload = {'image': 'bad'}

res = self.client.post(url, payload, format='multipart')

self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
File renamed without changes.
Loading

0 comments on commit 4bce68e

Please sign in to comment.