Dynamic choices in Django ChoiceField set based on object field value


(Josh Diblin) #1

Hello, I am trying to set the choices in a form used in a Django generic update view to change based on the value of a field in the current object being updated. In forms.py below, I am trying to replace CHOICES with a callable that takes the value of current_status for the object being updated and returns a list of tuples based on the argument it is passed. E.g. if current_status = 1, then I want to the callable to return [(2, Choice 2)]

Is this possible? I am having trouble figuring out how to do this. Thanks! Below are snips from my current code.

forms.py

from django import forms
from .models import BatchRecord

CHOICES = [
      (1, 'Choice 1'),
      (2, 'Choice 2'),
]
      
class BatchRecordChangeStatusForm(forms.ModelForm):
  current_status = forms.ChoiceField(choices=CHOICES)     
  class Meta:
      model = BatchRecord
      fields = ['current_status']

views.py

class BatchRecordChangeStatusView(UpdateView):
  model = BatchRecord
  form_class = BatchRecordChangeStatusForm

urls.py

urlpatterns = [
  path('changestatus/<int:pk>/', BatchRecordChangeStatusView.as_view(), name='changestatus'),
]

models.py

class BatchRecord(models.Model):
  STATUS_CHOICES = (
      (1, 'Creation in Progress'),
      (2, 'Ready for Production'),
      (3, 'In Production'),
      (4, 'Tier 1 Review'),
      (5, 'Tier 2 Review'),
      (6, 'Complete'),
  )
  current_status = models.IntegerField(choices=STATUS_CHOICES, blank=False, default=1)

(Josh Diblin) #2

This explains how to do what I needed


(Vitor Freitas) #3

Just for future reference, in case someone also needs it. Here is how I would implement it:

forms.py

from django import forms
from .models import BatchRecord

CHOICES = [
      (1, 'Choice 1'),
      (2, 'Choice 2'),
]

def get_current_status_choices(current_status):
    if current_status == 1:
        return [(2, 'Choice 2')]
    elif current_status == 2:
        return [(1, 'Choice 1')]
    else:
        return CHOICES

      
class BatchRecordChangeStatusForm(forms.ModelForm):
    current_status = forms.ChoiceField(choices=CHOICES)

    class Meta:
        model = BatchRecord
        fields = ['current_status']

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        current_status = None
        if 'current_status' in self.data:
            try:
                current_status = int(self.data.get('current_status'))
            except (ValueError, TypeError):
                pass
        elif self.instance.pk:
            current_status = self.instance.current_status

        if current_status is not None:
            self.fields['current_status'].choices = get_current_status_choices(current_status)

This implementation cover both cases where you are updating an existing object, but the form is not bound (with data). So we use the self.instance.current_status value.

But there are also the cases where you are creating an object, and the self.instance is None. But you have to also cover the cases where the form is being processed but is invalid, so Django will return a bound form to the view (in this case we can inspect the data attribute for the data that was posted from the client).


(Josh Diblin) #4

Thanks, Vitor. This way seems to be better for the reasons you mentioned. I also like that it only requires changing forms.py.

However, I did have to make one modification to make this work. Instead of passing current_status to get_current_status_choices, I needed to pass the object_id. When I tried using the current_status to create the list of choices, it would not only create a list of choices if the request method was GET but would then create a new choices list based on the input from the form.

For example, if current_status of the object being updated equals 1, the choice list should be current_status equal to 2. Then since the current_status in the form now equals 2 and we are using the value of current_status to set the choices, the only choice now available for when current_status equals 2 is status 3. Thus when the form is submitted we get an error.

I’m not quite sure exactly how this works, but this is what I experienced. I am not quite sure the technical details of what is happening as I am new to Django. Below is what I did… using the value of current_status in the object, rather than the value of current_status in the form. It seems to work but please let me know if you think there are any problems with this.

Maybe there is also a way to only execute the code in init if the request method is GET? I’m not sure how to do this though.

from django import forms
from .models import BatchRecord

def get_current_status_choices(object_id):
	if BatchRecord.objects.get(pk=object_id).current_status == 1:
		return [(2, 'Choice 2')]
	elif BatchRecord.objects.get(pk=object_id).current_status == 2:
		return [(3, 'Choice 3')]

class BatchRecordChangeStatusForm(forms.ModelForm):
	current_status = forms.ChoiceField(choices=CHOICES)
	
	def __init__(self, *args, **kwargs):
		super().__init__(*args, **kwargs)
		current_status = None
		if 'current_status' in self.data:
			try:
				current_status = int(self.data.get('current_status'))
			except (ValueError, TypeError):
				pass
		elif self.instance.pk:
			current_status = self.instance.current_status
			
		if current_status is not None:
   			self.fields['current_status'].choices = get_current_status_choices(self.instance.pk)