10 KiB
10 KiB
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
...
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 && \
...
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 subdirectoriosstatic
ymedia
Mapping
Development
Al desarrollar se utiliza el servidor de desarrollo de Django.
%%{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
%%{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
...
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
...
...
services:
app:
...
volumes:
- ./app:/app
+ - dev-static-data:/vol/
...
volumes:
dev-db-data:
+ dev-static-data:
EOF
Actualizar setttings.py
- 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
...
+ 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
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
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
[+] 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
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
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
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
@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
SPECTACULAR_SETTINGS = {
'COMPONENT_SPLIT_REQUEST': True,
}
Incluir imagen en detalle receta
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