Friday, March 6, 2009

Numeric IP field for Django

Some time ago I needed to add an IP field to my model with more records (some hundred thousands). I was going to just add Django's IPAddressField, but I realized that it stores the data as text on the database, and I didn't like the idea.

Basically, and IP address is just 4 bytes of data, but it's text representation can use between 7 and 15 bytes. That's not a big different when your model will have few rows, but it's a different when you'll have a huge set of IP addresses, and specially if you want to join tables by it.

The only inconvenient of storing the IPs as numbers is that are not human readable if you want to check them directly to database.

So, here you have my code that can be used as a replacement of IPAddressField:


import IPy
from django.db import models
from django import forms
from django.utils.translation import ugettext as _

def _ip_to_int(ip):
return IPy.IP(ip).ip

def _int_to_ip(numeric_ip):
return IPy.IP(numeric_ip).strNormal()

class IPFormField(forms.fields.Field):
def clean(self, value):
try:
_ip_to_int(value)
except ValueError:
raise forms.ValidationError, \
_('You must provide a valid IP address')

return super(IPFormField, self).clean(value)

class IPField(models.fields.PositiveIntegerField):
'''
IP field for django for storing IPs as integers on database
(Django's field IPAddressField stores them as text)
'''
__metaclass__ = models.SubfieldBase

def to_python(self, value):
if value:
if isinstance(value, long):
return _int_to_ip(value)
else:
return value
else:
return None

def get_db_prep_save(self, value):
try:
result = _ip_to_int(value)
except ValueError:
result = None
return result

def get_db_prep_value(self, value):
if value:
return _ip_to_int(value)
else:
return None

def get_db_prep_lookup(self, lookup_type, value):
return super(IPField, self).get_db_prep_lookup(
lookup_type,
_ip_to_int(value)
)

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



NOTE: This code requires IPy, a single file python library to work with IP addresses.

5 comments:

  1. That's a neat solution, but if you are using a database that has its own storage backend for IP-addresses, I'd suggest to use that instead – like these from PostgreSQL:

    http://www.postgresql.org/docs/8.3/static/datatype-net-types.html

    ReplyDelete
  2. Do you think you could post an example that works with both ipv4 and ipv6 ip addresses?

    ReplyDelete
  3. This is working for ipv4 and ipv6 ip's using postgres inet network fields..

    from django.db import models
    from django.contrib import admin

    from django.forms import ValidationError as FormValidationError
    from django.core.exceptions import ValidationError
    from django.forms import fields

    from decimal import Decimal
    from IPy import IP


    class IPAddressFormField(fields.Field):
    default_error_messages = {
    'invalid': u'Enter a valid IPv4 or IPv6 address.',
    }

    def clean(self, value):
    'Method for validating IPs on forms'
    if value in fields.EMPTY_VALUES:
    return u''

    try:
    IP(value)
    except Exception, e:
    raise FormValidationError(self.error_messages['invalid'])

    return super(IPAddressFormField, self).clean(value)

    class IPAddressField(models.Field):

    __metaclass__ = models.SubfieldBase

    def db_type(self):
    return 'inet'

    def to_python(self, value):
    if not value or ((isinstance(value, str) or isinstance(value, unicode)) and value == ''):
    return None

    try:
    return IP(value)
    except Exception, e:
    raise ValidationError(e)

    def get_db_prep_value(self, value):
    value = self.to_python(value)

    if not value:
    return None
    else:
    return value

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

    ReplyDelete
  4. Here is the example I came up with. Sorry for pasting the code in here previously.

    http://www.djangosnippets.org/snippets/1453/

    ReplyDelete