Загрузка нескольких файлов не работает в мастере форм Django

Я работаю над проектом, который требует загрузки нескольких изображений на одном из шагов мастера форм. Мастер форм также имеет несколько моделей, используемых для мастера, что, я думаю, еще больше усложняет весь процесс. Вот соответствующий код:

модели.py

from django.db import models
from django.contrib.auth.models import User
from location_field.models.plain import PlainLocationField
from PIL import Image
from django.core.validators import MaxValueValidator, MinValueValidator
from listing_admin_data.models import (Service, SubscriptionType, PropertySubCategory,
        PropertyFeatures, VehicleModel, VehicleBodyType, VehicleFuelType,
        VehicleColour, VehicleFeatures, BusinessAmenities, Currency
    )

class Listing(models.Model):
    listing_type_choices = [('P', 'Property'), ('V', 'Vehicle'), ('B', 'Business/Service'), ('E', 'Events')]

    listing_title = models.CharField(max_length=255)
    listing_type = models.CharField(choices=listing_type_choices, max_length=1, default='P')
    status = models.BooleanField(default=False)
    featured = models.BooleanField(default=False)
    city = models.CharField(max_length=255, blank=True)
    location = PlainLocationField(based_fields=['city'], zoom=7, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    expires_on = models.DateTimeField(auto_now=True)
    created_by = models.ForeignKey(User,
        on_delete=models.CASCADE, editable=False, null=True, blank=True
    )
    listing_owner = models.ForeignKey(User,
        on_delete=models.CASCADE, related_name='list_owner'
    )

    def __str__(self):
        return self.listing_title


def get_image_filename(instance, filename):
    title = instance.listing.listing_title
    slug = slugify(title)
    return "listings_pics/%s-%s" % (slug, filename)


class ListingImages(models.Model):
    listing = models.ForeignKey(Listing, on_delete=models.CASCADE)
    image_url = models.ImageField(upload_to=get_image_filename,
                              verbose_name='Listing Images')
    main_image = models.BooleanField(default=False)

    class Meta:
        verbose_name_plural = "Listing Images"

    def __str__(self):
        return f'{self.listing.listing_title} Image'


class Subscriptions(models.Model):
    subscription_type = models.ForeignKey(SubscriptionType, on_delete=models.CASCADE)
    subscription_date = models.DateTimeField(auto_now_add=True)
    subscription_amount = models.DecimalField(max_digits=6, decimal_places=2)
    subscribed_by = models.ForeignKey(User, on_delete=models.CASCADE)
    duration = models.PositiveIntegerField(default=0)
    listing_subscription = models.ManyToManyField(Listing)
    updated_at = models.DateTimeField(auto_now=True)
    status = models.BooleanField(default=False)

    class Meta:
        verbose_name_plural = "Subscriptions"

    def __str__(self):
        return f'{self.listing.listing_title} Subscription'


class Property(models.Model):
    sale_hire_choices = [('S', 'Sale'), ('R', 'Rent')]
    fully_furnished_choices = [('Y', 'Yes'), ('N', 'No')]

    listing = models.OneToOneField(Listing, on_delete=models.CASCADE)
    sub_category = models.ForeignKey(PropertySubCategory, on_delete=models.CASCADE)
    for_sale_rent = models.CharField(choices=sale_hire_choices, max_length=1, default=None)
    bedrooms = models.PositiveIntegerField(default=0)
    bathrooms = models.PositiveIntegerField(default=0)
    rooms = models.PositiveIntegerField(default=0)
    land_size = models.DecimalField(max_digits=10, decimal_places=2)
    available_from = models.DateField()
    car_spaces = models.PositiveIntegerField(default=0)
    fully_furnished = models.CharField(choices=fully_furnished_choices, max_length=1, default=None)
    desc = models.TextField()
    property_features = models.ManyToManyField(PropertyFeatures)
    price = models.DecimalField(max_digits=15, decimal_places=2)
    currency = models.ForeignKey(Currency, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    created_by = models.ForeignKey(User, on_delete=models.CASCADE)

    class Meta:
        verbose_name_plural = "Properties"

    def __str__(self):
        return f'{self.listing.listing_title}'

Формы для этого приложения выглядят следующим образом: forms.py

    from django import forms
from .models import Listing, Property, Vehicle, Business, ListingImages

class ListingDetails(forms.ModelForm):
    class Meta:
        model = Listing
        fields = ['listing_title', 'city', 'location']

class PropertyDetails1(forms.ModelForm):
    class Meta:
        model = Property
        fields = ['sub_category', 'for_sale_rent', 'bedrooms', 'bathrooms',
            'rooms', 'land_size', 'available_from', 'car_spaces', 'fully_furnished',
            'desc', 'currency', 'price'
        ]

class PropertyDetails2(forms.ModelForm):
    class Meta:
        model = Property
        fields = ['property_features']

class ListingImagesForm(forms.ModelForm):
    class Meta:
        model = ListingImages
        fields = ['image_url']

Представление, которое обрабатывает все это, хотя еще не завершено, поскольку я все еще изучаю лучший способ сохранения данных в базе данных, как показано ниже: views.py

    from django.shortcuts import render
import os
from .forms import ListingDetails, PropertyDetails1, PropertyDetails2, ListingImagesForm
from .models import ListingImages
from formtools.wizard.views import SessionWizardView
from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django.forms import modelformset_factory
from django.contrib import messages
from django.http import HttpResponseRedirect

class PropertyView(SessionWizardView):
    ImageFormSet = modelformset_factory(ListingImages, form=ListingImagesForm, extra=3)
    template_name = "listings/create_property.html"
    formset = ImageFormSet(queryset=Images.objects.none())
    form_list = [ListingDetails, PropertyDetails1, PropertyDetails2, ListingImagesForm]
    file_storage = FileSystemStorage(location=os.path.join(settings.MEDIA_ROOT, 'media'))
    def done(self, form_list, **kwargs):
        return render(self.request, 'done.html', {
            'form_data': [form.cleaned_data for form in form_list],
        })

Шаблон, который используется для обработки полей формы, выглядит следующим образом: create_property.py

    <p>Step {{ wizard.steps.step1 }} of {{ wizard.steps.count }}</p>
<form action="" method="post">
    {% csrf_token %}
    <table>
    {{ wizard.management_form }}
    {% if wizard.form.forms %}
        {{ wizard.form.management_form }}
        {% for form in wizard.form.forms %}
            {{ form }}
        {% endfor %}
        {% else %}
            {% for field in wizard.form %}
                <div class="form-group">
                    <label for="{{ field.id_for_label }}">{{ field.label }}</label>
                    {{ field }}
                    <span class="message">{{ field.errors }}</span>
                </div>
            {% endfor %}
    {% endif %}
    </table>
    {% if wizard.steps.prev %}
    <div class="d-flex justify-content-around">
        <button name="wizard_goto_step" type="submit" class="btn btn-primary" value="{{ wizard.steps.first }}">First Step</button>
        <button name="wizard_goto_step" type="submit" class="btn btn-primary" value="{{ wizard.steps.prev }}">Previous Step</button>
    </div>
    {% endif %}

    <div class="d-flex justify-content-end col-12 mb-30 pl-15 pr-15">
        <input type="submit" value="{% trans "submit" %}"/>
    </div>
</form>

Я попытался прикрепить всю необходимую информацию для этого, чтобы работать.

Основная проблема, с которой я столкнулся, заключается в том, что шаблон не дает места для нескольких загрузок, и снова, после того, как я прикрепил один предоставленный файл и попытался отправить его, я получаю сообщение об ошибке field cannot be empty.

Я все еще новичок в Django и пытаюсь учиться, когда я кодирую, но до сих пор не так много было задокументировано о мастере форм и проблемах с загрузкой нескольких изображений. Большинство доступных сообщений кажутся некачественными и используют только контактную форму, которая не хранит никаких подробностей в базе данных.


person japheth    schedule 24.06.2019    source источник
comment
По крайней мере, ваша форма нуждается в enctype="multipart/form-data" в теге <form>, чтобы включить загрузку файла.   -  person HenryM    schedule 25.06.2019
comment
Конечно, я думаю, что это была проблема, вызвавшая ошибку невозможности отправить форму с одним изображением. Спасибо за указатель. Теперь большая проблема, с которой я столкнулся, — это загрузка нескольких изображений. Я буду признателен за любой указатель на это.   -  person japheth    schedule 26.06.2019
comment
Я не использовал formtools с набором форм, но мне кажется, что это проблема: github. com/django/django-formtools/issues/43   -  person HenryM    schedule 26.06.2019
comment
Привет. Вы когда-нибудь работали над этим? Я собираюсь попробовать что-то подобное, и мне было бы интересно узнать, как вы решили это.   -  person GerryDevine    schedule 06.02.2020
comment
@GerryDevine, извините за поздний ответ. Я справился с этим, используя dropzone. Это просто и имеет лучший пользовательский интерфейс.   -  person japheth    schedule 13.02.2021


Ответы (1)


Решение взято из этого проекта.

MultipleUpload.py

from django import forms

FILE_INPUT_CONTRADICTION = object()


class ClearableMultipleFilesInput(forms.ClearableFileInput):
    
    # Taken from:
    # https://stackoverflow.com/questions/46318587/django-uploading-multiple-files-list-of-files-needed-in-cleaned-datafile#answer-46409022

    def value_from_datadict(self, data, files, name):
        upload = files.getlist(name)  # files.get(name) in Django source

        if not self.is_required and forms.CheckboxInput().value_from_datadict(
                data, files, self.clear_checkbox_name(name)):

            if upload:
                # If the user contradicts themselves (uploads a new file AND
                # checks the "clear" checkbox), we return a unique marker
                # objects that FileField will turn into a ValidationError.
                return FILE_INPUT_CONTRADICTION
            # False signals to clear any existing value, as opposed to just None
            return False
        return upload


class MultipleFilesField(forms.FileField):
    # Taken from:
    # https://stackoverflow.com/questions/46318587/django-uploading-multiple-files-list-of-files-needed-in-cleaned-datafile#answer-46409022

    widget = ClearableMultipleFilesInput

    def clean(self, data, initial=None):
        # If the widget got contradictory inputs, we raise a validation error
        if data is FILE_INPUT_CONTRADICTION:
            raise forms.ValidationError(self.error_message['contradiction'], code='contradiction')
        # False means the field value should be cleared; further validation is
        # not needed.
        if data is False:
            if not self.required:
                return False
            # If the field is required, clearing is not possible (the widg    et
            # shouldn't return False data in that case anyway). False is not
            # in self.empty_value; if a False value makes it this far
            # it should be validated from here on out as None (so it will be
            # caught by the required check).
            data = None
        if not data and initial:
            return initial
        return data
enter code here
from django.core.files.uploadedfile import UploadedFile
from django.utils import six
from django.utils.datastructures import MultiValueDict


from formtools.wizard.storage.exceptions import NoFileStorageConfigured
from formtools.wizard.storage.base import BaseStorage


class MultiFileSessionStorage(BaseStorage):
    """
    Custom session storage to handle multiple files upload.
    """
    storage_name = '{}.{}'.format(__name__, 'MultiFileSessionStorage')

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.prefix not in self.request.session:
            self.init_data()

    ################################################################################################
    # Helper
    ################################################################################################

    def _get_data(self):
        self.request.session.modified = True
        return self.request.session[self.prefix]

    def _set_data(self, value):
        self.request.session[self.prefix] = value
        self.request.session.modified = True

    data = property(_get_data, _set_data)

    ################################################################################################
    # formtools.wizard.storage.base.BaseStorage API overrides
    ################################################################################################

    def reset(self):
        # Store unused temporary file names in order to delete them
        # at the end of the response cycle through a callback attached in
        # `update_response`.
        wizard_files = self.data[self.step_files_key]
        for step_files in six.itervalues(wizard_files):
            for file_list in six.itervalues(step_files):
                for step_file in file_list:
                    self._tmp_files.append(step_file['tmp_name'])
        self.init_data()

    def get_step_files(self, step):
        wizard_files = self.data[self.step_files_key].get(step, {})

        if wizard_files and not self.file_storage:
            raise NoFileStorageConfigured(
                "You need to define 'file_storage' in your "
                "wizard view in order to handle file uploads.")

        files = {}
        for field in wizard_files.keys():
            files[field] = {}
            uploaded_file_list = []

            for field_dict in wizard_files.get(field, []):
                field_dict = field_dict.copy()
                tmp_name = field_dict.pop('tmp_name')
                if(step, field, field_dict['name']) not in self._files:
                    self._files[(step, field, field_dict['name'])] = UploadedFile(
                        file=self.file_storage.open(tmp_name), **field_dict)
                uploaded_file_list.append(self._files[(step, field, field_dict['name'])])
            files[field] = uploaded_file_list

        return MultiValueDict(files) or MultiValueDict({})

    def set_step_files(self, step, files):
        if files and not self.file_storage:
            raise NoFileStorageConfigured(
                "You need to define 'file_storage' in your "
                "wizard view in order to handle file uploads.")

        if step not in self.data[self.step_files_key]:
            self.data[self.step_files_key][step] = {}

        for field in files.keys():
            self.data[self.step_files_key][step][field] = []
            for field_file in files.getlist(field):
                tmp_filename = self.file_storage.save(field_file.name, field_file)
                file_dict = {
                    'tmp_name': tmp_filename,
                    'name': field_file.name,
                    'content_type': field_file.content_type,
                    'size': field_file.size,
                    'charset': field_file.charset
                }
                self.data[self.step_files_key][step][field].append(file_dict)

form.py

from MultipleUpload import MultipleFilesField, ClearableMultipleFilesInput
class ListingImagesForm(forms.ModelForm):
    image_url = MultipleFilesField(widget=ClearableMultipleFilesInput(
    attrs={'multiple': True, 'accept':'.jpg,.jpeg,.png'}), label='Files')
    class Meta:
        model = ListingImages
        fields = ['image_url']

просмотры.py

from MultipleUpload import MultiFileSessionStorage
class PropertyView(SessionWizardView):
    storage_name = MultiFileSessionStorage.storage_name
    ImageFormSet = modelformset_factory(ListingImages, form=ListingImagesForm, extra=3)
    template_name = "listings/create_property.html"
    formset = ImageFormSet(queryset=Images.objects.none())
    form_list = [(....), ('ListingImagesForm',ListingImagesForm)]
    file_storage = FileSystemStorage(location=os.path.join(settings.MEDIA_ROOT, 'media'))
    def done(self, form_list, **kwargs):
        cleaned_data = self.get_cleaned_data_for_step('ListingImagesForm')
        for f in cleaned_data.get('image_url',[]):
            instance = ListingImages(image_url=f, .....)  
            instance.save()
        return render(self.request, 'done.html', {
            'form_data': [form.cleaned_data for form in form_list],
        })
person DYEZ    schedule 28.11.2020