Hi,

I have a model with a number of fields that use Django's 'choices'
attribute, to limit the allowed values of those fields to a
pre-defined list. I assumed that the Model system would validate my
fields when I create or update them, to ensure that invalid choices
weren't being assigned to fields. However, it seems that this doesn't
happen. I'm aware that such validation occurs at the form layer, but
I'd really like it to occur at the model layer as well.

I'm very new to Django (and Python) - I only really started using it
about two weeks ago. I haven't even started working with templates
yet, as I want to understand the model system and to get my models
stable before moving into the front-end.

I found http://code.djangoproject.com/ticket/6845 (Model validation
and its propagation to ModelForms), so I know that people are working
on this for future versions of Django. It will be great when model
validation is built-in for future versions of Django. However, I
really wanted to see if it was possible to add such validation
manually for Django 1.0.x.

I wrote a custom __setattr__ handler and exception class, to implement
choice validation for my model. I'm putting my code here to get
feedback about whether I've taken the right approach here, and whether
I've followed the Django conventions (as this is my first piece of
Django code), and also for the benefit of anyone else who would like
to implement this in their models. Feel free to copy and use this in
your own apps:

file 'bar/foo/models.py':

-----

from django.db import models

FOO_CHOICES = (
    ('this', 'This'),
    ('that', 'That'),
    ('the other', 'The other'),
)

class Foo(models.Model):
    """
    Foo is a fuuber model.
    """
    name = models.CharField(max_length=255)
    foo_type = models.CharField(max_length=255, choices=FOO_CHOICES)

    def __unicode__(self):
        return self.name

    def __setattr__(self, name, value):
        """
        Whenever foo_type is set, it is validated to ensure that it is
one of the allowed pre-defined choices. This method fires when a foo
object is instantiated (hence no need to duplicate it in __init__),
and also whenever foo_type is modified.
        """
        choice_validator = ChoiceValidator()

        if name == 'foo_type':
            choice_validator.validate_choice(value, FOO_CHOICES, name)

        models.Model.__setattr__(self, name, value)

class ChoiceValidator:
    """
    Validation that a model's fields match that field's allowed choices.
    """

    def validate_choice(self, choice_provided, choices_available, choice_type):
        """
        Checks that the provided choice matches one of the choices
available. If not, an InvalidChoiceError exception is raised.
        """
        is_valid_choice = False
        valid_choices = []

        # choices_available is a tuple of tuples. We convert it into a
list, for easy printing in the exception message.
        for choice in choices_available:
            valid_choices.append(choice[0])

            # And while we're in the loop, we perform the actual
matching check. Don't break inside this condition, because we always
want all the choices appended to valid_choices.
            if choice[0] == choice_provided:
                is_valid_choice = True

        if not is_valid_choice:
            raise InvalidChoiceError(choice_provided, valid_choices,
choice_type)

class InvalidChoiceError(Exception):
    """
    Raised when an attempt is made to set an invalid choice, when
setting a model field that has a list of allowed choices.
    """

    def __init__(self, choice_provided, valid_choices, choice_type):
        self.choice_provided = choice_provided
        self.valid_choices = valid_choices
        self.choice_type = choice_type

    def __str__(self):
        return 'invalid %s \'%s\', must be one of: \'%s\'' %
(self.choice_type, self.choice_provided, '\',
\''.join(self.valid_choices))

-----

How to test it:

$ python manage.py shell

>>> from bar.foo.models import Foo
>>> foo1 = Foo(name='Foo Hoo', foo_type='that')

(works fine, 'that' is a valid choice)

>>> foo2 = Foo(name='Foo Too', foo_type='not this')

(prints a traceback, and this message:)

InvalidChoiceError: invalid foo_type 'not this', must be one of:
'this', 'that', 'the other'

Cheers,

Jeremy Epstein
http://www.greenash.net.au/

--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"Django users" group.
To post to this group, send email to django-users@googlegroups.com
To unsubscribe from this group, send email to 
django-users+unsubscr...@googlegroups.com
For more options, visit this group at 
http://groups.google.com/group/django-users?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to