简体   繁体   中英

Django ModelChoiceField dynamic queryset from instance

I have a dataset where users may need to edit old records but also choices need to be limited to a set of "active" values. This means the legacy choices need to be added to the queryset of the ModelChoiceField when an instance containing a non-active value is selected.

In my mind the ideal way to do this would be to subclass ModelChoiceField however I cannot seem to find the insertion point of instance data into ModelChoiceField. I am able to make it work by setting the queryset in forms.py but I have a large number of fields this is needed for and was really hoping for a more pythonic/DRY solution.

For example:

models.py

class ActiveChoiceModel(models.Model):
    name = models.CharField(max_length=10)
    active = models.NullBooleanField(default=False)

class MyModel(models.Model):
   fk_activechoicemodel = models.ForeignKey(to='mydb.ActiveChoiceModel')

The queryset for the ModelChoiceField should be:

    ActiveChoiceModel.objects.filter(active=True) | ActiveChoiceModel.objects.filter(pk=instance.fk_activechoicemodel.id)

This can be achieved in forms.py as:

 Class MyForm(forms.ModelForm):
     def __init__(self, *args, **kwargs):
         super(MyForm, self).__init__(*args, **kwargs)
         if self.instance.fk_activechoicemodel:
             self.fields['fk_activechoicemodel'].queryset = ActiveChoiceModel.objects.filter(active=True) | ActiveChoiceModel.objects.filter(pk=instance.fk_activechoicemodel.id)

Any thoughts on how to do this clean and DRY for 10's or 100's of 'ActiveChoiceModels'?

Option 1

I think the best solution is a custom models.Manager to get the special queryset, but the real work is done with a Q expression, so if you don't have to pull this queryset anywhere else, it might be possible to only use the Q expression. Not having your code I haven't tested this, but its modified from a custom manager that I'm running, plus this answer.

# models.py
from django.db import models
from django.db.models import Q

class AvailableChoiceManager(models.Manager):
    """Active choices + existing choice even if its now inactive.
    """
    def get_queryset(self, pk=None):
        qs = super().get_queryset()  # get every possible choice
        current_choice = ChoiceModel.objects.get(pk=pk):
        return qs.filter((Q(pk=pk) | Q(active=True))

In your model definition, you need to add an instance of this manager:

    #models.ActiveChoiceModel
    AvailableChoices=AvailableChoiceManager()

Invoke with:

qs = AvailableChoiceModel.AvailableChoices(pk=pk)

Option 2.

get a list of the active choices:

  choice_list = list(Choices.objects.filter(active=True).values_list('id', 'slug'))
  if fk.pk not in choice_list:      
      choice_list.insert(0, (fk.pk, fk.slug))

set the choices attribute of the widget to choices_list:

self.fields['fk'].widget = forms.Select(
            choices=choice_list,
            attrs={'pk': 'lug', 'size': 7, 'style': "width:100%"})

I am not sure there is a way to do this with less than three or four lines of code, but I don't see that as a lot.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM