0

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)
rolandist_scim
  • 145
  • 1
  • 8

1 Answers1

1

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

Ivan Starostin
  • 8,798
  • 5
  • 21
  • 39
  • Thank you for your answer. Now the form is rendered without errors! However, I still can't get validate the Image form, even if I upload an image. No errors in console, the file field gets empty when clicking submit. Any ideas? – rolandist_scim Dec 29 '22 at 08:49
  • Please show ImagenForm definition – Ivan Starostin Dec 29 '22 at 08:54
  • I edited the first post with the forms.py and model of Imagen. – rolandist_scim Dec 29 '22 at 08:56
  • I did not work with `render_field` so I'm not sure if it is used right. `enctype` is set, so `request.FILES` should be passed. Try printing all the passed data as author of [this question](https://stackoverflow.com/questions/19628979/django-modelform-imagefield-upload) for debug purposes. Maybe try defining the field explicitly in the form, e.g. `imagen = FileField()` before `class Meta` – Ivan Starostin Dec 29 '22 at 09:40
  • I tried rendering without render_field, and adding the field manually in forms.py but neither work. If I print out the 'request.FILES' I get: Certainly no data is being passed when uploading image... unfortunately many of the similar questions are solved with the 'enctype' – rolandist_scim Dec 29 '22 at 09:47
  • Form has some "modal" classes applied - try [validating](https://validator.w3.org/#validate_by_input) final HTML markup with the form shown. Could be this field is not treated as a part of the form at all because of markup issue. – Ivan Starostin Dec 29 '22 at 09:57
  • If I understood correctly about validation, I copy-pasted to that link the final HTML file, with all the text inside the
    tags. I got multiple errors as it's not pure html because of Django template language and htmx, but it seemed it is incorporated inside the form the same way as 'producto' for example. What do you mean by markup issue? I keep researching about it.
    – rolandist_scim Dec 29 '22 at 10:06
  • "Final markup" as rendered in the browser, not source files. – Ivan Starostin Dec 29 '22 at 10:15
  • Can't see something that looks like an error for not uploading images... Image input renders like this by the way: – rolandist_scim Dec 29 '22 at 10:24
  • See update about htmx – Ivan Starostin Dec 29 '22 at 10:37
  • Oh! Now I'm getting ']}>' when clicking submit and printing 'request.FILES'. However this is in the else: statement of the '.is_valid()' if, so for some reason it seams is not validating but there are request.FILES. I updated the first post code. – rolandist_scim Dec 29 '22 at 10:46
  • Well, now specific "invalid form" reasons matter. Which form, which field, what exactly is wrong. – Ivan Starostin Dec 29 '22 at 10:58
  • There was another field (multiple-choice-field) the one throwing the error now. The problem was with the 'enctype' for HTMX (hx-enctype). It is solved now. Thank you very much! – rolandist_scim Dec 29 '22 at 11:06