Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I get the grappelli autocomplete widget to work in place of a ModelMultipleChoiceField (in the one-to-many direction)?

Using the Django Grappelli admin tools, I can configure a ForeignKey (many-to-one) field to display as an autocomplete widget, rather than a drop down field, as follows:

class MyModel(models.Model):
    related = models.ForeignKey(RelatedModel, related_name='my_models')

class MyModelAdmin(admin.ModelAdmin):
    raw_id_fields = ('related',)
    autocomplete_lookup_fields = {
        'fk': ['related'],
    }

However, what I'd like to do is define autocomplete widget lookups in the other (one-to-many) direction (i.e. in the admin for the RelatedModel so I can lookup one or more MyModel objects). Right now, I'm just using a ModelMultipleChoiceField:

class RelatedModelForm(forms.ModelForm):
    class Meta:
        model = RelatedModel
        fields = ('my_models',)
    my_models = forms.ModelMultipleChoiceField(queryset=MyModel.objects.all())

    def __init__(self, *args, **kwargs):
        super(SaleAdminForm, self).__init__(*args, **kwargs)
        if self.instance:
            self.fields['foods'].initial = self.instance.foods.all()

    def save(self, *args, **kwargs):
        instance = super(RelatedModelForm, self).save(commit=False)
        self.fields['my_models'].initial.update(related=None)
        self.cleaned_data['my_models'].update(related=instance)
        return instance

class RelatedModelAdmin(admin.ModelAdmin):
    model = RelatedModel
    form = RelatedModelForm

However, there are just too many MyModel instances to work well with that type of widget. It would be ideal to just have one or more autocomplete lookup widgets for MyModel objects, in place of the ModelMultipleChoiceField.

Grappelli has an easy way to make autocomplete lookups for FK relations and for m2m relations, but is there a way for one-to-many relations? It seems like autocompletes for those would be just as useful as for the other two types of relations, so I would have guessed that Grappelli would provide an easy way there also, but I'm not finding it...

like image 363
Troy Avatar asked Nov 02 '22 19:11

Troy


1 Answers

What I ended up doing was I added a ForeignKey field directly to the RelatedModel class so that I could use that to make Grappelli place the autocomplete on the page. That field isn't used to store anything, it's just for the autocomplete lookups. So, my classes look something like the following:

class MyModel(models.Model):
    related = models.ForeignKey(RelatedModel, related_name='my_models')

class RelatedModel(models.Model):
    # Used only for autocomplete lookups in the admin (Grappelli requires that a FK or m2m relationship exists in the model).
    mockMyModel = models.ForeignKey(MyModel, verbose_name='Add a MyModel', related_name='for_mymodel_lookups')
    # my_models - implied one-to-many field

class RelatedModelAdmin(admin.ModelAdmin):
    model = RelatedModel
    form = RelatedModelAdminForm

    raw_id_fields = ('mockMyModel',)
    autocomplete_lookup_fields = {
        'fk': ['mockMyModel'],
    }

class RelatedModelAdminForm(forms.ModelForm):
    class Meta:
        model = RelatedModel
        fields = ('mockMyModel', 'my_models',)

    # Note that you need to override __init__() and save() to get this field to work.
    my_models = forms.ModelMultipleChoiceField(queryset=MyModel.objects.all(), required=False)

    def __init__(self, *args, **kwargs):
        super(RelatedModelAdminForm, self).__init__(*args, **kwargs)
        if self.instance:
            self.fields['my_models'].initial = self.instance.my_models.all()

    def save(self, *args, **kwargs):
        # TODO: Wrap reassignments into transaction
        instance = super(RelatedModelAdminForm, self).save(commit=False)
        self.fields['my_models'].initial.update(sale=None)
        self.cleaned_data['my_models'].update(sale=instance)
        return instance

The above renders an autocomplete widget on the page (for the mock FK relationship) and a multi-select widget (for the one-to-many relationship). Then I made a jquery plugin that hides the multi-select field from view and hijacks the autocomplete widget to copy each value the user selects there into the multi-select field, blanking out the autocomplete after each selection. It's a little bit of a hack... but it gets the job done.

like image 163
Troy Avatar answered Nov 09 '22 08:11

Troy