# 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//upload-image/` | Método HTTP | Función | | - | - | | `POST` | Subir imagen | ### Dependencias adicionales - Pillow (Python Imaging Library) - zlib1g, zlib1g-dev - libjpeg-dev [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 && \ ... ``` [`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. ```mermaid %%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'basis'}}}%% flowchart subgraph "Django Development" direction RL subgraph "HTTP" direction LR GT1["GET /static/static/admin/style.css"] GT2["GET /static/static/restframework/icon.ico"] GT3["GET /static/media/recipe/123.jpg"] end subgraph "Django" DJ{"Django Dev Server"} end subgraph "Files" direction LR FL1(" [admin]/static/style.css") FL2(" [restframework]/static/icon.ico") FL3(" /vol/static/media/123.jpg") 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["GET /static/static/admin/style.css"] GT2["GET /static/static/restframework/icon.ico"] GT3["GET /static/media/recipe/123.jpg"] end subgraph "Nginx" NG{"Reverse Proxy Server"} end subgraph "Files" direction LR FL1(" [admin]/static/style.css") FL2(" [restframework]/static/icon.ico") FL3(" /vol/static/media/123.jpg") 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 [`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 ... ``` [`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` [`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` [`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 [`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 [`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 [`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 [`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 [`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. configuración en [`settings.py`](../app/app/settings.py) ```py SPECTACULAR_SETTINGS = { 'COMPONENT_SPLIT_REQUEST': True, } ``` ### Incluir imagen en detalle receta [`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) ---- - [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)