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

标签 django python-3.x django-forms django-views

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

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

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

到目前为止我所拥有的:

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

    现在向你展示我的代码

    My Models (Just the django part no asynchronous part added as yet)


    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)])
    

    My views (Just the django part no asynchronous part added as yet)


    @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)
    

    Now Just to keep things simple I will not add any Javascript in this question. Adding the below script tag to my form makes the image saved asynchronously to the server. You can read more about Filepond if you wish


    '''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
    

    My plan to make it work



    在现有 2 个模型下方添加一个新模型
    class ReducedImages(models.Model):
        image = models.ImageField()
        post = models.ForeignKey(Post, blank=True, null=True, upload_to='reduced_post_images/')
    

    更改 View 如下(目前只处理主图像。不确定如何获得额外图像)
    ''' 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)
    

    my urls.py


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

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

    My Templates


    {% 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 alt="" id="preview" src="" width="100" />
            <img alt="" 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="form{{forloop.counter}}" >
                    <p class="text-warning">Extra Image {{forloop.counter}}</p>
                    {% bootstrap_form f %}
    
                    <img alt="" src="" width="60" id="extra_image{{forloop.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 因为它们不相关

    最佳答案

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

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

  • 解决方案
    1.为异步上传的文件制作临时文件存储跟踪器。
    您临时上传的文件将存储在 TemporaryImage 模型中的 temp_folder 中,结构如下。
    更新您的 models.py
    模型.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 代表 缩略图 是文件上传后生成的。

    In order to send asynchronous java script request you need to install django-restframewrok by following command.

    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 函数将完成这项工作。这个方法的实现可以从 here 找到。
    View 集 中添加此序列化程序,如下所示
    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()),
      ...
    ]
    
    这将创建以下端点来处理异步上传
  • <domain>/ajax/temp_upload/ POST
  • <domain>/ajax/temp_upload/{id}/ 删除

  • 现在这些端点已准备好处理文件上传
    2. 用户选择图片后立即上传文件
    为此,您需要更新 template.py 以在用户选择额外图像并使用 image 字段发布时处理图像上传 使用 <domain>/ajax/temp_upload/ 方法将其上传到 POST 这将返回以下示例 json 数据
    {
        "id": 12,
        "image": "/media/temp_folder/image12.jpg",
        "reduced_image": "/media/temp_thumb_folder/image12.jpg",
    }
    
    您可以从 json 中的 reduced_image 键预览图像。

    id is reference for your temporary uploaded file you need to store it somewhere to pass in the Post create form. i.e As hidden field.


    我不是在写 javascript 代码,因为答案会变得更冗长。
    3. 当用户发布仅传递对这些图像的引用的表单时。
    上传文件的 id 设置为 HTML 页面中 formset 上的隐藏字段。为了处理表单集,您需要执行以下操作。
    表单.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")
    

    This is single uploaded temporary file form.


    您可以通过在 Django 中使用 formset 来处理这个问题,如下所示
    表单.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)
    

    This would save Post with given references(temporary uploaded files).


    4. 有效处理临时位置。
    您需要处理 temp_foldertemp_thumb_folder 以保持文件系统干净。
    假设用户上传文件并且不提交帖子表单,而不是您需要删除该文件。

    I know the answer became too lengthy to read, apologise for that yet edit this post if any improvements


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

    关于django - 如何在表单提交之前将多个图像异步添加到 django 表单中,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55410457/

    相关文章:

    python - 如何在 Django 项目中的应用程序之间正确共享代码

    python - 在字符串拆分期间删除空条目

    python-3.x - python3中的多处理不起作用

    python - 你如何在 Django 中动态隐藏表单字段?

    django - 如何在django中创建密码输入字段

    python - 将评论框架迁移到 Django 时出错

    python - 全文搜索: Whoosh Vs SOLR

    jquery - 向 Django REST Framework 发送 jQuery 请求导致未找到 JSON 对象错误

    python-3.x - 美汤不解析嵌套表数据

    django - 如何将上下文变量从 View 传递到 Django 模板中的自定义字段/小部件?