# django-entangled
Edit JSON-Model Fields using a Standard Django Form.
[![Build Status](https://travis-ci.org/jrief/django-entangled.svg?branch=master)](https://travis-ci.org/jrief/django-entangled)
[![Coverage](https://codecov.io/github/jrief/django-entangled/coverage.svg?branch=master)](https://codecov.io/github/jrief/django-entangled?branch=master)
[![PyPI](https://img.shields.io/pypi/pyversions/django-entangled.svg)]()
[![PyPI version](https://img.shields.io/pypi/v/django-entangled.svg)](https://https://pypi.python.org/pypi/django-entangled)
[![PyPI](https://img.shields.io/pypi/l/django-entangled.svg)]()
## Use-Case
A Django Model may contain fields which accept arbitrary data stored as JSON. Django itself, provides a
[JSON field](https://docs.djangoproject.com/en/stable/ref/models/fields/#django.db.models.JSONField) to store arbitrary
serializable data.
When creating a form from a model, 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](https://github.com/josdejong/jsoneditor),
which with some JavaScript, transforms the widget into an attribute-value-pair editor. This approach however requires
to manage the field keys ourself. It furthermore 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:
```bash
pip install django-entangled
```
There is no need to add any configuration 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.
```python
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=50)
price = models.DecimalField(max_digits=5, decimal_places=2)
properties = models.JSONField()
```
In a typical form editing view, we would create a form inheriting from
[ModelForm](https://docs.djangoproject.com/en/stable/topics/forms/modelforms/#modelform) and refer to this model using
the `model` attribute in its `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 alternative class `EntangledModelForm`.
```python
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 case our form inherits from another `ModelForm`, rewrite the class declarartion as:
```python
class ProductForm(EntangledModelFormMixin, BaseProductForm):
...
```
In addition 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](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#generic-relations)
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. Alternatively, you can use `fields`
to manage which entangled **and** untangled fields are shown. If `fields` is not defined, django-entangled will
internally create it based on the `untangled` option.
We can use this form in any Django form view. A typical use-case, is the built-in Django `ModelAdmin`:
```python
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](https://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.form),
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.
## Nested Data Structures
Sometimes it can be desirable to store the data in a nested hierarchy of dictionaries, rather than having all
attribute-value-pairs in the first level of our JSON field. This can for instance be handy when merging more than one
form, all themselves ineriting from `EntangledModelFormMixin`.
Say that we have different types of products, all of which share the same base product form:
```python
from django.contrib.auth import get_user_model
from django.forms import models
from entangled.forms import EntangledModelFormMixin
from .models import Product
class BaseProductForm(EntangledModelFormMixin):
tenant = models.ModelChoiceField(
queryset=get_user_model().objects.filter(is_staff=True),
)
class Meta:
model = Product
entangled_fields = {'properties': ['tenant']}
untangled_fields = ['name', 'price']
```
In order to specialize our base product towards, say clothing, we typically would inherit from the base form
and add some additional fields, here `color` and `size`:
```python
from django.forms import fields
from .forms import BaseProductForm
from .models import Product
class ClothingProductForm(BaseProductForm):
color = fields.RegexField(
regex=r'^#[0-9a-f]{6}$',
)
size = fields.ChoiceField(
choices=[('s', "small"), ('m', "medium"), ('l', "large"), ('xl', "extra large")],
)
class Meta:
model = Product
entangled_fields = {'properties': ['color', 'size']}
retangled_fields = {'color': 'variants.color', 'size': 'variants.size'}
```
By adding a name mapping from our existing field names, we can group the fields `color` and `size`
into a sub-dictionary named `variants` inside our `properties` fields. Such a field mapping is
declared through the optional Meta-option `retangled_fields`. In this dictionary, all entries are
optional; if a field name is missing, it just maps to itself.
This mapping table can also be used to map field names to other keys inside the resulting JSON
datastructure. This for instance is handy to map fields containg an underscore into field-names
containing instead a dash.
## 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 write our code in a defensive manner.
## Contributing to the Project
* Please ask question on the [discussion board](https://github.com/jrief/django-entangled/discussions).
* Ideas for new features shall as well be discussed on that board.
* The [issue tracker](https://github.com/jrief/django-entangled/issues) shall *exclusively* be used to report bugs.
* Except for very small fixes (typos etc.), do not open a pull request without an issue.
* Before writing code, adopt your IDE to respect the project's [.editorconfig](https://github.com/jrief/django-entangled/blob/master/.editorconfig).
[![Twitter Follow](https://img.shields.io/twitter/follow/jacobrief?style=social)](https://twitter.com/jacobrief)
Raw data
{
"_id": null,
"home_page": "https://github.com/jrief/django-entangled",
"name": "django-entangled",
"maintainer": null,
"docs_url": null,
"requires_python": null,
"maintainer_email": null,
"keywords": "Django Forms, JSON",
"author": "Jacob Rief",
"author_email": "jacob.rief@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/6c/48/387598c125a02ed1b4d866672f59df4c916015c39230220224f0cd8379db/django_entangled-0.6.2.tar.gz",
"platform": "OS Independent",
"description": "# django-entangled\n\nEdit JSON-Model Fields using a Standard Django Form.\n\n[![Build Status](https://travis-ci.org/jrief/django-entangled.svg?branch=master)](https://travis-ci.org/jrief/django-entangled)\n[![Coverage](https://codecov.io/github/jrief/django-entangled/coverage.svg?branch=master)](https://codecov.io/github/jrief/django-entangled?branch=master)\n[![PyPI](https://img.shields.io/pypi/pyversions/django-entangled.svg)]()\n[![PyPI version](https://img.shields.io/pypi/v/django-entangled.svg)](https://https://pypi.python.org/pypi/django-entangled)\n[![PyPI](https://img.shields.io/pypi/l/django-entangled.svg)]()\n\n\n## Use-Case\n\nA Django Model may contain fields which accept arbitrary data stored as JSON. Django itself, provides a\n[JSON field](https://docs.djangoproject.com/en/stable/ref/models/fields/#django.db.models.JSONField) to store arbitrary\nserializable data.\n\nWhen creating a form from a model, the input field associated with a JSON field, typically is a `<textarea ...></textarea>`.\nThis textarea widget is very inpracticable for editing, because it just contains a textual representation of that\nobject notation. One possibility is to use a generic [JSON editor](https://github.com/josdejong/jsoneditor),\nwhich with some JavaScript, transforms the widget into an attribute-value-pair editor. This approach however requires\nto manage the field keys ourself. It furthermore prevents us from utilizing all the nice features provided by the Django\nform framework, such as field validation, normalization of data and the usage of foreign keys.\n\nBy using **django-entangled**, one can use a Django `ModelForm`, and store all,\nor a subset of that form fields in one or more JSON fields inside of the associated model.\n\n\n## Installation\n\nSimply install this Django app, for instance by invoking:\n\n```bash\npip install django-entangled\n```\n\nThere is no need to add any configuration directives to the project's `settings.py`.\n\n\n## Example\n\nSay, we have a Django model to describe a bunch of different products. The name and the price fields are common to all\nproducts, whereas the properties can vary depending on its product type. Since we don't want to create a different\nproduct model for each product type, we use a JSON field to store these arbitrary properties.\n\n```python\nfrom django.db import models\n\nclass Product(models.Model):\n name = models.CharField(max_length=50)\n\n price = models.DecimalField(max_digits=5, decimal_places=2)\n\n properties = models.JSONField()\n```\n\nIn a typical form editing view, we would create a form inheriting from\n[ModelForm](https://docs.djangoproject.com/en/stable/topics/forms/modelforms/#modelform) and refer to this model using\nthe `model` attribute in its `Meta`-class. Then the `properties`-field would show up as unstructured JSON, rendered\ninside a `<textarea ...></textarea>`. This definitely is not what we want! Instead we create a typical Django Form using\nthe alternative class `EntangledModelForm`.\n\n```python\nfrom django.contrib.auth import get_user_model\nfrom django.forms import fields, models\nfrom entangled.forms import EntangledModelForm\nfrom .models import Product\n\nclass ProductForm(EntangledModelForm):\n color = fields.RegexField(\n regex=r'^#[0-9a-f]{6}$',\n )\n\n size = fields.ChoiceField(\n choices=[('s', \"small\"), ('m', \"medium\"), ('l', \"large\"), ('xl', \"extra large\")],\n )\n\n tenant = models.ModelChoiceField(\n queryset=get_user_model().objects.filter(is_staff=True),\n )\n\n class Meta:\n model = Product\n entangled_fields = {'properties': ['color', 'size', 'tenant']} # fields provided by this form\n untangled_fields = ['name', 'price'] # these fields are provided by the Product model\n```\n\nIn case our form inherits from another `ModelForm`, rewrite the class declarartion as:\n\n```python\nclass ProductForm(EntangledModelFormMixin, BaseProductForm):\n ...\n```\n\nIn addition we add a special dictionary named `entangled_fields` to our `Meta`-options. In this dictionary, the key\n(here `'properties'`) refers to the JSON-field in our model `Product`. The value (here `['color', 'size', 'tenant']`)\nis a list of named form fields, declared in our form- or base-class of thereof. This allows us to assign all standard\nDjango form fields to arbitrary JSON fields declared in our Django model. Moreover, we can even use a `ModelChoiceField`\nor a `ModelMultipleChoiceField` to refer to another model object using a\n[generic relation](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#generic-relations)\n\nSince in this form we also want to access the non-JSON fields from our Django model, we add a list named\n`untangled_fields` to our `Meta`-options. In this list, (here `['name', 'price']`) we refer to the non-JSON fields\nin our model `Product`. From both of these iterables, `entangled_fields` and `untangled_fields`, the parent class\n`EntangledModelForm` then builds the `Meta`-option `fields`, otherwise required. Alternatively, you can use `fields` \nto manage which entangled **and** untangled fields are shown. If `fields` is not defined, django-entangled will\ninternally create it based on the `untangled` option.\n\nWe can use this form in any Django form view. A typical use-case, is the built-in Django `ModelAdmin`:\n\n```python\nfrom django.contrib import admin\nfrom .models import Product\nfrom .forms import ProductForm\n\n@admin.register(Product)\nclass ProductAdmin(admin.ModelAdmin):\n form = ProductForm\n```\n\nSince the form used by this `ModelAdmin`-class\n[can not be created dynamically](https://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.form),\nwe have to declare it explicitly using the `form`-attribute. This is the only change which has to be performed, in\norder to store arbitrary content inside our JSON model-fields.\n\n\n## Nested Data Structures\n\nSometimes it can be desirable to store the data in a nested hierarchy of dictionaries, rather than having all\nattribute-value-pairs in the first level of our JSON field. This can for instance be handy when merging more than one\nform, all themselves ineriting from `EntangledModelFormMixin`.\n\nSay that we have different types of products, all of which share the same base product form:\n\n```python\nfrom django.contrib.auth import get_user_model\nfrom django.forms import models\nfrom entangled.forms import EntangledModelFormMixin\nfrom .models import Product\n\nclass BaseProductForm(EntangledModelFormMixin):\n tenant = models.ModelChoiceField(\n queryset=get_user_model().objects.filter(is_staff=True),\n )\n\n class Meta:\n model = Product\n entangled_fields = {'properties': ['tenant']}\n untangled_fields = ['name', 'price']\n```\n\nIn order to specialize our base product towards, say clothing, we typically would inherit from the base form\nand add some additional fields, here `color` and `size`:\n\n```python\nfrom django.forms import fields\nfrom .forms import BaseProductForm\nfrom .models import Product\n\nclass ClothingProductForm(BaseProductForm):\n color = fields.RegexField(\n regex=r'^#[0-9a-f]{6}$',\n )\n\n size = fields.ChoiceField(\n choices=[('s', \"small\"), ('m', \"medium\"), ('l', \"large\"), ('xl', \"extra large\")],\n )\n\n class Meta:\n model = Product\n entangled_fields = {'properties': ['color', 'size']}\n retangled_fields = {'color': 'variants.color', 'size': 'variants.size'}\n```\n\nBy adding a name mapping from our existing field names, we can group the fields `color` and `size`\ninto a sub-dictionary named `variants` inside our `properties` fields. Such a field mapping is\ndeclared through the optional Meta-option `retangled_fields`. In this dictionary, all entries are\noptional; if a field name is missing, it just maps to itself.\n\nThis mapping table can also be used to map field names to other keys inside the resulting JSON\ndatastructure. This for instance is handy to map fields containg an underscore into field-names\ncontaining instead a dash. \n\n\n## Caveats\n\nDue to the nature of JSON, indexing and thus building filters or sorting rules based on the fields content is not as\nsimple, as with standard model fields. Therefore, this approach is best suited, if the main focus is to store data,\nrather than digging through data.\n\nForeign keys are stored as `\"fieldname\": {\"model\": \"appname.modelname\", \"pk\": 1234}` in our JSON field, meaning that\nwe have no database constraints. If a target object is deleted, that foreign key points to nowhere. Therefore always\nkeep in mind, that we don't have any referential integrity and hence must write our code in a defensive manner.\n\n\n## Contributing to the Project\n\n* Please ask question on the [discussion board](https://github.com/jrief/django-entangled/discussions).\n* Ideas for new features shall as well be discussed on that board.\n* The [issue tracker](https://github.com/jrief/django-entangled/issues) shall *exclusively* be used to report bugs.\n* Except for very small fixes (typos etc.), do not open a pull request without an issue.\n* Before writing code, adopt your IDE to respect the project's [.editorconfig](https://github.com/jrief/django-entangled/blob/master/.editorconfig).\n\n\n[![Twitter Follow](https://img.shields.io/twitter/follow/jacobrief?style=social)](https://twitter.com/jacobrief)\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Edit JSON field using Django Model Form",
"version": "0.6.2",
"project_urls": {
"Homepage": "https://github.com/jrief/django-entangled"
},
"split_keywords": [
"django forms",
" json"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "6b0ab705af7606a4dbb8b2d8de07b192ffbece174611f84827d7d35682a4f645",
"md5": "a1587d882d225f6dd8c7f69812a5392c",
"sha256": "a8a59de3204b6b636224b169bba4f8edc1f1a364171339d66c384695dc2b24f3"
},
"downloads": -1,
"filename": "django_entangled-0.6.2-py3-none-any.whl",
"has_sig": false,
"md5_digest": "a1587d882d225f6dd8c7f69812a5392c",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": null,
"size": 15215,
"upload_time": "2024-10-23T08:13:21",
"upload_time_iso_8601": "2024-10-23T08:13:21.936829Z",
"url": "https://files.pythonhosted.org/packages/6b/0a/b705af7606a4dbb8b2d8de07b192ffbece174611f84827d7d35682a4f645/django_entangled-0.6.2-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "6c48387598c125a02ed1b4d866672f59df4c916015c39230220224f0cd8379db",
"md5": "557a4388672970380983baaa7ccc9890",
"sha256": "8aa4988950b19f55264edfd20e71e917765d43af8f408d912a9beaf5e2aa345b"
},
"downloads": -1,
"filename": "django_entangled-0.6.2.tar.gz",
"has_sig": false,
"md5_digest": "557a4388672970380983baaa7ccc9890",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 12393,
"upload_time": "2024-10-23T08:13:23",
"upload_time_iso_8601": "2024-10-23T08:13:23.513616Z",
"url": "https://files.pythonhosted.org/packages/6c/48/387598c125a02ed1b4d866672f59df4c916015c39230220224f0cd8379db/django_entangled-0.6.2.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-10-23 08:13:23",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "jrief",
"github_project": "django-entangled",
"travis_ci": false,
"coveralls": true,
"github_actions": true,
"lcname": "django-entangled"
}