django-eav

Introduction

django-eav provides an Entity-Attribute-Value storage model for django apps.

For a decent explanation of what an Entity-Attribute-Value storage model is, check Wikipedia.

Note

This software was inspired / derived from the excellent eav-django written by Andrey Mikhaylenko.

There are a few notable differences between this implementation and the eav-django implementation.

  • This one is called django-eav, whereas the other is called eav-django.
  • This app allows you to to ‘attach’ EAV attributes to any existing django model (even from third-party apps) without making any changes to the those models’ code.
  • This app has slightly more robust (but still not perfect) filtering.

Installation

You can install django-eav directly from guthub:

pip install -e git+git://github.com/mvpdev/django-eav.git#egg=django-eav

After installing, add eav to your INSTALLED_APPS in your project’s settings.py file.

Usage

In order to attach EAV attributes to a model, you first need to register it (just like you may register your models with django.contrib.admin).

Registration

Registering a model with eav does a few things:

  • Adds the eav eav.managers.EntityManager to your class. By default, it will replace the default objects manager of the model, but you can choose to have the eav manager named something else if you don’t want it to replace objects (see Advanced Registration).
  • Connects the model’s post_init signal to attach_eav_attr(). This function will attach the eav eav.models.Entity helper class to every instance of your model when it is instatiated. By default, it will be attached to your models as an attribute named eav, which will allow you to access it through my_model_instance.eav, but you can choose to name it something else if you want (again see Advanced Registration).
  • Connect’s the model’s pre_save signal to eav.models.Entity.pre_save_handler().
  • Connect’s the model’s post_save signal to eav.models.Entity.post_save_handler().
  • Adds a generic relation helper to the class.
  • Sets an attribute called _eav_config_cls on the model class to either the default eav.registry.EavConfig config class, or to the config class you provided during registration.

If that all sounds too complicated, don’t worry, you really don’t need to think about it. Just thought you should know.

Simple Registration

To register any model with EAV, you simply need to add the registration line somewhere that will be executed when django starts:

import eav
eav.register(MyModel)

Generally, the most appropriate place for this would be in your models.py immediately after your model definition.

Advanced Registration

Advanced registration is only required if:

  • You don’t want eav to replace your model’s default objects manager.
  • You want to name the Entity helper attribute something other than eav
  • You don’t want all eav Attribute objects to be able to be set for all of your registered models. In other words, you have different types of entities, each with different attributes.

Advanced registration is simple, and is performed the exact same way you override the django.contrib.admin registration defaults.

You just need to define your own config class that subclasses EavConfig and override the default class attributes and method.

There are five EavConfig class attributes you can override:

Class Attribute Default Description
manager_attr 'objects' The name of the eav manager
manager_only False boolean Whether to only replace the manager, and do nothing else
eav_attr 'eav' The attribute name of the entity helper
generic_relation_attr 'eav_values' The attribute name of the generic relation helper
generic_relation_related_name The model’s __class__.__name__ The related name of the related name of the generic relation to your model

An example of just choosing a different name for the manager (and thus leaving objects intact):

class MyEavConfigClass(EavConfig):
    manager_attr = 'eav_objects'

eav.register(MyModel, MyEavConfigClass)

Additionally, EavConfig defines a classmethod called get_attributes that, by default will return Attribute.objects.all() This method is used to determine which Attribute can be applied to the entity model you are registering. If you want to limit which attributes can be applied to your entity, you would need to override it.

For example:

class MyEavConfigClass(EavConfig):
    @classmethod
    def get_attributes(cls):
        return Attribute.objects.filter(type='person')

eav.register(MyModel, MyEavConfigClass)

Using Attributes

Once you’ve registered your model(s), you can begin to use them with EAV attributes. Let’s assume your model is called Person and it has one normal django CharField called name, but you want to be able to dynamically store other data about each Person.

First, let’s create some attributes:

>>> Attribute.objects.create(name='Weight', datatype=Attribute.TYPE_FLOAT)
>>> Attribute.objects.create(name='Height', datatype=Attribute.TYPE_INT)
>>> Attribute.objects.create(name='Is pregant?', datatype=Attribute.TYPE_BOOLEAN)

Now let’s create a patient, and set some of these attributes:

>>> p = Patient.objects.create(name='Bob')
>>> p.eav.height = 46
>>> p.eav.weight = 42.2
>>> p.eav.is_pregnant = False
>>> p.save()
>>> bob = Patient.objects.get(name='Bob')
>>> bob.eav.height
46
>>> bob.eav.weight
42.2
>>> bob.is_pregnant
False

Additionally, assuming we’re using the eav manager, we can also do:

>>> p = Patient.objects.create(name='Jen', eav__height=32, eav__pregnant=True)

Filtering

eav attributes are filterable, using the same __ notation as django foreign keys:

Patient.objects.filter(eav__weight=42.2)
Patient.objects.filter(eav__weight__gt=42)
Patient.objects.filter(name='Bob', eav__weight__gt=42)
Patient.objects.exclude(eav__is_pregnant=False)

You can even use Q objects, however there are some known issues (see Q Object Filters) with Q object filters:

Patient.objects.filter(Q(name='Bob') | Q(eav__is_pregnant=False))

What about if you have a foreign key to a model that uses eav, but you want to filter from a model that doesn’t use eav? For example, let’s say you have a Patient model that doesn’t use eav, but it has a foreign key to Encounter that does use eav. You can even filter through eav across this relationship, but you need to use the eav manager for Patient.

Just register Patient with eav, but set manager_only = True see (see Advanced Registration). Then you can do:

Patient.objects.filter(encounter__eav__weight=2)

Admin Integration

You can even have your eav attributes show up just like normal fields in your models admin pages. Just register using the eav admin class:

from django.contrib import admin
from eav.forms import BaseDynamicEntityForm
from eav.admin import BaseEntityAdmin

class PatientAdminForm(BaseDynamicEntityForm):
    model = Patient

class PatientAdmin(BaseEntityAdmin):
    form = PatientAdminForm

admin.site.register(Patient, PatientAdmin)

Known Issues

Q Object Filters

Due to an unexplained Q object / generic relation issue, exclude filters with EAV Q objects, or EAV Q objects ANDed together may produce inaccurate results.