ModelChoiceField returns a tuple in select options

Hello, I have a model with 2 tables

Employee
Manager

each employee has a field “manager” thats a foreign key to Manager table

Employee model


class Employee(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, blank=True, null=True, related_name="user_object", db_index=True)
    manager = models.ForeignKey(Manager, on_delete=models.SET_NULL, blank=True, null=True, related_name="manager")
    first_name = models.CharField(max_length=30, null=True, verbose_name="employee first name")
    last_name = models.CharField(max_length=40, null=True, verbose_name="employee last name")

the EmployeeForm looks like this


class EmployeeForm(forms.ModelForm):
    dob = forms.DateField(widget=DateInput(format="%Y-%m-%d"), input_formats=("%Y-%m-%d",))
    manager = forms.ModelChoiceField(queryset=Manager.objects.filter(groups__name="manager1").values_list("first_name", "last_name"))

    def __init__(self, *args, **kwargs):
        super(EmployeeForm, self).__init__(*args, **kwargs)
  
    class Meta:
        model = Application
        exclude = ("on_hold")

the form is genearated in template and creates an employee form with a select field of Managers who match group “manager1”

The problem is that the Manager select field shows a tuple like this,

Select your Manager:

  • (‘Joe’, ‘Smith’)
  • (‘Fred’, ‘Jones’)

etc

How can I format it so it shows first and last names normally and not show a tuple in the option value?
ie

  • Joe Smith
  • Fred Jones
    etc

my template field is genearated like this

<div class="row mt-3">
  <div class="col"><label>Your Manager </label></div>
  <div class="col">{% bootstrap_field form.manager show_label=False %}</div>
</div>

ps - I also noticed that the generated Select field option is a tuple, not the ID of the manager,

<option value="('Joe', 'Smith')">('Joe', 'Smith')</option>

I want it to be like this, ie, the PK of the Manager as the value

<option value="26">Joe Smith</option>

Hey there!
I think that what’s wrong is that ModelChoiceField expects a model instance, not a tuple. If you want to use this method, you’re going to need to either:

  • Use ChoiceField instead; In the current example, first name is going to be the id, and the last_name the value.
  • Subclass ModelChoiceField; I suggest that you read this documentation about the ModelChoiceField to instructions on how to do that.

I got it to work like this

  1. changed the model, manager = One To One field,
class Employee(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, blank=True, null=True, related_name="user_object", db_index=True)
    manager = models.OneToOneField(Manager, on_delete=models.SET_NULL, blank=True, null=True, related_name="manager")
  1. manually created select choices from a list of tuples

class EmployeeForm(forms.ModelForm):
    dob = forms.DateField(widget=DateInput(format="%Y-%m-%d"), input_formats=("%Y-%m-%d",))
 
    def __init__(self, *args, **kwargs):
        super(EmployeeForm, self).__init__(*args, **kwargs)
        query = Manager.objects.filter(groups__name="group1")
        choices = []
        for m in query:
            choices.append((m.id, f"{m.first_name} {m.last_name}"))
   
        self.fields["manager"].choices = choices
  
    class Meta:
        model = Application
        exclude = ("on_hold")

ugly but it works

Does it work though? Each Manager is only going to have one Employee?

you maybe right, each employee can only have 1 manager but each manager can have multiple employees,

ill set it to models.ForeignKey

Regarding the construction of the selection list, within the section of the docs linked above, see the section on iterator for defining a function to be called to generate that list if you don’t wish to use the __str__ function.

To change the display format of the options in your ModelChoiceField and have the primary key of the Manager model as the value of the selected option, you can modify the init() method of your EmployeeForm as follows:

class EmployeeForm(forms.ModelForm):
dob = forms.DateField(widget=DateInput(format=“%Y-%m-%d”), input_formats=(“%Y-%m-%d”,))
manager = forms.ModelChoiceField(queryset=Manager.objects.filter(groups__name=“manager1”), label=“Select your Manager”)

def __init__(self, *args, **kwargs):
    super(EmployeeForm, self).__init__(*args, **kwargs)
    self.fields['manager'].label_from_instance = lambda obj: f"{obj.first_name} {obj.last_name}"
    self.fields['manager'].to_field_name = 'id'

Explanation:

label_from_instance is a method of the ModelChoiceField that allows you to customize the display format of each option. It takes a callable that accepts a model instance and returns the string to display for that instance in the dropdown. In this case, we are using a lambda function that returns the formatted full name of the Manager instance.
to_field_name is an attribute of the ModelChoiceField that specifies the name of the field to use as the value of each option. By default, it uses the primary key of the model. In this case, we are setting it to ‘id’ to use the primary key of the Manager model as the value of each option.

1 Like