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

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

Documentation

For comprehensive documentation, please see the docs folder.

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.0.tar.gz (25.6 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.0-py2.py3-none-any.whl (14.1 kB view details)

Uploaded Python 2Python 3

File details

Details for the file djhierarchical-0.1.0.tar.gz.

File metadata

  • Download URL: djhierarchical-0.1.0.tar.gz
  • Upload date:
  • Size: 25.6 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.0.tar.gz
Algorithm Hash digest
SHA256 e0feb653bf2eb58c17b94b098f1c36c5dcdf4cb0b3f7dec7ab3916ceed296f2b
MD5 9e61ea8c809247c9f4c73ae005148ab2
BLAKE2b-256 0a9ab85362bfcc17006da1c367bf7a8f974a4478ce5b0dcb9797a6cdc08841ef

See more details on using hashes here.

Provenance

The following attestation bundles were made for djhierarchical-0.1.0.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.0-py2.py3-none-any.whl.

File metadata

File hashes

Hashes for djhierarchical-0.1.0-py2.py3-none-any.whl
Algorithm Hash digest
SHA256 cc2bdd2d7461d6ef46131adbf793e852391dd0d13ed410a15b0e31275f10efdb
MD5 2903f78ddae9f46f167f603e758955ce
BLAKE2b-256 6e618c75c70dcfe2f55950d26ecc38c7aa5e500dc1c59b1784ac321f77a4c1d4

See more details on using hashes here.

Provenance

The following attestation bundles were made for djhierarchical-0.1.0-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