'Please choose correct value' in ModelForm

I have a model have ForeignKey and I want to use HTML <datalist> in ModelForm.
But the value in <option> should be key of ForeignKey, not the __str__ or something else.
I can use label for <option> but, it rendered different by browser.
So, I tried to override clean or clean_<fieldname> method, but I don’t know how to handle input from there.
How can I intercept form input and fix it to correct value?

models.py

from django.conf import settings
from django.db import models


# Create your models here.
class Schedule(models.Model):
    srl = models.BigAutoField(
        primary_key=True,
        verbose_name="Serial",
    )
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        verbose_name="User",
    )
    branch = models.ForeignKey(
        "branches.Branch",
        on_delete=models.CASCADE,
        verbose_name="Branch",
    )
    date = models.DateField(
        verbose_name="Date",
    )
    period = models.DecimalField(
        max_digits=2,
        decimal_places=0,
        verbose_name="Period",
    )

    class Meta:
        verbose_name = "Schedule"
        verbose_name_plural = "Schedules"
        ordering = [
            "branch",
            "date",
            "period",
        ]
        unique_together = [
            "user",
            "date",
            "period",
        ]

    def __str__(self):
        if self.user.gender == "M":
            gender_short = "M"
        elif self.user.gender == "F":
            gender_short = "F"

        return f"{self.branch} {self.date.strftime('%y%m%d')} {self.period}th {self.user.name}{self.user.birthday.strftime('%y%m%d')}{gender_short}"

forms.py

import datetime

from django import forms
from django.forms import ModelChoiceField, ModelForm

from branches.models import Branch
from schedules.models import Schedule
from timetables.models import Timetable
from users.models import User


class ScheduleForm(ModelForm):
    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop("user")
        super(ScheduleForm, self).__init__(*args, **kwargs)
        if self.user.superuser:
            self.fields["branch"] = ModelChoiceField(
                queryset=Branch.objects.all(),
                required=True,
                label="Branch",
                widget=forms.Select(
                    attrs={
                        "class": "form-select",
                    }
                ),
            )
        else:
            self.fields["branch"] = ModelChoiceField(
                queryset=Branch.objects.filter(branch=self.user.branch),
                required=True,
                label="Branch",
                widget=forms.Select(
                    attrs={
                        "class": "form-select",
                    }
                ),
            )

    def clean(self):
        cleaned_data = super(ScheduleForm, self).clean()
        user_input = cleaned_data["user"]
        name = user_input.split(" ")[0]
        user_info = user_input.split(" ")[1]
        user_info = user_info.strip("()")
        user_info = user_info.split("/")
        branch = Branch.objects.get(name=user_info[0])
        birthday = datetime.datetime.strptime(user_info[1], "%y%m%d")
        if user_info[2] == "M":
            gender = "M"
        else:
            gender = "F"
        user = User.objects.get(
            name=name,
            birthday=birthday,
            gender=gender,
            branch=branch,
        )
        cleaned_data["user"] = user
        return cleaned_data

    class Meta:
        model = Schedule
        fields = (
            "branch",
            "user",
            "date",
            "period",
        )
        widgets = {
            "user": forms.TextInput(
                attrs={
                    "class": "form-control",
                    "list": "user-list",
                },
            ),
            "date": forms.DateInput(
                attrs={
                    "type": "date",
                    "class": "form-control",
                },
            ),
            "period": forms.NumberInput(
                attrs={
                    "class": "form-control",
                    "min": 1,
                },
            ),
        }

views.py

...
class ScheduleCreateView(LoginRequiredMixin, CreateView):
    model = Schedule
    form_class = ScheduleForm
    success_url = reverse_lazy("schedules:list")
    login_url = reverse_lazy("users:login")

    def get_form_kwargs(self):
        kwargs = super(ScheduleCreateView, self).get_form_kwargs()
        kwargs.update({"user": self.request.user})
        return kwargs

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["page_title"] = "Add Schedule"
        if self.request.user.superuser:
            context["user_list"] = User.objects.all()
        else:
            context["user_list"] = User.objects.filter(branch=self.request.user.branch)

        return context

I add print(locals()) to clean() method after super().clean() and there is no user.
What’s the problem?

How are you rendering the datalist? What does your template for that look like?

I render datalist with including list of QuerySet of User objects in get_context_data() in views.py like the code below.

def get_context_data(self, **kwargs):
    context = super().get_context_data(**kwargs)
    context["page_title"] = "Add schedule"
    if self.request.user.superuser:
        context["user_list"] = User.objects.all()
    else:
        context["user_list"] = User.objects.filter(branch=self.request.user.branch)

    return context

Here is the form template that I’m using.
schedule_form.html

<!DOCTYPE html>
{% extends "main/base.html" %}
{% load static %}
{% block content %}
    <form action="" method="POST" class="container d-grid gap-3 justify-content-evenly align-items-center">
        {% csrf_token %}
        {{ form.non_field_errors }}
        <div class="row">
            <label class="col-3 col-form-label text-nowrap" for="{{ form.user.id_for_label }}">{{ form.user.label }}</label>
            <div class="col">{{ form.user }}</div>
            {{ form.user.errors }}
        </div>
        
        {% if user_list %}
            <datalist id="user-list">
                {% for user in user_list %}
                    <option value="{{ user }}">
                {% endfor %}
            </datalist>
        {% endif %}
        
        <div class="row">
            <label class="col-3 col-form-label text-nowrap" for="{{ form.branch.id_for_label }}">{{ form.branch.label }}</label>
            <div class="col">{{ form.branch }}</div>
            {{ form.branch.errors }}
        </div>
        
        <div class="row">
            <label class="col-3 col-form-label text-nowrap" for="{{ form.date.id_for_label }}">{{ form.date.label }}</label>
            <div class="col">{{ form.date }}</div>
            {{ form.date.errors }}
        </div>
        
        <div class="row">
            <label class="col-3 col-form-label text-nowrap" for="{{ form.period.id_for_label }}">{{ form.period.label }}</label>
            <div class="col">{{ form.period }}</div>
            {{ form.period.errors }}
        </div>
        
        <div class="row">
            <div class="col d-flex justify-content-end">
                <button class="btn btn-primary" type="submit">저장</button>
            </div>
        </div>
    </form>
{% endblock %}

For your information, I tried replacing <option value="{{ user }}"> with <option value="{{ user.srl }} label={{ user }}">.
This works well without overriding any method, but this one shows user.srl as major and __str__ of User object as additional information in MS Edge,

I’m still not quite following what you want to have happen here.

What do you want the <option> elements in the datalist to look like? (Specifically - please post the html of an option element as you would want it rendered.)

I want to keep <option value="{{ user }}"> this one for cosmetic reason.
For example, let’s assume there is an object of the user ‘John Doe’, who belong to branch 1 and his birthday is 2022-09-02.
This object’s pk or srl will be 1 here.

There is def __str__ defined in the User model like this

def __str__(self):
    return f"{name} ({branch}/{birthday}/{gender})"

When users try to add schedule, datalist shows list like John Doe (Branch 1/220902/Male).
But Django can’t handle this directly because the value of the option is string, not the User object or pk of User object.
So, what I know that can handle this is change HTML option tag into <option value="{{ user.srl }} label={{ user }}"> this.
But this not showing list like I want.
This shows 1 instead of John Doe (Branch 1/220902/Male).
I tried to override clean() to change John Doe (Branch 1/220902/Male) this to correct User object, but form doesn’t pass this input.


In conclusion, what I want in HTML is <option value="{{ user }}">.
But I don’t know how to handle this in Django.

First, you can’t, under any circumstance, pass an “object” through the template to the browser. It’s always going to be a string. Whether that string is a representation of the object or a field of the object doesn’t matter. You’re always passing a string through the form.

If you define your form field to be a CharField that is not directly associated with the model, the form will return that string to the form.

In other words, you need to define that field as an “extra” field and not as one of the fields automatically created by the model form. Remove it from the fields list and remove the widgets definition from Meta. Define it as a normal form field as a CharField to accept the input, and then in your processing of the form, find the related user object and set the Model field to the corresponding value.

Can you check my understanding is correct?
I can’t do what I want with current code (the one that I uploaded here).
So, I need to remove user from Meta.
Next, I need define other field to get input from the user in the __init__ of ModelForm.
Then, I can handle this extra field with clean_<field>() or something.
This makes using datalist available.
Right?

A ModelForm is a Form.

It is a form where Django creates some fields automatically based upon the Model defined for it.

A form with a ForeignKey field in the form is expecting to get the PK of that related object.

You are not supplying the PK.

Therefore, you must not use that field in your form as defined by the model.

Which means:

Yes, that’s what you need to do.

You need to define what you’re going to accept as a field just like in any other form.

I don’t know what you are trying to say by this.

You have at least three ways of handling this that I can see.

  • clean_user() - you’re still going to need to assign the input to the model’s field for that object.
  • to_python() for that field - you could define that field as a ForeignKey field using a TextInput widget, and use the to_python method on that field to convert the text string input to the ForeignKey reference.
  • In the view after the form has been processed - find the FK value to assign it to the model to be saved.

In the way that you want to use it yes.

Isn’t it the place that I should define extra field like

class ScheduleForm(ModelForm):
    def __init(self):
        self.fields["user"] = forms.TextField()

this?

No.

How would you define any field in any normal form?

Ah.
Just user = forms.TextField()
Right?

Correct.
(At the class level, not within a function or inner class)

Thanks.
I will try it.


Thank you very much.
Thanks to your answer, myt problem has solved.