Skip to main content

A Django app for hierarchical configuration models with automatic inheritance

Project description

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

djhierarchical-0.1.1.tar.gz (27.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

djhierarchical-0.1.1-py2.py3-none-any.whl (14.2 kB view details)

Uploaded Python 2Python 3

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

Hashes for djhierarchical-0.1.1.tar.gz
Algorithm Hash digest
SHA256 7e5f2f19409baab4f8093a33500b67cf5b666d63f9a1180a6460d6744f78e720
MD5 35ae09d345bf802cdaf7745f429a8b13
BLAKE2b-256 1e14d1b8241a1bee7d1a5b26194cfc3b023ce459af588f82c4670724d13be5e1

See more details on using hashes here.

Provenance

The following attestation bundles were made for djhierarchical-0.1.1.tar.gz:

Publisher: publish.yml on aibin/djhierarchical

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file djhierarchical-0.1.1-py2.py3-none-any.whl.

File metadata

File hashes

Hashes for djhierarchical-0.1.1-py2.py3-none-any.whl
Algorithm Hash digest
SHA256 7ff4d2e7f9dbdfc8087b6c63c0e94bb9d18ffd5c76f0c81b87f83677849e9a17
MD5 d0f40802dd4ab8d13eb087c060a7457e
BLAKE2b-256 ef5e7e3de3d6794e5586a066b6c9a441dc374c518591428eee91e26039bf10c0

See more details on using hashes here.

Provenance

The following attestation bundles were made for djhierarchical-0.1.1-py2.py3-none-any.whl:

Publisher: publish.yml on aibin/djhierarchical

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page