Skip to content
Edit JSON-Model Fields using a Standard Django Form.
Branch: master
Clone or download
Latest commit 67a781b Jun 15, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
entangled Bump to version 0.3 Jun 12, 2019
tests Fix unit tests runing on Django<2.1 Jun 4, 2019
.gitignore WiP: Initial revision May 25, 2019
.travis.yml add Django-2.2 to test-matrix May 27, 2019
LICENSE Initial commit May 25, 2019
MANIFEST.in add files for Travis-CI May 27, 2019
README.md Update README.md Jun 15, 2019
manage.py retest with dummy file May 27, 2019
pytest.ini WiP: Initial revision May 25, 2019
setup.py add Django-2.2 to test-matrix May 27, 2019
tox.ini tests require lxml May 27, 2019

README.md

django-entangled

Edit JSON-Model Fields using a Standard Django Form.

Build Status PyPI PyPI version PyPI Twitter Follow

Use-Case

A Django Model may contain fields which accept arbitrary data stored as JSON. Django itself, provides a JSON field specific to Postgres. For other database implementations, there are plenty of alternatives.

When creating a form from a models, the input field associated with a JSON field, typically is a <textarea ...><textarea>. This textarea widget is very inpracticable for editing, because it just contains a textual representation of that object notation. One possibility is to use a generic JSON editor, which with some JavaScript, transforms the widget into an attribute-value-pair editor. This approach however prevents us from utilizing all the nice features provided by the Django form framework, such as field validation, normalization of data and the usage of foreign keys. By using django-entangled, one can use a Django ModelForm, and store all, or a subset of that form fields in one or more JSON fields inside of the associated model.

Installation

Simply install this Django app, for instance by invoking:

pip install django-entangled

There is no need to add any configurations directives to the project's settings.py.

Example

Say, we have a Django model to describe a bunch of different products. The name and the price fields are common to all products, whereas the properties can vary depending on its product type. Since we don't want to create a different product model for each product type, we use a JSON field to store these arbitrary properties.

from django.db import models
from django.contrib.postgres.fields import JSONField

class Product(models.Model):
    name = models.CharField(max_length=50)

    price = models.DecimalField(max_digits=5, decimal_places=2)
    
    properties = JSONField()

In a typical form editing view, we would create a form inheriting from ModelForm and refer to this model using the model attribute in the Meta-class. Then the properties-field would show up as unstructured JSON, rendered inside a <textarea ...><textarea>. This definitely is not what we want! Instead we create a typical Django Form using the special mixin class EntangledModelForm.

from django.contrib.auth import get_user_model
from django.forms import fields, models
from entangled.forms import EntangledModelForm
from .models import Product

class ProductForm(EntangledModelForm):
    color = fields.RegexField(
        regex=r'^#[0-9a-f]{6}$',
    )

    size = fields.ChoiceField(
        choices=[('s', "small"), ('m', "medium"), ('l', "large"), ('xl', "extra large")],
    )

    tenant = models.ModelChoiceField(
        queryset=get_user_model().objects.filter(is_staff=True),
    )

    class Meta:
        model = Product
        entangled_fields = {'properties': ['color', 'size', 'tenant']}  # fields provided by this form
        untangled_fields = ['name', 'price']  # these fields are provided by the Product model

In addition to the mixin class EntangledModelFormMixin we add a special dictionary named entangled_fields to our Meta-options. In this dictionary, the key (here 'properties') refers to the JSON-field in our model Product. The value (here ['color', 'size', 'tenant']) is a list of named form fields, declared in our form- or base-class of thereof. This allows us to assign all standard Django form fields to arbitrary JSON fields declared in our Django model. Moreover, we can even use a ModelChoiceField or a ModelMultipleChoiceField to refer to another model object using a generic relation

Since in this form we also want to access the non-JSON fields from our Django model, we add a list named untangled_fields to our Meta-options. In this list, (here ['name', 'price']) we refer to the non-JSON fields in our model Product. From both of these iterables, entangled_fields and untangled_fields, the parent class EntangledModelForm then builds the Meta-option fields, otherwise required. Therefore you should not use fields to declare this list, but rather rely on entangled_fields and untangled_fields.

We can use this form in any Django detail view. A typical use-case, is the built-in Django ModelAdmin:

from django.contrib import admin
from .models import Product
from .forms import ProductForm

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    form = ProductForm

Since the form used by this ModelAdmin-class can not be created dynamically, we have to declare it explicitly using the form-attribute. This is the only change which has to be performed, in order to store arbitrary content inside our JSON model-fields.

Caveats

Due to the nature of JSON, indexing and thus building filters or sorting rules based on the fields content is not as simple, as with standard model fields. Therefore, this approach is best suited, if the main focus is to store data, rather than digging through data.

Foreign keys are stored as "fieldname": {"model": "appname.modelname", "pk": 1234} in our JSON field, meaning that we have no database constraints. If a target object is deleted, that foreign key points to nowhere. Therefore always keep in mind, that we don't have any referential integrity and hence must writing our code in a defensive manner.

Changes

  • 0.3

    • Add support for ModelMultipleChoiceField.
    • Fix: Make a deep copy of entangled_fields and untangled_fields before merging.
    • Add covenience class EntangledModelForm.
    • Moving data from entangled fields onto their compressed representation, now is performed in after the form has performed its own clean(), so that accessing form fields is more natural.
    • Add functions get_related_object and get_related_queryset to get the model object from its JSON representation.
  • 0.2

    • Introduce Meta-option untangled_fields, because the approach in 0.1 didn't always work.
    • Use formfield()-method, for portability reasons with Django's Postgres JSON field.
  • 0.1

    • Initial release.
You can’t perform that action at this time.