How to use autocompleteselect widget in a modelform
Asked Answered
R

3

12

I know there is a new feature in Django 2.0, which is AutocompleteSelect widget in ModelAdmin. I am trying to use it in my custom modelForm but just failed.

Tried like this

#unit is the foreign key to the incident

class AccountForm(forms.ModelForm):
    class Meta:
        model = Invoice
        ...
        ...
        widgets = {       'incident':widgets.AutocompleteSelect(Invoice._meta.get_field('incident').remote_field, admin.site)
        }
        ...
#Invoice model

class Invoice(models.Model):
    ...
    incident = models.ForeignKey(Unit, on_delete=models.CASCADE,null=True)
    ...

Hope anyone can help me. Thanks

Rossuck answered 26/4, 2019 at 4:17 Comment(5)
in your ModelAdmin you just need to add autocomplete_fields = ('your_field',)Hoppe
No I am trying to do it outside the ModelAdmin. I am trying to do it in my own formRossuck
@Rossuck were you able to find a solution to this?Bogey
The widget relies on being able to query an 'autocomplete' endpoint that requires admin access, so it'll only work outside of admin if the person is signed in and has 'staff' and 'view' access to the given model.Filemon
see below: Invoice.incident.field.remote_field was used until Django 3.1 In Django 3.2 it is only Invoice.incident.field code.djangoproject.com/ticket/32619 – darklessSafford
I
10

The AutocompleteSelect widget will not work outside of the admin site. If you are using AccountForm in admin site you can use the following code:

class AccountForm(forms.ModelForm):
    ...
    incident = forms.ModelChoiceField(
                 queryset= Unit.objects.all(),
                 widget=AutocompleteSelect(Invoice.incident.field.remote_field, admin.site),
               )
    ...
    class Meta:
        model = Invoice
        fields = [
            'incident',
            ...
        ]

@admin.register(Invoice)
class InvoiceAdmin(admin.ModelAdmin):
    form = AccountForm
Instep answered 1/9, 2019 at 18:59 Comment(1)
Invoice.incident.field.remote_field was used until Django 3.1 In Django 3.2 it is only Invoice.incident.field code.djangoproject.com/ticket/32619Fusain
F
2

AutocompleteSelect has 2 required args, rel and admin_site. The rel is used to extract the model used to query the data from and relates to an attribute on a ForeignKey or ManyToManyField. Since I wanted to use this on a field that wasn't actually a ForeignKey, I needed to override a few things to make it work:

class ClientAutocompleteSelect(AutocompleteSelect):
    def get_url(self):
        model = Client
        return reverse(self.url_name % (self.admin_site.name, model._meta.app_label, model._meta.model_name))

class ClientChoiceField(forms.ModelChoiceField):
    def __init__(self, queryset=None, widget=None, **kwargs):
        if queryset is None:
            queryset = Client.objects.all()
        if widget is None:
            widget = ClientAutocompleteSelect(None, admin.site)  # pass `None` for `rel`
        super().__init__(queryset, widget=widget, **kwargs)

    def to_python(self, value):
        return value  # just return the client_id and not a Client object

class MyAdminForm(forms.ModelForm):
    client_id=ClientChoiceField()

    ...

This requires that the end user has admin read access to the autocomplete endpoint of the model being queried. You may be able to do more hacking to change that get_url and use your own endpoint to give search results, though.

Filemon answered 16/10, 2020 at 20:53 Comment(0)
S
0

I spent a few hours trying to understand why my code (built on @Tim 's answer) would not work, until I stumble on a comment about missing references to css/js files.

Here is a complete working solution to use the AutocompleteSelect widget in any custom form for signed-in users having both 'staff' and 'view' access to the given model:

from django.urls import reverse
from django.contrib.admin.widgets import AutocompleteSelect
from django.contrib import admin

class UserAutocompleteSelect(AutocompleteSelect):
    def get_url(self):
        model = CustomUser
        return reverse(self.url_name % (self.admin_site.name, model._meta.app_label, model._meta.model_name))

class UserChoiceField(forms.ModelChoiceField):
    def __init__(self, queryset=None, widget=None, **kwargs):
        if queryset is None:
            queryset = CustomUser.objects.all()
        if widget is None:
            widget = UserAutocompleteSelect(None, admin.site)  # pass `None` for `rel`
        super().__init__(queryset, widget=widget, **kwargs)

class UserAutocompleteSelectForm(forms.ModelForm):
    """
    for changing user on Play objects
    using amdin module autocomplete
    """
    user = UserChoiceField(
        # queryset=CustomUser.objects.all(),
        help_text=_('Select the user to replace the current one')
    )

    class Meta:
        model = Play
        fields = ('user', )

You can use the same, replacing CustomUser and Play by your own models

If (like me) this is not working out-of-the-box with the html template you're using, that means that you need to include the required css/js files to your template. Here is a simple way to do it :

Providing that the form is declared as such in the view:

form = UserAutocompleteSelectForm()
...
context = {
            'form': form, 
            ...
            }
return render(request, 'users/change_user.html', context)

you should add the following lines to the html template to include the required css/js files:

{% block extrahead %}
{{ block.super }}
{{ form.media }}
{% endblock %}
Septemberseptembrist answered 24/3, 2021 at 8:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.