Django Custom Model Field - formfield not working

388 views Asked by At

I've been scratching my head for a while now over this one. I'm trying to build a custom model field to store postal address.

The general idea is to store all data after serializing in a models.TextField, and to use a form.MultiValueField to catch each value.

For some reason, even if migration works fine, in the admin, the field appear as a simple TextField. As if the MultiValueField part was completely ignored...

Here's my code:

from django.db import models
from django.forms import MultiValueField, CharField
from django.utils.translation import ugettext_lazy as _

class Address(object):
    """A postal address."""

    def __init__(self, street, postal_code, city, 
                 country, complement=None, region=None):
        self.street = street
        self.complement = complement
        self.postal_code = postal_code
        self.city = city
        self.region = region
        self.country = country

    def print_address_inline(self):
        complement = ''
        region = ''
        if self.complement:
            complement = ', %s' % self.complement
        if self.region:
            region = ', %s' % self.region
        data = {
            'street': self.street,
            'complement': complement,
            'code': self.postal_code,
            'city': self.city,
            'region': region,
            'country': self.country
        }
        return '%(street)s%(complement)s, %(code)s %(city)s%(region)s' \
               ', %(country)s' % data

    def __str__(self):
        return self.print_address_inline()


class AddressFormField(MultiValueField):
    def __init__(self, *args, **kwargs):
        del kwargs['max_length']
        error_messages = {
            'incomplete': _('Enter a complete address: ' \
                            'street, postal code, city and country.'),
        }
        fields = (
            CharField(
                label='street', max_length=1024,
                error_messages={
                    'incomplete': _('Enter the number and street.')
                }
            ),
            CharField(
                label='complement', max_length=1024, required=False
            ),
            CharField(
                label='code', max_length=10,
                error_messages={'incomplete': _('Enter the postal code.')}
            ),
            CharField(
                label='city', max_length=255,
                error_messages={'incomplete': _('Enter the city.')}
            ),
            CharField(
                label='region', max_length=255, required=False
            ),
            CharField(
                label='country', max_length=255,
                error_messages={'incomplete': _('Enter the country.')}
            )
        )
        super(AddressFormField, self).__init__(
            error_messages=error_messages, fields=fields,
            require_all_fields=False, *args, **kwargs
        )

    def compress(self, data_list):
        if data_list:
            if data_list[0] in self.empty_values:
                raise ValidationError(
                    _('Enter the number and street.'),
                    code='incomplete'
                )
            if data_list[2] in self.empty_values:
                raise ValidationError(
                    _('Enter the postal code.'),
                    code='incomplete'
                )
            if data_list[3] in self.empty_values:
                raise ValidationError(
                    _('Enter the city.'),
                    code='incomplete'
                )
            if data_list[5] in self.empty_values:
                raise ValidationError(
                    _('Enter the country.'),
                    code='incomplete'
                )
            address = Address(
                street=data_list[0],
                complement=data_list[1],
                code=data_list[2],
                city=data_list[3].title(),
                region=data_list[4],
                country=data_list[5].upper()
            )
            return address
        return None


class AddressField(models.TextField):
    description = "A postal address."

    def __init__(self, *args, **kwargs):
        super(AddressField, self).__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(AddressField, self).deconstruct()
        return name, path, args, kwargs

    def from_db_value(self, value, expression, connection, context):
        if value is None:
            return value
        data = value.split('@@')
        return Address(*data)

    def to_python(self, value):
        if isinstance(value, Address):
            return value
        if value is None:
            return value
        data = value.split('@@')
        return Address(*data)

    def get_prep_value(self, value):
        if isinstance(value, Address):
            data = []
            for v in [
                value.street,
                value.complement,
                value.postal_code,
                value.city,
                value.region,
                value.country,
            ]:
                if v is not None:
                    data.append(v)
                else:
                    data.append('')
            return '@@'.join(data)
        else:
            return value

    def formfield(self, **kwargs):
        defaults = {'form_class': AddressFormField}
        defaults.update(kwargs)
        return super(AddressField, self).formfield(**defaults)

If you have any clue...

Thanks!

1

There are 1 answers

0
TonyEight On BEST ANSWER

I finally made it through with this piece of code.

from django.db import models
from django.forms import MultiValueField, CharField, TextInput
from django.forms.widgets import MultiWidget
from django.utils.translation import ugettext_lazy as _

class Address(object):
    """A postal address."""
    def __init__(self, street, complement, postal_code, city, region, country):
        self.street = street
        self.complement = complement
        self.postal_code = postal_code
        self.city = city
        self.region = region
        self.country = country

    def print_address_inline(self):
        complement = ''
        region = ''
        if self.complement != '':
            complement = ', %s' % self.complement
        if self.region != '':
            region = ', %s' % self.region
        data = {
            'street': self.street,
            'complement': complement,
            'code': self.postal_code,
            'city': self.city,
            'region': region,
            'country': self.country
        }
        return '%(street)s%(complement)s, %(code)s %(city)s%(region)s' \
               ', %(country)s' % data

    def get_values_list(self):
        data = [
            self.street,
            self.complement,
            self.postal_code,
            self.city,
            self.region,
            self.country,
        ]
        value_list = []
        for value in data:
            if value is not None:
                value_list.append(value)
            else:
                value_list.append('')
        return value_list

    def __str__(self):
        return self.print_address_inline()


class AddressWidget(MultiWidget):
    """A special widget to render Address form field."""
    def __init__(self, attrs=None):
        # TODO Look for django-localflavor to improve this custom field.
        # https://django-localflavor.readthedocs.io/en/latest/localflavor/fr/
        widgets = [
            TextInput(attrs={'placeholder':'street'}),
            TextInput(attrs={'placeholder':'complement'}),
            TextInput(attrs={'placeholder':'postal code'}),
            TextInput(attrs={'placeholder':'city'}),
            TextInput(attrs={'placeholder':'region'}),
            TextInput(attrs={'placeholder':'country'}),
        ]
        super(AddressWidget, self).__init__(widgets, attrs)

    def decompress(self, value):
        if value:
            return [
                value.street,
                value.complement,
                value.postal_code,
                value.city,
                value.region,
                value.country,
            ]
        return [None, None, None, None, None, None]


class AddressFormField(MultiValueField):
    """A special form field to handle Address model field."""
    widget = AddressWidget

    def __init__(self, *args, **kwargs):
        error_messages = {
            'incomplete': _('Enter a complete address: ' \
                            'street, postal code, city and country.'),
        }
        fields = (
            CharField(
                label='street', max_length=1024,
                error_messages={
                    'incomplete': _('Enter the number and street.')
                }
            ),
            CharField(
                label='complement', max_length=1024, required=False
            ),
            CharField(
                label='code', max_length=10,
                error_messages={'incomplete': _('Enter the postal code.')}
            ),
            CharField(
                label='city', max_length=255,
                error_messages={'incomplete': _('Enter the city.')}
            ),
            CharField(
                label='region', max_length=255, required=False
            ),
            CharField(
                label='country', max_length=255,
                error_messages={'incomplete': _('Enter the country.')}
            )
        )
        super(AddressFormField, self).__init__(
            error_messages=error_messages, fields=fields,
            require_all_fields=False, *args, **kwargs
        )

    def compress(self, data_list):
        if data_list:
            if data_list[0] in self.empty_values:
                raise ValidationError(
                    _('Enter the number and street.'),
                    code='incomplete'
                )
            if data_list[2] in self.empty_values:
                raise ValidationError(
                    _('Enter the postal code.'),
                    code='incomplete'
                )
            if data_list[3] in self.empty_values:
                raise ValidationError(
                    _('Enter the city.'),
                    code='incomplete'
                )
            if data_list[5] in self.empty_values:
                raise ValidationError(
                    _('Enter the country.'),
                    code='incomplete'
                )
            address = Address(
                street=data_list[0],
                complement=data_list[1],
                postal_code=data_list[2],
                city=data_list[3].title(),
                region=data_list[4],
                country=data_list[5].upper()
            )
            return address
        return None


class AddressField(models.Field):
    description = "A postal address."

    def __init__(self, *args, **kwargs):
        super(AddressField, self).__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(AddressField, self).deconstruct()
        return name, path, args, kwargs

    def from_db_value(self, value, expression, connection, context):
        if value is None or value == '':
            return value
        data = value.split('@@')
        address = Address(*data)
        return address

    def to_python(self, value):
        if isinstance(value, Address):
            return value
        if value is None:
            return value
        data = value.split('@@')
        return Address(*data)

    def get_prep_value(self, value):
        if value:
            return '@@'.join(value.get_values_list())
        return ''

    def get_internal_type(self):
        return 'TextField'

    def formfield(self, **kwargs):
        defaults = {'form_class': AddressFormField}
        defaults.update(kwargs)
        return super(AddressField, self).formfield(**defaults)

Feel free to comment or share your thoughts about it. I made the choice of using double '@' as a separator. It is debatable, but it works in my case.