goinnn/django-multiselectfield

A Multiple Choice model field

django-multiselectfield

image

image

image


Support this package by donating here! ➡️

If you find this package useful, consider supporting it:

Buy Me a Coffee

Paypal


django-multiselectfield provides new model and form fields for Django models, allowing multiple selections from a list of choices. The selected values are stored in the database as a CharField containing a comma-separated values.

This package is inspired by this snippet.

Note: This snippet is from 2008, and a lot has changed since then.

Supported Python versions: 3.8+

Supported Django versions: 3.2+

1. Installation

1.1 Install with pip

$ pip install django-multiselectfield

1.2 Configure your models.py

from multiselectfield import MultiSelectField

# ...

MY_CHOICES = (('item_key1', 'Item title 1.1'),
              ('item_key2', 'Item title 1.2'),
              ('item_key3', 'Item title 1.3'),
              ('item_key4', 'Item title 1.4'),
              ('item_key5', 'Item title 1.5'))

MY_CHOICES2 = (('1', 'Item title 2.1'),
               ('2', 'Item title 2.2'),
               ('3', 'Item title 2.3'),
               ('4', 'Item title 2.4'),
               ('5', 'Item title 2.5'))

class MyModel(models.Model):

    # .....

    my_field = MultiSelectField(choices=MY_CHOICES, default=['item_key1', 'item_key5'])
    my_field2 = MultiSelectField(choices=MY_CHOICES2, min_choices=2, max_choices=3, max_length=3)


# Do not use integer choices like this:

MY_INTEGER_CHOICES2 = ((1, 'Item title 2.1'),
                       (2, 'Item title 2.2'),
                       (3, 'Item title 2.3'),
                       (4, 'Item title 2.4'),
                       (5, 'Item title 2.5'))

# Because when MultiSelectField retrieves data from db, it cannot know if the values are integers or strings.
# In other words, MultiSelectField save the same data for MY_CHOICES2 and MY_INTEGER_CHOICES2
# Or in practice it should be the same MY_CHOICES2 and MY_INTEGER_CHOICES2

1.3 In your settings.py

Only required if you want the translation of django-multiselectfield or need its static files.

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.admin',

    #.....................#

    'multiselectfield',
)

1.4 SortMultiSelectField

Since version 1.0.0 (2025-06-12), this package also includes a another field type called: SortMultiSelectField.

For this field to work, you need to include jQuery (already included in the Django admin) and jQuery UI.

You can include them by updating the ModelAdmin's form or directly in change_form.html (less efficient but faster), as shown in the example project: change_form.html line 11.

1.5 Other recommendations

As django recommended: Avoid using null on string-based fields such as CharField and TextField.

MultiSelectField is based on CharField (MultiSelectField inheritances of CharField). So, if the field is not required, use only blank=True (null=False by default):

class MyModel(models.Model):

    # .....

    my_field = MultiSelectField(choices=MY_CHOICES, blank=True)

2. Custom and integrations

2.1 Customizing templates

You can customize the HTML of this widget in your form template. To do so, you will need to loop through form.{field}.field.choices. Here is an example that displays the field label underneath/after the checkbox for a MultiSelectField called providers:

{% for value, text in form.providers.field.choices %}
  <div class="ui slider checkbox">
    <input id="id_providers_{{ forloop.counter0 }}" name="{{ form.providers.name }}" type="checkbox" value="{{ value }}"{% if value in checked_providers %} checked="checked"{% endif %}>
    <label>{{ text }}</label>
  </div>
{% endfor %}

2.2 Fixing CSS alignment in the Django administration

This fixes alignment. The labels appear slightly lower than the checkboxes, and the label width is very small.

Include the following CSS file: multiselectfield/css/admin-multiselectfield.css

You can include it by updating the ModelAdmin's form or directly in change_form.html (less efficient but faster), as shown in the example project: change_form.html line 7.

2.3 Add a filter to the Django administration

You can see it in example project: admin.py line 23

from django.contrib import admin


def _multiple_choice_filter(field_name, label):

    class MultiSelectFilter(admin.SimpleListFilter):
        title = label
        parameter_name = field_name

        def lookups(self, request, model_admin):
            return model_admin.model._meta.get_field(field_name).flatchoices

        def queryset(self, request, queryset):
            value = self.value()
            if value:
                queryset = queryset.filter(Q(**{
                    f'{self.parameter_name}__exact': value,
                }) | Q(**{
                    f'{self.parameter_name}__startswith': f'{value},',
                }) | Q(**{
                    f'{self.parameter_name}__endswith': f',{value}'
                }) | Q(**{
                    f'{self.parameter_name}__icontains': f',{value},'
                }))

            return queryset
    return MultiSelectFilter

class BookAdmin(admin.ModelAdmin):
    list_display = ('title', 'categories', 'tags', 'published_in')
    list_filter = (
        _multiple_choice_filter('categories', _('categories')),
        _multiple_choice_filter('tags', _('tags')),
        _multiple_choice_filter('favorite_tags', _('favourite tags')),
        _multiple_choice_filter('published_in', _('province or state')),
        _multiple_choice_filter('chapters', _('chapters')),
    )

2.4 Add a django multiselect field to list_display in Django administration

Django doesn\'t provide built-in support for custom fields.

2.4.1 Option 1. Use get_FOO_display

Change them individually

@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
    list_display = ('title', 'get_categories_display',)

    @admin.display(description=_('categories'), ordering='categories')
    def get_categories_display(self, obj):
        return obj.get_categories_display()

2.4.2 Option 2. Monkey patching Django

If you have many django multiselect fields in list_display, the previous option can be much work.

You can see it in the example project: apps.py line 34.

This code is inspired by django code. It is possible that for other versions of Django you may need to adapt it.

from django.apps import AppConfig
from django import VERSION
from django.contrib.admin import utils
from django.utils.hashable import make_hashable

from multiselectfield.db.fields import MultiSelectField


class AppAppConfig(AppConfig):
    name = 'app'
    verbose_name = 'app'

    def ready(self):
        if not hasattr(utils, '_original_display_for_field'):
            utils._original_display_for_field = utils.display_for_field
            utils.display_for_field = patched_display_for_field


# Monkey patching for use multiselect field in list_display

def patched_display_for_field(value, field, empty_value_display, avoid_link=False):
    if isinstance(field, MultiSelectField) and getattr(field, "flatchoices", None):
        try:
            flatchoices = dict(field.flatchoices)
            return ', '.join([str(flatchoices.get(v, empty_value_display)) for v in value]) or empty_value_display
        except TypeError:
            # Allow list-like choices.
            flatchoices = dict(make_hashable(field.flatchoices))
            value = make_hashable(value)
            return ', '.join([str(flatchoices.get(v, empty_value_display)) for v in value]) or empty_value_display

    if VERSION < (5, 2):
        return utils._original_display_for_field(value, field, empty_value_display)
    return utils._original_display_for_field(value, field, empty_value_display, avoid_link=avoid_link)

2.5 Add support for read-only fields in the Django administration

Django doesn\'t provide built-in support for custom fields.

You can see it in the example project: apps.py line 52. Log in to the Django admin in the sample project using the following credentials: user-readonly / DMF-123.

This code is inspired by django code. It is possible that for other versions of Django you may need to adapt it.

from django.apps import AppConfig
from django.contrib.admin.helpers import AdminReadonlyField
from django.contrib.admin.utils import display_for_field, lookup_field
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.fields.related import (
    ForeignObjectRel,
    ManyToManyRel,
    OneToOneField,
)
from django.template.defaultfilters import linebreaksbr
from django.utils.html import conditional_escape
from django.utils.translation import gettext_lazy as _

from multiselectfield.db.fields import MultiSelectField


class AppAppConfig(AppConfig):
    name = 'app'
    verbose_name = 'app'

    def ready(self):
        if not hasattr(AdminReadonlyField, '_original_contents'):
            AdminReadonlyField._original_contents = AdminReadonlyField.contents
            AdminReadonlyField.contents = patched_contents

def patched_contents(self):
    from django.contrib.admin.templatetags.admin_list import _boolean_icon

    field, obj, model_admin = (
        self.field["field"],
        self.form.instance,
        self.model_admin,
    )
    try:
        f, attr, value = lookup_field(field, obj, model_admin)
    except (AttributeError, ValueError, ObjectDoesNotExist):
        result_repr = self.empty_value_display
    else:
        if field in self.form.fields:
            widget = self.form[field].field.widget
            # This isn't elegant but suffices for contrib.auth's
            # ReadOnlyPasswordHashWidget.
            if getattr(widget, "read_only", False):
                return widget.render(field, value)
        if f is None:
            if getattr(attr, "boolean", False):
                result_repr = _boolean_icon(value)
            else:
                if hasattr(value, "__html__"):
                    result_repr = value
                else:
                    result_repr = linebreaksbr(value)
        else:
            if isinstance(f.remote_field, ManyToManyRel) and value is not None:
                result_repr = ", ".join(map(str, value.all()))
            elif (
                isinstance(f.remote_field, (ForeignObjectRel, OneToOneField))
                and value is not None
            ):
                result_repr = self.get_admin_url(f.remote_field, value)
            # Custom: start
            elif isinstance(f, MultiSelectField):
                if value in f.empty_values:
                    result_repr = self.empty_value_display
                else:
                    result_repr = getattr(obj, f'get_{f.name}_display')()
            # Custom: end
            else:
                result_repr = display_for_field(value, f, self.empty_value_display)
            result_repr = linebreaksbr(result_repr)
    return conditional_escape(result_repr)

2.6 Django REST Framework

Django REST Framework comes with a MultipleChoiceField that works perfectly with this:

from rest_framework import fields, serializers

from myapp.models import MY_CHOICES, MY_CHOICES2

class MyModelSerializer(serializers.HyperlinkedModelSerializer):
    # ...
    my_field = fields.MultipleChoiceField(choices=MY_CHOICES)
    my_field2 = fields.MultipleChoiceField(choices=MY_CHOICES2)
    # ...

3. Tests

All tests pass on Django 3.2.0, 4.0.0, 4.1.0, 4.2.0, 5.0.0 and 5.1.0

4. Development

You can get the last bleeding edge version of django-multiselectfield by doing a clone of its git repository:

git clone https://github.com/goinnn/django-multiselectfield

5. Example project

There is a fully configured example project in the example directory. You can run it as usual:

python manage.py migrate
python manage.py loaddata app_data
python manage.py runserver
# And go to http://localhost:8000. You will be automatically authenticated as a superuser.
Stars
419
100.00% more than last month
Forks
196
Open Issues
46