How to properly validate a form with radio buttons

I’ve got a relatively simple two field form that I can’t seem to properly validate. The meal_type field is a radio button with three choices (Breakfast, Lunch, & Dinner). The date field is just that: date only, no time.

What the current code does when Save is clicked:

  • If no radio button is checked, the error message is the default “Please select one of these options”. I’ve not yet figured out how to change that message.
  • If a radio button is checked, e.g., Dinner, the error message is " Select a valid choice. 1 is not one of the available choices.. I’ve not yet figured how how to change that message either.

Current code, such as it is:
models.py:

...
MEAL_CHOICES = [
    (3, 'Breakfast'),
    (2, 'Lunch'),
    (1, 'Dinner'), 
]

def validate_meal_choice(value):
    if value not in [1, 2, 3]:
        raise ValidationError('Meal type not selected')

class Meal(models.Model):
    meal_type = models.CharField(
        choices=MEAL_CHOICES,
        max_length=9,
        default = None,
        validators=[validate_meal_choice]
        )
    date = models.DateField()
    foods = models.ManyToManyField(Food)

    def __str__(self):
        if self.meal_type == '1':
            return 'Dinner'
        elif self.meal_type == '2':
            return 'Dinner'
        else:
            return 'Breakfast'

    def save(self, *args, **kwargs) :
        print('are we ok?')
        return super().save(*args, **kwargs)

    class Meta:
        db_table = "meal"
        ordering = ['-date', 'meal_type']
        verbose_name = 'Meal type'
        unique_together = [['meal_type', 'date']]
        # constraints = [
        #     models.UniqueConstraint(
        #         fields=['meal_type', 'date'], 
        #         condition=Q(date__date__gt=datetime.date(2005, 1, 1)),
        #         # condition=Q('meal_type_in["1", "2", "3"]'),
        #         name='unique_meal_date', 
        #         # violation_error_message="Meal already created" 
        #     )]


class MealForm(ModelForm):
    class Meta:
        model = Meal
        fields = ['meal_type', 'date']    
        widgets = {
            'meal_type' : forms.RadioSelect(),
            'date' : DatePickerInput}
        
    def clean_date(self):
        print('so far, date check')
        beginning = Meal.objects.all()[0].date
        data = self.cleaned_data['date']
        if data < beginning:
            raise ValidationError("Date is earlier than earliest meal")
        return data
    
    def clean_meal(self):
        print('so far, meal check')
        ml = self.cleaned_data['meal_type']
        if ml == "":
            raise ValidationError("Date is earlier than earliest meal")
        return ml

views.py:

...
def mealNew(request):
    if request.method == 'POST':
        form = MealForm(request.POST)
        if form.is_valid():
            print('is this really good?')
        else:
            print('failed post')
    else:
        form = MealForm()
        print('not posted!')
    body = 'meal/new_meal_form.html'
    context = {
        'body' : body,
        'title' : 'New Meal',
        'form' : form
    }
    return render(request, 'base.html', context)

First, as a side-note:

This is not how you should be doing this.

You should be rendering new_meal_form.html, and it should be extending base.html.

Now, regarding the error messages, the first message about not selecting an option might be generated within the browser itself.
When these errors are showing up, are you seeing the POST request being made to the server? If not, it’s the client-side doing this validation.

First, I’ve sorted out the rendering - thanks for the pointer.

And, no, I’m not seeing either of the method == 'POST' messages, so I must be still stuck on the client side.

Don’t only look for the printed output. What you want to look for are POST messages from your server console. You should see the GET requests when the form is requested, and then the POST requests when the forms are submitted. (There are things that can happen between when the POST is issued and when your view is called.)
You could also check your browser’s developer tools on the network tab to see if the request was made.

Anyway, under the assumption that this validation is being done in the browser, we’d need to know if you are specifically loading any JavaScript libraries to perform that validation, and if so, which ones. (That would also be the key toward identifying what needs to be configured to replace those messages.)

Firefox reports a POST. After POST are several GETs of *.min.js. (bootstrap.bundle, moment-with-locales, bootstrap-datetimepicker), and datepicker-widget.js.

If that is a POST of the form, and the form fails the is_valid test, you should also see another GET for the form as it rerenders the page.

There is one thing I do see that may be affecting this:

You have:

identifying meal_type as a string.

However, your validate:

and your choices:

Define the options as an integer.

You either want to make the field an integer field, or change your choices - and I would really recommend the latter. (You don’t gain anything by using an integer here, unless there’s some specific ordering you want to enforce by the choices. Even then I would typically recommend using more descriptive values for the choices.

Additionally, you don’t need to specify your own validator for a select-field. That’s part of what Django does for you - which is most likely why you’re not seeing your messages from your validator. (Django is rejecting the form before you see it.)

Re: integer vs string: I’ve tried both ways. When the choices are strings, the new_meal_form (presented earlier) behaves OK. But the page index does not. Its rendering of the choices is numerals, even though the template uses {{ meal.get_meal_type_display }}. Choices are rendered as numerals regardless of the template. So maybe that’s the issue I should be chasing. Here’s a sample plus some code:

sample:

Date 	Meal
Friday, January 20, 2023 	1
Friday, January 20, 2023 	2
Friday, January 20, 2023 	3

index.html

{% extends 'base.html' %}
{% block body %}
<div class="row">
    <div class="col-4"></div>
    <div class="col-4">
        <h3>Meals to date</h3>
        {% if meals is not null %}
        <div class="overflow-auto" style="height: 500px;">
            <table class="table">
                <thead>
                    <th>Date</th>
                    <th>Meal</th>
                </thead>
            {% for meal in meals  %}
                <tr>
                    <td>{{ meal.date|date:'l, F j, Y' }}</td>
                    <td>{{ meal.meal_type }}</td>            {# renders numerals regardless of get_..._display #}
                </tr>
            {% endfor %}
            </table>
        </div>
        {% else %}
            <h4>No meals entered</h4>
        {% endif %}
    </div>
</div>
{% endblock %}

views.py:

...
def mealIndex(request):
    allMeals = Meal.objects.order_by('-date', 'meal_type')
    context = {'meals' : allMeals} 
    return render(request, 'meal/index.html', context)

I don’t understand what you’re trying to say here.

Character strings are not rendered with quotes in any normal circumstance. What you see on the page has no relevance to how it’s stored internally. What you’re showing is what I would expect to see.

Surely I’m missing something here. The index page should be able to list the date and the meal, as in

January 20, 2023 Dinner

Why is it expected that one could only get January 20, 2023 1? Especially when what the index page renders depends on whether the choice is an integer (shows Dinner) or string (shows 1). You have me stumped.

Ok, let’s take a step back.

You have:

Each choice has two parts.

For example, the first entry is (3, 'Breakfast').

The string ‘Breakfast’ is what is used in the display of the widget, only.

The integer, 3, is used for all internal purposes.

If you want to render the string, you would need to get the appropriate entry from that list.

This sort of situation is why people generally take other approaches:

  • Use an Enumeration type

  • Use the character string for both the internal and external representation (e.g. ('Breakfast', 'Breakfast') - what I was referring to in my earlier response)

  • Use a “code table”, where these entries are stored in a different table and referenced as a foreign key. (Actually, my personal preference 90% of the time.)

This “default” mechanism is best when you need to have a specific internal representation that is different from the external display. If you don’t have a specific need for those to be different, you’re frequently best off using the same value for both.

The primary reason for not using, e.g., 'Breakfast', 'Breakfast' is to have a sort order so that Dinner appears before Lunch, etc., in a date descending list. Is there an alternative mechanism to gain a sort order for text items that aren’t to be sorted alphabetically? Or is that what a ‘meal_type’ table would do?

That makes a lot of sense.

If you want to keep those values as numeric or ordered, you can create your own reference dict.
e.g.

MEAL_CODES = {1: 'Dinner', 2: 'Lunch', 3: 'Breakfast'}

(This is in addition to your MEAL_CHOICES, not instead of.)

This gives you a way to reference the strings associated with a value directly. e.g.
MEAL_CODES[meal_type]

The code_table provides essentially the same idea. If meal_type is a ForeignKey to a MealType model, then the meal_name may be referenced as meal_type.meal_name

Since the project is an in-house, single user item I’ve settled on a brute force method:

<td>{% if meal.meal_type == 1 %}Dinner
{% elif  meal.meal_type == 2 %}Lunch
 {% else %}Breakfast{% endif %}</td>

While I knew this worked I tried to implement a replacement with MEAL_CODES. Too much time rummaging about in that rabbit hole.

Still, many thanks for your help.