Skip to main content

Django Hierarchical Configuration

PyPI version Python Versions Django Versions CI Status Documentation License

A Django app that provides a hierarchical configuration pattern with automatic inheritance between related models at any number of levels deep.

Documentation

📚 Read the full 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:

  1. Checks if the shadow field has a value
  2. If not, traverses up the parent hierarchy (through any number of levels) to find a value
  3. 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)

Source distribution for djhierarchical 0.1.1
File Size Uploaded
djhierarchical-0.1.1.tar.gz 27.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for djhierarchical 0.1.1
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page