如何在表单提交之前将多个图像异步添加到 django 表单

Posted

技术标签:

【中文标题】如何在表单提交之前将多个图像异步添加到 django 表单【英文标题】:How to add multiple images to a django form asynchronously before form submit 【发布时间】:2019-08-19 23:35:39 【问题描述】:

简介:我有一个 python Django 网络应用程序,允许用户创建帖子。每个帖子都有 1 张主图片,然后是与该帖子相关的额外图片(最多 12 张和最少 2 张)。我想让用户总共添加 13 张图像。 1 个主图像和 12 个额外图像。

问题:通常用户使用智能手机拍照。这使得图像大小高达 10MB 。 13张图片可以变成130MB的形式。我的 django 服务器最多可以接受 10MB 的表单。所以我不能减少图像服务器端

我想要做什么:我想要这样当用户将每个图像上传到表单时。该图像的大小在客户端减小,并使用 Ajax 异步保存在我的服务器上的临时位置。创建帖子后,所有这些图像都链接到帖子。所以基本上当用户在帖子创建表单上点击提交时。它是一种没有图像的超轻形式。听起来太雄心勃勃了..哈哈也许

我目前所拥有的:

    我有没有异步部分的模型/视图(所有创建帖子的 django 部分)。如,如果添加所有图像后的表单小于 10MB。我的帖子是用多少张额外的图片创建的 我的 javascript 代码可以减小客户端图像的大小并将其异步添加到我的服务器。我需要做的就是给它一个端点,这是一个简单的 url 我对我计划如何实现这一目标有一个粗略的了解

现在向您展示我的代码

我的模型(只是 django 部分,还没有添加异步部分)

class Post(models.Model):
    user = models.ForeignKey(User, related_name='posts')
    title = models.CharField(max_length=250, unique=True)
    slug = models.SlugField(allow_unicode=True, unique=True, max_length=500)
    message = models.TextField()
    post_image = models.ImageField()

class Extra (models.Model): #(Images)
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='post_extra')
    image = models.ImageField(upload_to='images/', blank=True, null=True, default='')
    image_title = models.CharField(max_length=100, default='')
    image_description = models.CharField(max_length=250, default='')
    sequence = models.SmallIntegerField(validators=[MaxValueValidator(12), MinValueValidator(1)])

我的观点(只是 django 部分,还没有添加异步部分)

@login_required
def post_create(request):
    ImageFormSet = modelformset_factory(Extra, fields=('image', 'image_title', 'image_description'), extra=12, max_num=12,
                                        min_num=2)
    if request.method == "POST":
        form = PostForm(request.POST or None, request.FILES or None)
        formset = ImageFormSet(request.POST or None, request.FILES or None)
        if form.is_valid() and formset.is_valid():
            instance = form.save(commit=False)
            instance.user = request.user
            instance.save()
            for index, f in enumerate(formset.cleaned_data):
                try:
                    photo = Extra(sequence=index+1, post=instance, image=f['image'],
                                 image_title=f['image_title'], image_description=f['image_description'])
                    photo.save()
                except Exception as e:
                    break   

            return redirect('posts:single', username=instance.user.username, slug=instance.slug)

现在为了简单起见,我不会在这个问题中添加任何 Javascript。将以下脚本标记添加到我的表单会使图像异步保存到服务器。如果您愿意,可以阅读更多关于Filepond 的信息

'''See the urls below to see where the **new_image** is coming from'''
    FilePond.setOptions( server: "new_image/",
                          headers: "X-CSRF-Token": "% csrf_token %"
    ); #I need to figure how to pass the csrf to this request Currently this is throwing error

我的计划

在现有的 2 个模型下面添加一个新模型

class ReducedImages(models.Model):
    image = models.ImageField()
    post = models.ForeignKey(Post, blank=True, null=True, upload_to='reduced_post_images/')

如下更改视图(目前仅在主图像上工作。不确定如何获取额外图像)

''' This could be my asynchronous code  '''
@login_required
def post_image_create(request, post):
    image = ReducedImages.objects.create(image=request.FILES)
    image.save()
    if post:
        post.post_image = image


@login_required
def post_create(request):
    ImageFormSet = modelformset_factory(Extra, fields=('image', 'image_title', 'image_description'), extra=12, max_num=12,
                                        min_num=2)
    if request.method == "POST":
        form = PostForm(request.POST or None)
        formset = ImageFormSet(request.POST or None, request.FILES or None)
        if form.is_valid() and formset.is_valid():
            instance = form.save(commit=False)
            instance.user = request.user
            post_image_create(request=request, post=instance) #This function is defined above
            instance.save()
            for index, f in enumerate(formset.cleaned_data):
                try:
                    photo = Extra(sequence=index+1, post=instance, image=f['image'],
                                 image_title=f['image_title'], image_description=f['image_description'])
                    photo.save()

                except Exception as e:
                    break
            return redirect('posts:single', username=instance.user.username, slug=instance.slug)
    else:
        form = PostForm()
        formset = ImageFormSet(queryset=Extra.objects.none())
    context = 
        'form': form,
        'formset': formset,
    
    return render(request, 'posts/post_form.html', context)

我的 urls.py

url(r'^new_image/$', views.post_image_create, name='new_image'),

关于如何完成这项工作的任何建议

我的模板

% extends 'posts/post_base.html' %
% load bootstrap3 %
% load staticfiles %

% block postcontent %
<head>

    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet" type="text/css"/>
    <link href="https://unpkg.com/filepond-plugin-image-edit/dist/filepond-plugin-image-edit.css" rel="stylesheet" type="text/css"/>
    <link href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css" rel="stylesheet" type="text/css"/>
    <link href="% static 'doka.min.css' %" rel="stylesheet" type="text/css"/>
    <style>
    html 
        font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
        font-size: 1em;
    

    body 
        padding: 2em;
        max-width: 30em;
    
    </style>
</head>
<body>
<div class="container">
    <h2> Add a new Recipe</h2>
    <form action="" method="post" enctype="multipart/form-data" id="form">
        % csrf_token %
        % bootstrap_form form %
        <img  id="preview" src=""  />
        <img  id="new_image" src="" style="display: none;"  />
        formset.management_form
          <h3 class="text-danger">You must be present in at least 1 image making the dish. With your face clearly visible and
            matching your profile picture
        </h3>
        <h5>(Remember a picture is worth a thousand words) try to add as many extra images as possible
            <span class="text-danger"><b>(Minimum 2)</b></span>.
            People love to see how its made. Try not to add terms/language which only a few people understand.

         Please add your own images. The ones you took while making the dish. Do not copy images</h5>
        % for f in formset %
            <div style="border-style: inset; padding:20px; display: none;" id="formforloop.counter" >
                <p class="text-warning">Extra Image forloop.counter</p>
                % bootstrap_form f %

                <img  src=""  id="extra_imageforloop.counter"  />
            </div>
        % endfor %

        <br/><button type="button" id="add_more" onclick="myFunction()">Add more images</button>

        <input type="submit" class="btn btn-primary" value="Post" style="float:right;"/>

    </form>

</div>
<script>
    [
        supported: 'Promise' in window, fill: 'https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js',
        supported: 'fetch' in window, fill: 'https://cdn.jsdelivr.net/npm/fetch-polyfill@0.8.2/fetch.min.js',
        supported: 'CustomEvent' in window && 'log10' in Math && 'sign' in Math &&  'assign' in Object &&  'from' in Array &&
                    ['find', 'findIndex', 'includes'].reduce(function(previous, prop)  return (prop in Array.prototype) ? previous : false; , true), fill: 'doka.polyfill.min.js'
    ].forEach(function(p) 
        if (p.supported) return;
        document.write('<script src="' + p.fill + '"><\/script>');
    );
    </script>

    <script src="https://unpkg.com/filepond-plugin-image-edit"></script>
    <script src="https://unpkg.com/filepond-plugin-image-preview"></script>
    <script src="https://unpkg.com/filepond-plugin-image-exif-orientation"></script>
    <script src="https://unpkg.com/filepond-plugin-image-crop"></script>
    <script src="https://unpkg.com/filepond-plugin-image-resize"></script>
    <script src="https://unpkg.com/filepond-plugin-image-transform"></script>
    <script src="https://unpkg.com/filepond"></script>

    <script src="% static 'doka.min.js' %"></script>

    <script>

    FilePond.registerPlugin(
        FilePondPluginImageExifOrientation,
        FilePondPluginImagePreview,
        FilePondPluginImageCrop,
        FilePondPluginImageResize,
        FilePondPluginImageTransform,
        FilePondPluginImageEdit
    );

// Below is my failed attempt to tackle the csrf issue

const csrftoken = $("[name=csrfmiddlewaretoken]").val();


FilePond.setOptions(
    server: 
        url: 'http://127.0.0.1:8000',
        process: 
            url: 'new_image/',
            method: 'POST',
            withCredentials: false,
            headers: 
                headers:
        "X-CSRFToken": csrftoken
            ,
            timeout: 7000,
            onload: null,
            onerror: null,
            ondata: null
        
    
);


// This is the expanded version of the Javascript code that uploads the image


    FilePond.create(document.querySelector('input[type="file"]'), 

        // configure Doka
        imageEditEditor: Doka.create(
            cropAspectRatioOptions: [
                
                    label: 'Free',
                    value: null
                                   
            ]
        )

    );

The below codes are exacty like the one above. I have just minimised it

FilePond.create(document.querySelector('input[type="file"]'), ...);
FilePond.create(document.querySelector('input[type="file"]'), ...);
FilePond.create(document.querySelector('input[type="file"]'), ...);
FilePond.create(document.querySelector('input[type="file"]'), ...);
FilePond.create(document.querySelector('input[type="file"]'), ...);
FilePond.create(document.querySelector('input[type="file"]'), ...);
FilePond.create(document.querySelector('input[type="file"]'), ...);
FilePond.create(document.querySelector('input[type="file"]'), ...);
FilePond.create(document.querySelector('input[type="file"]'), ...);
FilePond.create(document.querySelector('input[type="file"]'), ...);
FilePond.create(document.querySelector('input[type="file"]'), ...);
FilePond.create(document.querySelector('input[type="file"]'), ...);


// ignore this part This is just to have a new form appear when the add more image button is pressed. Default is 3 images


<script>
    document.getElementById("form1").style.display = "block";
    document.getElementById("form2").style.display = "block";
    document.getElementById("form3").style.display = "block";   

    let x = 0;
    let i = 4;
    function myFunction() 

          if( x < 13) 
            x = i ++
          
      document.getElementById("form"+x+"").style.display = "block";
    
</script>
</body>


% endblock %

我没有添加 forms.py,因为它们不相关

【问题讨论】:

你能显示表单所在的html部分吗? @Jay 我已经添加了表单的 html 部分。很抱歉我应该从一开始就添加它 【参考方案1】:

下面是我觉得解决上述问题可能比较简单的答案

我是怎么想到的

我想给某人发一封电子邮件。我点击了撰写我没有输入任何内容。我被某事分心,不小心关闭了浏览器。当我再次打开电子邮件时。我看到有一个草稿。它里面没有任何东西。我就像 尤里卡!

电子邮件有什么

sender = (models.ForeignKey(User))
receiver =  models.ForeignKey(User
subject =  models.CharField()
message = models.TextFied()
created_at = models.DateTimefield()


#Lets assume that Multiple attachments are like my model above.

现在要注意的是当我单击撰写并关闭窗口时。它只有上述两个属性

  sender = request.user
  created_at = timezone.now()

它只用这 2 个东西创建了 email 对象。所以所有剩余的属性都是可选的。它也将其保存为草稿,因此还有另一个名为

的属性
is_draft = models.BooleanField(default=True)

很抱歉,我打了这么多东西,但我还没有说到重点(我一直在看很多法庭剧。都是相关的)

现在让我们将所有这些应用到我的问题上。(我相信你们中的一些人已经猜到了解决方案)

我的模型

'''I have made a lot of attributes optional'''
class Post(models.Model):
    user = models.ForeignKey(User, related_name='posts') #required
    title = models.CharField(max_length=250, unique=True, blank=True, null=True,) #optional
    slug = models.SlugField(allow_unicode=True, unique=True, max_length=500, blank=True, null=True,) #optional
    message = models.TextField(blank=True, null=True,) #optional
    post_image = models.ImageField(blank=True, null=True,) #optional
    created_at = models.DateTimeField(auto_now_add=True) #auto-genetrated
    is_draft = models.BooleanField(default=True) #I just added this new field

class Extra (models.Model): #(Images)
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='post_extra') #This is required 
    image = models.ImageField(upload_to='images/', blank=True, null=True, default='') #optional
    image_title = models.CharField(max_length=100, default='') #optional
    image_description = models.CharField(max_length=250, default='') #optional
    sequence = models.SmallIntegerField(validators=[MaxValueValidator(12), MinValueValidator(1)]) #optional

现在在我上面的代码中,创建这篇文章唯一需要的是登录用户

我在导航栏上创建了一个名为草稿

的标签

之前:当用户点击添加帖子时。呈现了一个空白表单。用户填写的内容以及满足所有要求时创建的帖子对象。上面的create_post 函数管理创建此帖子的视图

现在:当用户点击添加帖子时。立即创建一个帖子,用户现在看到的空白表单是post_edit 表单。我正在添加 Javascript 障碍来阻止表单提交,除非我之前的所有必填字段都得到满足。

图像是从我的post_edit 表单异步添加的。它们不再是孤立的图像。我不需要像以前那样的另一个模型来临时保存图像。当用户添加图像时,他们将一一发送到服务器。如果一切正常。在异步添加所有图像之后。用户在点击提交时提交了一个超轻量级的表单。如果用户放弃表单,它会以 Draft(1) 的形式留在用户导航栏上。您可以让用户删除此草稿。如果他不需要它。或者有一个简单的代码,比如

如果它仍然是草稿,请在 1 周后删除草稿。你可以在用户登录时添加这个

if post.is_draft and post.created_at > date__gt=datetime.date.today() + datetime.timedelta(days=6)

我将尝试制作一个 github 代码,以便使用 javascript 组件进行精确执行。

请告诉我您对这种方法的看法。我怎样才能更好地做到这一点。或者有什么不清楚的地方问我

【讨论】:

【参考方案2】:

根据你的问题,有四件事要做。

    制作临时文件存储跟踪器。 在用户选择图像后立即上传文件(存储中的某处可能是临时位置)服务器响应缩小图像的链接。 当用户发布表单仅传递对这些图像的引用时,然后使用给定的引用保存发布。 有效地处理临时位置。 (通过一些批处理或一些 celery 任务。)

解决方案

1。为异步上传的文件制作临时文件存储跟踪器。

您的临时上传文件将存储在temp_folder 中的TemporaryImage 模型中,结构如下。

更新您的 models.py

models.py

class TemporaryImage(models.Model):
    image = models.ImageField(upload_to="temp_folder/")
    reduced_image = models.ImageField(upload_to="temp_thumb_folder/")
    image_title = models.CharField(max_length=100, default='')
    image_description = models.CharField(max_length=250, default='')
    sequence = models.SmallIntegerField(validators=[MaxValueValidator(12), MinValueValidator(1)])


class Post(models.Model):
    user = models.ForeignKey(User, related_name='posts')
    title = models.CharField(max_length=250, unique=True)
    slug = models.SlugField(allow_unicode=True, unique=True, max_length=500)
    message = models.TextField()
    post_image = models.ImageField()

class Extra (models.Model): #(Images)
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='post_extra')
    image = models.ImageField(upload_to='images/', blank=True, null=True, default='')
    image_thumbnail = models.ImageField(upload_to='images/', blank=True, null=True, default='')
    image_title = models.CharField(max_length=100, default='')
    image_description = models.CharField(max_length=250, default='')
    sequence = models.SmallIntegerField(validators=[MaxValueValidator(12), MinValueValidator(1)])

这里TemporaryImage包含临时上传文件,字段raw_image代表原始上传文件,reduced_image代表文件上传后生成的缩略图

为了发送异步java脚本请求,您需要通过以下命令安装django-restframewrok

pip install djangorestframework

安装 restframework 后,添加 serializers.py 和以下代码。

serializers.py

from rest_framework import serializers


class TemporaryImageUploadSerializer(serializers.ModelSerializer):
    class Meta:
        model = TemporaryImage
        field = ('id', 'image',)

    def create(self, validated_data):
        raw_image = validated_data['raw_image']
        # Generate raw image's thumbnail here
        thumbnail = generate_thumbnail(raw_image)
        validated_data['reduced_image'] = thumbnail
        return super(TemporaryImageUploadSerializer, self).create(validated_data)

当用户异步上传文件时,此序列化程序会生成缩略图。 generate_thumbnail 函数将完成这项工作。可以从这里找到此方法的实现。

viewset 中添加此序列化程序,如下所示

apis.py

from rest_framework.generics import CreateAPIView, DestroyAPIView
from .serializers import TemporaryImageUploadSerializer

# This api view is used to create model entry for temporary uploaded file
class TemporaryImageUploadView(CreateAPIView):
    serializer_class = TemporaryImageUploadSerializer
    queryset = TemporaryImage.objects.all()

class TemporaryImageDeleteView(DestroyAPIView):
    lookup_field = 'id'
    serializer_class = TemporaryImageUploadSerializer
    queryset = TemporaryImage.objects.all()

TemporaryImageUploadViewSet 为您的上传创建POSTPUTPATCHDELETE 方法。

如下更新您的 urls.py

urls.py

from .apis import TemporaryImageUploadView, TemporaryImageDeleteView

urlpatterns = [
  ...
  url(r'^ajax/temp_upload/$', TemporaryImageUploadView.as_view()),
  url(r'^ajax/temp_upload/(?P<user_uuid>[0-9]+)/$', TemporaryImageDeleteView.as_view()),
  ...
]

这将创建以下端点来处理异步上传

&lt;domain&gt;/ajax/temp_upload/发帖 &lt;domain&gt;/ajax/temp_upload/id/删除

现在这些端点已准备好处理文件上传

2。用户选择图片后立即上传文件

为此,您需要更新 template.py 以在用户选择额外图像并使用 image 字段发布时使用 POST 方法将其上传到 &lt;domain&gt;/ajax/temp_upload/ 时处理 iamge 上传,这将返回您正在关注示例 json 数据。


    "id": 12,
    "image": "/media/temp_folder/image12.jpg",
    "reduced_image": "/media/temp_thumb_folder/image12.jpg",

您可以从 json 中的reduced_image 键预览图像。

id 是您临时上传文件的参考,您需要将其存储在某处以在Post 创建表单中传递。即作为隐藏字段。

我不写 javascript 代码,因为答案会变得更冗长。

3。当用户发布仅传递对这些图像的引用的表单时。

上传文件的id 在 HTML 页面中设置为 formset 上的隐藏字段。为了处理表单集,您需要执行以下操作。

forms.py

from django import forms

class TempFileForm(forms.ModelForm):
    id = forms.HiddenInput()
    class Meta:
        model = TemporaryImage
        fields = ('id',)

    def clean(self):
        cleaned_data = super().clean()
        temp_id = cleaned_data.get("id")
        if temp_id and not TemporaryImage.objects.filter(id=temp_id).first():
            raise forms.ValidationError("Can not find valida temp file")

这是单个上传的临时文件表单。

您可以通过在 django 中使用formset 来处理此问题,如下所示

forms.py

from django.core.files.base import ContentFile

@login_required
def post_create(request):
    ImageFormSet = formset_factory(TempFileForm, extra=12, max_num=12,
                                        min_num=2)
    if request.method == "POST":
        form = PostForm(request.POST or None)
        formset = ImageFormSet(request.POST or None, request.FILES or None)
        if form.is_valid() and formset.is_valid():
            instance = form.save(commit=False)
            instance.user = request.user
            post_image_create(request=request, post=instance) #This function is defined above
            instance.save()
            for index, f in enumerate(formset.cleaned_data):
                try:
                    temp_photo = TemporaryImage.objects.get(id=f['id'])

                    photo = Extra(sequence=index+1, post=instance,
                                 image_title=f['image_title'], image_description=f['image_description'])
                    photo.image.save(ContentFile(temp_photo.image.name,temp_photo.image.file.read()))
                    
                    # remove temporary stored file
                    temp_photo.image.file.close()
                    temp_photo.delete()
                    photo.save()

                except Exception as e:
                    break
            return redirect('posts:single', username=instance.user.username, slug=instance.slug)
    else:
        form = PostForm()
        formset = ImageFormSet(queryset=Extra.objects.none())
    context = 
        'form': form,
        'formset': formset,
    
    return render(request, 'posts/post_form.html', context)

这将使用给定的引用(临时上传的文件)保存帖子。

4。有效处理临时位置。

您需要处理 temp_foldertemp_thumb_folder 以保持文件系统清洁。

假设用户上传文件但未提交帖子表单,您需要删除该文件。

我知道答案太长而无法阅读,对此深表歉意,但如果有任何改进,请编辑此帖子

与此相关的帖子请参考https://medium.com/zeitcode/asynchronous-file-uploads-with-django-forms-b741720dc952

【讨论】:

非常感谢您提供如此详细的解释。我已授予您此答案的 100 名声望。但它对我的口味来说太复杂了。我想要更简单的东西 哦!非常感谢!

以上是关于如何在表单提交之前将多个图像异步添加到 django 表单的主要内容,如果未能解决你的问题,请参考以下文章

如何在表单的输入类型=“文件”中添加图像并将它们都提交到同一个表单后生成缩略图

我想提交一个包含多个图像文件的反应式表单

html 在提交之前将params添加到表单中

html 在提交之前将params添加到表单中

在 asp .NET MVC 中提交表单之前动态地将项目添加到列表中

如何将确认对话框添加到 html5 表单中的提交按钮?