views:

75

answers:

2

Scenario: I'm building an order-form. Like every other order-form on the planet, it has separate invoicing shipping addresses. I've just added a "Use billing address" checkbox to let the user save time.

The problem is, the shipping fields are still there. They will fail validation if the user don't enter any shipping address data (like if they want to use the billing address).

What I think I'd like to do override the ModelForm validation for these duplicate fields. In there, if the box is checked (not sure how I get that data from within a validator), I return the billing version. If it's not checked, I pass it back to the original validation.

Sounds like a plan doesn't it? Well I fell at the first hurdle. My clean_functions aren't working. Doesn't look like they're even being called.

Here's some code:

# shipping_street is a field in my Order Model

class OrderForm(ModelForm):
    class Meta:
        model = Order

    def clean_shipping_street(self):
        print "JUST GET ME SOME OUTPUT!!!"
        raise forms.ValidationError('RAWRAWR')

Here's how I'm testing:

def checkout(request):
    of = OrderForm()
    if request.method == "POST":
        of = OrderForm(request.POST)
        print 'Form valid:', of.is_valid()

    # ...
    # return my HttpResponse with 'of' in the context.
+2  A: 

I'm not sure if I was just being a clutz but the following worked (and answers my whole question):

class OrderForm(ModelForm): class Meta: model = Order

def clean_shipping_street(self):
    print 'VALIDATING!!! YEY!'
    if self.cleaned_data['ship_to_billing']:
        return self.clean_billing_street()
    return super(OrderForm, self).clean_shipping_street()

But if you think I'm going about this the wrong way, please let me know!

As Nick points out below, cleaned_data isn't filled in a guaranteed order, meaning ship_to_billing might not exist when clean_shipping_street() is called. The way around this is to call the clean_shipping_street() method instead accessing cleaned_data.

def clean_shipping_street(self):
    print 'VALIDATING!!! YEY!'
    if self.clean_ship_to_billing():
        return self.clean_billing_street()
    return super(OrderForm, self).clean_shipping_street()

If you weren't as lazy as I was when I wrote the code, you might want to avoid so many duplicate validations of the boolean field. This should be faster (provided the default field isn't run unless it's needed - not sure on that myself):

def clean_shipping_street(self):
    print 'VALIDATING!!! YEY!'
    if self.cleaned_data.get('ship_to_billing', self.clean_ship_to_billing):
        return self.clean_billing_street()
    return super(OrderForm, self).clean_shipping_street()

OR even better than that:

def clean_shipping_street(self):
    if not self.cleaned_data.has_key['ship_to_billing']:
        self.cleaned_data['ship_to_billing'] = self.clean_ship_to_billing()
    if self.cleaned_data['ship_to_billing']:
        return self.clean_billing_street()
    return super(OrderForm, self).clean_shipping_street()

It's only slightly different but it should mean clean_ship_to_billing() gets called a lot less than my previous efforts. But seriously, I doubt you could even detect these "improvements" in a profiling session.

Oli
this is the way i'd do it. maybe not the best, but custom clean methods is probably the easiest.
Brandon H
Beware that the `clean_<attribute-name>` methods aren't called in any particular order, so that in the above, `ship_to_billing` may not be set yet in `cleaned_data`. In general if your clean method depends on more than one item in the form, use the plain `clean` method.
Nick Craig-Wood
I had an idea about thatm Nick. Instead of calling `self.cleaned_data['field_name']`, called `self.clean_field_name()` - That way you're guaranteed a result (unless your custom cleans have messy circular dependencies). It's a little more work for the CPU but it should always work.
Oli
A: 

There were several problems with my last answer. Copied data wasn't rendered back into the form (might be something you want, I do) and it was a little unreliable.

Here is what I'm using now. Instead of adding dozens of clean_field_name() definitions, I have one just on the BooleanField:

def clean_ship_to_billing(self):
    if self.cleaned_data.get('ship_to_billing', False):
        data = self.data.copy()
        for f in ['street', 'street_2', 'post_code', 'city', 'county', 'country', ]:
            data['shipping_%s' % f] = data['billing_%s' % f]
        self.data = data

If checked, it copies the raw data across for billing fields into shipping fields. It's important that the field is before the shipping fields in the model's (or form's) field order.

And I'm copying self.data because the POST data is immutable.

Oli