如何在表单提交之前将多个图像异步添加到 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
为您的上传创建POST
、PUT
、PATCH
、DELETE
方法。
如下更新您的 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/
发帖
<domain>/ajax/temp_upload/id/
删除
现在这些端点已准备好处理文件上传
2。用户选择图片后立即上传文件
为此,您需要更新 template.py 以在用户选择额外图像并使用 image
字段发布时使用 POST
方法将其上传到 <domain>/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_folder
和 temp_thumb_folder
以保持文件系统清洁。
假设用户上传文件但未提交帖子表单,您需要删除该文件。
我知道答案太长而无法阅读,对此深表歉意,但如果有任何改进,请编辑此帖子
与此相关的帖子请参考https://medium.com/zeitcode/asynchronous-file-uploads-with-django-forms-b741720dc952
【讨论】:
非常感谢您提供如此详细的解释。我已授予您此答案的 100 名声望。但它对我的口味来说太复杂了。我想要更简单的东西 哦!非常感谢!以上是关于如何在表单提交之前将多个图像异步添加到 django 表单的主要内容,如果未能解决你的问题,请参考以下文章
如何在表单的输入类型=“文件”中添加图像并将它们都提交到同一个表单后生成缩略图