[Django] 24 - DRF: Add Image API
除了教程中提到的,还要考虑S3上的问题。
Docker volumes
- Store persistent data.
- Volume we'll setup:
- /vol/web - store static and media subdirectories
GET /static/media/file.jpeg <- /vol/web/media/file.jpeg GET /static/static/admin/style.css <-- /vol/web/static/admin/style.css
-
Dockerfile
In the Dockerfile
mkdir -p /vol/web/media mkdir -p /vol/web/static chown -R django-user:django-user /vol chmod -R 755 /vol
-
docker-compose.yml
volumes: - ./app:/app - dev-static-data:/vol/web volumes: dev-db-data: dev-static-data:
-
settings.py
STATIC_URL = '/static/static/' MEDIA_URL = '/static/static/' STATIC_ROOT = '/vol/web/media' STATIC_ROOT = '/vol/web/static'
-
urls.py
from django.conf.urls.static import static from django.conf import settings if setting.DEBUG: urlpatterns += static( settings.MEDIA_URL, document_root=settings.MEDIA_ROOT, )
Collect Static
Puts all static files into STATIC_ROOT.
python manage.py collectstatic
Model
-
Test Case
from unittest.mock import patch
# ---------------------------------------------------------------------
@patch('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')
-
Model
上传文件,文件名再自定义 by uuid.
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): """Recipe object.""" ... ... image = models.ImageField(null=True, upload_to=recipe_image_file_path) def __str__(self): return self.title
View
-
Test Case
test_recipe_api.py
import tempfile import os from PIL import Image
Test methods.
class ImageUploadTests(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 an 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)
-
Serializers
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'}}
-
View
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 return self.serializer_class
@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)
Settings
SPECTACULAR_SETTINGS = { 'COMPONENT_SPLIT_REQUEST': True, }
End.