[Python-modules-commits] [python-django-contact-form] 01/05: New upstream version 1.4.2

Andrew Starr-Bochicchio asb at moszumanska.debian.org
Mon Jul 3 14:55:57 UTC 2017


This is an automated email from the git hooks/post-receive script.

asb pushed a commit to branch master
in repository python-django-contact-form.

commit 0bbefda541a91ffaf1dd423f0d7b5553992eb858
Author: Andrew Starr-Bochicchio <a.starr.b at gmail.com>
Date:   Mon Jul 3 10:43:30 2017 -0400

    New upstream version 1.4.2
---
 .gitignore                                |  5 +-
 .travis.yml                               | 21 +------
 LICENSE                                   |  2 +-
 MANIFEST.in                               |  1 +
 Makefile                                  | 60 +++++++++++++++++++
 README.rst                                |  4 +-
 contact_form/{urls.py => akismet_urls.py} | 12 ++--
 contact_form/forms.py                     | 83 ++++++++++++++++++++------
 contact_form/runtests.py                  |  8 ++-
 contact_form/tests/test_forms.py          | 75 +++++++++++++++++++++++-
 contact_form/tests/test_urls.py           |  8 ++-
 contact_form/tests/test_views.py          | 53 ++++++++++++++++-
 contact_form/urls.py                      |  4 +-
 contact_form/views.py                     | 27 ++-------
 docs/conf.py                              |  6 +-
 docs/faq.rst                              | 67 ++++++++++++---------
 docs/forms.rst                            | 97 +++++++++++++++++++++++++++----
 docs/index.rst                            | 11 ++--
 docs/install.rst                          | 60 ++++++++-----------
 docs/quickstart.rst                       | 62 ++++++++++++++++----
 docs/views.rst                            | 21 ++-----
 setup.cfg                                 |  4 ++
 setup.py                                  | 12 ++--
 test_requirements.txt                     |  3 +
 tox.ini                                   | 46 +++++++++++++++
 25 files changed, 566 insertions(+), 186 deletions(-)

diff --git a/.gitignore b/.gitignore
index 955f665..518cfaf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,7 @@ __pycache__
 *.egg-info
 docs/_build/
 dist/
-.coverage
\ No newline at end of file
+.coverage
+.python-version
+.tox/
+*.sh
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index 67dd0d4..b97d414 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,25 +1,10 @@
 language: python
 sudo: false
 python:
+ - "3.6"
  - "3.5"
  - "3.4"
  - "3.3"
  - "2.7"
-env:
- - DJANGO_VERSION=1.8.16
- - DJANGO_VERSION=1.9.11
- - DJANGO_VERSION=1.10.3
-install:
- - pip install coverage>=4.0
- - pip install flake8
- - pip install -q Django==$DJANGO_VERSION
-matrix:
- exclude:
-   - python: "3.3"
-     env: DJANGO_VERSION=1.9.11
-   - python: "3.3"
-     env: DJANGO_VERSION=1.10.3
-script:
- - coverage run setup.py test
- - flake8 contact_form
- - coverage report -m
+install: pip install tox-travis
+script: tox
diff --git a/LICENSE b/LICENSE
index 5f67d07..ccf813a 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2007-2016, James Bennett
+Copyright (c) 2007-2017, James Bennett
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
diff --git a/MANIFEST.in b/MANIFEST.in
index 93e2657..814c0bb 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,5 @@
 include LICENSE
 include MANIFEST.in
 include README.rst
+include Makefile
 recursive-include docs *
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..2b88f7e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,60 @@
+PACKAGE_NAME=contact_form
+TESTING_VENV_NAME=${PACKAGE_NAME}_test
+
+ifndef PYTHON_VERSION
+PYTHON_VERSION=3.6.0
+endif
+
+ifndef DJANGO_VERSION
+DJANGO_VERSION=1.10
+endif
+
+.PHONY: clean
+clean:
+	python setup.py clean
+	rm -rf build/
+	rm -rf dist/
+	rm -rf *.egg*/
+	rm -rf ${PACKAGE_NAME}/__pycache__/
+	rm -rf ${PACKAGE_NAME}/tests/__pycache__/
+	find ${PACKAGE_NAME} -type f -name '*.pyc' -delete
+	rm -f MANIFEST
+	rm -rf coverage .coverage .coverage*
+	pip uninstall -y Django
+
+.PHONY: venv
+venv:
+	[ -e ~/.pyenv/versions/${TESTING_VENV_NAME} ] && \
+	echo "\033[1;33mA ${TESTING_VENV_NAME} pyenv already exists. Skipping pyenv creation.\033[0m" || \
+	pyenv virtualenv ${PYTHON_VERSION} ${TESTING_VENV_NAME}
+	pyenv local ${TESTING_VENV_NAME}
+	pip install --upgrade pip setuptools
+
+.PHONY: teardown
+teardown: clean
+	pyenv uninstall -f ${TESTING_VENV_NAME}
+	rm .python-version
+
+.PHONY: django
+django:
+	pip install Django~=${DJANGO_VERSION}.0
+
+.PHONY: test_dependencies
+test_dependencies:
+	[ -e test_requirements.txt ] && \
+	pip install -r test_requirements.txt || \
+	echo "\033[1;33mNo test_requirements.txt found. Skipping install of test_requirements.txt.\033[0m"
+	pip install -r test_requirements.txt
+
+.PHONY: lint
+lint: test_dependencies
+	flake8 ${PACKAGE_NAME}
+
+.PHONY: test
+test: django lint
+	[ -e requirements.txt ] \
+	&& pip install -r requirements.txt || \
+	echo "\033[1;33mNo requirements.txt found. Skipping install of requirements.txt.\033[0m"
+	pip install -e .
+	coverage run ${PACKAGE_NAME}/runtests.py
+	coverage report -m
diff --git a/README.rst b/README.rst
index e7f5cdb..f6c4684 100644
--- a/README.rst
+++ b/README.rst
@@ -3,8 +3,8 @@
 .. image:: https://travis-ci.org/ubernostrum/django-contact-form.svg?branch=master
     :target: https://travis-ci.org/ubernostrum/django-contact-form
 
-This application provides simple, extensible contact-form functionality
-for `Django <https://www.djangoproject.com/>`_ sites.
+This application provides extensible contact-form functionality for
+`Django <https://www.djangoproject.com/>`_ sites.
 
 Full documentation for all functionality is included and is also
 `available online <http://django-contact-form.readthedocs.io/>`_.
\ No newline at end of file
diff --git a/contact_form/urls.py b/contact_form/akismet_urls.py
similarity index 56%
copy from contact_form/urls.py
copy to contact_form/akismet_urls.py
index e925044..3906355 100644
--- a/contact_form/urls.py
+++ b/contact_form/akismet_urls.py
@@ -1,21 +1,23 @@
 """
-Example URLConf for a contact form.
+Example URLConf for a contact form with Akismet spam filtering.
 
-If all you want is the basic ContactForm with default behavior, just
+If all you want is the basic contact-form plus spam filtering,
 include this URLConf somewhere in your URL hierarchy (for example, at
-``/contact/``)>
+``/contact/``).
 
 """
 
 from django.conf.urls import url
 from django.views.generic import TemplateView
 
-from contact_form.views import ContactFormView
+from .forms import AkismetContactForm
+from .views import ContactFormView
 
 
 urlpatterns = [
     url(r'^$',
-        ContactFormView.as_view(),
+        ContactFormView.as_view(
+            form_class=AkismetContactForm),
         name='contact_form'),
     url(r'^sent/$',
         TemplateView.as_view(
diff --git a/contact_form/forms.py b/contact_form/forms.py
index 4506859..d49bc02 100644
--- a/contact_form/forms.py
+++ b/contact_form/forms.py
@@ -3,14 +3,14 @@ A base contact form for allowing users to send email messages through
 a web interface.
 
 """
+import os
 
 from django import forms
 from django.conf import settings
-from django.contrib.sites.models import Site
-from django.contrib.sites.requests import RequestSite
+from django.contrib.sites.shortcuts import get_current_site
 from django.utils.translation import ugettext_lazy as _
 from django.core.mail import send_mail
-from django.template import RequestContext, loader
+from django.template import loader
 
 
 class ContactForm(forms.Form):
@@ -49,12 +49,12 @@ class ContactForm(forms.Form):
         Render the body of the message to a string.
 
         """
-        if callable(self.template_name):
-            template_name = self.template_name()
-        else:
-            template_name = self.template_name
-        return loader.render_to_string(template_name,
-                                       self.get_context())
+        template_name = self.template_name() if \
+            callable(self.template_name) \
+            else self.template_name
+        return loader.render_to_string(
+            template_name, self.get_context(), request=self.request
+        )
 
     def subject(self):
         """
@@ -64,8 +64,9 @@ class ContactForm(forms.Form):
         template_name = self.subject_template_name() if \
             callable(self.subject_template_name) \
             else self.subject_template_name
-        subject = loader.render_to_string(template_name,
-                                          self.get_context())
+        subject = loader.render_to_string(
+            template_name, self.get_context(), request=self.request
+        )
         return ''.join(subject.splitlines())
 
     def get_context(self):
@@ -88,13 +89,7 @@ class ContactForm(forms.Form):
             raise ValueError(
                 "Cannot generate Context from invalid contact form"
             )
-        if Site._meta.installed:
-            site = Site.objects.get_current()
-        else:
-            site = RequestSite(self.request)
-        return RequestContext(self.request,
-                              dict(self.cleaned_data,
-                                   site=site))
+        return dict(self.cleaned_data, site=get_current_site(self.request))
 
     def get_message_dict(self):
         """
@@ -130,3 +125,55 @@ class ContactForm(forms.Form):
 
         """
         send_mail(fail_silently=fail_silently, **self.get_message_dict())
+
+
+class AkismetContactForm(ContactForm):
+    """
+    Contact form which doesn't add any extra fields, but does add an
+    Akismet spam check to the validation routine.
+
+    Requires the Python Akismet library, and two configuration
+    parameters: an Akismet API key and the URL the key is associated
+    with. These can be supplied either as the settings AKISMET_API_KEY
+    and AKISMET_BLOG_URL, or the environment variables
+    PYTHON_AKISMET_API_KEY and PYTHON_AKISMET_BLOG_URL.
+
+    """
+    SPAM_MESSAGE = _(u"Your message was classified as spam.")
+
+    def _is_unit_test(self):
+        """
+        Determine if we're in a test run.
+
+        During test runs, Akismet should be passed the ``is_test``
+        argument to ensure no effect on training corpus.
+
+        django-contact-form's tox.ini will set the environment
+        variable ``CI`` to indicate this, as will most online
+        continuous-integration systems.
+
+        """
+        return os.getenv('CI') == 'true'
+
+    def clean_body(self):
+        if 'body' in self.cleaned_data:
+            from akismet import Akismet
+            akismet_api = Akismet(
+                key=getattr(settings, 'AKISMET_API_KEY', None),
+                blog_url=getattr(settings, 'AKISMET_BLOG_URL', None)
+            )
+            akismet_kwargs = {
+                'user_ip': self.request.META['REMOTE_ADDR'],
+                'user_agent': self.request.META.get('HTTP_USER_AGENT'),
+                'comment_author': self.cleaned_data.get('name'),
+                'comment_author_email': self.cleaned_data.get('email'),
+                'comment_content': self.cleaned_data['body'],
+                'comment_type': 'contact-form',
+            }
+            if self._is_unit_test():
+                akismet_kwargs['is_test'] = 1
+            if akismet_api.comment_check(**akismet_kwargs):
+                raise forms.ValidationError(
+                    self.SPAM_MESSAGE
+                )
+            return self.cleaned_data['body']
diff --git a/contact_form/runtests.py b/contact_form/runtests.py
index 549e2db..deccca3 100644
--- a/contact_form/runtests.py
+++ b/contact_form/runtests.py
@@ -26,7 +26,7 @@ SETTINGS_DICT = {
         'django.contrib.contenttypes',
         'django.contrib.sites',
     ),
-    'ROOT_URLCONF': 'contact_form.tests.urls',
+    'ROOT_URLCONF': 'contact_form.tests.test_urls',
     'DATABASES': {
         'default': {
             'ENGINE': 'django.db.backends.sqlite3',
@@ -75,6 +75,10 @@ def run_tests():
     TestRunner = get_runner(settings)
 
     # And then we run tests and return the results.
-    test_runner = TestRunner(verbosity=1, interactive=True)
+    test_runner = TestRunner(verbosity=2, interactive=True)
     failures = test_runner.run_tests(['contact_form.tests'])
     sys.exit(bool(failures))
+
+
+if __name__ == '__main__':
+    run_tests()
diff --git a/contact_form/tests/test_forms.py b/contact_form/tests/test_forms.py
index 0691163..d223396 100644
--- a/contact_form/tests/test_forms.py
+++ b/contact_form/tests/test_forms.py
@@ -1,11 +1,19 @@
+import os
+import unittest
+
 from django.conf import settings
 from django.core import mail
 from django.test import RequestFactory, TestCase
+from django.utils.six import text_type
 
-from ..forms import ContactForm
+from ..forms import AkismetContactForm, ContactForm
 
 
 class ContactFormTests(TestCase):
+    """
+    Tests the base ContactForm.
+
+    """
     valid_data = {'name': 'Test',
                   'email': 'test at example.com',
                   'body': 'Test message'}
@@ -139,3 +147,68 @@ class ContactFormTests(TestCase):
 
         self.assertEqual(overridden_data,
                          form.get_message_dict())
+
+
+ at unittest.skipUnless(
+    getattr(
+        settings,
+        'AKISMET_API_KEY',
+        os.getenv('PYTHON_AKISMET_API_KEY')
+    ) is not None,
+    "AkismetContactForm requires Akismet configuration"
+)
+class AkismetContactFormTests(TestCase):
+    """
+    Tests the Akismet contact form.
+
+    """
+    def request(self):
+        return RequestFactory().request()
+
+    def test_akismet_form_test_detection(self):
+        """
+        The Akismet contact form correctly detects a test environment.
+
+        """
+        form = AkismetContactForm(request=self.request())
+        self.assertTrue(form._is_unit_test())
+        try:
+            old_environ = os.getenv('CI')
+            os.environ['CI'] = ''
+            form = AkismetContactForm(request=self.request())
+            self.assertFalse(form._is_unit_test())
+        finally:
+            if old_environ is not None:
+                os.environ['CI'] = old_environ
+
+    def test_akismet_form_spam(self):
+        """
+        The Akismet contact form correctly rejects spam.
+
+        """
+        data = {'name': 'viagra-test-123',
+                'email': 'email at example.com',
+                'body': 'This is spam.'}
+        form = AkismetContactForm(
+            request=self.request(),
+            data=data
+        )
+        self.assertFalse(form.is_valid())
+        self.assertTrue(
+            text_type(form.SPAM_MESSAGE) in
+            form.errors['body']
+        )
+
+    def test_akismet_form_ham(self):
+        """
+        The Akismet contact form correctly accepts non-spam.
+
+        """
+        data = {'name': 'Test',
+                'email': 'email at example.com',
+                'body': 'Test message.'}
+        form = AkismetContactForm(
+            request=self.request(),
+            data=data
+        )
+        self.assertTrue(form.is_valid())
diff --git a/contact_form/tests/test_urls.py b/contact_form/tests/test_urls.py
index 19b4289..2e6843c 100644
--- a/contact_form/tests/test_urls.py
+++ b/contact_form/tests/test_urls.py
@@ -6,6 +6,7 @@ URLConf for testing django-contact-form.
 from django.conf.urls import url
 from django.views.generic import TemplateView
 
+from ..forms import AkismetContactForm
 from ..views import ContactFormView
 
 
@@ -16,10 +17,15 @@ urlpatterns = [
     url(r'^sent/$',
         TemplateView.as_view(
             template_name='contact_form/contact_form_sent.html'
-            ),
+        ),
         name='contact_form_sent'),
     url(r'^test_recipient_list/$',
         ContactFormView.as_view(
             recipient_list=['recipient_list at example.com']),
         name='test_recipient_list'),
+    url(r'^test_akismet_form/$',
+        ContactFormView.as_view(
+            form_class=AkismetContactForm
+        ),
+        name='test_akismet_form'),
 ]
diff --git a/contact_form/tests/test_views.py b/contact_form/tests/test_views.py
index 9dee61f..1086400 100644
--- a/contact_form/tests/test_views.py
+++ b/contact_form/tests/test_views.py
@@ -1,11 +1,18 @@
+import os
+import unittest
+
 from django.conf import settings
 from django.core import mail
-from django.core.urlresolvers import reverse
 from django.test import RequestFactory, TestCase
 from django.test.utils import override_settings
 
 from ..forms import ContactForm
 
+try:
+    from django.urls import reverse
+except ImportError:  # pragma: no cover
+    from django.core.urlresolvers import reverse  # pragma: no cover
+
 
 @override_settings(ROOT_URLCONF='contact_form.tests.test_urls')
 class ContactFormViewTests(TestCase):
@@ -87,3 +94,47 @@ class ContactFormViewTests(TestCase):
         message = mail.outbox[0]
         self.assertEqual(['recipient_list at example.com'],
                          message.recipients())
+
+
+ at unittest.skipUnless(
+    getattr(
+        settings,
+        'AKISMET_API_KEY',
+        os.getenv('PYTHON_AKISMET_API_KEY')
+    ) is not None,
+    "AkismetContactForm requires Akismet configuration"
+)
+class AkismetContactFormViewTests(TestCase):
+    """
+    Tests the views with the Akismet contact form.
+
+    """
+    def test_akismet_view_spam(self):
+        """
+        The Akismet contact form errors on spam.
+
+        """
+        contact_url = reverse('test_akismet_form')
+        data = {'name': 'viagra-test-123',
+                'email': 'email at example.com',
+                'body': 'This is spam.'}
+        response = self.client.post(contact_url,
+                                    data=data)
+        self.assertEqual(200, response.status_code)
+        self.assertFalse(response.context['form'].is_valid())
+        self.assertTrue(response.context['form'].has_error('body'))
+
+    def test_akismet_view_ham(self):
+        contact_url = reverse('test_akismet_form')
+        data = {'name': 'Test',
+                'email': 'email at example.com',
+                'body': 'Test message.'}
+        response = self.client.post(contact_url,
+                                    data=data)
+        self.assertRedirects(response,
+                             reverse('contact_form_sent'))
+        self.assertEqual(1, len(mail.outbox))
+
+        message = mail.outbox[0]
+        self.assertEqual(['noreply at example.com'],
+                         message.recipients())
diff --git a/contact_form/urls.py b/contact_form/urls.py
index e925044..f5f0115 100644
--- a/contact_form/urls.py
+++ b/contact_form/urls.py
@@ -1,9 +1,9 @@
 """
 Example URLConf for a contact form.
 
-If all you want is the basic ContactForm with default behavior, just
+If all you want is the basic ContactForm with default behavior,
 include this URLConf somewhere in your URL hierarchy (for example, at
-``/contact/``)>
+``/contact/``)
 
 """
 
diff --git a/contact_form/views.py b/contact_form/views.py
index a06e967..8aa1d83 100644
--- a/contact_form/views.py
+++ b/contact_form/views.py
@@ -3,11 +3,15 @@ View which can render and send email from a contact form.
 
 """
 
-from django.core.urlresolvers import reverse
 from django.views.generic.edit import FormView
 
 from .forms import ContactForm
 
+try:
+    from django.urls import reverse
+except ImportError:  # pragma: no cover
+    from django.core.urlresolvers import reverse  # pragma: no cover
+
 
 class ContactFormView(FormView):
     form_class = ContactForm
@@ -18,27 +22,6 @@ class ContactFormView(FormView):
         form.save()
         return super(ContactFormView, self).form_valid(form)
 
-    def form_invalid(self, form):
-        # tl;dr -- this method is implemented to work around Django
-        # ticket #25548, which is present in the Django 1.9 release
-        # (but not in Django 1.8).
-        #
-        # The longer explanation is that in Django 1.9,
-        # FormMixin.form_invalid() does not pass the form instance to
-        # get_context_data(). This causes get_context_data() to
-        # construct a new form instance with the same data in order to
-        # put it into the template context, and then any access to
-        # that form's ``errors`` or ``cleaned_data`` runs that form
-        # instance's validation. The end result is that validation
-        # gets run twice on an invalid form submission, which is
-        # undesirable for performance reasons.
-        #
-        # Manually implementing this method, and passing the form
-        # instance to get_context_data(), solves this issue (which was
-        # fixed in Django 1.9.1 and will not be present in Django
-        # 1.10).
-        return self.render_to_response(self.get_context_data(form=form))
-
     def get_form_kwargs(self):
         # ContactForm instances require instantiation with an
         # HttpRequest.
diff --git a/docs/conf.py b/docs/conf.py
index a171844..13ecbff 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -7,9 +7,9 @@ templates_path = ['_templates']
 source_suffix = '.rst'
 master_doc = 'index'
 project = u'django-contact-form'
-copyright = u'2007-2016, James Bennett'
-xversion = '1.3'
-release = '1.3'
+copyright = u'2007-2017, James Bennett'
+version = '1.4'
+release = '1.4.2'
 exclude_trees = ['_build']
 pygments_style = 'sphinx'
 html_static_path = ['_static']
diff --git a/docs/faq.rst b/docs/faq.rst
index be9dbd2..4f9bbf6 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -11,13 +11,11 @@ you when installing, configuring or using django-contact-form.
 What versions of Django and Python are supported?
 -------------------------------------------------
 
-As of django-contact-form |version|, Django 1.8, 1.9, and 1.10 are
-supported, on Python 2.7, 3.3, 3.4 or 3.5. Although Django 1.8
-supported Python 3.2 at initial release, Python 3.2 is now at its
-end-of-life and django-contact-form no longer supports it.
-
-It is expected that django-contact-form |version| will also work
-without modification on Python 3.6 once it is released.
+As of django-contact-form |release|, Django 1.8, 1.10, and 1.11 are
+supported, on Python 2.7, 3.3, (Django 1.8 only), 3.4, 3.5, or 3.6
+(Django 1.11 only). Although Django 1.8 supported Python 3.2 at
+initial release, Python 3.2 is now at its end-of-life and
+django-contact-form no longer supports it.
 
 
 What license is django-contact-form under?
@@ -41,27 +39,10 @@ reused, are essentially impossible to produce; variations in site
 design, block structure, etc. cannot be reliably accounted for. As
 such, django-contact-form provides bare-bones (i.e., containing no
 HTML structure whatsoever) templates in its source distribution to
-enable running tests, and otherwise simply provides good documentation
+enable running tests, and otherwise just provides good documentation
 of all required templates and the context made available to them.
 
 
-What happened to the spam-filtering form in previous versions?
---------------------------------------------------------------
-
-Older versions of django-contact-form shipped a subclass of
-:class:`~contact_form.forms.ContactForm` which used `the Akismet web
-service <http://akismet.com/>`_ to identify and reject spam
-submissions.
-
-Unfortunately, the Akismet Python library -- required in order to use
-such a class -- does not currently support all versions of Python on
-which django-contact-form is supported, meaning it cannot be
-included in django-contact-form by default. The author of
-django-contact-form is working on producing a version of the
-Akismet library compatible with Python 3, but it was not yet ready as
-of the release of django-contact-form |version|.
-
-
 Why am I getting a bunch of ``BadHeaderError`` exceptions?
 ----------------------------------------------------------
 
@@ -77,8 +58,9 @@ allow spammers and other malicious users to manipulate email and
 potentially cause automated systems to send mail to unintended
 recipients), `Django's email-sending framework does not permit
 newlines in message headers
-<https://docs.djangoproject.com/en/dev/topics/email/#preventing-header-injection>`_. ``BadHeaderError``
-is the exception Django raises when a newline is detected in a header.
+<https://docs.djangoproject.com/en/1.11/topics/email/#preventing-header-injection>`_.
+``BadHeaderError`` is the exception Django raises when a newline is
+detected in a header.
 
 Note that this only applies to the headers of an email message; the
 message body can (and usually does) contain newlines.
@@ -98,3 +80,34 @@ documentation for any changes made, and that following `PEP 8
 requests without documentation won't be merged, and PEP 8 style
 violations or test coverage below 100% are both configured to break
 the build.
+
+
+I'm getting errors about "akismet" when trying to run tests?
+------------------------------------------------------------
+
+The full test suite of django-contact-form exercises all of its
+functionality, including the spam-filtering
+:class:`~contact_forms.forms.AkismetContactForm`. That class uses `the
+Wordpress Akismet spam-detection service <https://akismet.com/>`_ to
+perform spam filtering, and so requires the Python ``akismet`` module
+to communicate with the Akismet service, and some additional
+configuration (in the form of a valid Akismet API key and associated
+URL).
+
+By default, the tests for
+:class:`~contact_forms.forms.AkismetContactForm` will be skipped
+unless the required configuration (in the form of either a pair of
+Django settings, or a pair of environment variables) is
+detected. However, if you have supplied Akismet configuration but do
+*not* have the Python ``akismet`` module, you will see test errors
+from attempts to import ``akismet``. You can resolve this by running::
+
+    pip install akismet
+
+or (if you do not intend to use
+:class:`~contact_forms.forms.AkismetContactForm`) by no longer
+configuring the Django settings/environment variables used by Akismet.
+
+Additionally, if the :class:`~contact_forms.forms.AkismetContactForm`
+tests are skipped, the default code-coverage report will fail due to
+the relevant code not being exercised during the test run.
\ No newline at end of file
diff --git a/docs/forms.rst b/docs/forms.rst
index 9d34e93..200c8a6 100644
--- a/docs/forms.rst
+++ b/docs/forms.rst
@@ -1,17 +1,26 @@
 .. _forms:
 .. module:: contact_form.forms
 
+Contact form classes
+====================
+
+There are two contact-form classes included in django-contact-form;
+one provides all the infrastructure for a contact form, and will
+usually be the base class for subclasses which want to extend or
+modify functionality. The other is a subclass which adds spam
+filtering to the contact form.
+
 
 The ContactForm class
-=====================
+---------------------
 
 .. class:: ContactForm
 
     The base contact form class from which all contact form classes
     should inherit.
 
-    If you don't need any customization, you can simply use this form
-    to provide basic contact functionality; it will collect name,
+    If you don't need any customization, you can use this form to
+    provide basic contact-form functionality; it will collect name,
     email address and message.
 
     The :class:`~contact_form.views.ContactFormView` included in this
@@ -19,7 +28,7 @@ The ContactForm class
     types of subclasses as well (see below for a discussion of the
     important points), so in many cases it will be all that you
     need. If you'd like to use this form or a subclass of it from one
-    of your own views, just do the following:
+    of your own views, here's how:
 
     1. When you instantiate the form, pass the current ``HttpRequest``
        object as the keyword argument ``request``; this is used
@@ -42,7 +51,10 @@ The ContactForm class
     order to make it easier to subclass and add functionality.
 
     The following attributes play a role in determining behavior, and
-    any of them can be implemented as an attribute or as a method:
+    any of them can be implemented as an attribute or as a method (for
+    example, if you wish to have ``from_email`` be dynamic, you can
+    implement a method named ``from_email()`` instead of setting the
+    attribute ``from_email``):
 
     .. attribute:: from_email
 
@@ -66,8 +78,8 @@ The ContactForm class
        The name of the template to use when rendering the body of the
        message. By default, this is ``contact_form/contact_form.txt``.
 
-    And two methods are involved in actually producing the contents of
-    the message to send:
+    And two methods are involved in producing the contents of the
+    message to send:
 
     .. method:: message()
 
@@ -126,12 +138,75 @@ The ContactForm class
 
     .. method:: save
 
-       If the form has data and is valid, will actually send the
-       email, by calling :meth:`get_message_dict` and passing the
-       result to Django's ``send_mail`` function.
+       If the form has data and is valid, will send the email, by
+       calling :meth:`get_message_dict` and passing the result to
+       Django's ``send_mail`` function.
 
     Note that subclasses which override ``__init__`` or :meth:`save`
     need to accept ``*args`` and ``**kwargs``, and pass them via
     ``super``, in order to preserve behavior (each of those methods
     accepts at least one additional argument, and this application
-    expects and requires them to do so).
\ No newline at end of file
+    expects and requires them to do so).
+
+
+The Akismet (spam-filtering) contact form class
+-----------------------------------------------
+
+.. class:: AkismetContactForm
+
+   A subclass of :class:`ContactForm` which adds spam filtering, via
+   `the Wordpress Akismet spam-detection service
+   <https://akismet.com/>`_.
+
+   Use of this class requires you to provide configuration for the
+   Akismet web service; you'll need to obtain an Akismet API key, and
+   you'll need to associate it with the site you'll use the contact
+   form on. You can do this at <https://akismet.com/>. Once you have,
+   you can configure in either of two ways:
+
+   1. Put your Akismet API key in the Django setting
+      ``AKISMET_API_KEY``, and the URL it's associated with in the
+      setting ``AKISMET_BLOG_URL``, or
+
+   2. Put your Akismet API key in the environment variable
+      ``PYTHON_AKISMET_API_KEY``, and the URL it's associated with in
+      the environment variable ``PYTHON_AKISMET_BLOG_URL``.
+
+   You will also need `the Python Akismet module
+   <http://akismet.readthedocs.io/>`_ to communicate with the Akismet
+   web service. You can install it by running ``pip install akismet``,
+   or django-contact-form can install it automatically for you if you
+   run ``pip install django-contact-form[akismet]``.
+
+   Once you have an Akismet API key and URL configured, and the
+   ``akismet`` module installed, you can drop in
+   ``AkismetContactForm`` anywhere you would have used
+   :class:`ContactForm`. For example, you could define a view
+   (subclassing :class:`~contact_form.views.ContactFormView`) like so,
+   and then point a URL at it:
+
+   .. code-block:: python
+
+      from contact_form.forms import AkismetContactForm
+      from contact_form.views import ContactFormView
+
+      class AkismetContactFormView(ContactFormView):
+          form_class = AkismetContactForm
+
+   Or directly specify the form in your URLconf:
+
+   .. code-block:: python
+
+      from django.conf.urls import url
+
+      from contact_form.forms import AkismetContactForm
+      from contact_form.views import ContactFormView
+
+      urlpatterns = [
+          # other URL patterns...
+          url(r'^contact-form/$',
+              ContactForm.as_view(
+	          form_class=AkismetContactForm
+	      ),
+              name='contact_form'),
+      ]
diff --git a/docs/index.rst b/docs/index.rst
index d98c031..e20b528 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,9 +1,8 @@
-django-contact-form |version|
+django-contact-form |release|
 =============================
 
-``django-contact-form`` provides simple, customizable contact-form
-functionality for `Django <https://www.djangoproject.com/>`_-powered
-Web sites.
+django-contact-form provides customizable contact-form functionality
+for `Django <https://www.djangoproject.com/>`_-powered Web sites.
 
 Basic functionality (collecting a name, email address and message) can
 be achieved out of the box by setting up a few templates and adding
@@ -14,8 +13,8 @@ one line to your site's root URLconf:
     url(r'^contact/', include('contact_form.urls')),
 
 For notes on getting started quickly, and on how to customize
-``django-contact-form``'s behavior, read through the full
-documentation below.
+django-contact-form's behavior, read through the full documentation
+below.
 
 
 Contents:
diff --git a/docs/install.rst b/docs/install.rst
index 57120fb..033fd02 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -10,17 +10,15 @@ information on obtaining and installing Django, consult the `Django
 download page <https://www.djangoproject.com/download/>`_, which
 offers convenient packaged downloads and installation instructions.
 
-The |version| release of django-contact-form supports Django 1.8,
-1.9, and 1.10, on the following Python versions:
+The |release| release of django-contact-form supports Django 1.8,
+1.10, and 1.11 on the following Python versions (matching the versions
+supported by Django itself):
 
-* Django 1.8 suports Python 2.7, 3.3, 3.4 and 3.5.
+* Django 1.8 supports Python 2.7, 3.3, 3.4, and 3.5.
 
-* Django 1.9 supports Python 2.7, 3.4 and 3.5.
+* Django 1.10 supports Python 2.7, 3.4, and 3.5.
 
-* Django 1.10 supports Python 2.7, 3.4 and 3.5.
-
-It is expected that django-contact-form |version| will work
-without modification on Python 3.6 once it is released.
+* Django 1.11 supports Python 2.7, 3.4, 3.5, and 3.6
 
 .. important:: **Python 3.2**
 
@@ -40,49 +38,41 @@ the standard Python package-installation tool. If you don't have
 Python 2.7.9 or later (for Python 2) or Python 3.4 or later (for
 Python 3), ``pip`` came bundled with your installation of Python.
 
-Once you have ``pip``, simply type::
+Once you have ``pip``, type::
 
     pip install django-contact-form
 
+If you plan to use the included spam-filtering contact form class,
+:class:`~contact_form.forms.AkismetContactForm`, you will also need
+the Python ``akismet`` module. You can manually install it via ``pip
+install akismet``, or tell ``django-contact-form`` to install it for
+you, by running::
 
-Manual installation
--------------------
-
-It's also possible to install django-contact-form manually. To do
-so, obtain the latest packaged version from `the listing on the Python
-Package Index
-<https://pypi.python.org/pypi/django-contact-form/>`_. Unpack the
-``.tar.gz`` file, and run::
-
-    python setup.py install
-
-Once you've installed django-contact-form, you can verify
-successful installation by opening a Python interpreter and typing
-``import contact_form``.
-
-If the installation was successful, you'll simply get a fresh Python
-prompt. If you instead see an ``ImportError``, check the configuration
-of your install tools and your Python import path to ensure
-django-contact-form installed into a location Python can import
-from.
+    pip install django-contact-form[akismet]
 
 
 Installing from a source checkout
 ---------------------------------
 
+If you want to work on django-contact-form, you can obtain a source
+checkout.
+
 The development repository for django-contact-form is at
-<https://github.com/ubernostrum/django-contact-form>. Presuming you
-have `git <http://git-scm.com/>`_ installed, you can obtain a copy of
-the repository by typing::
+<https://github.com/ubernostrum/django-contact-form>. If you have `git
+<http://git-scm.com/>`_ installed, you can obtain a copy of the
+repository by typing::
... 283 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-django-contact-form.git



More information about the Python-modules-commits mailing list