Determine ChoiceField choices based on the field's current value

I have the following model for a “Task:”

class Task(models.Model):
    """Model representing an Employee task."""
    short_description = models.CharField(max_length=100)
    detailed_description = models.TextField()
    assignee = models.ForeignKey(Employee, on_delete=models.CASCADE)
    start_date = models.DateTimeField(default=timezone.now)
    due_date = models.DateTimeField()

    TASK_STATUS = (
        ('o', 'Open'),
        ('i', 'In progress'),
        ('h', 'Impeded'),
        ('c', 'Completed'),
    )

    status = models.CharField(
        max_length=1,
        choices=TASK_STATUS,
        blank=True,
        default='o',
        help_text='Current status of the task',
    )

I’m working on creating a view that doubles as a “detailed view” for the task instance and a sort of “update view” that will allow users to update only the status of the task when needed. The other task fields are not meant to be updated after creation of the task instance.

For the form that will be loaded by the view to allow users to update status, I’m looking to make it such that only certain ChoiceField choices are made available depending on the current value of the status field in the task instance. For example, if the current status of a task is “Impeded” only “In progress” should be shown as an option. If the current status is anything other than “Open,” then “Open” will no longer be shown as a task can never return to that state.

What’s the best way to create a form where available ChoiceField choices are determined dynamically based on the current value of the field in the model instance being updated? Would it be to do something like this below:

class TaskStatusUpdateForm(forms.Form):
    updated_status = forms.ChoiceField(help_text="Enter the updated status of this task")

    def __init__(self, *args, **kwargs):
        super(TaskStatusUpdateForm, self).__init__(*args, **kwargs)
        self.fields['updated_status'].choices = some_function(object_instance)

You could do it that way.

Or, you could set up a related table with the status and the statuses related to it, and change your field to be a ModelChoiceField with a queryset that limits the selections to the valid options.

The second approach sounds like it might be more suited to what I’m trying to do here, particularly in terms of extensibility. How would I go about setting up the related table? I’m just getting started with django so I’m still getting familiar with some of the concepts.

Well, this topic really isn’t Django specific. Shift your mindset away from Django for the moment and think about this in more general terms.
How might you design a table such that you can identify an “X” based upon “Y”?

This was the approach I ultimately ended up taking:

def get_status_choices(instance):
    status = instance.status

    if status == 'o':
        choices = (
            ('o', 'Open'),
            ('i', 'In progress'),
            ('h', 'Impeded'),
            ('c', 'Completed'),
        )
    elif status == 'i':
        choices = (
            ('h', 'Impeded'),
            ('c', 'Completed'),
        )
    elif status == 'h':
        choices = (
            ('i', 'In progress'),
        )
    else:
        choices = (
            ('c', 'Completed'),
        )
    return choices

class TaskStatusUpdateForm(forms.Form):
    updated_status = forms.ChoiceField(help_text="Enter the updated status of this task")

    def __init__(self, *args, **kwargs):
        self.instance = kwargs.pop('instance')
        super(TaskStatusUpdateForm, self).__init__(*args, **kwargs)
        self.fields['updated_status'].choices = get_status_choices(self.instance)
    
    def clean_updated_status(self):
        new_status = self.cleaned_data['updated_status']
        if self.instance.status == new_status:
            raise ValidationError(_('Ivalid status - New status must be different from old status.'))
        
        return new_status

def TaskDetailView(request, pk):
    task_instance = get_object_or_404(Task, pk=pk)

    if request.method == 'POST':

        form = TaskStatusUpdateForm(request.POST, instance=task_instance)

        if form.is_valid():
            task_instance.status = form.cleaned_data['updated_status']
            task_instance.save()

            return redirect('list-tasks')
    
    else:
        form = TaskStatusUpdateForm(initial={'updated_status': task_instance.status}, instance=task_instance)
    
    context = {
        'form': form,
        'task_instance': task_instance,
    }

    return render(request, 'tasks/detail_update.html', context=context)

In my testing so far, it seems to work as expected. The only part I was unsure of was passing the model instance to the form for use in the clean_updated_status function, but it seems to work properly.