1

I am running Django 2.2 and have written a simple shopping cart. I wish to validate two fields at the same time in such a way that both cannot be empty at the same time. In my forms.py,

from django import forms

class CartAddProductForm(forms.Form):
    cc_handle = forms.CharField(required=False, label='CC Handle', empty_value='')
    lc_handle = forms.CharField(required=False, label='LC Handle', empty_value='')

    def clean(self):
        cleaned_data = super().clean()
        if cleaned_data.get('cc_handle') == '' and cleaned_data.get('lc_handle') == '':
            print("issue detected")
            raise forms.ValidationError('Either cc or lc handle is required.')
        return cleaned_data

This is following the official Django docs on cleaning and validating fields that depend on each other. The print() statement above lets me know that the issue has been detected, i.e. both fields are empty. Running the Django server, I see that the issue was indeed detected but no validation error message was displayed on top of the originating page. The originating page is the product page that contains the product and a link to add the product to the shopping cart. Normally the validation error message is displayed at the top of the page.

According to the docs, the validation is done when is_valid() is called. So I put a diagnostic print of my views.py

from django.shortcuts import render, redirect, get_object_or_404
from django.views.decorators.http import require_POST
from shop.models import Product
from .cart import Cart
from .forms import CartAddProductForm

@require_POST
def cart_add(request, product_id):
    cart = Cart(request)
    product = get_object_or_404(Product, id=product_id)
    form = CartAddProductForm(request.POST)
    if form.is_valid():
        cd = form.cleaned_data
        cart.add(product=product,
                cc_handle=cd['cc_handle'],
                lc_handle=cd['lc_handle'])
    else:
        print('invalid form')
    return redirect('cart:cart_detail')

And indeed the words 'invalid form' popped up. The code then takes me to the shopping cart. Instead, what I want is to be at the product page and show the validation error informing the reader that both fields cannot be empty. Is there a simple way of doing it?

For required=True fields in the forms, if I leave it blank, there will be a message popping up saying that I need to fill it in. So I want to do something similar except the validation requires that both fields cannot be empty.

This is different from this Stackoverflow answer because that is a registration form. You can redirect it to the same form whereas for this case, the CartAddProductForm is embedded in all the products page on the site. If possible, I want the validation to occur at the same stage as the field with required=True option.

The product/detail.html template looks like the following.

{% extends "shop/base.html" %}
{% load static %}

{% block title %}
{{ product.name }}
{% endblock %}

{% block content %}
<div class="product-detail">
    <img src="{% if product.image %}{{ product.image.url }}{% else %}{% static "img/no_image.png" %}{% endif %}">
    <h1>{{ product.name }}</h1>
    <h2><a href="{{ product.category.get_absolute_url }}">{{ product.category }}</a></h2>
    <p class="price">${{ product.price }}</p>
    <form action="{% url "cart:cart_add" product.id %}" method="post">
    {{ cart_product_form }}
    {% csrf_token %}
    <input type="submit" value="Add to cart">
    </form>
    {{ product.description|linebreaks }}
</div>
{% endblock %}
Spinor8
  • 1,587
  • 4
  • 21
  • 48
  • Hope [this](https://stackoverflow.com/a/22513989/8353711) will helpful. How is your `form` template look? – shaik moeed May 21 '19 at 06:34
  • Possible duplicate of [Django, show ValidationError in template](https://stackoverflow.com/questions/22470637/django-show-validationerror-in-template) – shaik moeed May 21 '19 at 06:35
  • Well, this is different from the post you showed. In that, you have one registration form so you can redirect back to it. I have multiple products, each with a CartAddProductForm embedded in it. – Spinor8 May 21 '19 at 06:41
  • How `{{ form }}` is rendered? Can you add `form.html` code in question? – shaik moeed May 21 '19 at 06:45
  • Try `.add_errors()` method. Check in `.non_field_errors()` is it having any errors raised. – shaik moeed May 21 '19 at 07:43
  • try to replace `{{ cart_product_form }}` this with `{{ form }}` and check – shaik moeed May 21 '19 at 07:48
  • I did also try with add_errors() method as detailed in the Django docs but I didn't see anything. I see the non_field_errors in the documentation but no explicit example is shown. – Spinor8 May 21 '19 at 07:51
  • I shouldn't replace the cart_product_form with form. Everything works fine except for the dual-field validation. Besides cart_product_form is defined in my view.py for my product app. – Spinor8 May 21 '19 at 07:53
  • `{{ cart_product_form.non_field_errors }}` add this in template and check – shaik moeed May 21 '19 at 08:26
  • @shaikmoeed Great news. I got it to work with non_field_errors. Do you want to write it up and I will accept it. Thanks. – Spinor8 May 21 '19 at 08:56
  • Great. Sure, I will post as answer. – shaik moeed May 21 '19 at 09:06

2 Answers2

2

Adding this line in form template has cleared your issue.

{{ cart_product_form.non_field_errors }}

product/detail.html:

{% extends "shop/base.html" %}
{% load static %}

{% block title %}
{{ product.name }}
{% endblock %}

{% block content %}
<div class="product-detail">
    <img src="{% if product.image %}{{ product.image.url }}{% else %}{% static "img/no_image.png" %}{% endif %}">
    <h1>{{ product.name }}</h1>
    <h2><a href="{{ product.category.get_absolute_url }}">{{ product.category }}</a></h2>
    <p class="price">${{ product.price }}</p>
    <form action="{% url "cart:cart_add" product.id %}" method="post">
    {{ cart_product_form }}
    {% csrf_token %}
    {{ cart_product_form.non_field_errors }} // This line will raise validation errors
    <input type="submit" value="Add to cart">
    </form>
    {{ product.description|linebreaks }}
</div>
{% endblock %}

Doc:(Copied from official documentation)

Note that any errors raised by your Form.clean() override will not be associated with any field in particular. They go into a special “field” (called all), which you can access via the non_field_errors() method if you need to. If you want to attach errors to a specific field in the form, you need to call add_error().

shaik moeed
  • 5,300
  • 1
  • 18
  • 54
0

Your view is inconditionnally redirecting to cart_details so no surprise you don't see the validation errors - you'd have to render the invalid form for this. You should only redirect when the post succeeded.

bruno desthuilliers
  • 75,974
  • 6
  • 88
  • 118
  • Yeah, I know that. That's what the diagnostic print() is showing. – Spinor8 May 21 '19 at 07:41
  • I'm afraid you don't get the point : **how do you expect the validation errors to show up if you dont render the form and just throw it away ???** Do you think they will automagically survive somewhere in the ether and reappear in the next GET request ??? – bruno desthuilliers May 21 '19 at 07:54
  • No point getting all worked up and discourteous. I was looking for a solution that validated without allowing the form to post which is what is done when required=True is set. Now, it seems that is not possible. I am open to suggestions as to how best to persist the error message for the next GET request. The code posted above is the current state of the code which we know doesn't work. – Spinor8 May 21 '19 at 08:05