Dynamic fieldsets for your Django ModelAdmin using JSON schema and react-jsonschema-form.
Define dynamic fieldsets with react-jsonschema-form. Store the schema on a foreign-keyed model. Serialize the fieldsets' back to a native Django JSON field in the admin.
Let's explain by way of an example: At POLITICO, we use Django to model content.
Often we'll define a model that represents an individual piece of content and relate that model via a foreign key to another one representing a type of content. A common problem across those models are types that require a special field to contain information about a piece.
Think of a story template. It will have some fields in common with all other types of stories: a title, a byline, etc. So we define a story model that has those fields.
We know we have different types of stories -- feature pieces, breaking news items, whatever -- and so define a story template model to group story instances by a foreign key.
In Django, our models now look like this:
class StoryTemplate(models.Model):
name = models.CharField()
class Story(models.Model):
story_type = models.ForeignKey(StoryTemplate)
headline = models.CharField()
# etc.
But now we realize we need a template for a video story. Can do, but this template needs a new field: a video embed code. No other story type needs this field.
We now consider two options: 1) Add the embed field to our Story
model, which will be useless for the 90% of our stories that are text-based. 2) Break out a new model, which will foreign key to Story
and contain video-specific fields we can access via a reverse relationship.
We don't really like either of those options.
In our mind, the video embed field belongs to our StoryTemplate
because it applies to all video stories. So why can't we simply add it there? Well, because the information contained in that field belongs to a Story
instance, each with its own individual embed code.
What we really need is a way to define fields on the StoryTemplate
model that can be filled out in the Story
ModelAdmin.
With django-foreignform you can! We can use JSON Schema to define any fields specific to a template on our StoryTemplate
model and then render those fields dynamically in a ModelAdmin for our Story
model. As users fill out these dynamic fields, their values are serialized back to a JSON field on the Story
model.
So instead our models look something like this:
class StoryTemplate(models.Model):
name = models.CharField()
json_schema = JSONField()
class Story(models.Model):
story_type = models.ForeignKey(StoryTemplate)
headline = models.CharField()
form_data = JSONField()
# etc.
We can then define our video field via JSON schema on the StoryTemplate
like this:
{
"type": "object",
"properties": {
"video": {
"type": "string",
"title": "Video embed code"
}
}
}
... which, once it's filled in by a user on a Story
model, may then serialize to data like this:
{
"video": "https://www.youtube.com/watch?v=wuK0f-Zur9A"
}
A long way to say django-foreignform lets us write more complex models while keeping the relationships in our database very simple. It helps us avoid extra models to handle edge cases or cluttering up the models we have with rarely used fields.
This library is very early days for us. It may not be ready for your own production needs.
Forms it creates definitely should not be used by anyone who's not a friend to your organization. While react-jsonschema-form gives us some light data validation options on the form it renders, you're trading against Django's robust model validation. It's also very easy for a bad actor to circumvent the form and insert values into the hidden JSON field.
For all those reasons, this library only renders forms in Django's admin, which should encourage you to only give access to trusted staff.
All that said, you should try it out! Pull requests welcome!
- PostgreSQL ≥ 9.4
- Django ≥ 2.0
- Install the app.
$ pip install django-foreignform
- Add the app to your Django project and configure settings.
INSTALLED_APPS = [
# ...
'foreignform',
]
- Define a template model from our base class and an instance foreign-keyed to the first with a dedicated JSON field for your form data.
from django.contrib.postgres.fields import JSONField
from django.db import models
from foreignform.models import ForeignFormBaseModel
class StoryTemplate(ForeignFormBaseModel):
name = models.CharField(max_length=100)
# ...
class Story(models.Model):
template = models.ForeignKey(StoryTemplate, on_delete=models.PROTECT)
name = models.CharField(max_length=100)
form_data = JSONField(blank=True, null=True)
# ...
- Use our mixin to create a ModelAdmin for your foreign-keyed model with properties for the foreign key field and the JSON field for your form data.
from django.contrib import admin
from foreignform.mixins import ForeignFormAdminMixin
from .models import StoryTemplate, Story
class StoryAdmin(ForeignFormAdminMixin, admin.ModelAdmin):
foreignform_foreign_key = 'template'
foreignform_field = 'form_data'
admin.site.register(StoryTemplate)
admin.site.register(Story, StoryAdmin)
- In the admin for a template model, define some fields on your model using JSONSchema syntax and react-jsonschema-form's UI schema.
- In the admin for an instance, create an instance with a foreign key to the template model. Click
Save and continue editing
.
- With the foreign key saved, you can now fill out fields defined in the template model's JSON schema. Behind the scenes, the formset is being serialized and saved to your instance model's JSON field.
- Use the serialized JSON data from your form in your model's JSON field.
Story.objects.first().form_data
Developing python files? Move into example directory and run the development server with pipenv.
$ cd example
$ pipenv run python manage.py runserver
Developing static assets? Move into the pluggable app's staticapp directory and start the node development server, which will automatically proxy Django's development server.
$ cd foreignform/staticapp
$ gulp
Want to not worry about it? Use the shortcut make command.
$ make dev
- Run the make command to setup a fresh database.
$ make database
- Add a connection URL to the
.env
file.
DATABASE_URL="postgres://localhost:5432/foreignform"
- Run migrations from the example app.
$ cd example
$ pipenv run python manage.py migrate