我现在已经构建了许多应用程序,它们使用内置的 fetch API 或 Axios 来处理将 JSON 数据发送到后端。对于 Django REST Framework (DRF) 等应用程序,这通常非常简单。 DRF 的序列化程序实际上为您完成了所有工作,将 Python 数据转换为 JSON 并返回。

我最近遇到的一个问题是第一次尝试将图像上传到我的一个应用程序时。我遇到了各种各样的错误,例如:

"The submitted data was not a file. Check the encoding type on the form." or "No file was submitted."

这就是我了解 JavaScript 的 FormData 的方式,当接收文件时,Django REST Framework 期望文件以这种格式通过,“Content-Type”标头设置为“multipart/form-data”并使用解析器正确处理这个数据。

因此,为了尽量减少未来我自己的挖掘工作,以及其他可能像我一样花费数天时间试图理解问题并弄清楚如何进行的人,以下是我如何在我的项目中上传图片:

姜戈

1.将媒体文件/设置媒体位置添加到settings.py

  1. 将媒体位置添加到 urls.py

  2. 在模型上创建 ImageField

  3. 将解析器添加到 Viewset

5.将ImageField添加到序列化器

反应

1.从表单接收状态数据

2.将数据转换为FormData

  1. 使用正确的标头创建 Axios 调用

4.接收任何错误以显示在表单上


Djangoside

1.将媒体文件/设置媒体位置添加到 settings.py

将 MEDIA_ROOT 和 MEDIA_URL 添加到 settings.py MEDIA_ROOT 是我们的文件实际存储的位置。 MEDIA_URL 是从前端通过 URL 访问它们的位置。

设置.py

import os

# Actual directory user files go to
MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), 'mediafiles')

# URL used to access the media
MEDIA_URL = '/media/'

进入全屏模式 退出全屏模式

2.将媒体位置添加到 urls.py

在我们的主项目文件夹中添加静态 URL 到 urls.py。这允许应用程序在收到来自 MEDIA_URL 的请求时知道要访问服务器端的哪个文件夹。没有这个,应用程序在收到 'mysite.com/media/' 的 urlpattern 时将不知道该怎么做

网址.py

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/users/', include('users.urls')),
    path('api/', include('commerce.urls')),

] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

进入全屏模式 退出全屏模式

3.在模型上创建 ImageField

接下来,我们在模型上创建字段“image_url”并将其设置为 ImageField()。 kwarg upload_to 设置为我们的同名函数。

模型.py


# lets us explicitly set upload path and filename
def upload_to(instance, filename):
    return 'images/{filename}'.format(filename=filename)

class MyModel(models.Model):
    creator = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name="listings")
    title = models.CharField(
        max_length=80, blank=False, null=False)
    description = models.TextField()
    image_url = models.ImageField(upload_to=upload_to, blank=True, null=True)

进入全屏模式 退出全屏模式

不要忘记任何时候我们更新我们需要运行的模型

python manage.py makemigrations

祖兹 100033

4.将解析器添加到 Viewset

解析器是 DRF 提供的工具,将自动用于解析出 FormData。没有这个我们会得到错误,因为数据不会被正确解码以供 DRF 的序列化程序读取。请参阅“解析器_classes”。

视图.py

from .models import MyModel
from .serializers import MyModelSerializer
from rest_framework import permissions
from rest_framework.parsers import MultiPartParser, FormParser

class MyModelViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.order_by('-creation_date')
    serializer_class = MyModelSerializer
    parser_classes = (MultiPartParser, FormParser)
    permission_classes = [
        permissions.IsAuthenticatedOrReadOnly]

    def perform_create(self, serializer):
        serializer.save(creator=self.request.user)

进入全屏模式 退出全屏模式

5.将 ImageField 添加到序列化程序

在我们的序列化程序上添加字段定义并将其设置为 serializers.ImageField()。由于我们的模型不需要图像_url,我们将添加 kwarg 'requiredu003dfalse' 以避免在接收没有图像的 FormData 时出现问题。

from rest_framework import serializers
from .models import MyModel

class MyModelSerializer(serializers.ModelSerializer):

    creator = serializers.ReadOnlyField(source='creator.username')
    creator_id = serializers.ReadOnlyField(source='creator.id')
    image_url = serializers.ImageField(required=False)

    class Meta:
        model = MyModel
        fields = ['id', 'creator', 'creator_id', 'title', 'description', 'image_url']

进入全屏模式 退出全屏模式

那应该为后端做!如果我没有忘记任何事情,我们现在应该能够通过 Postman 发送表单数据并接收成功提交的数据,或者任何错误/缺少必填字段。


Reactside

1.从表单接收状态数据

我假设您已经有一个表格和任何必要的 onChange 数据。我犯的主要错误是没有为表单上的文件输入编写单独的 handleImageChange 或 handleFileChange,因为常规文本输入与文件输入不同。

我们使用useState hook来创建[数据,setData]和errors,setErrors

在我的标题和描述输入字段中,您会看到我只使用了一个简单的onChange={(e)=>{handleChange(e)}}。 handleChange 接受 onChange 事件并适当地分配data[input.name] = input.value

但是,这不适用于文件,因为文件的处理方式不同。所以在我们的文件输入中,我们需要指定一些东西:

我们需要设置“文件”的_type_,以便它知道打开文件选择器对话框。我们告诉它只_接受_我们想要的文件格式,我们的 onChange 现在指向一个单独的函数来处理这些文件。

这个单独的函数几乎和以前一样工作,但我们分配的是 event(e).target.files[0],而不是分配 input.value,0 是提交的任何文件列表的第一个索引。我们在这里明确地只接收一个文件。

 <input type="file" 
    name="image_url"
    accept="image/jpeg,image/png,image/gif"
    onChange={(e) => {handleImageChange(e)}}>

进入全屏模式 退出全屏模式

CreateMyModelForm.js

import React, { useState } from "react";

// React-Bootstrap
import Form from "react-bootstrap/Form";
import Button from "react-bootstrap/Button";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import API from "../../API";

const CreateMyModel = () => {

    const [data, setData] = useState({
        title: "",
        description: "",
        image_url: "",
    });
    const [errors, setErrors] = useState({
        title: "",
        description: "",
        image_url: "",
    });


    const handleChange = ({ currentTarget: input }) => {
        let newData = { ...data };
        newData[input.name] = input.value;
        setData(newData);
    };

    const handleImageChange = (e) => {
        let newData = { ...data };
        newData["image_url"] = e.target.files[0];
        setData(newData);
    };

    const doSubmit = async (e) => {
        e.preventDefault();
        const response = await API.createMyModelEntry(data);
        if (response.status === 400) {
            setErrors(response.data);
        }
    };

    return (

        <Form>
            <Row>
                <Form.Group className="mb-3" controlId="titleInput">
                    <Form.Label>Title</Form.Label>
                    <Form.Control
                        type="text"
                        name="title"
                        value={data.title}
                        isInvalid={errors.title}
                        onChange={(e) => {
                            handleChange(e);
                        }}
                        maxLength={80}
                    />
                    {errors.title && (
                        <Form.Text className="alert-danger" tooltip>
                            {errors.title}
                        </Form.Text>
                    )}
                </Form.Group>
            </Row>
            <Row>
                <Form.Group controlId="formFile" className="mb-3">
                    <Form.Label>My Image</Form.Label>
                    <Form.Control
                        type="file"
                        name="image_url"
                        accept="image/jpeg,image/png,image/gif"
                        onChange={(e) => {
                            handleImageChange(e);
                        }}
                    />
                    {errors.image_url && (
                        <Form.Text className="alert-danger" tooltip>
                            {errors.image_url}
                        </Form.Text>
                    )}
                </Form.Group>
            </Row>
            <Form.Group className="mb-3" controlId="descriptionInput">
                <Form.Label>Description</Form.Label>
                <Form.Control
                    as="textarea"
                    rows={10}
                    name="description"
                    value={data.description}
                    isInvalid={errors.description}
                    onChange={(e) => {
                        handleChange(e);
                    }}
                />
                {errors.description && (
                    <Form.Text className="alert-danger" tooltip>
                        {errors.description}
                    </Form.Text>
                )}
            </Form.Group>
            <Button
                variant="primary"
                type="submit"
                onClick={(e) => doSubmit(e)}
            </Button>
        </Form>
    );
};

export default CreateMyModel;

进入全屏模式 退出全屏模式

2.将数据转换为 FormData

我已经学会了将我的 API 调用写在一个单独的文件中,以避免违反 DRY 原则并总体上保持更简洁的代码。在我的 API 调用中,我知道每当进行此特定调用时我都需要提交 FormData,因此我们将在此处处理 FormData 创建。

在最后一步中,您将看到我们的 doSubmit 将我们的数据发送到 API 调用。在 API 调用中,我们接收该数据并将上一步中的状态数据显式附加到 FormData,以便可以为我们的后端解析器正确格式化。

回想一下前面的图像是可选的。我们不能上传空图像数据,因为这会返回错误,所以我们只会将图像附加到表单数据中(如果有的话)。如果没有,我们将完全忽略它。

来自 API.js 的片段

...
createMyModelEntry: async (data) => {
    let form_data = new FormData();
    if (data.image_url)
        form_data.append("image_url", data.image_url, 
        data.image_url.name);
    form_data.append("title", data.title);
    form_data.append("description", data.description);
    form_data.append("category", data.category);

... 
};

进入全屏模式 退出全屏模式

3.使用正确的标头创建 Axios 调用

我正在使用 Axios 将 JWT 令牌发送到我的应用程序的后端,因此我的项目已经设置了一些默认设置。但是,我需要确保使用此特定 API 调用发送正确的内容类型。

我从我的 axios 设置文件中导入我的 axiosInstance 并在我的mymodels/端点创建一个 POST 请求,附加我的表单数据,并用"Content-Type": "multipart/form-data"覆盖我的默认"Content-Type": "application/json"以便我们可以发送这个文件,我们在 Django Rest Framework 中的解析器将识别它并且知道期望/接受一个文件。

我返回结果并检查状态。如果我们有一个成功的 POST,状态将是“201 CREATED”,我知道我可以从那里重定向。如果数据不被接受并且我在后端的序列化器返回了一个错误,这些将可以通过我的 catch 块中的 error.response 访问。

API.js

import axiosInstance from "./axios";

const apiSettings = {

createListing: async (data) => {
    let form_data = new FormData();
    if (data.image_url)
        form_data.append("image_url", data.image_url, data.image_url.name);
    form_data.append("title", data.title);
    form_data.append("description", data.description);
    form_data.append("category", data.category);
    form_data.append("start_bid", data.start_bid);
    form_data.append("is_active", true);

const myNewModel = await axiosInstance
        .post(`mymodels/`, form_data, {
            headers: {
                "Content-Type": "multipart/form-data",
            },
        }).then((res) => {
            return res;
        }).catch((error) => {
            return error.response;
        });

    if (myNewModel.status === 201) {
        window.location.href = `/mymodels/${myNewModel.data.id}`;
    }
    return myNewModel;
    },
};

export default apiSettings;

进入全屏模式 退出全屏模式

4.接收任何错误以显示在表单上

最后,我们确保 Django REST Framework 序列化程序返回的任何错误都可以显示在我们的表单中。

回到我们在 CreateMyModelForm.js 中的 doSubmit 中,我们正在等待 API.createMyModelEntry() 的响应。回想一下,如果遇到此 API 调用,则会在 catch 块中返回 error.response。从这里我们可以在 response.data 上调用 setErrors。

CreateMyModelForm.js doSubmit() 函数

...
const doSubmit = async (e) => {
    e.preventDefault();
    const response = await API.createMyModelEntry(data);
    if (response.status === 400) {
        setErrors(response.data);
    }
};
...

进入全屏模式 退出全屏模式

DRF 的序列化程序将返回一个带有字段名称及其相应错误的 JSON 对象。以下是发送的空白表单的示例输出,并尝试上传 .txt 文件而不是有效图像。我们的错误状态现在看起来像下面的 response.data:

控制台日志(错误)

{
    "title": [
        "This field is required."
    ],
    "description": [
        "This field is required."
    ],
    "image_url": [
        "Upload a valid image. The file you uploaded was either not an image or a corrupted image."
    ]
}

进入全屏模式 退出全屏模式

因此,现在对于我们的每个输入字段,我们可以说如果错误。[inputName] 不是假的,那么该字段必须有一个与之相关的错误。下面我使用 React-Bootstrap 来呈现我的标题输入。

isInvalid 设置为errors.title,这意味着如果errors.title 为真/有数据,则该字段将被标记为无效。下面我们使用 JSX 来判断 errors.title 是否为真,然后在字段下方呈现一个带有 errors.title 文本的工具提示。

您可以在本文的 React 部分的第一步中看到其他字段的详细信息。

CreateMyModelForm.js 标题输入字段

...
<Form.Group className="mb-3" controlId="titleInput">
    <Form.Label>Title</Form.Label>
    <Form.Control
        type="text"
        name="title"
        value={data.title}
        isInvalid={errors.title}
        onChange={(e) => { handleChange(e);}}
        maxLength={80}
        />
     {errors.title && (
         <Form.Text className="alert-danger" tooltip>
             {errors.title}
         </Form.Text>
     )}
</Form.Group>
...

进入全屏模式 退出全屏模式

表单域显示服务器端验证错误

这是显示错误的所有 3 个字段的示例。

而已!我们可以通过我们的 React 前端表单将文件上传到我们的 Django Rest Framework 后端。

Logo

Python社区为您提供最前沿的新闻资讯和知识内容

更多推荐