多个文件上传在Form Wizard Django中不起作用

时间:2019-06-24 13:01:04

标签: django django-formwizard django-file-upload

我正在一个需要在表单向导步骤之一中上传多个图像的项目中。表单向导还具有用于该向导的几种模型,我认为这会使整个过程更加复杂。这是相关代码:

models.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的新手,尝试在编写代码时学习,但是到目前为止,关于表单向导和多个图像上传问题的文献还很少。大多数可用的帖子似乎都是伪劣的,仅使用不在数据库中存储任何详细信息的联系表。

1 个答案:

答案 0 :(得分:0)

解决方案来自此Project

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']

views.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],
        })