Skip to content

Instantly share code, notes, and snippets.

@kaedroho
Last active July 6, 2022 12:03
Show Gist options
  • Save kaedroho/e86d491f9608b1d938269321b557a29c to your computer and use it in GitHub Desktop.
Save kaedroho/e86d491f9608b1d938269321b557a29c to your computer and use it in GitHub Desktop.
Django Structured Fields

django-structured-fields

A package containing a set of structural field types for Django.

This package is inspired by Wagtail's StreamField, but with the following changes:

  • Allows any Django field type to be used
  • Provides a separate "enum" type
  • structs/enums/lists can be added directly to models

I hope to support all of features of Django's fields, including adding the necessary database constraints and supporting all lookups.

Installation

pip install django-structured-fields

Usage

StructField

This field type combines a group of fields together. A StructField can either be defined within your model or as a subclass that can be reused. For example:

from django.db import models
from structured_fields.fields import StructField

class ImageWithCaptionField(StructField):
    image = models.ImageField()
    caption = models.TextField()


class MyModel(models.Model):
    image_with_caption = ImageWithCaptionField()

    # here's how you could define this inline
    inline_image_with_caption = StructField([
        ('image', models.ImageField()),
        ('caption', models.TextField()),
    ])

Internals

Internally, this field type creates a JSON field with a single dict containing the sub-field values. For example:

{
     "image": "/images/my-image.jpg",
     "caption": "This is my image",
}
CompositeField

If you want a field type that converts to multiple fields when applied to a model, you can swap this out for django-composite-field. This field type will also work when nested within an EnumField/ListField too.

EnumField

This field type allows the user to choose from one or more field types. Validation is only performed on the type that was chosen. Like StructField, this could also be defined inline or as a subclass

from django.db import models
from structured_fields.fields import EnumField

class LinkField(EnumField)
    """
    Links either to an internal page or an external URL
    """
    page = models.ForeignKey("wagtailcore.Page")
    url = models.URLField()
    
    
class MyModel(models.Model):
    link = LinkField()

    # here's how you could define this inline
    inline_link = EnumField([
        ('page', models.ForeignKey("wagtailcore.Page")),
        ('url', models.URLField()),
    ])

Internals

When used directly on a model, this will add each field directly on to the model prefixed with the field name and with an additional "type" field added. For example, the MyModel.link field would generate the following model structure:

class MyModel(models.Model):
    link_type = models.CharField(max_length=255, choices=["page", "url"])
    link_page = models.ForeignKey("wagtailcore.Page", null=True)
    link_url = models.URLField(null=True)
    
    # If possible, we should automatically add constraints too
    class Meta:
        constraints = [
            CheckConstraint(
                Q(link_type="page", link_page__isnull=False)
                | Q(link_type="url", link_url__isnull=False)
            )
        ]

When used within a ListField, this will use the following JSON representation:

{
    "type": "page",
    "value": 1,
}
{
    "type": "url",
    "value": "https://www.github.com/",
}

ListField

This field type allows you to have zero or more values of another field. It is similar to the ArrayField type in django.contrib.postgres, except the underlying data type will always be a JSONField. Like the field types before, this could also be defined inline or as a sub-class:

from django.db import models
from structured_fields.fields import EnumField, ListField, 

class LinksField(ListField)
    item = EnumField([
        ('page', models.ForeignKey("wagtailcore.Page")),
        ('url', models.URLField()),
    ])


class MyModel(models.Model):
    links = LinksField()
    
    # here's how you could define this inline
    inline_links = ListField(
        item=EnumField([
            ('page', models.ForeignKey("wagtailcore.Page")),
            ('url', models.URLField()),
        ])
    )

Internals

ListField is a sub-class of Django's JSONField, so when used directly on a model, a JSON field will be added.

The JSON representation is a list of dictionaries, each dictionary contains an id (UUID) and a value key.

When nested in another JSON field, this same representation is used.

StreamField

Internally, this field type is equivalent to ListField(item=EnumField(...)), however it provides a UI similar to Wagtail's StreamField and some extra helper methods.

Here's an example showing StreamField but also how you can combine the different field types.

from django.db import models
from structured_fields.fields import StructField, StreamField


class ImageWithCaptionField(StructField):
    image = models.ImageField()
    caption = models.TextField()


class LinkField(EnumField):
    page = models.ForeignKey("wagtailcore.Page")
    url = models.URLField()


class BodyField(StreamField)
    paragraph = models.TextField()
    image = ImageWithCaptionField()
    link = LinkField()


class MyPage(models.Model):
    body = BodyField()
    
    # here's how you could define this inline
    inline_body = StreamField([
        ("paragraph", models.TextField()),
        ("image", ImageWithCaptionField()),
        ("link", LinkField()),
    ])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment