Image field form not validating

Question:

I have this view with two forms.

def anunciocreateview(request):
    anuncio_form = AnuncioForm(request.POST or None)
    producto_form = ProductoForm(request.POST or None)
    if request.method == "POST":
        if all([anuncio_form.is_valid(), producto_form.is_valid(), imagen_form.is_valid()]):
            anuncio = anuncio_form.save(commit=False)
            anuncio.anunciante = request.user
            anuncio.save()
            producto = producto_form.save(commit=False)
            producto.anuncio = anuncio
            producto.save()
            return HttpResponse(status=204, headers={'HX-Trigger' : 'eventsListChanged'})
        else:
            anuncio_form = AnuncioForm()
            producto_form = ProductoForm()
    context = {
        'anuncio_form' : anuncio_form,
        'producto_form' : producto_form,
    }
    return render(request, 'buyandsell/formulario.html', context)

This view works OKAY; it allows the user to create instances of both models with the correct relation. I’m trying to add another form for the image of the product. I tried adding this:

def anunciocreateview(request):
    anuncio_form = AnuncioForm(request.POST or None)
    producto_form = ProductoForm(request.POST or None)
    imagen_form = ImagenForm(request.POST, request.FILES)
    if request.method == "POST":
        if all([anuncio_form.is_valid(), producto_form.is_valid(), imagen_form.is_valid()]):
            anuncio = anuncio_form.save(commit=False)
            anuncio.anunciante = request.user
            anuncio.save()
            producto = producto_form.save(commit=False)
            producto.anuncio = anuncio
            producto.save()
            imagen = imagen_form.request.FILES.get('imagen')
            if imagen:
                Imagen.objects.create(producto=producto, imagen=imagen)
            return HttpResponse(status=204, headers={'HX-Trigger' : 'eventsListChanged'})
         else:
               print(request.FILES)
    else:
        anuncio_form = AnuncioForm()
        producto_form = ProductoForm()
        imagen_form = ImagenForm()
    context = {
        'anuncio_form' : anuncio_form,
        'producto_form' : producto_form,
        'imagen_form' : imagen_form
    }
    return render(request, 'buyandsell/formulario.html', context)

But this happens:
1- The ‘Image upload’ form field shows error ‘This field is required’ at the moment of rendering the form.
2- If uploading an image and clicking submit, the redirect doesn’t happen as the imagen_form is not marked as valid. No error pops in the console whatsoever (I’m handling requests with HTMX).

If I omit the 'request.FILES' when instantiating the form, the error when the form renders disappear, but it anyway doesn’t upload the image nor the form.

What am I missing?


For reference, here is the HTML file:

<form class="row g-3" method="post" enctype="multipart/form-data">
    {% csrf_token %}
    <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Create new listing</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
        <div class="modal-body">
            <div class="mb-3">
                <label for="{{ anuncio_form.titulo.auto_id }}">{{ anuncio_form.titulo.label }}</label>
                {% render_field anuncio_form.titulo|add_error_class:"is-invalid" class="form-control" %}
            </div>
            <div class="mb-3">
                <label for="{{ anuncio_form.envio.auto_id }}">{{ anuncio_form.envio.label }}</label>
                {% translate "Select available delivery options" as input_place_holder %}
                {% render_field anuncio_form.envio|add_error_class:"is-invalid" class="form-control" placeholder=input_place_holder %}
            </div>
            <h5>Product:</h5>
            <div class="mb-3">
                <label for="{{ producto_form.nombre.auto_id }}">{{ producto_form.nombre.label }}</label>
                {% translate "Name of your product" as input_place_holder %}
                {% render_field producto_form.nombre|add_error_class:"is-invalid" class=" form-control" placeholder=input_place_holder %}               
            </div>
            <div class="mb-3">
                <label for="{{ producto_form.descripcion.auto_id }}">{{ producto_form.descripcion.label }}</label>
                {% translate "Give a detailed description of your product" as input_place_holder %}
                {% render_field producto_form.descripcion|add_error_class:"is-invalid" class=" form-control" placeholder=input_place_holder %}              
            </div>
            <div class="row g-3">
                <div class="col-md-6">
                    <div class="mb-3">
                        <label for="{{ producto_form.estado.auto_id }}">{{ producto_form.estado.label }}</label>
                        {% render_field producto_form.estado|add_error_class:"is-invalid" class=" form-control" %}              
                    </div> 
                </div>
                <div class="col-md-6">
                    <div class="mb-3">
                        <label for="{{ producto_form.cantidad.auto_id }}">{{ producto_form.cantidad.label }}</label>
                        {% render_field producto_form.cantidad|add_error_class:"is-invalid" class=" form-control" %}                
                    </div> 
                </div>
            </div>
            <div class="row g-3">
                <div class="col-md-6">
                    <div class="mb-3">
                        <label for="{{ producto_form.precio_unitario.auto_id }}">{{ producto_form.precio_unitario.label }}</label>
                        {% render_field producto_form.precio_unitario|add_error_class:"is-invalid" class=" form-control" %}                 
                    </div> 
                </div>
                <div class="col-md-6">
                    <div class="mb-3">
                        <label for="{{ producto_form.disponibilidad.auto_id }}">{{ producto_form.disponibilidad.label }}</label>
                        {% render_field producto_form.disponibilidad|add_error_class:"is-invalid" class=" form-control" %}              
                    </div> 
                </div>
            </div>
            <h5>Images:</h5>
            <div class="mb-3">
                <label for="{{ imagen_form.imagen.auto_id }}">{{ imagen_form.imagen.label }}</label>
                {% render_field imagen_form.imagen|add_error_class:"is-invalid" class=" form-control" %}                
            </div>
            <div id="productforms"></div>
        </div>
        <div class="modal-footer">
            <button type="button" class="btn btn-tertiary" hx-get="{% url 'buyandsell:create-product' %}" hx-target="#productforms" hx-swap="beforeend">Add new product</button>
            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
            <button type="submit" class="btn btn-primary" hx-post="{% url 'buyandsell:createview' %}">Submit</button>
        </div>
    </div>
</form>

Edit:

forms.py for ImagenForm:

class ImagenForm(ModelForm):
    class Meta:
        model = Imagen
        fields = ['imagen']

models.py for the Imagen model:

class Imagen(models.Model):
    producto = models.ForeignKey(Producto, on_delete=models.CASCADE, related_name='imagen', blank=True, null=True)
    imagen = models.ImageField(upload_to='marketplace/')

    def __str__(self):
        return f"Imagen de {self.producto}"

    def save(self, *args, **kwargs):
        self.image = resize_image(self.image, size=(350, 350))
        super().save(*args, **kwargs)
Asked By: rolandist_scim

||

Answers:

Don’t initialize forms with request.POST if it is not a POST request.

Two changes to fix your issue:

  • move first form init block under if request.method == "POST"
  • move else block one tab to the left so it becomes else to if request.method == "POST" instead of current version where it is else to if .is_valid()
    if request.method == "POST":
        anuncio_form = AnuncioForm(request.POST or None)
        producto_form = ProductoForm(request.POST or None)
        imagen_form = ImagenForm(request.POST, request.FILES)

        if all([anuncio_form.is_valid(), producto_form.is_valid(), imagen_form.is_valid()]):
            ...
    else:
        anuncio_form = AnuncioForm()
        producto_form = ProductoForm()
        imagen_form = ImagenForm()

Now you use request.POST only on POST requests, instead you show empty forms. If a form is invalid on POST then it will be rendered with errors (if template is done cottectly) instead of showing empty forms back again.

For handling multiple forms with one view/template take a look at these answers: one two three

upd

For HTMX-requests it is also required to set hx-encoding="multipart/form-data" in form element attributes. Similar question HTMX docs

Answered By: Ivan Starostin