Django Form Credit Card Field with Pattern, Length and Luhn Validation

I’ve been doing a bit of ecommerce work lately and one of my needs was a credit card field for a Django form. Ideally it needed to support all major cards (PayPal offers a good reference list) and be easily extendable in the future.

All I could find via Google was this older Django project and various blog posts, none of which really fit my needs.

After a bit more digging I decided to port over portions of Stripe’s jQuery.payment which does almost exactly what I needed.

from django import forms
from django.forms.widgets import TextInput
from django.utils.translation import ugettext_lazy as _
class TelephoneInput(TextInput):
# switch input type to type tel so that the numeric keyboard shows on mobile devices
input_type = 'tel'
class CreditCardField(forms.CharField):
# validates almost all of the example cards from PayPal
# https://www.paypalobjects.com/en_US/vhelp/paypalmanager_help/credit_card_numbers.htm
cards = [
{
'type': 'maestro',
'patterns': [5018, 502, 503, 506, 56, 58, 639, 6220, 67],
'length': [12, 13, 14, 15, 16, 17, 18, 19],
'cvvLength': [3],
'luhn': True
}, {
'type': 'forbrugsforeningen',
'patterns': [600],
'length': [16],
'cvvLength': [3],
'luhn': True
}, {
'type': 'dankort',
'patterns': [5019],
'length': [16],
'cvvLength': [3],
'luhn': True
}, {
'type': 'visa',
'patterns': [4],
'length': [13, 16],
'cvvLength': [3],
'luhn': True
}, {
'type': 'mastercard',
'patterns': [51, 52, 53, 54, 55, 22, 23, 24, 25, 26, 27],
'length': [16],
'cvvLength': [3],
'luhn': True
}, {
'type': 'amex',
'patterns': [34, 37],
'length': [15],
'cvvLength': [3, 4],
'luhn': True
}, {
'type': 'dinersclub',
'patterns': [30, 36, 38, 39],
'length': [14],
'cvvLength': [3],
'luhn': True
}, {
'type': 'discover',
'patterns': [60, 64, 65, 622],
'length': [16],
'cvvLength': [3],
'luhn': True
}, {
'type': 'unionpay',
'patterns': [62, 88],
'length': [16, 17, 18, 19],
'cvvLength': [3],
'luhn': False
}, {
'type': 'jcb',
'patterns': [35],
'length': [16],
'cvvLength': [3],
'luhn': True
}
]
def __init__(self, placeholder=None, *args, **kwargs):
super(CreditCardField, self).__init__(
# override default widget
widget=TelephoneInput(attrs={
'placeholder': placeholder
})
, *args, **kwargs)
default_error_messages = {
'invalid': _(u'The credit card number is invalid'),
}
def clean(self, value):
# ensure no spaces or dashes
value = value.replace(' ', '').replace('-', '')
# get the card type and its specs
card = self.card_from_number(value)
# if no card found, invalid
if not card:
raise forms.ValidationError(self.error_messages['invalid'])
# check the length
if not len(value) in card['length']:
raise forms.ValidationError(self.error_messages['invalid'])
# test luhn if necessary
if card['luhn']:
if not self.validate_mod10(value):
raise forms.ValidationError(self.error_messages['invalid'])
return value
def card_from_number(self, num):
# find this card, based on the card number, in the defined set of cards
for card in self.cards:
for pattern in card['patterns']:
if (str(pattern) == str(num)[:len(str(pattern))]):
return card
def validate_mod10(self, num):
# validate card number using the Luhn (mod 10) algorithm
checksum, factor = 0, 1
for c in reversed(num):
for c in str(factor * int(c)):
checksum += int(c)
factor = 3 factor
return checksum % 10 == 0

view raw
fields.py
hosted with ❤ by GitHub

You can then use this field in your form:

class CreditCardForm(ModelForm):
card_number = CreditCardField(placeholder=u'0000 0000 0000 0000', min_length=12, max_length=19)

view raw
form.py
hosted with ❤ by GitHub

Limiting Upload File Size with Django Forms

A common use case in any web application is to allow users to upload files such as images, videos, PDFs, etc. But left unrestricted, this could lead to all sorts of problems including users uploading files that are too large for the system or application to handle.

File size restrictions can be placed on the upload in multiple ways including with JavaScript, web server configuration changes and within your application code. I definitely recommend using all of the above, but for this post, we’ll look at how to handle this in Django by subclassing Django’s FileField and ImageField form fields and adding some extra logic to the clean method.

First, define a max upload size within your settings (in bytes).

# Limit uploads to 5MB
MAX_UPLOAD_SIZE = 5242880

view raw
settings.py
hosted with ❤ by GitHub

Next, define the new fields (I usually stick these in app/forms/fields.py):

from django import forms
from django.template.defaultfilters import filesizeformat
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
class RestrictedFileField(forms.FileField):
def __init__(self, *args, **kwargs):
self.content_types = kwargs.pop('content_types', None)
self.max_upload_size = kwargs.pop('max_upload_size', None)
if not self.max_upload_size:
self.max_upload_size = settings.MAX_UPLOAD_SIZE
super(RestrictedFileField, self).__init__(*args, **kwargs)
def clean(self, *args, **kwargs):
data = super(RestrictedFileField, self).clean(*args, **kwargs)
try:
if data.content_type in self.content_types:
if data.size > self.max_upload_size:
raise forms.ValidationError(_('File size must be under %s. Current file size is %s.') % (filesizeformat(self.max_upload_size), filesizeformat(data.size)))
else:
raise forms.ValidationError(_('File type (%s) is not supported.') % data.content_type)
except AttributeError:
pass
return data
class RestrictedImageField(forms.ImageField):
def __init__(self, *args, **kwargs):
self.max_upload_size = kwargs.pop('max_upload_size', None)
if not self.max_upload_size:
self.max_upload_size = settings.MAX_UPLOAD_SIZE
super(RestrictedImageField, self).__init__(*args, **kwargs)
def clean(self, *args, **kwargs):
data = super(RestrictedImageField, self).clean(*args, **kwargs)
try:
if data.size > self.max_upload_size:
raise forms.ValidationError(_('File size must be under %s. Current file size is %s.') % (filesizeformat(self.max_upload_size), filesizeformat(data.size)))
except AttributeError:
pass
return data

view raw
fields.py
hosted with ❤ by GitHub

You can now make use of these new fields from within your form class. Note that content_types is required for RestrictedFileField while max_upload_size is optional for both fields (defaults to whatever you specified for MAX_UPLOAD_SIZE in your settings).

image = RestrictedFileField(content_types=['video/quicktime', 'application/pdf']) # limit to QuickTime and PDF
image = RestrictedImageField(max_upload_size=2621440) # limit to 2.5MB

view raw
forms.py
hosted with ❤ by GitHub

And remember that the content-type is still user supplied (i.e. it’s a header coming from whatever submitted the form), so be sure to verify that the uploaded file contains the content-type you’re expecting.

Django ModelForm and Conditionally Disabled (Readonly) Fields

I ran into a use case today that required a model form to have some fields disabled (readonly), but only when a certain condition was met (specifically when a property of the model was set to true) . Since the model form is used with the fields both enabled and disabled, simply duplicating the model form and removing the necessary fields wasn’t an option (plus that’s not exactly DRY now is it).

The solution ended up being quite simple. First, you’ll need to override the clean method for each field that needs to be disabled within your model form class:

# the following fields can only be updated when the is_disabled property is set to false
def clean_field_1(self):
if self.instance.is_disabled:
return self.instance.field_1
else:
return self.cleaned_data.get('field_1')
def clean_field_2(self):
if self.instance.is_disabled:
return self.instance.field_2
else:
return self.cleaned_data.get('field_2')

view raw
forms.py
hosted with ❤ by GitHub

The clean field method simply checks the is_disabled property and if it’s true, returns the existing field data. It only returns the new submitted data, pulled from cleaned_data, if is_disabled is false.

The second part of the solution is to set the field to readonly in your template. Note that you need to use readonly rather than disabled as the field won’t actually be submitted if set to disabled (more info on this here). This will cause Django to throw a missing field error before the clean field method ever runs.

You can add the necessary html to the form field via the widget’s attributes property:

modelform.fields['field_1'].widget.attrs['readonly'] = True

view raw
views.py
hosted with ❤ by GitHub

Django Form Inheritance

Form inheritance (docs) in Django is extremely straightforward:

class EnhancedModelForm(ModelForm):
class SuperEnhancedModelForm(EnhancedModelForm):

view raw
forms.py
hosted with ❤ by GitHub

You can also subclass the parent’s Meta inner class if you want to change the Meta.fields or Meta.excludes lists:

class EnhancedModelForm(ModelForm):
class Meta:
model = MyModel
fields = ('field1', 'field2')
class SuperEnhancedModelForm(EnhancedModelForm):
class Meta(EnhancedModelForm.Meta):
model = MyModel
fields = ('field3', 'field4')

view raw
forms.py
hosted with ❤ by GitHub

But what if you want to add to the fields rather than replace them? This is actually straightforward as well:

class EnhancedModelForm(ModelForm):
class Meta:
model = MyModel
fields = ('field1', 'field2')
class SuperEnhancedModelForm(EnhancedModelForm):
class Meta(EnhancedModelForm.Meta):
model = MyModel
fields = EnhancedModelForm.Meta.fields + ('field3', 'field4')

view raw
forms.py
hosted with ❤ by GitHub

Django Form Wizard and Getting Data from Previous Steps

I’ve spent the last few days implementing a Django SessionWizardView and ran into what I thought would be a simple problem – how to access the data of a previous form in order to initialize a later form (i.e. use data submitted on step 1 to initialize the step 3 form). Google didn’t turn up much so I turned to the documentation for a solution.

At first glance it seemed simple enough – use get_cleaned_data_for_step or get_all_cleaned_data. The problem with these, I quickly discovered, is that they call get_form which in turn calls get_form_initial (which is where I’m trying to use the submitted form data in the first place). So that was a dead end.

Next up was using get_form_step_data. This method is called before get_form_initial and has access to both the form’s data and cleaned_data properties. The issue with this technique, however, is that it would require the data to be stored in some way for use in get_form_initial (either via a session variable or an instance variable). While this works, it still seemed like there had to be a better way. After all, the wizard is storing this data somewhere already.

So after a bit of digging in the source, the solution was obvious – self.storage.get_step_data(step).

This method allows you to pull the data from any step and will return either None or a MultiValueDict.

class MyWizardView(SessionWizardView):
# this runs for the step it's on as well as for the steps before
def get_form_initial(self, step):
# steps are named 'step1', 'step2', 'step3'
# get the data for step 1
prev_data = self.storage.get_step_data('step1')
some_var = prev_data.get('step1-some_var','')
return self.initial_dict.get(step, {'some_var': some_var})

view raw
views.py
hosted with ❤ by GitHub

One final thing to note is that because get_form_initial is called for every previous step (with that previous step as the step argument), it’s probably likely you’ll need to know what step you’re actually on. You can do this by using self.storage.current_step.

class MyWizardView(SessionWizardView):
# this runs for the step it's on as well as for the step before
def get_form_initial(self, step):
# steps are named 'step1', 'step2', 'step3'
current_step = self.storage.current_step
# get the data for step 1 on step 3
if current_step == 'step3':
prev_data = self.storage.get_step_data('step1')
some_var = prev_data.get('step1-some_var','')
return self.initial_dict.get(step, {'some_var': some_var})
return self.initial_dict.get(step, {})

view raw
views.py
hosted with ❤ by GitHub

If anyone knows of a better way to do this, I’d love to know.

Django Class-based Views with Multiple Forms

By default some of Django’s class-based views support just a single form per view. This, of course, does not always play nicely with what you’re trying to accomplish. I’ve yet to come across a concrete example of how to accomplish this so after a bit of experimentation and some time spent diving into Django’s code, I’ve created a decent workaround. Please let me know in the comments if there’s a better way to do this.

For this example I’ll use a generic UpdateView and two ModelForms. The same technique should work with most of the class-based views as well as regular forms.

I’ll also mention that this handles the validation and submission of either form, but not both forms at the same time. The user is only submitting a single form, so you only need to handle that one.

There’s nothing special about the models or the templates, so let’s look at the view:

from django.views.generic import UpdateView
from django.shortcuts import get_object_or_404
from django.core.urlresolvers import reverse_lazy
from app.models import Model
from app.forms import Form1, Form2
class MyView(UpdateView):
template_name = 'template.html'
form_class = Form1
second_form_class = Form2
success_url = reverse_lazy('success')
def get_context_data(self, **kwargs):
context = super(MyView, self).get_context_data(**kwargs)
if 'form' not in context:
context['form'] = self.form_class(initial={'some_field': context['model'].some_field})
if 'form2' not in context:
context['form2'] = self.second_form_class(initial={'another_field': context['model'].another_field})
return context
def get_object(self):
return get_object_or_404(Model, pk=self.request.session['someval'])
def form_invalid(self, **kwargs):
return self.render_to_response(self.get_context_data(**kwargs))
def post(self, request, *args, **kwargs):
# get the user instance
self.object = self.get_object()
# determine which form is being submitted
# uses the name of the form's submit button
if 'form' in request.POST:
# get the primary form
form_class = self.get_form_class()
form_name = 'form'
else:
# get the secondary form
form_class = self.second_form_class
form_name = 'form2'
# get the form
form = self.get_form(form_class)
# validate
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(**{form_name: form})

view raw
views.py
hosted with ❤ by GitHub

The first thing the view does is set the template, success_url and the two forms. Next we define the get_context_data method which adds either form to the context if it is missing.

UPDATE: There was an issue with the get_context_data method in which the form would not be populated with values from the model if a form was submitted with invalid data (and self.form_invalid was called). The current fix is to manually pass the data to the form using the initial argument.

The get_object method overrides the default behavior and gets a model instance based on a session variable. This isn’t necessary if you’re using the standard method of providing a pk or slug in the URL.

The next change is very important – override the default form_invalid method. The default method returns a single form in the context:

return self.render_to_response(self.get_context_data(form=form))

view raw
views.py
hosted with ❤ by GitHub

Since we have two forms, with different names, we need to switch to using **kwargs instead.

Finally, we override the post method to determine which form was submitted based on the name of the submit button (you could alternatively use a hidden field), validate the form and call form_valid or form_invalid. These methods will save the form and redirect to the success_url, or return to the view and show any errors, respectively.

And for the sake of clarity, the forms in your templates simply need a named submit button:

<form action="{% url 'myview' %}" method="post" enctype="multipart/form-data">
<input type="submit" name="form" value="Submit" />
</form>
<form action="{% url 'myview' %}" method="post" enctype="multipart/form-data">
<input type="submit" name="form2" value="Submit" />
</form>

view raw
template.html
hosted with ❤ by GitHub

Trim Spaces in Django Forms

Not sure if I agree with this ticket, but it appears Django won’t be adding support for removing leading / trailing whitespace from form fields. Other frameworks make this super easy. Hopefully the Django folks will put something together for 1.6+.

For now the best approach appears to be creating a base form class which all other forms subclass. The class looks like the following (thanks to Dave Dash and his library):

from django.forms import Form
from django.forms import ValidationError
from django.forms import FileField
# Django 1.6 Version
class BaseForm(Form):
def _clean_fields(self):
for name, field in self.fields.items():
# value_from_datadict() gets the data from the data dictionaries.
# Each widget type knows how to retrieve its own data, because some
# widgets split data over several HTML fields.
value = field.widget.value_from_datadict(self.data, self.files,
self.add_prefix(name))
try:
if isinstance(field, FileField):
initial = self.initial.get(name, field.initial)
value = field.clean(value, initial)
else:
if isinstance(value, basestring):
value = field.clean(value.strip())
else:
value = field.clean(value)
self.cleaned_data[name] = value
if hasattr(self, 'clean_%s' % name):
value = getattr(self, 'clean_%s' % name)()
self.cleaned_data[name] = value
except ValidationError, e:
self._errors[name] = self.error_class(e.messages)
if name in self.cleaned_data:
del self.cleaned_data[name]
# Django 1.7 Version
# (makes use of new add_error API – https://docs.djangoproject.com/en/1.7/ref/forms/api/#django.forms.Form.add_error)
class BaseForm(Form):
# strip leading or trailing whitespace
def _clean_fields(self):
for name, field in self.fields.items():
# value_from_datadict() gets the data from the data dictionaries.
# Each widget type knows how to retrieve its own data, because some
# widgets split data over several HTML fields.
value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
try:
if isinstance(field, FileField):
initial = self.initial.get(name, field.initial)
value = field.clean(value, initial)
else:
if isinstance(value, basestring):
value = field.clean(value.strip())
else:
value = field.clean(value)
self.cleaned_data[name] = value
if hasattr(self, 'clean_%s' % name):
value = getattr(self, 'clean_%s' % name)()
self.cleaned_data[name] = value
except ValidationError as e:
self.add_error(name, e)

view raw
forms.py
hosted with ❤ by GitHub

I have seen other posts recommending doing the work in the clean method but I find that this runs after any validation. So if you’re doing some regex validation that doesn’t allow leading / trailing spaces for instance, the validation will fail. It’s better to trim the whitespace first, then do the validation to avoid the user having to manually trim the spaces.

Django ModelForm, FormView and the Request Object

It’s often necessary to get access to the request object from within a ModelForm. To do this, you’ll need to override a single method in both the ModelForm and FormView.

First the FormView:

class RegisterView(FormView):
template_name = 'register.html'
form_class = RegisterForm
success_url = reverse_lazy('home')
# add the request to the kwargs
def get_form_kwargs(self):
kwargs = super(RegisterView, self).get_form_kwargs()
kwargs['request'] = self.request
return kwargs

view raw
views.py
hosted with ❤ by GitHub

And now the ModelForm:

class RegisterForm(forms.ModelForm):
username = forms.CharField(max_length=User._meta.get_field('username').max_length)
email = forms.CharField(max_length=User._meta.get_field('email').max_length, widget=HTML5EmailInput())
password = forms.CharField(min_length=6, max_length=16, widget=forms.PasswordInput())
# the request is now available, add it to the instance data
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request')
super(RegisterForm, self).__init__(*args, **kwargs)

view raw
forms.py
hosted with ❤ by GitHub

Django Class-based Forms

Diving into Django’s class-based forms and ran into a common problem with reverse(). As documented here the form takes three parameters – template name, form_class and success_url.

My first instinct was to do the following:

class Form(FormView):
template_name = 'template.html'
form_class = FormClassName
success_url = reverse('my_url_name')

view raw
forms.py
hosted with ❤ by GitHub

This blows up rather nicely. The solution is rather simple – reverse_lazy():

class Form(FormView):
template_name = 'template.html'
form_class = FormClassName
success_url = reverse_lazy('my_url_name')

view raw
forms.py
hosted with ❤ by GitHub