简介:我有一个python Django网络应用程序,允许用户创建帖子。每个帖子都有1个主图像,然后是与该帖子相关联的额外图像(最多12个和最小2个)。我想让用户添加总共13张图片。 1张主图像和12张额外图像。
问题:通常,用户使用智能手机拍照。使图像大小最大为10MB。具有13张图像,可以变成130MB的格式。我的django服务器最多可以接受10MB的表格。所以我无法减少ServerSide的图像
我要做什么:我希望这样,当用户将每个图像上传到表单时。该映像的大小在客户端减小了,并使用Ajax异步保存在我的服务器上的临时位置。创建帖子后,所有这些图像都将链接到该帖子。因此,基本上,当用户点击时,在帖子创建表单上提交。它是没有图像的超轻形式。听起来太雄心勃勃了..哈哈也许
我到目前为止所拥有的:
我的模型(只是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,因为它们不相关
答案 0 :(得分:4)
根据您的问题,有四件事要做。
您的临时上传文件将按照以下结构存储在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()
创建用于上传的TemporaryImageUploadViewSet
,POST
,PUT
,PATCH
方法。
如下更新您的 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/
删除现在这些端点已准备好处理文件上传
为此,您需要更新 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代码,因为答案会变得冗长。
在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保存具有给定的引用(临时上传的文件)。
您需要处理formset
和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)
以保持文件系统的清洁。
假设用户上载文件,并且提交的表单不超过您删除该文件所需的数量。
我知道答案太冗长而无法阅读,对此表示歉意,如果有任何改进,请编辑这篇文章
请参考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组件精确执行。
请让我知道您对这种方法的看法。我如何才能更好地做到这一点。或问我是否不清楚的地方