recipes_api/docs/05_image_api.md

450 lines
10 KiB
Markdown
Raw Permalink Normal View History

# Images API
- Manejar archivos estaticos/media
- Agregar dependencias para manejar imagenes
- Actualización del modelo receta con el campo imagen
- Agregar endpoint para subir imagenes para recetas
### Endpoint
- `/api/recipes/<id>/upload-image/`
| Método HTTP | Función |
| - | - |
| `POST` | Subir imagen |
### Dependencias adicionales
- Pillow (Python Imaging Library)
- zlib1g, zlib1g-dev
- libjpeg-dev
2023-10-12 14:14:02 -03:00
[Dockerfile](../Dockerfile)
```diff
...
RUN python -m venv /py && \
apt update && \
- apt install -y postgresql-client python3-dev libpq-dev gcc python3-psycopg2 && \
+ apt install -y postgresql-client libjpeg-dev python3-dev libpq-dev \
+ gcc python3-psycopg2 zlib1g zlib1g-dev && \
apt clean && \
/py/bin/pip install --upgrade pip && \
/py/bin/pip install -r /tmp/requirements.txt && \
...
```
2023-10-12 14:14:02 -03:00
[`requirements.txt`](../requirements.txt)
```txt
Django==4.2.5
djangorestframework==3.14.0
psycopg2>=2.9.9
drf-spectacular>=0.16
Pillow>=10
```
`docker compose build`
### Media and Static
Imagenes, CSS, JS, Iconos, etc.
- Media: Cargadas en tiempo de ejecución (ej. user uploads)
- Static: Generadas al construir la app. (on build)
## Configuración archivos estaticos
- `STATIC_URL` Base static URL ej. `/static/static/`
- `MEDIA_URL` Base media URL ej. `/static/media/`
- `MEDIA_ROOT` Path to media files on filesystem ej. `/vol/web/media`
- `STATC_ROOT` Path to static files on filesystem ej. `/vol/web/static`
### Docker volumes
Son los volumenes que almacenan los datos persistentes
- `/vol/web` Almanecenamiento de subdirectorios `static` y `media`
## Mapping
### Development
Al desarrollar se utiliza el servidor de desarrollo de Django.
<style>div.mermaid{text-align: center;}</style>
```mermaid
%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'basis'}}}%%
flowchart
subgraph "Django Development"
direction RL
subgraph "HTTP"
direction LR
GT1["<b>GET</b> <code>/static/static/admin/style.css</code>"]
GT2["<b>GET</b> <code>/static/static/restframework/icon.ico</code>"]
GT3["<b>GET</b> <code>/static/media/recipe/123.jpg</code>"]
end
subgraph "Django"
DJ{"Django Dev Server"}
end
subgraph "Files"
direction LR
FL1(" <code><b>[admin]</b>/static/style.css</code>")
FL2(" <code><b>[restframework]</b>/static/icon.ico</code>")
FL3(" <code>/vol/static/media/123.jpg</code>")
end
Django --> GT1
Django --> GT3
Django --> GT2
FL1 --- Django
FL2 --- Django
FL3 --- Django
end
```
### Deployment
```mermaid
%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'basis'}}}%%
flowchart
subgraph "Nginx Development"
direction RL
subgraph "HTTP"
direction LR
GT1["<b>GET</b> <code>/static/static/admin/style.css</code>"]
GT2["<b>GET</b> <code>/static/static/restframework/icon.ico</code>"]
GT3["<b>GET</b> <code>/static/media/recipe/123.jpg</code>"]
end
subgraph "Nginx"
NG{"Reverse Proxy Server"}
end
subgraph "Files"
direction LR
FL1(" <code><b>[admin]</b>/static/style.css</code>")
FL2(" <code><b>[restframework]</b>/static/icon.ico</code>")
FL3(" <code>/vol/static/media/123.jpg</code>")
end
Nginx --> GT1
Nginx --> GT3
Nginx --> GT2
FL1 --- Nginx
FL2 --- Nginx
FL3 --- Nginx
end
```
- ej. `GET` `/static/media/file.jpeg` <- `/vol/web/media/file.jpeg`
- ej. `GET` `/static/static/admin/style.css` <- `/vol/web/static/admin/style.css`
### Collect Static
Comando de django que recolecta los archivos estaticos
`python manage.py collectstatic` coloca todos los archivos estaticos en el
directorio `STATIC_ROOT` especificado en `settings.py`
## Configuracion para archivos estaticos
Subdirectorio para manejar archivos
2023-10-12 14:14:02 -03:00
[`Dockerfile`](../Dockerfile)
```diff
...
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
...
```
2023-10-12 14:14:02 -03:00
[`docker-compose`](../docker-compose.yml)
```diff
...
services:
app:
...
volumes:
- ./app:/app
+ - dev-static-data:/vol/
...
volumes:
dev-db-data:
+ dev-static-data:
EOF
```
### Actualizar `setttings.py`
2023-10-12 14:14:02 -03:00
[`settings.py`](../app/app/settings.py)
```diff
- STATIC_URL = 'static/'
+ STATIC_URL = '/static/static/'
+ MEDIA_URL = '/static/media/'
+
+ MEDIA_ROOT = '/vol/web/media'
+ STATIC_ROOT = '/vol/web/static'
```
### Actualizar `app/urls.py`
2023-10-12 14:14:02 -03:00
[`app/urls.py`](../app/app/urls.py)
```diff
...
+ from django.conf.urls.static import static
+ from django.conf import settings
...
+ if settings.DEBUG:
+ urlpatterns += static(
+ settings.MEDIA_URL,
+ document_root=settings.MEDIA_ROOT,
+ )
EOF
```
## Test agregar campo imagen en el modelo de receta
2023-10-12 14:14:02 -03:00
[`test_models.py`](../app/core/tests/test_models.py)
```py
from unittest.mock import patch
...
...
@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')
```
## Implementación imagen en el modelo
2023-10-12 14:14:02 -03:00
[`models.py`](../app/core/models.py)
```py
import uuid
import os
...
def recipe_image_file_path(instance, filename):
"""Generate 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 Recipe(models.Model):
...
image = models.ImageField(null=True, upload_to=recipe_image_file_path)
def __str__(self):
...
```
### Crear migraciones
Crear migraciones para el nuevo campo en el modelo receta
`docker compose run --rm app sh -c python manage.py makemigrations`
```sh
[+] Creating 1/0
✔ Container recipes_api_django-db-1 Running 0.0s
Migrations for 'core':
core/migrations/0005_recipe_image.py
- Add field image to recipe
```
## Implementación funcionalidad de la API para subir imagenes
### Test cargar/subir imagen
2023-10-12 14:14:02 -03:00
[`test_recipe_api.py`](../app/recipe/tests/test_recipe_api.py)
```py
import tempfile
import os
from PIL import Image
...
def image_upload_url(recipe_id):
"""Create and return an image upload URL."""
return reverse('recipe:recipe-detail', args=[recipe_id])
...
class ImageUploadTest(TestCase):
"""Tests for the image upload API."""
def setUp(self):
self.client = APIClient()
self.user = get_user_model().objects.create_user(
'user@example.com',
'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):
"""Test uploading invalid image."""
url = image_upload_url(self.recipe.id)
payload = {'image': 'notanimage'}
res = self.client.post(url, payload, format='multipart')
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
```
## Implementación subir imagen
Implementación de la funcionalidad para subir imagenes a travez de
*recipe endpoint*
### Serializador imagen receta
2023-10-12 14:14:02 -03:00
[`serializers.py`](../app/recipe/serializers.py)
```py
class RecipeImageSerializer(serializers.ModelSerializer):
"""Serializer for uploading images to recipes."""
class Meta:
model = Recipe
fields = ['id', 'image']
read_only_fields = ['id']
extra_kwargs = {'image': {'required': 'True'}}
```
### Vista imagen receta
2023-10-12 14:14:02 -03:00
[`recipe/views.py`](../app/recipe/views.py)
```diff
from rest_framework import (
viewsets,
mixins,
+ status,
)
+ from rest_framework.decorators import action
+ from rest_framework.response import Response
...
class RecipeViewSet(viewsets.ModelViewSet):
...
def get_serializer_class(self):
"""Return the serializer class for request."""
if self.action == 'list':
return serializers.RecipeSerializer
+ elif self.action == 'upload_image':
+ return serializers.RecipeImageSerializer
```
```py
@action(methods=['POST'], detail=True, url_path='upload-image')
def upload_image(self, request, pk=None):
"""Upload an image to recipe."""
recipe = self.get_object()
serializer = self.get_serializer(recipe, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
```
Para subir imagenes a travez de la interfaz web establecer la sgte.
2023-10-12 14:14:02 -03:00
configuración en [`settings.py`](../app/app/settings.py)
```py
SPECTACULAR_SETTINGS = {
'COMPONENT_SPLIT_REQUEST': True,
}
```
### Incluir imagen en detalle receta
2023-10-12 14:14:02 -03:00
[`serializers.py`](../app/recipe/serializers.py)
```py
class RecipeDetailSerializer(RecipeSerializer):
"""Serializer for recipe detail view."""
class Meta(RecipeSerializer.Meta):
fields = RecipeSerializer.Meta.fields + ['description', 'image']
```
## Prueba en el navegador
Levantar aplicación `docker compose up` y visitar `locahost:8000/api/docs`
### Swagger
![img](./imgs_readme/api_swagger_04.png)
### Django Admin
![img](./imgs_readme/django_admin_16.png)
----
2023-10-12 14:14:02 -03:00
- [Inicio](../README.md)
- [User API](./01_user_api.md)
- [Recipe API](./02_recipe_api.md)
- [Tag API](./03_tag_api.md)
- [Ingredient API](./04_ingredient_api.md)
- [**Image API**](./05_image_api.md)
- [Filters](./06_filters.md)