A Django app for hierarchical configuration models with automatic inheritance
Project description
Django Hierarchical Configuration
A Django app that provides a hierarchical configuration pattern with automatic inheritance between related models at any number of levels deep.
Documentation
Features
- 🔄 Automatic inheritance of values from parent models through unlimited hierarchy levels
- 🛠️ Simple API using standard Django model field access
- 🎯 Override values at any level in the hierarchy
- 📋 Shadow fields with underscore prefix for storing overrides
- ✅ Works with any Django model field type, including ManyToMany relationships
- 📊 Visual indicators of inheritance in admin interface
- 🔄 Unlimited depth of inheritance (not limited to just parent-child relationships)
- 🧩 Easy integration with Django forms using shadow fields
- ⚙️ Configurable via Django settings
Installation
pip install djhierarchical
Add hierarchical to your INSTALLED_APPS setting:
INSTALLED_APPS = [
# ...
'hierarchical',
# ...
]
Note: The package name on PyPI is djhierarchical, but the Django app name to use in INSTALLED_APPS is hierarchical.
Configuration (Optional)
You can configure the behavior of hierarchical models through Django settings:
# Optional settings for hierarchical models
HIERARCHICAL_MODELS = {
# Default attribute name to use for the hierarchical parent relationship
'DEFAULT_PARENT_ATTR': 'hierarchical_parent',
# Enable/disable debug logging
'DEBUG': False,
}
Quick Start
Create hierarchical models by using the HierarchicalModelMixin with your models:
from django.db import models
from hierarchical.models import HierarchicalModelMixin
# Define a base config model
class ConfigBase(models.Model, HierarchicalModelMixin):
# Regular fields
name = models.CharField(max_length=100)
# Shadow fields with underscore prefix for hierarchical inheritance
_tax = models.IntegerField(null=True, blank=True)
_percentage = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
class Meta:
abstract = True
# Create concrete models in hierarchy - you can create as many levels as needed
class Country(ConfigBase):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Province(ConfigBase):
name = models.CharField(max_length=100)
country = models.ForeignKey(Country, on_delete=models.CASCADE)
# Define the hierarchical parent relationship
@property
def hierarchical_parent(self):
return self.country
def __str__(self):
return self.name
class City(ConfigBase):
name = models.CharField(max_length=100)
province = models.ForeignKey(Province, on_delete=models.CASCADE)
@property
def hierarchical_parent(self):
return self.province
def __str__(self):
return self.name
class District(ConfigBase):
name = models.CharField(max_length=100)
city = models.ForeignKey(City, on_delete=models.CASCADE)
@property
def hierarchical_parent(self):
return self.city
def __str__(self):
return self.name
class Building(ConfigBase):
name = models.CharField(max_length=100)
district = models.ForeignKey(District, on_delete=models.CASCADE)
@property
def hierarchical_parent(self):
return self.district
def __str__(self):
return self.name
Using the models
# Create a hierarchy with values at different levels
canada = Country.objects.create(name="Canada", tax=5)
ontario = Province.objects.create(name="Ontario", country=canada, percentage=8.0)
toronto = City.objects.create(name="Toronto", province=ontario)
downtown = District.objects.create(name="Downtown", city=toronto, tax=6)
tower = Building.objects.create(name="CN Tower", district=downtown)
# Access values that traverse multiple levels in the hierarchy
tower.tax # Returns 6 (from district)
tower.percentage # Returns 8.0 (from province, skipping city level)
# Deep inheritance is handled automatically
if ontario.tax is None:
ontario.tax = 7
ontario.save()
downtown.percentage = 9.0
downtown.save()
tower.tax # Still returns 6 (from district)
tower.percentage # Returns 9.0 (from district now)
ManyToMany Fields
You can also use hierarchical ManyToMany fields:
class Tag(models.Model):
name = models.CharField(max_length=100)
class Organization(models.Model, HierarchicalModelMixin):
name = models.CharField(max_length=100)
parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE)
# Hierarchical ManyToMany field (note the underscore prefix)
_tags = models.ManyToManyField(Tag, blank=True)
@property
def hierarchical_parent(self):
return self.parent
Forms Support
The simplest way to work with hierarchical fields in Django forms is to directly use the shadow fields (with underscore prefix):
from django import forms
class BuildingForm(forms.ModelForm):
class Meta:
model = Building
fields = ['name', 'district', '_tax', '_percentage']
labels = {
'_tax': 'Tax',
'_percentage': 'Percentage'
}
help_texts = {
'_tax': 'Enter a value or leave blank to inherit from district',
'_percentage': 'Enter a value or leave blank to inherit from district'
}
For more complex scenarios, where you want to show inherited values and explicitly handle inheritance, you can use both property fields and shadow fields:
from django import forms
class BuildingForm(forms.ModelForm):
# Define form fields for visibility
tax = forms.IntegerField(required=False)
percentage = forms.DecimalField(max_digits=10, decimal_places=2, required=False)
class Meta:
model = Building
fields = ['name', 'district', 'tax', 'percentage', '_tax', '_percentage']
widgets = {
'_tax': forms.HiddenInput(),
'_percentage': forms.HiddenInput(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Set initial values for the form fields
if self.instance.pk:
self.fields['tax'].initial = self.instance.tax
self.fields['percentage'].initial = self.instance.percentage
def clean(self):
cleaned_data = super().clean()
# Map form fields to shadow fields
if 'tax' in cleaned_data:
cleaned_data['_tax'] = cleaned_data['tax'] if cleaned_data['tax'] != '' else None
if 'percentage' in cleaned_data:
cleaned_data['_percentage'] = cleaned_data['percentage'] if cleaned_data['percentage'] != '' else None
return cleaned_data
For more detailed examples, see the forms integration documentation.
How It Works
The mixin creates shadow fields for each field in your model, prefixed with an underscore (e.g., _tax).
These shadow fields store the override values. When you access a field, the mixin:
- Checks if the shadow field has a value
- If not, traverses up the parent hierarchy (through any number of levels) to find a value
- If no value is found in the hierarchy, returns None
The recursion happens automatically when accessing properties, so no matter how deep the hierarchy, the system will find the nearest ancestor with a value.
Advanced Usage
Clearing an Override
To clear an override and fall back to the parent value:
city.clear_override('tax') # Clear the override, will now inherit from province
Or by setting the value to None:
city.tax = None # Also clears the override
city.save()
Using in Admin
Register your models with the admin site:
from django.contrib import admin
from hierarchical.admin import HierarchicalModelAdmin
@admin.register(City)
class CityAdmin(HierarchicalModelAdmin):
list_display = ('name', 'province', 'tax', 'percentage')
Development
Clone the repository:
git clone https://github.com/crewii/djhierarchical.git
cd djhierarchical
pip install -e .
Run tests:
python runtests.py
License
This project is licensed under the MIT License - see the LICENSE file for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file djhierarchical-0.1.1.tar.gz.
File metadata
- Download URL: djhierarchical-0.1.1.tar.gz
- Upload date:
- Size: 27.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7e5f2f19409baab4f8093a33500b67cf5b666d63f9a1180a6460d6744f78e720
|
|
| MD5 |
35ae09d345bf802cdaf7745f429a8b13
|
|
| BLAKE2b-256 |
1e14d1b8241a1bee7d1a5b26194cfc3b023ce459af588f82c4670724d13be5e1
|
Provenance
The following attestation bundles were made for djhierarchical-0.1.1.tar.gz:
Publisher:
publish.yml on aibin/djhierarchical
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
djhierarchical-0.1.1.tar.gz -
Subject digest:
7e5f2f19409baab4f8093a33500b67cf5b666d63f9a1180a6460d6744f78e720 - Sigstore transparency entry: 187336849
- Sigstore integration time:
-
Permalink:
aibin/djhierarchical@4a65f3dcb2be6cad1cbd93ee11f3b2647c14caf0 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/aibin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@4a65f3dcb2be6cad1cbd93ee11f3b2647c14caf0 -
Trigger Event:
release
-
Statement type:
File details
Details for the file djhierarchical-0.1.1-py2.py3-none-any.whl.
File metadata
- Download URL: djhierarchical-0.1.1-py2.py3-none-any.whl
- Upload date:
- Size: 14.2 kB
- Tags: Python 2, Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7ff4d2e7f9dbdfc8087b6c63c0e94bb9d18ffd5c76f0c81b87f83677849e9a17
|
|
| MD5 |
d0f40802dd4ab8d13eb087c060a7457e
|
|
| BLAKE2b-256 |
ef5e7e3de3d6794e5586a066b6c9a441dc374c518591428eee91e26039bf10c0
|
Provenance
The following attestation bundles were made for djhierarchical-0.1.1-py2.py3-none-any.whl:
Publisher:
publish.yml on aibin/djhierarchical
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
djhierarchical-0.1.1-py2.py3-none-any.whl -
Subject digest:
7ff4d2e7f9dbdfc8087b6c63c0e94bb9d18ffd5c76f0c81b87f83677849e9a17 - Sigstore transparency entry: 187336850
- Sigstore integration time:
-
Permalink:
aibin/djhierarchical@4a65f3dcb2be6cad1cbd93ee11f3b2647c14caf0 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/aibin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@4a65f3dcb2be6cad1cbd93ee11f3b2647c14caf0 -
Trigger Event:
release
-
Statement type: