CompositeField for Django Models

This is an implementation of a CompositeField for Django. Composite fields can be used to group fields together and reuse their definitions.

Installation instructions

Thanks for downloading django-composite-field.

To install it, run the following command inside this directory:

python setup.py install

If you have the Python pip utility available, you can type the following to download and install in one step:

pip install django-composite-field

Or if you’re using pipenv:

pipenv install django-composite-field

Or if you’d prefer you can simply place the included composite_field directory somewhere on your Python path, or symlink to it from somewhere on your Python path; this is useful if you’re working from a Git checkout.

Note that this application requires Python 3.4 or later, and a functional installation of Django 1.8 or newer. You can obtain Python from https://www.python.org/ and Django from https://www.djangoproject.com/.

Usage

Example

class CoordField(CompositeField):
    x = models.FloatField()
    y = models.FloatField()

class Place(models.Model):
    name = models.CharField(max_length=10)
    coord = CoordField()

p = Place(name='Foo', coord_x=42, coord_y=0)
q = Place(name='Foo', coord=p.coord)
q.coord.y = 42

How does it work?

The content of composite fields are stored inside the model, so they do not have to fiddle with any internals of the Django models. In the example above p.coord returns a proxy object that maps the fields x and y to the model fields coord_x and coord_y.

This is roughly equivalent to the following code:

class CoordProxy:

    def __init__(self, model):
        self.model = model

    def __get__(self, instance):
        return CoordProxy(instance)

    def __set__(self, instance, value):
        instance.coord_x = value.coord_x
        instance.coord_y = value.coord_y

    @property
    def x(self):
        return self.model.coord_x

    @x.setter
    def x(self, value):
        self.model.coord_x = value

    @property
    def y(self):
        return self.model.coord_y

    @y.setter
    def y(self, value):
        self.model.coord_y = value

class Place(models.Model):
    name = models.CharField(max_length=10)
    coord_x = models.FloatField()
    coord_y = models.FloatField()
    coord = CoordProxy()

Advanced usage

The proxy object also makes it possible to assign more than one property at once:

place1.coord = place2.coord

It also supports using dictionaries for the __init__ method or assigning them as a value:

place1 = Place(coord={'x': 42, 'y': 0})
place1.coord = {'x': 43, 'y': 1}

It is even possible to replace the Proxy object entirely and return a custom type. A good example for this is the included ComplexField which stores a complex number in two integer fields.

Examples

The following code is part of the unit tests and should be mostly self explaining.

models.py

from django.db import models

from composite_field import CompositeField
from composite_field import LocalizedCharField
from composite_field import ComplexField


class CoordField(CompositeField):
    x = models.FloatField(null=True)
    y = models.FloatField(null=True)

    class Proxy(CompositeField.Proxy):

        def __bool__(self):
            return self.x is not None and self.y is not None


class Place(models.Model):
    name = models.CharField(max_length=10)
    coord = CoordField()


class PlaceWithDefaultCoord(models.Model):
    name = models.CharField(max_length=10)
    coord = CoordField(default={'x': 1.0, 'y': 2.0})


class Direction(models.Model):
    source = CoordField()
    distance = models.FloatField()
    target = CoordField()


class LocalizedFoo(models.Model):
    id = models.AutoField(primary_key=True)
    name = LocalizedCharField(languages=('de', 'en'), max_length=50)

    def __str__(self):
        return self.name.current


class ComplexTuple(models.Model):
    x = ComplexField(blank=True, null=True)
    y = ComplexField(blank=False, null=False, verbose_name='Y')
    z = ComplexField(verbose_name='gamma')


class ComplexTupleWithDefaults(models.Model):
    x = ComplexField(blank=True, null=True, default=None)
    y = ComplexField(blank=False, null=False, default=42)
    z = ComplexField(default=42j)


class TranslatedAbstractBase(models.Model):
    name = LocalizedCharField(languages=('de', 'en'), max_length=50)

    class Meta:
        abstract = True


class TranslatedModelA(TranslatedAbstractBase):
    pass


class TranslatedModelB(TranslatedAbstractBase):
    pass


class TranslatedNonAbstractBase(models.Model):
    name = LocalizedCharField(languages=('de', 'en'), max_length=50)

    class Meta:
        abstract = False


class TranslatedModelC(TranslatedNonAbstractBase):
    pass


class TranslatedModelD(TranslatedNonAbstractBase):
    pass

tests.py

import math
import unittest

import django
import django.test
from django.test import TestCase
try:
    from django.urls import reverse
except ImportError:
    from django.core.urlresolvers import reverse
from django.utils import translation
from django.utils.encoding import force_text

from composite_field_test.models import Place
from composite_field_test.models import PlaceWithDefaultCoord
from composite_field_test.models import Direction
from composite_field_test.models import LocalizedFoo
from composite_field_test.models import ComplexTuple
from composite_field_test.models import ComplexTupleWithDefaults
from composite_field_test.models import TranslatedAbstractBase
from composite_field_test.models import TranslatedModelA
from composite_field_test.models import TranslatedModelB
from composite_field_test.models import TranslatedNonAbstractBase
from composite_field_test.models import TranslatedModelC
from composite_field_test.models import TranslatedModelD


class CompositeFieldTestCase(TestCase):

    def test_repr(self):
        place = Place(coord_x=12.0, coord_y=42.0)
        self.assertEqual(repr(place.coord), 'CoordField(x=12.0, y=42.0)')

    def test_cmp(self):
        place1 = Place(coord_x=12.0, coord_y=42.0)
        place2 = Place(coord_x=42.0, coord_y=12.0)
        self.assertNotEqual(place1.coord, place2.coord)
        place2.coord.x = 12.0
        place2.coord.y = 42.0
        self.assertEqual(place1.coord, place2.coord)

    def test_assign(self):
        place1 = Place(coord_x=12.0, coord_y=42.0)
        place2 = Place()
        place2.coord = place1.coord
        self.assertEqual(place1.coord, place2.coord)
        place2 = Place(coord=place1.coord)
        self.assertEqual(place1.coord, place2.coord)

    def test_setattr(self):
        place = Place()
        place.coord.x = 12.0
        place.coord.y = 42.0
        self.assertEqual(place.coord_x, 12.0)
        self.assertEqual(place.coord_y, 42.0)
        self.assertEqual(place.coord.x, 12.0)
        self.assertEqual(place.coord.y, 42.0)

    def test_field_order(self):
        fields = Place._meta.fields
        get_field = Place._meta.get_field
        name = get_field('name')
        coord_x = get_field('coord_x')
        coord_y = get_field('coord_y')
        self.assertTrue(fields.index(name) < fields.index(coord_x))
        self.assertTrue(fields.index(coord_x) < fields.index(coord_y))

    def test_field_order2(self):
        fields = Direction._meta.fields
        get_field = Direction._meta.get_field
        source_x = get_field('source_x')
        source_y = get_field('source_y')
        distance = get_field('distance')
        target_x = get_field('target_x')
        target_y = get_field('target_y')
        self.assertTrue(fields.index(source_x) < fields.index(source_y))
        self.assertTrue(fields.index(source_y) < fields.index(distance))
        self.assertTrue(fields.index(distance) < fields.index(target_x))
        self.assertTrue(fields.index(target_x) < fields.index(target_y))

    def test_modelform(self):
        from django import forms
        class DirectionForm(forms.ModelForm):
            class Meta:
                model = Direction
                exclude = ()
        form = DirectionForm()
        form = DirectionForm({})
        form.is_valid()

    def test_modelform_with_exclude(self):
        from django import forms
        class LocalizedFooForm(forms.ModelForm):
            class Meta:
                model = LocalizedFoo
                exclude = ()
        form = LocalizedFooForm()
        form = LocalizedFooForm({})
        self.assertFalse(form.is_valid())
        form = LocalizedFooForm({'name_de': 'Banane', 'name_en': 'Banana'})
        self.assertTrue(form.is_valid())
        foo = form.save(commit=False)
        self.assertEquals(foo.name.de, 'Banane')
        self.assertEquals(foo.name.en, 'Banana')

    def test_modelform_with_fields(self):
        from django import forms
        class LocalizedFooForm(forms.ModelForm):
            class Meta:
                model = LocalizedFoo
                fields = ('name_de', 'name_en')
        form = LocalizedFooForm()
        form = LocalizedFooForm({})
        self.assertFalse(form.is_valid())
        form = LocalizedFooForm({'name_de': 'Banane', 'name_en': 'Banana'})
        self.assertTrue(form.is_valid())
        foo = form.save(commit=False)
        self.assertEquals(foo.name.de, 'Banane')
        self.assertEquals(foo.name.en, 'Banana')

    def test_full_clean(self):
        place = Place(name='Answer', coord_x=12.0, coord_y=42.0)
        place.full_clean()

    def test_default_kwarg(self):
        place = PlaceWithDefaultCoord()
        self.assertEqual(place.coord.x, 1.0)
        self.assertEqual(place.coord.y, 2.0)

    def test_assign_dict(self):
        place = Place(name='Answer', coord_x=12.0, coord_y=42.0)
        place.coord = {'x': 1.0, 'y': 2.0}
        self.assertEqual(place.coord.x, 1.0)
        self.assertEqual(place.coord.y, 2.0)

    def test_assign_incomplete_dict(self):
        place = Place(name='Answer', coord_x=12.0, coord_y=42.0)
        with self.assertRaises(KeyError):
            place.coord = {'x': 0.0}

    def test_bool(self):
        place = Place(name='Answer')
        self.assertFalse(place.coord)
        place.coord = {'x': 0.0, 'y': None}
        self.assertFalse(place.coord)
        place.coord = {'x': None, 'y': 0.0}
        self.assertFalse(place.coord)
        place.coord = {'x': 0.0, 'y': 0.0}
        self.assertTrue(place.coord)


class LocalizedFieldTestCase(TestCase):

    def test_general(self):
        foo = LocalizedFoo()
        self.assertEqual(len(LocalizedFoo._meta.fields), 4)
        foo.name_de = 'Mr.'
        foo.name_en = 'Herr'
        self.assertEqual(foo.name.de, 'Mr.')
        self.assertEqual(foo.name.en, 'Herr')

    def test_verbose_name(self):
        foo = LocalizedFoo()
        get_field = foo._meta.get_field
        self.assertEqual(force_text(get_field('name_de').verbose_name), 'name (de)')
        self.assertEqual(force_text(get_field('name_en').verbose_name), 'name (en)')

    def test_get_current(self):
        foo = LocalizedFoo(name_de='Bier', name_en='Beer')
        with translation.override('de'):
            self.assertEqual(foo.name.current, 'Bier')
        with translation.override('en'):
            self.assertEqual(foo.name.current, 'Beer')

    def test_set_current(self):
        foo = LocalizedFoo()
        with translation.override('de'):
            foo.name.current = 'Bier'
        with translation.override('en'):
            foo.name.current = 'Beer'
        self.assertEqual(foo.name_de, 'Bier')
        self.assertEqual(foo.name_en, 'Beer')

    def test_set_all(self):
        foo = LocalizedFoo()
        foo.name.all = 'Felix'
        self.assertEqual(foo.name_de, 'Felix')
        self.assertEqual(foo.name_en, 'Felix')

    def test_verbose_name(self):
        foo = LocalizedFoo()
        get_field = foo._meta.get_field
        self.assertEqual(force_text(get_field('name').verbose_name), 'name')

    def test_filter(self):
        foo1 = LocalizedFoo(name_de='eins', name_en='one')
        foo2 = LocalizedFoo(name_de='zwei', name_en='two')
        try:
            foo1.save()
            foo2.save()
            with translation.override('de'):
                self.assertEqual(LocalizedFoo.objects.get(name='eins'), foo1)
                self.assertRaises(LocalizedFoo.DoesNotExist, LocalizedFoo.objects.get, name='one')
            with translation.override('en'):
                self.assertEqual(LocalizedFoo.objects.get(name='one'), foo1)
                self.assertRaises(LocalizedFoo.DoesNotExist, LocalizedFoo.objects.get, name='eins')
            with translation.override('de'):
                self.assertEqual(LocalizedFoo.objects.get(name='zwei'), foo2)
                self.assertRaises(LocalizedFoo.DoesNotExist, LocalizedFoo.objects.get, name='two')
            with translation.override('en'):
                self.assertEqual(LocalizedFoo.objects.get(name='two'), foo2)
                self.assertRaises(LocalizedFoo.DoesNotExist, LocalizedFoo.objects.get, name='zwei')
        finally:
            foo1.delete()
            foo2.delete()

    def test_order_by(self):
        foo1 = LocalizedFoo(name_de='Erdnuss', name_en='peanut')
        foo2 = LocalizedFoo(name_de='Schinken', name_en='ham')
        try:
            foo1.save()
            foo2.save()
            with translation.override('de'):
                self.assertEqual(
                        list(LocalizedFoo.objects.all().order_by('name')),
                        [foo1, foo2])
            with translation.override('en'):
                self.assertEqual(
                        list(LocalizedFoo.objects.all().order_by('name')),
                        [foo2, foo1])
        finally:
            foo1.delete()
            foo2.delete()

    @unittest.skipIf((1, 8) <= django.VERSION < (1, 10), 'Django introduced a infinite recursion bug for properties of deferred models that was fixed in Django 1.10')
    def test_raw_sql(self):
        foo = LocalizedFoo(name_de='Antwort', name_en='answer')
        try:
            foo.save()
            foo2 = LocalizedFoo.objects.raw('SELECT * FROM composite_field_test_localizedfoo')[0]
            with translation.override('de'):
                self.assertEqual(str(foo2.name), 'Antwort')
            with translation.override('en'):
                self.assertEqual(str(foo2.name), 'answer')
        finally:
            foo.delete()

    def test_bool(self):
        foo = LocalizedFoo()
        self.assertFalse(foo.name)
        foo.name_de = 'test'
        self.assertTrue(foo.name)

class ComplexFieldTestCase(TestCase):

    def test_attributes(self):
        t = ComplexTuple()
        get_field = t._meta.get_field
        self.assertEqual(get_field('x_real').blank, True)
        self.assertEqual(get_field('x_real').null, True)
        self.assertEqual(get_field('x_imag').blank, True)
        self.assertEqual(get_field('x_imag').null, True)
        self.assertEqual(get_field('y_real').blank, False)
        self.assertEqual(get_field('y_real').null, False)
        self.assertEqual(get_field('y_imag').blank, False)
        self.assertEqual(get_field('y_imag').null, False)
        self.assertEqual(get_field('z_real').blank, False)
        self.assertEqual(get_field('z_real').null, False)
        self.assertEqual(get_field('z_imag').blank, False)
        self.assertEqual(get_field('z_imag').null, False)

    def test_null(self):
        t = ComplexTuple()
        self.assertEqual(t.x, None)
        self.assertEqual(t.y, None)
        self.assertEqual(t.y, None)
        t.x = None
        t.y = None
        t.z = None
        self.assertEqual(t.x, None)
        self.assertEqual(t.y, None)
        self.assertEqual(t.y, None)

    def test_assignment(self):
        t = ComplexTuple(x=42, y=42j, z=42+42j)
        self.assertEqual(t.x, 42)
        self.assertEqual(t.y, 42j)
        self.assertEqual(t.z, 42+42j)
        t.x = complex(21, 0)
        self.assertEqual(t.x, 21)
        t.y = complex(0, 21)
        self.assertEqual(t.y, 21j)
        t.z = complex(21, 21)
        self.assertEqual(t.z, 21+21j)

    def test_calculation(self):
        t = ComplexTuple(x=1, y=1j)
        t.z = t.x * t.y
        self.assertEqual(t.z, 1j)
        t.y *= t.y
        self.assertEqual(t.y, -1)
        t.z = t.x * t.y
        self.assertEqual(t.x, 1)
        self.assertEqual(t.y, -1)
        self.assertEqual(t.z, -1)

    def test_defaults(self):
        t = ComplexTupleWithDefaults()
        self.assertEqual(t.x, None)
        self.assertEqual(t.y, 42)
        self.assertEqual(t.z, 42j)

    def test_verbose_name(self):
        t = ComplexTuple()
        get_field = t._meta.get_field
        self.assertEqual(get_field('x_real').verbose_name, 'Re(x)')
        self.assertEqual(get_field('x_imag').verbose_name, 'Im(x)')
        self.assertEqual(get_field('y_real').verbose_name, 'Re(Y)')
        self.assertEqual(get_field('y_imag').verbose_name, 'Im(Y)')
        self.assertEqual(get_field('z_real').verbose_name, 'Re(gamma)')
        self.assertEqual(get_field('z_imag').verbose_name, 'Im(gamma)')


class InheritanceTestCase(TestCase):

    def test_abstract_inheritance(self):
        a = TranslatedModelA(name_de='Max Mustermann', name_en='John Doe')
        b = TranslatedModelB(name_en='Petra Musterfrau', name_de='Jane Doe')
        get_a_field = a._meta.get_field
        get_b_field = b._meta.get_field
        self.assertIs(get_a_field('name').model, TranslatedModelA)
        self.assertIs(get_a_field('name_de').model, TranslatedModelA)
        self.assertIs(get_a_field('name_en').model, TranslatedModelA)
        self.assertIs(get_b_field('name').model, TranslatedModelB)
        self.assertIs(get_b_field('name_de').model, TranslatedModelB)
        self.assertIs(get_b_field('name_en').model, TranslatedModelB)

    def test_non_abstract_inheritance(self):
        c = TranslatedModelC(name_de='Max Mustermann', name_en='John Doe')
        d = TranslatedModelD(name_en='Petra Musterfrau', name_de='Jane Doe')
        get_c_field = c._meta.get_field
        get_d_field = d._meta.get_field
        self.assertIs(get_c_field('name').model, TranslatedNonAbstractBase)
        self.assertIs(get_c_field('name_de').model, TranslatedNonAbstractBase)
        self.assertIs(get_c_field('name_en').model, TranslatedNonAbstractBase)
        self.assertIs(get_d_field('name').model, TranslatedNonAbstractBase)
        self.assertIs(get_d_field('name_de').model, TranslatedNonAbstractBase)
        self.assertIs(get_d_field('name_en').model, TranslatedNonAbstractBase)


class RunChecksTestCase(TestCase):

    def test_checks(self):
        django.setup()
        from django.core import checks
        all_issues = checks.run_checks()
        errors = [str(e) for e in all_issues if e.level >= checks.ERROR]
        if errors:
            self.fail('checks failed:\n' + '\n'.join(errors))


class AdminTestCase(django.test.TestCase):

    def setUp(self):
        from django.contrib.auth.models import User
        self.factory = django.test.RequestFactory()
        self.user = self.user = User.objects.create_superuser(
                username='john.doe',
                email='john.doe@example.com',
                password='xxx12345')

    def test_login(self):
        self.assertTrue(self.client.login(username='john.doe', password='xxx12345'))

    def test_admin_index(self):
        self.client.login(username='john.doe', password='xxx12345')
        self.client.get('/admin/')

    @unittest.skipIf(django.VERSION < (1, 9), 'the admin URLs are slightly different in django 1.9+')
    def test_translated_model_a(self):
        self.client.login(username='john.doe', password='xxx12345')
        response = self.client.get('/admin/composite_field_test/translatedmodela/')
        self.assertEquals(response.status_code, 200)
        response = self.client.get('/admin/composite_field_test/translatedmodela/add/')
        self.assertEquals(response.status_code, 200)
        obj = TranslatedModelA.objects.create(name_de='Foo', name_en='Foo')
        response = self.client.get('/admin/composite_field_test/translatedmodela/')
        self.assertEquals(response.status_code, 200)
        response = self.client.get('/admin/composite_field_test/translatedmodela/%s/change/' % obj.pk)
        self.assertEquals(response.status_code, 200)
        response = self.client.get('/admin/composite_field_test/translatedmodela/%s/delete/' % obj.pk)
        self.assertEquals(response.status_code, 200)

    @unittest.skipIf(django.VERSION < (1, 9), 'the admin URLs are slightly different in django 1.9+')
    def test_crud_direction(self):
        self.client.login(username='john.doe', password='xxx12345')
        # create
        response = self.client.get('/admin/composite_field_test/direction/add/')
        self.assertEquals(response.status_code, 200)
        response = self.client.post('/admin/composite_field_test/direction/add/', {
            'source_x': '0.25',
            'source_y': '0.5',
            'distance': str(math.sqrt(2.0)),
            'target_x': '1.25',
            'target_y': '1.5',
        })
        direction = Direction.objects.get()
        self.assertEquals(direction.source_x, 0.25)
        self.assertEquals(direction.source_y, 0.5)
        self.assertAlmostEquals(direction.distance, math.sqrt(2))
        self.assertEquals(direction.target_x, 1.25)
        self.assertEquals(direction.target_y, 1.5)
        self.assertEquals(response.status_code, 302)
        # read
        response = self.client.get('/admin/composite_field_test/direction/')
        self.assertEquals(response.status_code, 200)
        response = self.client.get('/admin/composite_field_test/direction/1/change/')
        self.assertEquals(response.status_code, 200)
        # update
        response = self.client.post('/admin/composite_field_test/direction/1/change/', {
            'source_x': '0.5',
            'source_y': '0.75',
            'distance': str(math.sqrt(2.0)/2.0),
            'target_x': '1.0',
            'target_y': '1.25',
        })
        direction = Direction.objects.get()
        self.assertEquals(direction.source_x, 0.5)
        self.assertEquals(direction.source_y, 0.75)
        self.assertAlmostEquals(direction.distance, math.sqrt(2)/2.0)
        self.assertEquals(direction.target_x, 1.0)
        self.assertEquals(direction.target_y, 1.25)
        self.assertEquals(response.status_code, 302)
        # delete
        response = self.client.get('/admin/composite_field_test/direction/1/delete/')
        self.assertEquals(response.status_code, 200)
        response = self.client.post('/admin/composite_field_test/direction/1/delete/', {
            'post': 'yes',
        })
        self.assertEquals(response.status_code, 302)

    def test_readonly(self):
        self.client.login(username='john.doe', password='xxx12345')

        place = PlaceWithDefaultCoord.objects.create()

        response = self.client.get(reverse('admin:composite_field_test_placewithdefaultcoord_change', args=(place.id,)))
        self.assertEquals(response.status_code, 200)

FAQ

Which Python versions are supported?

It supports Python 3.4, 3.5, 3.6, 3.7 and 3.8.

The last version to support Python 2.7 is django-composite-field ==0.9.1.

Which Django versions are supported?

It supports Django 1.8, 1.9, 1.10, 1.11, 2.0, 2.1, 2.2 and 3.0.

Can I get commercial support?

You can also get commercial support from the maintainer Michael P. Jung and his company Terreon GmbH.

Changelog

Version 1.0.0, 2020-01-06

  • Remove Python 2 support
  • Add Django 3.0 support
  • Add Documentation

Version 0.9.1, 2019-04-09

  • Add Python 3.6 and 3,7 support
  • Fix Django 2.2 support

Version 0.9.0, 2017-12-22

  • Fix Django 2.0 private field
  • Add Python 2.6 support
  • Fix test_settings for Django 2.0

Version 0.8.1, 2017-10-14

  • Make it simpler to provide a custom proxy
  • Fix Python 2.7 compatibility for bool(LocalizedField)
  • Add test for custom Proxy with __bool__()

Version 0.8.0, 2017-03-02

  • Drop support for Python 3.2 and 3.3
  • Add tests for modelform with include and exclude meta parameter
  • Fix Python 3.x compatibility
  • Improve test cases
  • Add default kwarg to CompositeField
  • Drop support for Django < 1.8
  • Add docker configuration for running the tests
  • Add bitbucket-pipeline to run tests on push

Version 0.7.6, 2016-07-01

  • Add help_text, choices and max_length proxy properties
  • Add support for assigning ugettext_lazy to localized fields

Version 0.7.5, 2016-06-16

  • Fix Python 3 support
  • Add test case for raw SQL query
  • Fix ordering query sets by a localized field
  • Drop support for Python 2.6
  • Fix __eq__ and __lt__ method of CompositeField

Version 0.7.4, 2016-02-17

  • Fix Django 1.9 support
  • Add support for dicts as composite field values
  • Add django-rest-framework serializer support

Version 0.7.3, 2016-01-21

  • Fix Django 1.9 support
  • Fix LocalizedField for Django 1.8+ when no translation is active
  • Add remote_field=None to CompositeField

Version 0.7.2, 2015-11-27

  • Add empty flatchoices attribute to CompositeField

Version 0.7.1, 2015-10-28

  • Add primary_key=False to CompositeField

Version 0.7.0, 2015-10-26

  • Fix Model.full_clean() error when using a CompositeField
  • Add ‘get_col’ method to LocalizedField making it possible to use it in a QuerySet.

Version 0.6.0, 2015-08-21

  • Fix ModelForm for models with a CompositeField
  • Implement ‘current(_with_default)’ and ‘all’ property of LocalizedField

Version 0.5, 2015-07-29

  • Fix composite proxy __eq__ method when comparing against non composite values
  • Fix translation fallback
  • Fix verbose_name as positional argument in LocalizedField

Version 0.4, 2015-07-29

  • Fix Python 3.2 compatibility
  • Composite field as virtual field in model

Version 0.3, 2015-07-23

  • Remove deprecation warning

Version 0.2, 2015-07-23

  • Add support for Django 1.4-1.8 and Python 2.x and 3.x
  • Tests can be run via tox

Version 0.1, 2010-05-27

  • First release

License

Copyright (c) 2010-2020 Michael P. Jung
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

  * Redistributions of source code must retain the above copyright notice,
    this list of conditions and the following disclaimer.

  * Redistributions in binary form must reproduce the above copyright
    notice, this list of conditions and the following disclaimer in the
    documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.

Indices and tables