Django-Select2

version coverage license

This is a Django integration of Select2.

The app includes Select2 driven Django Widgets.

Documentation

Documentation available at https://django-select2.readthedocs.io/.

Note

Django’s admin comes with builtin support for Select2 via the autocomplete_fields feature.

Installation

Install django-select2:

python3 -m pip install django-select2

Add django_select2 to your INSTALLED_APPS in your project settings.

Add django_select to your URL root configuration:

from django.urls import include, path

urlpatterns = [
    # … other patterns
    path("select2/", include("django_select2.urls")),
    # … other patterns
]

django-select2 requires a cache backend which is persistent across all application servers..

This means that the DummyCache backend will not work!

The default cache backend is LocMemCache, which is persistent across a single node. For projects with a single application server this will work fine, however you will run into issues when you scale up into multiple servers.

Below is an example setup using Redis, which is a solution that works for multi-server setups:

Make sure you have a Redis server up and running:

# Debian
sudo apt-get install redis-server

# macOS
brew install redis

# install Redis python client
python3 -m pip install django-redis

Next, add the cache configuration to your settings.py as follows:

CACHES = {
    # … default cache config and others
    "select2": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/2",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        }
    }
}

# Tell select2 which cache configuration to use:
SELECT2_CACHE_BACKEND = "select2"

Note

A custom timeout for your cache backend, will serve as an indirect session limit. Auto select fields will stop working after, once the cache has expired. It’s recommended to use a dedicated cache database with an adequate cache replacement policy such as LRU, FILO, etc.

External Dependencies

  • jQuery is not included in the package since it is expected that in most scenarios this would already be available.

Quick Start

Here is a quick example to get you started:

First make sure you followed the installation instructions above. Once everything is setup, let’s start with a simple example.

We have the following model:

# models.py
from django.conf import settings
from django.db import models


class Book(models.Model):
    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    co_authors = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='co_authored_by')

Next, we create a model form with custom Select2 widgets.

# forms.py
from django import forms
from django_select2 import forms as s2forms

from . import models


class AuthorWidget(s2forms.ModelSelect2Widget):
    search_fields = [
        "username__icontains",
        "email__icontains",
    ]


class CoAuthorsWidget(s2forms.ModelSelect2MultipleWidget):
    search_fields = [
        "username__icontains",
        "email__icontains",
    ]


class BookForm(forms.ModelForm):
    class Meta:
        model = models.Book
        fields = "__all__"
        widgets = {
            "author": AuthorWidget,
            "co_authors": CoAuthorsWidget,
        }

A simple class based view will do, to render your form:

# views.py
from django.views import generic

from . import forms, models


class BookCreateView(generic.CreateView):
    model = models.Book
    form_class = forms.BookForm
    success_url = "/"

Make sure to add the view to your urls.py:

# urls.py
from django.urls import include, path

from . import views

urlpatterns = [
    # … other patterns
    path("select2/", include("django_select2.urls")),
    # … other patterns
    path("book/create", views.BookCreateView.as_view(), name="book-create"),
]

Finally, we need a little template, myapp/templates/myapp/book_form.html

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Create Book</title>
    {{ form.media.css }}
    <style>
        input, select {width: 100%}
    </style>
</head>
<body>
    <h1>Create a new Book</h1>
    <form method="POST">
        {% csrf_token %}
        {{ form.as_p }}
        <input type="submit">
    </form>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    {{ form.media.js }}
</body>
</html>

Done - enjoy the wonders of Select2!

Changelog

See Github releases.

All Contents

Contents:

API Documentation

Configuration
Widgets
URLs
Views
Cache
JavaScript

DjangoSelect2 handles the initialization of select2 fields automatically. Just include {{ form.media.js }} in your template before the closing body tag. That’s it!

If you insert forms after page load or if you want to handle the initialization yourself, DjangoSelect2 provides a jQuery plugin, replacing and enhancing the Select2 plugin. It will handle both normal and heavy fields. Simply call djangoSelect2(options) on your select fields.:

$('.django-select2').djangoSelect2();

You can pass see Select2 options if needed:

$('.django-select2').djangoSelect2({placeholder: 'Select an option'});

Please replace all your .select2 invocations with the here provided .djangoSelect2.

Security & Authentication

Security is important. Therefore make sure to read and understand what the security measures in place and their limitations.

Set up a separate cache. If you have a public form that uses a model widget make sure to setup a separate cache database for Select2. An attacker could constantly reload your site and fill up the select2 cache. Having a separate cache allows you to limit the effect to select2 only.

You might want to add a secure select2 JSON endpoint for data you don’t want to be accessible to the general public. Doing so is easy:

class UserSelect2View(LoginRequiredMixin, AutoResponseView):
    pass

class UserSelect2WidgetMixin(object):
    def __init__(self, *args, **kwargs):
        kwargs['data_view'] = 'user-select2-view'
        super(UserSelect2WidgetMixin, self).__init__(*args, **kwargs)

class MySecretWidget(UserSelect2WidgetMixin, ModelSelect2Widget):
    model = MySecretModel
    search_fields = ['title__icontains']

Extra

Chained select2

Suppose you have an address form where a user should choose a Country and a City. When the user selects a country we want to show only cities belonging to that country. So the one selector depends on another one.

Models

Here are our two models:

class Country(models.Model):
    name = models.CharField(max_length=255)


class City(models.Model):
    name = models.CharField(max_length=255)
    country = models.ForeignKey('Country', related_name="cities")
Customizing a Form

Lets link two widgets via a dependent_fields dictionary. The key represents the name of the field in the form. The value represents the name of the field in the model (used in queryset).

class AddressForm(forms.Form):
    country = forms.ModelChoiceField(
        queryset=Country.objects.all(),
        label=u"Country",
        widget=ModelSelect2Widget(
            model=Country,
            search_fields=['name__icontains'],
        )
    )

    city = forms.ModelChoiceField(
        queryset=City.objects.all(),
        label=u"City",
        widget=ModelSelect2Widget(
            model=City,
            search_fields=['name__icontains'],
            dependent_fields={'country': 'country'},
            max_results=500,
        )
    )
Interdependent select2

Also you may want not to restrict the user to which field should be selected first. Instead you want to suggest to the user options for any select2 depending of his selection in another one.

Customize the form in a manner:

class AddressForm(forms.Form):
    country = forms.ModelChoiceField(
        queryset=Country.objects.all(),
        label=u"Country",
        widget=ModelSelect2Widget(
            search_fields=['name__icontains'],
            dependent_fields={'city': 'cities'},
        )
    )

    city = forms.ModelChoiceField(
        queryset=City.objects.all(),
        label=u"City",
        widget=ModelSelect2Widget(
            search_fields=['name__icontains'],
            dependent_fields={'country': 'country'},
            max_results=500,
        )
    )

Take attention to country’s dependent_fields. The value of ‘city’ is ‘cities’ because of related name used in a filter condition cities which differs from widget field name city.

Caution

Be aware of using interdependent select2 in parent-child relation. When a child is selected, you are restricted to change parent (only one value is available). Probably you should let the user reset the child first to release parent select2.

Multi-dependent select2

Furthermore you may want to filter options on two or more select2 selections (some code is dropped for clarity):

class SomeForm(forms.Form):
    field1 = forms.ModelChoiceField(
        widget=ModelSelect2Widget(
        )
    )

    field2 = forms.ModelChoiceField(
        widget=ModelSelect2Widget(
        )
    )

    field3 = forms.ModelChoiceField(
        widget=ModelSelect2Widget(
            dependent_fields={'field1': 'field1', 'field2': 'field2'},
        )
    )

Contributing

This package uses the pyTest test runner. To run the tests locally simply run:

python setup.py test

If you need to the development dependencies installed of you local IDE, you can run:

python setup.py develop

Documentation pull requests welcome. The Sphinx documentation can be compiled via:

python setup.py build_sphinx

Bug reports welcome, even more so if they include a correct patch. Much more so if you start your patch by adding a failing unit test, and correct the code until zero unit tests fail.

The list of supported Django and Python version can be found in the CI suite setup. Please make sure to verify that none of the linters or tests failed, before you submit a patch for review.

Indices and tables