0

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'?

Austin Fox
  • 121
  • 1
  • 8

2 Answers2

2

Option 1

I think the best solution is a custom models.Manager to get the special queryset. However, the real work is done with a Q object, so if you don't have to pull this queryset anywhere else, it might be possible to only use the Q object. Not having your code I haven't tested this, but it's 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 it's 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 choice_list:

self.fields['fk'].widget = forms.Select(
            choices=choice_list,
            attrs={'pk': 'lug', 'size': 7, 'style': "width:100%"})
lauryn
  • 3
  • 1
  • 3
Atcrank
  • 439
  • 3
  • 11
  • thanks! I like where this is going but it will still require almost all the code in forms to still be present. Is there a way to get the instance in the manager with out explicitly having to pass pk? If it were possible the default manager could be overwritten making this the default behavior which would be great for my case. – Austin Fox Aug 07 '18 at 00:42
  • I'm pretty sure you will be able to overload the .objects. Manager in the form just by overwriting 'objects' instead of using the new name. I'll add a little bit to the answer about just inserting the foreignkey value. – Atcrank Aug 07 '18 at 06:32
  • If you have a lot of models and hence modelforms that will need to have similar management, the DRY way is to put the repeated code in a new subclass definition for either models (overwrite objects) or modelforms (change the queryset), and then subclass that for all your models, instead of going back to models.Model or forms.ModelForm for each. Without knowing about all those other models and how similar the cases are, I may not be helping with these suggestions. – Atcrank Aug 07 '18 at 06:49
  • Overwriting objects in models would be great (can handle many models in a single form) except that I don't see how pk can be passed to the manager without doing it explicitly (it would be great to not have to do anything special in forms.py). Is there a way? The implementation in option 1 will leave pk as None unless you directly invoke it. (On that the current_choice definition seems unneeded ). All the other models are basically the same as the ActiveChoiceModel as in my implementation it is actually an abstract class they all inherit. – Austin Fox Aug 07 '18 at 16:46
  • I thought the design of modelforms binds them to a single class in the 'class Meta:' If the modelform can infer what its model is from the instance, I could save myself a ton of code, but how could it raise a blank form, and can it infer type from POSTed data? My understanding is as per this answer - you define a modelform class for each model: https://stackoverflow.com/questions/11101651/django-forms-having-multiple-models-in-meta-class . The Manager object is probably flexible enough to use any model subclass. If you change the Manager in your ABC all the subclasses will inherit... – Atcrank Aug 07 '18 at 23:09
1

I think what you need is limit_choices_to eg:

fk_activechoicemodel = models.ForeignKey('mydb.ActiveChoiceModel',limit_choices_to={'active': True}, on_delete=models.CASCADE)

this condition will be applied in the select field or others in the form. so it will take values from model ActiveChoiceModel only if its active field's value is True. You can add other conditions also.

Another way is by using django-autocomplete. you can add your condition in a view just like filtering a queryset and it will be loaded dynamically so that 1000 instances won't take any time.

Pulath Yaseen
  • 395
  • 1
  • 3
  • 14