diff --git a/accounts/forms.py b/accounts/forms.py index 92c523fc2..3cef588a7 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -1,6 +1,6 @@ from django import forms -from django_registration.forms import RegistrationForm from django_recaptcha.fields import ReCaptchaField +from django_registration.forms import RegistrationForm from accounts.models import User diff --git a/accounts/templates/django_registration/registration_form.html b/accounts/templates/django_registration/registration_form.html index 88dc09a9c..9a8d36939 100644 --- a/accounts/templates/django_registration/registration_form.html +++ b/accounts/templates/django_registration/registration_form.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% load crispy_forms_tags %} +{% load honeypot %} {% block content %}

Register

@@ -10,6 +11,9 @@

Register

{% else %}
{% csrf_token %} + + {% render_honeypot_field honeypot_field_name %} + {{ form|crispy }} diff --git a/accounts/tests.py b/accounts/tests.py index 0e1dc487b..06ea6a478 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -1,5 +1,7 @@ from unittest.mock import PropertyMock, patch +from django.conf import settings from django.test import TestCase +from django.urls import reverse from .models import User from subscription.models import Subscription @@ -87,3 +89,12 @@ def test_user_without_subscription_is_not_subscriber(self) -> None: self.user.refresh_from_db() self.assertFalse(self.user.is_subscriber) + + +class HoneypotTest(TestCase): + def test_register_page_contains_honeypot_field(self): + response = self.client.get(reverse("django_registration_register")) + self.assertContains( + response, + settings.HONEYPOT_FIELD_NAME, + ) diff --git a/accounts/views.py b/accounts/views.py new file mode 100644 index 000000000..e2020d8bd --- /dev/null +++ b/accounts/views.py @@ -0,0 +1,23 @@ +from typing import Any +from django.conf import settings +from django.utils.decorators import method_decorator + +from django_registration.backends.activation.views import RegistrationView # type: ignore +from honeypot.decorators import check_honeypot # type: ignore + +from accounts.forms import CustomUserForm + + +@method_decorator(check_honeypot, name="post") +class CustomRegistrationView(RegistrationView): + form_class = CustomUserForm + success_url = "/" + template_name = "django_registration/registration_form.html" + + def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + """Add honeypot field to context.""" + context = super().get_context_data(**kwargs) + + context["honeypot_field_name"] = settings.HONEYPOT_FIELD_NAME + + return context diff --git a/core/settings.py b/core/settings.py index a235e6b3c..3197b4ff8 100644 --- a/core/settings.py +++ b/core/settings.py @@ -128,8 +128,10 @@ # Application definition INSTALLED_APPS = [ # First party (apps from this project) + # keep-sorted start "accounts", "addresses", + "blocks", "cart", "common", "community", @@ -140,54 +142,59 @@ "forms", "home", "library", + "magazine", "memorials", "navigation", "news", "orders", - "payment.apps.PaymentConfig", "pagination", + "payment.apps.PaymentConfig", "paypal", "search", "store", "subscription", - "magazine", "tags", - "blocks", "wf_pages", + # keep-sorted end # Third party (apps that have been installed) - "django_extensions", - "crispy_forms", + # keep-sorted start "crispy_bootstrap5", + "crispy_forms", + "django_extensions", "django_flatpickr", + "django_recaptcha", + "honeypot", "modelcluster", "storages", "taggit", + "wagtail", + "wagtail.admin", "wagtail.contrib.forms", "wagtail.contrib.modeladmin", "wagtail.contrib.redirects", "wagtail.contrib.routable_page", "wagtail.contrib.settings", "wagtail.contrib.styleguide", - "wagtail.embeds", - "wagtail.sites", - "wagtail.users", - "wagtail.snippets", "wagtail.documents", + "wagtail.embeds", "wagtail.images", "wagtail.search", - "wagtail.admin", - "wagtail", + "wagtail.sites", + "wagtail.snippets", + "wagtail.users", "wagtail_color_panel", "wagtailmedia", - "django_recaptcha", + # keep-sorted end # Contrib (apps that are included in Django) + # keep-sorted start "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", - "django.contrib.sessions", "django.contrib.messages", - "django.contrib.staticfiles", + "django.contrib.sessions", "django.contrib.sitemaps", + "django.contrib.staticfiles", + # keep-sorted end ] CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" @@ -385,3 +392,6 @@ RECAPTCHA_PUBLIC_KEY = os.environ.get("RECAPTCHA_PUBLIC_KEY", "") RECAPTCHA_PRIVATE_KEY = os.environ.get("RECAPTCHA_PRIVATE_KEY", "") NOCAPTCHA = True + +# Honeypot settings +HONEYPOT_FIELD_NAME = "email2" diff --git a/core/urls.py b/core/urls.py index 9991dc9e2..1d7608e76 100644 --- a/core/urls.py +++ b/core/urls.py @@ -3,16 +3,13 @@ from django.contrib import admin from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.urls import include, path -from django_registration.backends.activation.views import RegistrationView + from wagtail import urls as wagtail_urls from wagtail.admin import urls as wagtailadmin_urls from wagtail.contrib.sitemaps.views import sitemap from wagtail.documents import urls as wagtaildocs_urls -# TODO: Change this line to send verification emails when registering users -# Note: this will require two activation email tempates (subject and body) -# from django_registration.backends.activation.views import RegistrationView -from accounts.forms import CustomUserForm +from accounts.views import CustomRegistrationView from magazine import urls as magazine_urls from search import views as search_views @@ -20,7 +17,7 @@ path("django-admin/", admin.site.urls), path( "accounts/register/", - RegistrationView.as_view(form_class=CustomUserForm), + CustomRegistrationView.as_view(), name="django_registration_register", ), path("accounts/", include("django_registration.backends.activation.urls")), diff --git a/forms/models.py b/forms/models.py index 2d00eba15..3310fa51b 100644 --- a/forms/models.py +++ b/forms/models.py @@ -1,4 +1,7 @@ +from django.conf import settings from django.db import models +from django.utils.decorators import method_decorator +from honeypot.decorators import check_honeypot from modelcluster.fields import ParentalKey from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel from wagtail.contrib.forms.models import ( @@ -51,6 +54,7 @@ class FormField(AbstractFormField): ) +@method_decorator(check_honeypot, name="serve") class ContactFormPage(WfCaptchaEmailForm): intro = RichTextField(blank=True) thank_you_text = RichTextField(blank=True) @@ -80,3 +84,10 @@ class ContactFormPage(WfCaptchaEmailForm): parent_page_types = ["home.HomePage"] subpage_types: list[str] = [] + + def get_context(self, request, *args, **kwargs): + context = super().get_context(request, *args, **kwargs) + + context["honeypot_field_name"] = settings.HONEYPOT_FIELD_NAME + + return context diff --git a/forms/templates/forms/contact_form_page.html b/forms/templates/forms/contact_form_page.html index 370dc7c41..e5b6c0d8f 100644 --- a/forms/templates/forms/contact_form_page.html +++ b/forms/templates/forms/contact_form_page.html @@ -2,6 +2,7 @@ {% load wagtailcore_tags %} {% load crispy_forms_tags %} +{% load honeypot %} {% block title %} @@ -10,10 +11,16 @@ {% block content %}

{{ page.title }}

+ {{ page.intro|richtext }} + {% csrf_token %} + + {% render_honeypot_field honeypot_field_name %} + {{ form | crispy }} +
{% endblock content %} diff --git a/forms/tests.py b/forms/tests.py index a39b155ac..2fac319e0 100644 --- a/forms/tests.py +++ b/forms/tests.py @@ -1 +1,29 @@ -# Create your tests here. +from django.test import TestCase +from django.conf import settings +from wagtail.models import Page, Site + +from home.models import HomePage + +from .models import ContactFormPage + + +class ContactFormPageHoneypotTest(TestCase): + def setUp(self) -> None: + site_root = Page.objects.get(id=2) + + self.home_page = HomePage(title="Home") + site_root.add_child(instance=self.home_page) + + Site.objects.all().update(root_page=self.home_page) + self.contact_form_page = ContactFormPage( + title="Contact Form Page", + slug="contact-form-page", + ) + self.home_page.add_child(instance=self.contact_form_page) + + def test_honeypot_field_is_rendered(self) -> None: + response = self.client.get(self.contact_form_page.url) + self.assertContains( + response, + settings.HONEYPOT_FIELD_NAME, + ) diff --git a/pyproject.toml b/pyproject.toml index 0438ec39c..7d934fda4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,7 @@ dependencies = [ "django-crispy-forms", "django-extensions", "django-flatpickr", + "django-honeypot", "django-recaptcha", "django-registration", "django-storages", diff --git a/requirements-dev.txt b/requirements-dev.txt index f1bd36a27..205af0b40 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -54,6 +54,7 @@ django==4.2.7 # django-extensions # django-filter # django-flatpickr + # django-honeypot # django-modelcluster # django-permissionedforms # django-recaptcha @@ -81,6 +82,8 @@ django-filter==23.3 # via wagtail django-flatpickr==2.0.1 # via Western-Friend-website (pyproject.toml) +django-honeypot==1.0.4 + # via Western-Friend-website (pyproject.toml) django-modelcluster==6.1 # via wagtail django-permissionedforms==0.1 diff --git a/requirements.txt b/requirements.txt index 08795144b..452ca6488 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,6 +41,7 @@ django==4.2.7 # django-extensions # django-filter # django-flatpickr + # django-honeypot # django-modelcluster # django-permissionedforms # django-recaptcha @@ -64,6 +65,8 @@ django-filter==23.3 # via wagtail django-flatpickr==2.0.1 # via Western-Friend-website (pyproject.toml) +django-honeypot==1.0.4 + # via Western-Friend-website (pyproject.toml) django-modelcluster==6.1 # via wagtail django-permissionedforms==0.1