rogulski.it

My name is Piotr, a passionate pythonista and this is my blog!

    Django multi-select choice field in admin panel

    Posted at — Jan 24, 2022

    Let the first cast a stone who did not use Django only because it has already a built-in admin panel… (of course, Django is having a lot of useful features and possibilities apart from admin).

    Recently I found myself in a situation where I needed to allow admin users to set up a list of labels or tags on a table with a limited set of values. This value is “read-only” on the API side and “write-only” on the admin side. In this short post, I would like to present a couple of different ways how to set up such a multiple value field. The first option will be to use an array of char fields with or without an additional widget and the second one will use a JSON field and represent it nicely in the admin panel.

    Standard Django ArrayField

    class MyTable(models.Model):
        LABELS_CHOICES = (
            ("blue", "blue"),
            ("red", "red"),
            ("green", "green"),
        )
    
        labels = ArrayField(
            models.CharField(
                choices=LABELS_CHOICES,
                max_length=100,
                blank=True,
                null=True
            ),
            blank=True,
            null=True
        )
    

    Using a Django lib ArrayField with CharField without any modifications will result in a simple text field allowing to add of comma-separated choice values. In this case, the solution will work on the API side returned as a list of strings but it will not be super UX oriented because the user does not know about possible choices.

    Django ArrayField admin panel

    Modified Django ArrayField

    from django import forms
    from django.contrib.postgres.fields import ArrayField
    from django.db import models
    
    
    class ModifiedArrayField(ArrayField):
        def formfield(self, **kwargs):
            defaults = {
                "form_class": forms.MultipleChoiceField,
                "choices": self.base_field.choices,
                **kwargs
            }
            return super(ArrayField, self).formfield(**defaults)
    
    
    class MyTable(models.Model):
        LABELS_CHOICES = (
            ("blue", "blue"),
            ("red", "red"),
            ("green", "green"),
        )
    
        labels = ModifiedArrayField(
            models.CharField(
                choices=LABELS_CHOICES,
                max_length=100,
                blank=True,
                null=True
            ),
            blank=True,
            null=True
        )
    

    By modifying an ArrayField we can get a better display of the selected fields and all possible choice values. Each of them is selectable with CMD/ctr button.

    Django ArrayField admin panel modified

    Modified Django ArrayField with a CheckboxSelectMultiple widget

    from django import forms
    from django.contrib.postgres.fields import ArrayField
    from django.db import models
    
    
    class ModifiedArrayField(ArrayField):
        def formfield(self, **kwargs):
            defaults = {
                "form_class": forms.MultipleChoiceField,
                "choices": self.base_field.choices,
                "widget": forms.CheckboxSelectMultiple,
                **kwargs
            }
            return super(ArrayField, self).formfield(**defaults)
    

    Introducing a widget might make it more readable and visible in terms of the selection.

    Django ArrayField admin panel modified

    JSON field (by django-jsonform) with array of strings

    I found this package very (django-jsonform) interesting and useful due to its support to the JSON field. It is also having support to ArrayField which is similar to previous examples but includes a better visual display.

    I would like to present a JSON field for an array of strings and also a little bit more advanced schema including a list of dictionaries.

    from django.db import models
    
    from django_jsonform.models.fields import JSONField
    
    
    class MyTable(models.Model):
        LABELS_SCHEMA = {
            "type": "array",
            "items": {
                "type": "string",
                "choices": ["blue", "red", "green"],
            },
        }
    
        labels_json = JSONField(schema=LABELS_SCHEMA, null=True, blank=True)
    

    The most important definition apart from the field is a schema. JSON schema of input data will define how the UI will generate. Visual overview and dropdown with choice fields are way better. Additionally, JSON (JSONB field from PostgreSQL) if a way more flexible way of keeping those values

    Django JSONB field array admin panel

    JSON field (by django-jsonform) with an array of dicts

    The problem with a list of strings starts when we need to support more complex objects. In my case, a label should have multiple fields like the color (string), value (int), and groups (list of strings).

    I didn’t want to introduce a new table because of many reasons, mostly because of joins (table already had a lot of them) and the “labels” field is read-only without any filtering.

    This is a structure that needed to be supported:

    [
      {
        "color": "blue",
        "value": 10,
        "groups": ["discount", "voucher"]
      }
    ]
    

    Based on this object, similar to the previous example we would need to build a new schema and attach it to our JSON field.

    from django.db import models
    
    from django_jsonform.models.fields import JSONField
    
    
    class MyTable(models.Model):
        LABELS_SCHEMA = {
          "type": "array",
            "items": {
                "type": "dict",
                "keys": {
                    "value": {
                        "type": "int",
                    },
                    "color": {
                        "type": "string",
                        "choices": ["blue", "red", "green"],
                    },
                    "groups": {
                        "type": "array",
                        "items": {
                            "type": "string",
                            "choices": [
                              {"label": "discount", "value": "DI"},
                              {"label": "voucher", "value": "VO"}],
                        },
                    },
                },
            },
        }
    
        labels_json = JSONField(schema=LABELS_SCHEMA, null=True, blank=True)
    

    Django JSON Field array of dicts

    Summary

    This option was the best for me, fast to implement, and readable for admin users -> At the end of the day, this was all I needed. I found more ways of doing it but most of them required modifying templates or adding my own js, unfortunately, this option isn’t the best for me -> I’m too lazy!

    If you have a better implementation for representing JSON fields in the admin panel, please feel free to share your idea in the comments section!