Search code examples
djangodjango-viewsdjango-forms

Create Post and Upload Multiple Files in Django


I have a table of animals. For each animal I have multiple images. I would like the user to be able to register a new animal and add multiple images to it in one form. As of now no records are created and I do not understand why. I've been struggelig this for hours. I know I could get there by using formsets but would prefer something like this.

I suspect the view is causing this, but do not understand why.

This is my not-working sollution:

upl/models.py

from django.db import models 
from django.core.exceptions import ValidationError



def validate_img_extension(value):
    if not value.name.endswith(".jpg", ".jpeg", ".png"):
        raise ValidationError("Only .jpg, .jpeg og .png allowed.")

class Animal(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField(null=True, blank=True)

class AnimalImage(models.Model):
    animal = models.ForeignKey(Animal, on_delete=models.CASCADE)
    image = models.FileField(upload_to="products/", validators=[validate_img_extension])

upl/views.py

from django.shortcuts import redirect, render 
from .forms import AnimalImageForm, AnimalForm 
from .models import Animal, AnimalImage


def createAnimal(request):
    animalform = AnimalForm()
    imageform = AnimalImageForm()

    if request.method == "POST":
        animalform = AnimalForm(request.POST)
        imageform = AnimalImageForm(request.POST or None, request.FILES or None)
        images = request.FILES.getlist("image")
        if animalform.is_valid() and imageform.is_valid():
            title = animalform.cleaned_data["title"]
            describ = animalform.cleaned_data["description"]
            animal_instance = Animal.objects.create(title=title, description=describ)
            animal_instance.save()

            for i in images:
                AnimalImage.objects.create(animal=animal_instance, image=i)
            return redirect("uplhome")

    context = {"animalform": animalform, "imageform": imageform}
    return render(request, "upl/animal_form.html", context)


def uplhome(request):
    return render(request, "upl/uplhome.html")

forms.py

from django import forms
from django.forms import ClearableFileInput
from .models import Animal, AnimalImage


class MultipleFileInput(forms.ClearableFileInput):
    allow_multiple_selected = True


class AnimalForm(forms.ModelForm):

    class Meta:
        model = Animal
        fields = ["title", "description"]


class AnimalImageForm(forms.ModelForm):
    class Meta:
        model = AnimalImage
        fields = ["image"]
        widgets = {"image": MultipleFileInput()}

upl/urls.py

from django.urls import path, include 
from upl import views 
from django.conf import settings 
from django.conf.urls.static import static


urlpatterns = [
    path("uplhome", views.uplhome, name="uplhome"),
    path("createanimal", views.createAnimal, name="createanimal"), 
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

upl/animal_form.html

<h1>Animal Form </h1> 
<form class="form" method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{animalform.title.label_tag}}
{{animalform.title}}
<br><br>
{{animalform.description.label_tag}}
{{animalform.description}}
<br><br>
{{imageform.image.label_tag}}
{{imageform.image}}
<br><br>
<button type="submit">Submit</button>
</form>
<a href="{% url 'uplhome' %}">Animal Home</a>

Solution

  • As Django documentation says,

    If you want to upload multiple files using one form field, create a subclass of the field’s widget and set its allow_multiple_selected class attribute to True.

    In order for such files to be all validated by your form (and have the value of the field include them all), you will also have to subclass FileField.

    You already have the code for widget, so only FileField is left to be subclassed - again, example from documentation:

    class MultipleFileField(forms.FileField):
        def __init__(self, *args, **kwargs):
            kwargs.setdefault("widget", MultipleFileInput())
            super().__init__(*args, **kwargs)
    
        def clean(self, data, initial=None):
            single_file_clean = super().clean
            if isinstance(data, (list, tuple)):
                result = [single_file_clean(d, initial) for d in data]
            else:
                result = [single_file_clean(data, initial)]
            return result
    

    Then you can use this class in your form like this:

    class AnimalImageForm(forms.ModelForm):
        image = MultipleFileField()
        class Meta:
            model = AnimalImage
            fields = ["image"]
    

    And you'll have to adjust your validator to allow multiple values to be validated:

    def validate_img_extension(files_list):
        for file in files_list:
            if not file.name.endswith((".jpg", ".jpeg", ".png")):
                raise ValidationError("Only .jpg, .jpeg og .png allowed.")