Django contact form with hCaptcha

A few days ago, I migrated my static website to this custom developed Django website. I did that because I wanted to get rid of my Wordpress site where I was selling the course. I am now selling the course on this site (which you can find here).

Previously, I used Jekyll (a static site generator) to create this site and used formspree to host the form. This worked fine, but since I can easily add in a contact form myself in this "app", I figured why not. I first had it without any catpcha, but that didn't last long, I started to get quite a few spam messages like this one:

spam message

Since I added captcha, spam has been massively reduced. So, let's jump right into it.

Set up hCaptcha account and get keys

Go to hCpatcha.com and click on 'Signup' (pick the free version) and create an account.

When you are logged in, click on 'add site' and fill in the details there.

You will then get to see your sitekey. This is a public key, so there is no issue in sharing it. Here is mine:

site key

You will need that, so keep note of that. Up next, we need one more thing. The secret key. This one should not be shared and should be saved as an environment variable on the server. You can find this secret key on the settings page: https://dashboard.hcaptcha.com/settings.

Add contact form to your site

For this, I will use a normal MVT Django set up, to demonstrate how this works. Here is what I added to my urls.py file to create the URL for the form:

path('contact/', user_views.contact, name="contact"),

Up next, this is how I render my form in the template:

    <form
      action="{% url 'contact' %}"
      method="POST"
    >
      {% csrf_token %}
      {% for message in form.email.errors %}
        <div class="notification is-danger">{{ message }}</div>
      {% endfor %}
      <div class="field">
        <label class="label">Your email</label>
        <div class="control">
          {{ form.email }}
        </div>
      </div>
      {% for message in form.message.errors %}
        <div class="notification is-danger">{{ message }}</div>
      {% endfor %}
      <div class="field">
        <label class="label">Your message</label>
        <div class="control">
          {{ form.message }}
        </div>
      </div>

      <div class="h-captcha" data-sitekey="e01397bb-3069-4769-b7e8-528affaa87e2"></div>
      {% for message in form.captcha.errors %}
        <div class="notification is-danger">{{ message }}</div>
      {% endfor %}

      <button class="button is-info" style="margin-top: 20px; width: 100%" type="submit">Send</button>
    </form>

Please note the data-sitekey part. That's my sitekey that I added there (which I got earlier from hCaptcha). Please replace that with your own if you are going to use that form (it's compatible with Bulma css framework).

Backend logic

And here is the views.py:

from django.core.mail import EmailMessage
from users.forms import ContactForm

def contact(request):
    if request.method == 'POST':
        data = request.POST.copy()
        if 'h-captcha-response' in data:
            data['captcha'] = data['h-captcha-response']
        form = ContactForm(data)
        if form.is_valid():
            email = EmailMessage(
                'New contact form message',
                form.cleaned_data['message'],
                'hello@example.com',
                ['hello@example.com'],
                reply_to=[form.cleaned_data['email']]
            )
            email.send(fail_silently=False)
            messages.success(request, 'Message sent successfully!')
            return redirect('work-with-me')

    else:
        form = ContactForm()

    return render(request, 'contact.html', {'form': form})

From the form you probably noticed that we copied the POST data and then modify it. We do this, because our form won't be able to process a field that has dashes in it. Dashes are often seen as a - symbol (for subtracting a value). Therefore, we copy it to a new key that we are able to process through our form (called captcha). The goal of the form is to send a message back to us. In this case, we use EmailMessage instead of just using send_email as we want to set the reply_to back to the person that filled in the form. So, if we want to react to a message, we can just hit reply without having to think about it.

In the form, we have to validate the captcha. If it's not valid, we have to return the form with an error back to the user.

Our form will look like this:

from django import forms
from django.core.exceptions import ValidationError
import requests
from django.conf import settings

def validate_captcha(value):
    data = {'secret': settings.HCAPTCHA_SECRET_KEY, 'response': value}
    response = requests.post('https://hcaptcha.com/siteverify', data)
    if not 'success' in response.json() or not response.json()['success']:
        raise ValidationError('hcaptcha is not correct')

class ContactForm(forms.Form):
    email = forms.EmailField(max_length=200)
    message = forms.CharField(widget=forms.Textarea(attrs={'class':'input', 'rows': 10, 'placeholder': 'Hi...'}), max_length=2000)
    captcha = forms.CharField(max_length=10000, validators=[validate_captcha])

Let's check out the ContactForm itself first. We added two fields. email and message. Nothing really crazy going on there. Up next, we need to validate the captcha field to make sure everything goes through nicely. For that, we created a validator. That's what the function above the form is for.

So, in the validator we do make a call to the hcaptcha endpoint with the data that has been provide to us. If that call passes, then we get a JSON response back. Based on that, we can validate if everything was okay.

Hope this helps!

Three things to keep in mind

  • In this example, we are not catching any unexpected errors coming from the endpoint itself. Say, the hCaptcha server is down or the request times our, we would get a 500 error. You might want to wrap the request in a try/catch and show a proper error to the user.
  • If you want to keep everything clean, then you could move the function to a separate file called validators.py.
  • hCaptcha pays you for captchas that get completed by users. Personally, I set my earnings to go to a charity (you can do that in the settings). Unless you get millions of people to complete the captchas, I doubt you will earn a lot from this anyway.
Django contact form hcaptcha tutorial
Written by Stan Triepels

Stan is professional web developer working mainly with Django and VueJS. With years of experience under the belt, he is comfortable writing about his past mistakes and ongoing learnings.