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

时间:2019-03-29 04:21:05

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

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

问题:通常,用户使用智能手机拍照。使图像大小最大为10MB。具有13张图像,可以变成130MB的格式。我的django服务器最多可以接受10MB的表格。所以我无法减少ServerSide的图像

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

我到目前为止所拥有的:

  1. 我有没有异步部分的模型/视图(所有创建帖子的django部分)。如图所示,如果添加所有图像后的表单小于10MB。我的帖子是用多少张额外的图片创建的
  2. 我有Javascript代码,可以减少客户端上图像的大小并将其异步添加到我的服务器中。我需要做的就是给它一个端点,它是一个简单的
  3. 网址。
  4. 我对如何实现这一目标有一个大概的认识

现在向您展示我的代码

  

我的模型(只是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 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,因为它们不相关

2 个答案:

答案 0 :(得分:4)

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

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

解决方案

1。为临时上传的文件设置临时文件存储跟踪器。

您的临时上传文件将按照以下结构存储在TemporaryImage的{​​{1}}模型中。

更新您的 models.py

models.py

temp_folder

此处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代表文件上传后生成的缩略图

  

要发送异步Java脚本请求,您需要通过以下命令安装reduced_image

     

django-restframewrok

在安装restframework之后,使用以下代码添加 serializers.py

serializers.py

pip install djangorestframework

当用户异步上传文件时,此序列化器会生成缩略图。 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) 函数将完成此工作。可以在此处找到该方法的实现。

如下所示在 viewset 中添加此序列化器

apis.py

generate_thumbnail

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() 创建用于上传的TemporaryImageUploadViewSetPOSTPUTPATCH方法。

如下更新您的 urls.py

urls.py

DELETE

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

  • 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/删除

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

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

为此,您需要更新 template.py ,以在用户选择其他图片并使用<domain>/ajax/temp_upload/{id}/帖子发布时处理iamge上传,并使用{{1将其上传到image }}方法将返回以下示例json数据。

<domain>/ajax/temp_upload/

您可以通过json中的POST键预览图像。

  

{ "id": 12, "image": "/media/temp_folder/image12.jpg", "reduced_image": "/media/temp_thumb_folder/image12.jpg", } 是您临时上传的文件的参考,您需要将其存储在reduced_image创建表单中以进行传递。即作为隐藏字段。

我没有编写JavaScript代码,因为答案会变得冗长。

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

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

forms.py

id
  

这是单个上载的临时文件格式。

您可以通过在django中使用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")
  

这将为Post保存具有给定的引用(临时上传的文件)。

4。有效处理临时位置。

您需要处理formsetfrom 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) 以保持文件系统的清洁。

假设用户上载文件,并且提交的表单不超过您删除该文件所需的数量。

  

我知道答案太冗长而无法阅读,对此表示歉意,如果有任何改进,请编辑这篇文章

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

答案 1 :(得分: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()

它仅用这两件事创建了电子邮件对象。因此,所有其余属性都是可选的。还将其另存为草稿,因此还有另一个名为

的属性。
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
  

现在在我的代码中,创建此帖子所需的唯一内容是登录用户

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

的标签

之前:当用户点击添加帖子时。呈现了一个空白表格。用户填写的内容,并在满足所有要求时创建发布对象。上方的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组件精确执行。

  

请让我知道您对这种方法的看法。我如何才能更好地做到这一点。或问我是否不清楚的地方