9

I am having trouble understanding how to initialize a custom form field in a django view.

For example: http://djangosnippets.org/snippets/907/

from datetime import date, datetime
from calendar import monthrange

class CreditCardField(forms.IntegerField):
    @staticmethod
    def get_cc_type(number):
        number = str(number)
        #group checking by ascending length of number
        if len(number) == 13:
            if number[0] == "4":
                return "Visa"
        return "Unknown"

    def clean(self, value):
        if value and (len(value) < 13 or len(value) > 16):
            raise forms.ValidationError("Please enter in a valid "+\
                "credit card number.")
        elif self.get_cc_type(value) not in ("Visa", "MasterCard",
                                             "American Express"):
            raise forms.ValidationError("Please enter in a Visa, "+\
                "Master Card, or American Express credit card number.")
        return super(CreditCardField, self).clean(value)

class CCExpWidget(forms.MultiWidget):
    """ Widget containing two select boxes for selecting the month and year"""
    def decompress(self, value):
        return [value.month, value.year] if value else [None, None]

    def format_output(self, rendered_widgets):
        html = u' / '.join(rendered_widgets)
        return u'<span style="white-space: nowrap">%s</span>' % html


class CCExpField(forms.MultiValueField):
    EXP_MONTH = [(x, x) for x in xrange(1, 13)]
    EXP_YEAR = [(x, x) for x in xrange(date.today().year,
                                       date.today().year + 15)]
    default_error_messages = {
        'invalid_month': u'Enter a valid month.',
        'invalid_year': u'Enter a valid year.',
    }

    def __init__(self, *args, **kwargs):
        errors = self.default_error_messages.copy()
        if 'error_messages' in kwargs:
            errors.update(kwargs['error_messages'])
        fields = (
            forms.ChoiceField(choices=self.EXP_MONTH,
                error_messages={'invalid': errors['invalid_month']}),
            forms.ChoiceField(choices=self.EXP_YEAR,
                error_messages={'invalid': errors['invalid_year']}),
        )
        super(CCExpField, self).__init__(fields, *args, **kwargs)
        self.widget = CCExpWidget(widgets =
            [fields[0].widget, fields[1].widget])

    def clean(self, value):
        exp = super(CCExpField, self).clean(value)
        if date.today() > exp:
            raise forms.ValidationError(
            "The expiration date you entered is in the past.")
        return exp

    def compress(self, data_list):
        if data_list:
            if data_list[1] in forms.fields.EMPTY_VALUES:
                error = self.error_messages['invalid_year']
                raise forms.ValidationError(error)
            if data_list[0] in forms.fields.EMPTY_VALUES:
                error = self.error_messages['invalid_month']
                raise forms.ValidationError(error)
            year = int(data_list[1])
            month = int(data_list[0])
            # find last day of the month
            day = monthrange(year, month)[1]
            return date(year, month, day)
        return None


class PaymentForm(forms.Form):
    number = CreditCardField(required = True, label = "Card Number")
    holder = forms.CharField(required = True, label = "Card Holder Name",
        max_length = 60)
    expiration = CCExpField(required = True, label = "Expiration")
    ccv_number = forms.IntegerField(required = True, label = "CCV Number",
        max_value = 9999, widget = forms.TextInput(attrs={'size': '4'}))

    def __init__(self, *args, **kwargs):
        self.payment_data = kwargs.pop('payment_data', None)
        super(PaymentForm, self).__init__(*args, **kwargs)

    def clean(self):
        cleaned = super(PaymentForm, self).clean()
        if not self.errors:
            result = self.process_payment()
            if result and result[0] == 'Card declined':
                raise forms.ValidationError('Your credit card was declined.')
            elif result and result[0] == 'Processing error':
                raise forms.ValidationError(
                    'We encountered the following error while processing '+\
                    'your credit card: '+result[1])
        return cleaned

    def process_payment(self):
        if self.payment_data:
            # don't process payment if payment_data wasn't set
            datadict = self.cleaned_data
            datadict.update(self.payment_data)

            from virtualmerchant import VirtualMerchant
            vmerchant = VirtualMerchant(datadict)

            return vmerchant.process_virtualmerchant_payment()

In the above example payment form, how would you pass initial data to PaymentForm.expiration field?

I know you can do:

c = PaymentForm({'number':'1234567890', 'holder':'Bob Barker','ccv_number':'123'})

However, how do you pass data to a custom field such as the one implemented here?

Chris
  • 11,780
  • 13
  • 48
  • 70
  • 1
    I don't understand why you think this would be different to any other field. Just use `initial` when instantiating the form, as mentioned in [my other answer](http://stackoverflow.com/questions/936376/prepopulate-django-non-model-form/936622#936622) that you commented on. – Daniel Roseman Oct 30 '11 at 17:43
  • @DanielRoseman: What would I pass to PaymentForm defined above to prepopulate PaymentForm.expiration with initial data? I tried c = PaymentForm({'expiration':'01/2011'}) and other combinations and was unable to get it to prepopulate which is why I am asking this question. – Chris Oct 30 '11 at 19:56

1 Answers1

8

All fields have an 'initial' attribute so you set that even if it's a custom field

https://code.djangoproject.com/browser/django/trunk/django/forms/fields.py#L45

so you should just be able to overwrite the constructor:

class PaymentForm(forms.Form):
    def __init__(self, exp = None, *args, **kwargs):
        super(PaymentForm, self).__init__(*args, **kwargs)
        if exp:
            self.fields['expiration'].initial = exp

and in your view you can pass the required data:

form = PaymentForm(exp=...)
Chris
  • 11,780
  • 13
  • 48
  • 70
Timmy O'Mahony
  • 53,000
  • 18
  • 155
  • 177
  • That makes sense but how does the CCExpField get that data is what I do not understand. In your answer, my payment form when initialized checks for an argument "exp" and if passed, passes this onto the field PaymentForm.expiration. At this point how does PaymentForm.expiration which is a CCExpField get that data into its 2 char fields. – Chris Oct 30 '11 at 15:54
  • 1
    The CCExpField is given the data here: self.fields['expiration'].initial = exp where the 'initial' attribute of the field object is set. This is later fed to the widget to be displayed as the value if nothing has been previously saved. – Timmy O'Mahony Oct 30 '11 at 18:10