oscarmlage / django-cruds-adminlte

django-cruds is simple drop-in django app that creates CRUD for faster prototyping
BSD 3-Clause "New" or "Revised" License
424 stars 81 forks source link

autocomplete_fields feature? #133

Open benmaier opened 4 years ago

benmaier commented 4 years ago

First, thanks for the package, looking really great! I've tested a few features that worked quite well, but I have a particular problem:

Several of my models refer with a ForeignKey to a geo-database that has ~350,000 entries. I've let django-cruds-adminlte auto-create all CRUD pages. Whenever I'm trying to load a create/update page of a model that references the foreign key to the geo-database, the connection times out. I have had this problem before with the default django-admin page and it was caused by the model page trying to load all 350,000 entries into the select (see e.g. this StackOverflow). I'm suspecting something similar is happening here.

For the default admin page, this can be fixed by adding the autocomplete_fields attribute to the respective ModelAdmin, which tells the ModelAdmin that a particular field is to be searched asynchronously, and the way it is to be searched is defined in the ModelAdmin of the referenced Model.

Here's an example for the models:

# models.py

# this model has 350,000 entries
class Geoname(models.Model):
    name = models.CharField(max_length=255)
    population = models.PositiveIntegerField(blank=True,null=True)

class Institution(models.Model):
    name = models.CharField(max_length=200)
    city = models.ForeignKey(Geoname,models.SET_NULL,blank=True,null=True)
    responsible_for_places = models.ManyToManyField(Geoname,blank=True,null=True,related_name='responsible_institutions')

Using these models to auto-generate CRUDS, the create-page of Institution times out.

Here's how this is solved in the default admin-framework:

# admin.py
from .models import Geoname, Institution
@admin.register(Geoname)
class GeonameAdmin(admin.ModelAdmin):
    search_fields = ['name']
    ordering = ['-population']

@admin.register(Institution)
class InstitutionAdmin(admin.ModelAdmin):
    autocomplete_fields = ['city', 'responsible_for_places']

Which basically tells django "If a user wants to add a city to an institution, do not load all geoname-values in the select, rather do an Ajax request to the Geoname-table which is searched in the fields name and decreasingly order the search results by the field population. Also, paginate the returned list".

Is there a way to achieve this using django-cruds-adminlte? I've searched the documentation and the github-issues but could not find anything to achieve this.

Cheers

Edit: Forgot 'population' entry in Model definition.

benmaier commented 4 years ago

I solved this. One has to write a custom django_select2-Widget, then override the form and override the CRUDview.

I'm keeping this issue open though as I suspect that either of the following options would help other users:

  1. Incorporating this solution in the documentation at https://django-cruds-adminlte.readthedocs.io/en/latest/components.html#select2-widget
  2. automating the process described below such that an additional autocomplete_fields-property in the CRUDView-class can take care of this. I feel as if this should not be too hard but I'm a beginner with django so it's hard for me to judge.

Solution

I've based the following solution on these links:

myproject/urls.py

First, you have to register select2 in your projects/urls.py as

from django.urls import path, include

urlpatterns = [
    ...
    path('select2/', include('django_select2.urls')),
    ...
]

because select2 needs its own API to submit searchqueries.

myapp/views.py

The file is split so I can describe what's going on

from django import forms

from cruds_adminlte.crud import CRUDView
from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget

from .models import Institution        

First, we import cruds_adminlte's CRUDview so we can adjust the view. Then, we import ModelSelect2MultipleWidget, ModelSelect2Widget which handle Select2-selects in an asynchronous manner. Subsequently, we import the Model Institution for which we want the autocomplete-fields.

class SingleSelectWidget(ModelSelect2Widget):
    def filter_queryset(self, request, term, queryset, **dependent_fields):
        qs = super().filter_queryset(request, term, queryset, **dependent_fields)
        return qs.order_by(*self.ordering)

class MultipleSelectWidget(ModelSelect2MultipleWidget):
    def filter_queryset(self, request, term, queryset, **dependent_fields):
        qs = super().filter_queryset(request, term, queryset, **dependent_fields)
        return qs.order_by(*self.ordering)

class SingleGeonameSelectWidget(SingleSelectWidget):
    search_fields = ['name__icontains']
    ordering = ['-population']

class MultipleGeonameSelectWidget(MultipleSelectWidget):
    search_fields = ['name__icontains', 'englishname__icontains']
    ordering = ['-population']

The first two classes are helper classes that allow their children to just define the ordering property for filtering the search query, as one is used to from the admin-pages.

Now define a new form that uses the adjusted widgets:

class InstitutionForm(forms.ModelForm):
    class Meta:
        model = Institution
        exclude = [] # tell django that all fields should be rendered
        widgets = {
                    'city': SingleGeonameSelectWidget,
                    'responsible_for_places': MultipleGeonameSelectWidget,
                }

Use this form to override the CRUDView property:

class InstitutionView(CRUDView):
    model = Institution
    add_form = InstitutionForm
    update_form = InstitutionForm

myapp/urls.py

Register this Model's url

from django.urls import path, include
from django.conf.urls import url

from . import views

urlpatterns = [
            url('', include(views.InstitutionView().get_urls())),
        ]
benmaier commented 4 years ago

Another problem: the connection does not time out for the add_form, it still does, however, for the update_form

Edit: I misspelled update_form in the original solution. I've edited the solution above and it works now.