Search code examples
djangodjango-modelsdjango-formsdjango-widget

Get all records data to a Widget without making a new Query in the database


I have 2 Models Product and Category with - ManyToMany Relation Product-Category and self ForeignKey for Category

 class Category(models.Model):
    parent = models.ForeignKey('self', blank=True, null=True, verbose_name='parent category', on_delete=models.CASCADE)

class Product(models.Model):
    categories = models.ManyToManyField(Category)
    name = models.CharField(max_length=255)

and the Product creation form:

class ProductModelForm(ModelForm):

        class Meta:
            model = Product
            fields = ['categories', 'name', 'short_description', 'description']
            widgets = {
                'categories': MyWidget,
            }

By default ManyToMany becomes ModelMultipleChoiceField and coresponds to SelectMultiple Widget.

I'm creating a new widget inheriting from SelectMultiple.

My issue is that in all widgets by default Django send from the record just the value defined inside def __str__ on the Model. In case of Select also pk.

(None, [{'name': 'categories', 'value': 7, 'label': 'Category 2', 'selected': True, 'index': '1', 'attrs': {'selected': True}, 'type': 'select', 'template_name': 'django/forms/widgets/select_option.html'}]

I need to have in the Widget also other information like the parent value.

Off course I can make another query and get the data from the database, but that means I have 2 queries one done by me and one by Django.

Is there any way to make Django To send all record data to the widget instead of just what was defined in __str__ ?

Changing __str__ is not an option because it use also in other places and in admin.


Solution

  • That's not very easy: A ModelChoiceField (and ModelMultipleChoiceField) takes a queryset parameter that by default is the model's default_manager (Category.objects.all()). The queryset is consumed when the choices are produced for the widget, using the ModelChoiceIterator. That indeed only returns a 2-tuple of the object's id and label for each object in the queryset.

    So to make sure there's only one query to the database you'd have to subclass ModelMultipleChoiceField to use a subclass of ModelChoiceIterator which returns a 3-tuple (or more) with the parameters you need. Then make sure you also subclass the __init__() method of ChoiceWidget (in your case SelectMultiple) to split the tuple again into a 2-tuple to assign to self.choices and the other values to set as widget attributes (self.attrs) so you can use them in your template.

    Not sure this is worth the optimisation you get, especially if you use a good caching mechanism you wouldn't actually hit your db twice if you perform Category.objects.all() two times.