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.
Metadata
Release files for djhierarchical 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| djhierarchical-0.1.1.tar.gz | 27.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| djhierarchical-0.1.1-py2.py3-none-any.whl | Python 2, Python 3 | none | any | Details |
Total release size: 42.0 kB
Release files / djhierarchical-0.1.1.tar.gz
| Download URL | djhierarchical-0.1.1.tar.gz |
|---|---|
| Size | 27.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7e5f2f19409baab4f8093a33500b67cf5b666d63f9a1180a6460d6744f78e720
|
|
BLAKE2b-256 checksum How to use checksums |
1e14d1b8241a1bee7d1a5b26194cfc3b023ce459af588f82c4670724d13be5e1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.12.9
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 24, 2025.
Transparency logRelease files / djhierarchical-0.1.1-py2.py3-none-any.whl
| Download URL | djhierarchical-0.1.1-py2.py3-none-any.whl |
|---|---|
| Size | 14.2 kB |
| Tags | Python 2 Python 3 |
|
SHA-256 checksum How to use checksums |
7ff4d2e7f9dbdfc8087b6c63c0e94bb9d18ffd5c76f0c81b87f83677849e9a17
|
|
BLAKE2b-256 checksum How to use checksums |
ef5e7e3de3d6794e5586a066b6c9a441dc374c518591428eee91e26039bf10c0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.12.9
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 24, 2025.
Transparency log