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.
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.
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.
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.
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
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)
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!