Как асинхронно добавить несколько изображений в форму Django перед отправкой формы - PullRequest
6 голосов
/ 29 марта 2019

Введение: У меня есть веб-приложение на Python Django, где пользователям разрешено создавать сообщения.Каждое сообщение имеет 1 основное изображение и сопровождается дополнительными изображениями (максимум 12 и минимум 2), которые связаны с этим сообщением.Я хочу позволить пользователям добавлять в общей сложности 13 изображений.1 основное изображение и 12 дополнительных изображений.

Проблема: Обычно пользователи делают фотографии со своих смартфонов.что делает размер изображения до 10 МБ.с 13 изображениями, которые могут стать 130 МБ.Мой сервер django может принять форму размером до 10 МБ.Поэтому я не могу уменьшить изображения ServerSide

Что я хочу сделать: Я хочу, чтобы когда пользователь загружал каждое изображение в форму.Размер этого изображения уменьшается на стороне клиента и асинхронно сохраняется во временном месте на моем сервере с помощью Ajax.Когда сообщение создано, все эти изображения связаны с сообщением.Так что, в основном, когда пользователь нажимает кнопку отправить в форме создания сообщения.Это супер легкая форма без изображений.Звучит слишком амбициозно .. ха-ха, может быть,

Что у меня есть до сих пор:

  1. У меня есть модели / виды (все части django, которые создают сообщение)без асинхронной части.Например, если форма после добавления всех изображений составляет менее 10 МБ.Мой пост создается с количеством когда-либо дополнительных изображений
  2. У меня есть код Javascript, который уменьшает размер изображений на стороне клиента и асинхронно добавляет его на мой сервер.Все, что мне нужно сделать, это дать ему конечную точку, которая является простым URL-адресом
  3. У меня есть приблизительное представление о том, как я планирую достичь этого

Теперь, чтобы показать вам мой код

Мои модели (Только в части 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/')

Измените вид, как показано ниже (пока работает только с основным изображением. Не уверен, как получить Extraimages)

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

Любые предложения о том, как я могу сделать эту работу

Мои шаблоны

{% 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 %}

Я не добавил форм.пи, так как они не были актуальны

Ответы [ 2 ]

4 голосов
/ 04 апреля 2019

В зависимости от вашей проблемы есть четыре вещи.

  1. Сделать трекер хранения временных файлов.
  2. Загрузка файлов сразу после выбора пользователем изображения (где-то в хранилище может быть временное расположение) сервер отвечает ссылкой уменьшенного изображения.
  3. Когда пользовательские сообщения формируются, которые передают только ссылки на эти изображения, тогда сохраняют сообщение с заданными ссылками.
  4. Эффективно обрабатывать временное местоположение. (Некоторой пакетной обработкой или некоторыми задачами с сельдереем.)

Решение

1. Создайте временное хранилище файлов для файлов, которые загружаются асинхронно.

Ваши временно загруженные файлы будут сохранены в модели TemporaryImage в temp_folder в виде следующей структуры.

Обновите 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/ POST
  • <domain>/ajax/temp_upload/{id}/ УДАЛИТЬ

Теперь эти конечные точки готовы к загрузке файлов

2. Загрузка файлов сразу после выбора пользователем изображения

Для этого вам нужно обновить template.py для обработки загрузок iamge, когда пользователь выбирает дополнительные изображения и публикует с помощью поля image, загружает это в <domain>/ajax/temp_upload/ с помощью метода POST, это вернет вас следующий образец данных JSON.

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

Вы можете просмотреть изображение с помощью клавиши reduced_image внутри json.

id - это ссылка на временный загруженный файл, который необходимо сохранить где-то для передачи в форму Post create. как скрытое поле.

Я не пишу код JavaScript, потому что ответ станет более длинным.

3. Когда формируются сообщения пользователя, которые передают только ссылки на эти изображения.

Загруженные файлы 'id установлены как скрытое поле на formset на странице 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")

Это форма отдельного загруженного временного файла.

Вы можете справиться с этим, используя formset в Django, как показано ниже

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 к посту, связанному с этим

1 голос
/ 06 апреля 2019

Ниже приведен ответ, который, по моему мнению, может быть проще для решения вышеуказанной проблемы

Как я получил эту идею

Я хотел отправить кому-то электронное письмо.Я нажал compose, я ничего не печатал.Я чем-то отвлекся и случайно закрыл браузер.Когда я снова открыл письмо.Я видел, что был проект.В нем ничего не было.Я был похож на Эврика!

Что у электронной почты есть

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.

Теперь нужно обратить внимание на то, когда я нажал «Создать» и закрылокно.У него было только 2 из вышеуказанных атрибутов

  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

Теперь в моем коде выше единственное, что нужно для создания этого сообщения - это пользователь logged_in

Iсоздал на моей панели навигации вкладку с именем Черновики

До: Когда пользователь нажал на добавление сообщения.Бланк был предоставлен.который пользователь заполнил и когда все требования были выполнены, был создан объект post.Приведенная выше функция create_post управляла представлением для создания этого сообщения

Сейчас: Когда пользователь нажимает кнопку добавить сообщение.Сообщение создается немедленно, и пустая форма, которую теперь видит пользователь, - это форма post_edit.Я добавляю барьеры Javascript, чтобы помешать отправке формы, если не удовлетворены все мои ранее обязательные поля.

Изображения добавляются асинхронно из моей формы post_edit.Они больше не осиротевшие образы.Мне не нужна другая модель, как ранее, чтобы временно сохранить изображения.когда пользователь добавляет изображения, они будут отправляться по одному на сервер.Если все сделано правильно.После того, как все изображения добавляются асинхронно.Пользователь отправляет сверхлегкую форму, когда нажимает кнопку «Отправить».Если пользователь покидает форму, он остается на навигационной панели пользователя как Черновик (1) .Вы можете позволить пользователю удалить этот черновик.если ему это не нужно.Или используйте простой код, например

, удалите черновик через 1 неделю, если он все еще является черновиком.Вы можете добавить это при входе пользователя в систему.

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

Я постараюсь создать GitHub-код для точного выполнения с использованием компонентов JavaScript.

Пожалуйста, дайте мне знать, что вы думаете об этом подходе.как я могу улучшить это.Или спросите меня, если что-то не ясно

...