├── antispam
├── __init__.py
├── captcha
│ ├── __init__.py
│ ├── templatetags
│ │ ├── __init__.py
│ │ └── recaptcha.py
│ ├── default_settings.py
│ ├── widgets.py
│ └── forms.py
├── honeypot
│ ├── __init__.py
│ ├── widgets.py
│ └── forms.py
└── akismet
│ ├── __init__.py
│ ├── utils.py
│ ├── client.py
│ └── entities.py
├── tests
├── akismet
│ ├── __init__.py
│ ├── test_client.py
│ ├── test_utils.py
│ └── test_entities.py
├── captcha
│ ├── __init__.py
│ ├── test_templatetags.py
│ ├── test_widgets.py
│ └── test_forms.py
├── honeypot
│ ├── __init__.py
│ ├── test_forms.py
│ └── test_widgets.py
├── __init__.py
└── dj_settings.py
├── docs
├── api
│ ├── modules.rst
│ ├── antispam.rst
│ ├── antispam.captcha.templatetags.rst
│ ├── antispam.honeypot.rst
│ ├── antispam.captcha.rst
│ └── antispam.akismet.rst
├── images
│ ├── akismet.png
│ └── recaptcha.png
├── index.rst
├── Makefile
├── getstarted.rst
├── usage.rst
└── conf.py
├── setup.cfg
├── .gitignore
├── tox.ini
├── .travis.yml
├── LICENSE
├── setup.py
└── README.rst
/antispam/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/antispam/captcha/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/akismet/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/captcha/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/honeypot/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/antispam/honeypot/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/antispam/captcha/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/api/modules.rst:
--------------------------------------------------------------------------------
1 | API
2 | ===
3 |
4 | .. toctree::
5 | :maxdepth: 4
6 |
7 | antispam
8 |
--------------------------------------------------------------------------------
/docs/images/akismet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mixkorshun/django-antispam/HEAD/docs/images/akismet.png
--------------------------------------------------------------------------------
/docs/images/recaptcha.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mixkorshun/django-antispam/HEAD/docs/images/recaptcha.png
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 |
3 | if not settings.configured:
4 | settings.configure()
5 |
--------------------------------------------------------------------------------
/docs/api/antispam.rst:
--------------------------------------------------------------------------------
1 | antispam
2 | ========
3 |
4 | .. toctree::
5 | antispam.akismet
6 | antispam.captcha
7 | antispam.honeypot
8 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bumpversion]
2 | current_version = 1.0.3
3 | commit = True
4 | tag = True
5 | files = setup.py
6 |
7 | [bdist_wheel]
8 | python-tag = py3
9 |
--------------------------------------------------------------------------------
/antispam/akismet/__init__.py:
--------------------------------------------------------------------------------
1 | from .client import check, submit # noqa: F401
2 |
3 | from .entities import Request, Author, Site, Comment # noqa: F401
4 |
--------------------------------------------------------------------------------
/antispam/captcha/default_settings.py:
--------------------------------------------------------------------------------
1 | RECAPTCHA_WIDGET = 'antispam.captcha.widgets.ReCAPTCHA'
2 |
3 | RECAPTCHA_TIMEOUT = 5
4 |
5 | RECAPTCHA_PASS_ON_ERROR = False
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /.tox
3 | /.eggs
4 | /*.egg-info
5 |
6 | /.coverage
7 | /.eggs
8 | /dist
9 | /build
10 | /env
11 | /*.egg-info
12 | /docs/_build
13 |
14 | *.pyc
15 | __pycache__
16 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py34-django2, py37-django{2,3}
3 |
4 | [testenv]
5 | deps =
6 | django2: Django>=2.0,<3.0
7 | django3: Django>=3.0,<4.0
8 | passenv = TOXENV CI TRAVIS TRAVIS_*
9 | commands =
10 | python setup.py test
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: python
3 | cache: pip
4 | python:
5 | - '3.4'
6 | - '3.7'
7 |
8 | before_script:
9 | - pip install -q tox tox-travis
10 | - pip install -q flake8
11 | - pip install -q "codecov>=1.4"
12 |
13 | script:
14 | - flake8 antispam/
15 | - tox
16 |
17 | after_success:
18 | - codecov
19 |
--------------------------------------------------------------------------------
/docs/api/antispam.captcha.templatetags.rst:
--------------------------------------------------------------------------------
1 | antispam\.captcha\.templatetags
2 | ===============================
3 |
4 | antispam\.captcha\.templatetags\.recaptcha
5 | ------------------------------------------
6 |
7 | .. automodule:: antispam.captcha.templatetags.recaptcha
8 | :members:
9 | :undoc-members:
10 | :show-inheritance:
11 |
--------------------------------------------------------------------------------
/tests/captcha/test_templatetags.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from antispam.captcha.templatetags.recaptcha import recaptcha_init
4 |
5 |
6 | class ReCAPTCHAScriptTests(TestCase):
7 | def test_script(self):
8 | script = recaptcha_init()
9 |
10 | self.assertIn('' % url
16 | )
17 |
--------------------------------------------------------------------------------
/docs/api/antispam.captcha.rst:
--------------------------------------------------------------------------------
1 | antispam\.captcha
2 | =================
3 |
4 | .. toctree::
5 | antispam.captcha.templatetags
6 |
7 | antispam\.captcha\.default\_settings
8 | ------------------------------------
9 |
10 | antispam\.captcha\.forms
11 | ------------------------
12 |
13 | .. automodule:: antispam.captcha.forms
14 | :members:
15 | :undoc-members:
16 | :show-inheritance:
17 |
18 | antispam\.captcha\.widgets
19 | --------------------------
20 |
21 | .. automodule:: antispam.captcha.widgets
22 | :members:
23 | :undoc-members:
24 | :show-inheritance:
25 |
--------------------------------------------------------------------------------
/antispam/akismet/utils.py:
--------------------------------------------------------------------------------
1 | def get_client_ip(request):
2 | """
3 | Get client ip address.
4 |
5 | Detect ip address provided by HTTP_X_REAL_IP, HTTP_X_FORWARDED_FOR
6 | and REMOTE_ADDR meta headers.
7 |
8 | :param request: django request
9 | :return: ip address
10 | """
11 |
12 | real_ip = request.META.get('HTTP_X_REAL_IP')
13 | if real_ip:
14 | return real_ip
15 |
16 | x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
17 | if x_forwarded_for:
18 | return x_forwarded_for.split(',')[0]
19 |
20 | return request.META.get('REMOTE_ADDR')
21 |
--------------------------------------------------------------------------------
/docs/api/antispam.akismet.rst:
--------------------------------------------------------------------------------
1 | antispam\.akismet
2 | =================
3 |
4 | antispam\.akismet\.client
5 | -------------------------
6 |
7 | .. automodule:: antispam.akismet.client
8 | :members:
9 | :undoc-members:
10 | :show-inheritance:
11 |
12 | antispam\.akismet\.entities
13 | ---------------------------
14 |
15 | .. automodule:: antispam.akismet.entities
16 | :members:
17 | :undoc-members:
18 | :show-inheritance:
19 |
20 | antispam\.akismet\.utils
21 | ------------------------
22 |
23 | .. automodule:: antispam.akismet.utils
24 | :members:
25 | :undoc-members:
26 | :show-inheritance:
27 |
--------------------------------------------------------------------------------
/tests/honeypot/test_forms.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from django.core.exceptions import ValidationError
4 |
5 | from antispam.honeypot.forms import HoneypotField
6 |
7 |
8 | class HoneypotFieldTests(TestCase):
9 | def test_not_required(self):
10 | self.assertFalse(HoneypotField().required)
11 |
12 | with self.assertRaises(AssertionError):
13 | HoneypotField(required=False)
14 |
15 | def test_validate(self):
16 | field = HoneypotField()
17 |
18 | field.validate('')
19 |
20 | with self.assertRaises(ValidationError):
21 | field.validate('v')
22 |
--------------------------------------------------------------------------------
/antispam/honeypot/widgets.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.utils.safestring import mark_safe
3 |
4 |
5 | class HoneypotInput(forms.TextInput):
6 | """
7 | Default honeypot field widget.
8 |
9 | Display text input in hidden div.
10 | """
11 |
12 | @property
13 | def is_hidden(self):
14 | return True
15 |
16 | def render(self, *args, **kwargs):
17 | """
18 | Returns this widget rendered as HTML.
19 | """
20 | return mark_safe(
21 | '
%s
' % str(
22 | super(HoneypotInput, self).render(*args, **kwargs))
23 | )
24 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = python -msphinx
7 | SPHINXPROJ = django-antispam
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
--------------------------------------------------------------------------------
/docs/getstarted.rst:
--------------------------------------------------------------------------------
1 | .. _getstarted:
2 |
3 | Get started
4 | ===========
5 |
6 | The package can be installed using::
7 |
8 | pip install django-antispam
9 |
10 | Add the following settings::
11 |
12 | INSTALLED_APPS += [
13 | 'antispam',
14 |
15 | # 'antispam.akismet',
16 | # 'antispam.honeypot',
17 | # 'antispam.captcha',
18 | ]
19 |
20 | # Akismet protection configuration (optional)
21 |
22 | AKISMET_API_KEY = ''
23 |
24 | AKISMET_SITE_URL = ''
25 |
26 | AKISMET_TEST_MODE = False
27 |
28 | # reCAPTCHA default configuration (optional)
29 |
30 | RECAPTCHA_SITEKEY = 'sitekey'
31 |
32 | RECAPTCHA_SECRETKEY = 'secretkey'
33 |
34 | RECAPTCHA_WIDGET = 'antispam.captcha.widgets.ReCAPTCHA'
35 |
36 | RECAPTCHA_TIMEOUT = 5
37 |
38 | RECAPTCHA_PASS_ON_ERROR = False
--------------------------------------------------------------------------------
/tests/dj_settings.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | SECRET_KEY = 'secret'
4 |
5 | DIRNAME = os.path.dirname(__file__)
6 |
7 | DEBUG = True
8 | DATABASES = {
9 | 'default': {
10 | 'ENGINE': 'django.db.backends.sqlite3',
11 | 'NAME': 'mydatabase'
12 | }
13 | }
14 |
15 | INSTALLED_APPS = (
16 | 'django.contrib.contenttypes',
17 | )
18 |
19 | STATIC_URL = '/static/'
20 |
21 | MIDDLEWARE = [
22 | 'django.middleware.common.CommonMiddleware',
23 | ]
24 |
25 | TEMPLATES = [
26 | {
27 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
28 | 'DIRS': [],
29 | 'APP_DIRS': True,
30 | 'OPTIONS': {
31 | 'context_processors': [
32 | 'django.template.context_processors.debug',
33 | 'django.template.context_processors.request',
34 | ],
35 | },
36 | },
37 | ]
38 |
--------------------------------------------------------------------------------
/tests/honeypot/test_widgets.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from django.utils.safestring import SafeData
4 | from mock import Mock
5 |
6 | from antispam.honeypot.widgets import HoneypotInput
7 |
8 |
9 | class HoneypotInputTests(TestCase):
10 | def setUp(self):
11 | self.widget = HoneypotInput()
12 |
13 | # patch for support both django versions - 1.11 and 1.10
14 | self.widget._render = Mock(
15 | return_value=''
16 | )
17 |
18 | def test_markup(self):
19 | html = self.widget.render('honeypot_field', '')
20 | html = str(html)
21 |
22 | self.assertIn('display: none;', html)
23 |
24 | def test_render_safe_html(self):
25 | html = self.widget.render('honeypot_field', '')
26 | self.assertIsInstance(html, SafeData)
27 |
28 | def test_is_hidden(self):
29 | self.assertTrue(self.widget.is_hidden)
30 |
--------------------------------------------------------------------------------
/tests/akismet/test_client.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from django.conf import settings
4 | from mock import Mock
5 |
6 | from antispam.akismet import client
7 | from antispam.akismet.entities import Request, Comment
8 |
9 |
10 | class ClientTests(TestCase):
11 | def setUp(self):
12 | self.get_connection_backup = client.get_connection
13 |
14 | self.connection = Mock()
15 | client.get_connection = Mock(return_value=self.connection)
16 | settings.AKISMET_API_KEY = 'api-key'
17 |
18 | def test_get_connection(self):
19 | client.get_connection = self.get_connection_backup
20 | del client.settings.AKISMET_API_KEY
21 |
22 | def test_submit(self):
23 | client.submit(Request(), comment=Comment('my comment'), is_spam=True)
24 |
25 | self.assertTrue(self.connection.submit.called)
26 |
27 | def test_check(self):
28 | client.check(Request(), comment=Comment('my comment'))
29 |
30 | self.assertTrue(self.connection.check.called)
31 |
--------------------------------------------------------------------------------
/tests/captcha/test_widgets.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from django.utils.safestring import SafeData
4 |
5 | from antispam.captcha.widgets import ReCAPTCHA, InvisibleReCAPTCHA
6 |
7 |
8 | class ReCAPTCHATests(TestCase):
9 | def setUp(self):
10 | self.widget = ReCAPTCHA(sitekey='mysitekey')
11 |
12 | def test_render_return_html(self):
13 | html = self.widget.render('captcha', '1234')
14 |
15 | self.assertIsInstance(html, SafeData)
16 |
17 | def test_render(self):
18 | html = self.widget.render('captcha', '1234')
19 |
20 | self.assertIn('g-recaptcha', html)
21 |
22 | def test_get_value_from_datadict(self):
23 | value = self.widget.value_from_datadict({
24 | 'g-recaptcha-response': 'my-response'
25 | }, {}, 'recaptcha')
26 |
27 | self.assertEqual('my-response', value)
28 |
29 |
30 | class InvisibleReCAPTCHATests(ReCAPTCHATests):
31 | def setUp(self):
32 | self.widget = InvisibleReCAPTCHA(sitekey='mysitekey')
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Vladislav Bakin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/tests/akismet/test_utils.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from mock import Mock
4 |
5 | from antispam.akismet.utils import get_client_ip
6 |
7 |
8 | class GetClientIpAddressTests(TestCase):
9 | def setUp(self):
10 | self.request = Mock(
11 | META={}
12 | )
13 |
14 | def test_ip_by_remote_addr(self):
15 | self.request.META['REMOTE_ADDR'] = '121.0.0.1'
16 |
17 | self.assertEqual('121.0.0.1', get_client_ip(self.request))
18 |
19 | def test_ip_by_x_forwarded_for(self):
20 | self.request.META['REMOTE_ADDR'] = '121.0.0.1'
21 | self.request.META['HTTP_X_FORWARDED_FOR'] = '122.0.0.1,'
22 |
23 | self.assertEqual('122.0.0.1', get_client_ip(self.request))
24 |
25 | def test_ip_by_real_ip(self):
26 | self.request.META['REMOTE_ADDR'] = '121.0.0.1'
27 | self.request.META['HTTP_X_FORWARDED_FOR'] = '122.0.0.1,'
28 | self.request.META['HTTP_X_REAL_IP'] = '123.0.0.1'
29 |
30 | self.assertEqual('123.0.0.1', get_client_ip(self.request))
31 |
32 | def test_no_ip_address(self):
33 | self.assertIsNone(get_client_ip(self.request))
34 |
--------------------------------------------------------------------------------
/antispam/honeypot/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.core.exceptions import ValidationError
3 | from django.utils.translation import ugettext_lazy as _
4 |
5 | from .widgets import HoneypotInput
6 |
7 |
8 | class HoneypotField(forms.CharField):
9 | """
10 | Honeypot form field.
11 | """
12 | default_error_messages = {
13 | 'invalid': _('Enter a number.'),
14 | 'honeypot': _('Invalid value for honey pot field.'),
15 | }
16 |
17 | def __init__(self, **kwargs):
18 | assert 'required' not in kwargs
19 | kwargs['required'] = False
20 |
21 | kwargs.setdefault('max_length', 255)
22 | kwargs.setdefault('widget', HoneypotInput)
23 |
24 | super(HoneypotField, self).__init__(**kwargs)
25 |
26 | def validate(self, value):
27 | """
28 | Validates form field value entered by user.
29 |
30 | :param value: user-input
31 | :raise: ValidationError with code="spam-protection"
32 | if honeypot check failed.
33 | """
34 | super(HoneypotField, self).validate(value)
35 |
36 | if value:
37 | raise ValidationError(self.error_messages['honeypot'],
38 | code='spam-protection')
39 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | setup(
4 | name='django-antispam',
5 | version='1.0.3',
6 | url='https://github.com/mixkorshun/django-antispam',
7 | description='Anti-spam protection tools for django applications.',
8 | keywords=['anti-spam', 'antispam', 'spam'],
9 |
10 | long_description=open('README.rst', 'r').read(),
11 |
12 | author='Vladislav Bakin',
13 | author_email='mixkorshun@gmail.com',
14 | maintainer='Vladislav Bakin',
15 | maintainer_email='mixkorshun@gmail.com',
16 |
17 | license='MIT',
18 |
19 | install_requires=[
20 | 'django',
21 |
22 | 'python-akismet',
23 | ],
24 |
25 | tests_require=[
26 | 'mock~=3.0',
27 | ],
28 |
29 | packages=find_packages(exclude=['tests.*', 'tests']),
30 |
31 | test_suite='tests',
32 |
33 | classifiers=[
34 | 'Development Status :: 5 - Production/Stable',
35 | 'Intended Audience :: Developers',
36 | 'Environment :: Web Environment',
37 | 'Framework :: Django',
38 | 'Natural Language :: English',
39 | 'License :: OSI Approved :: MIT License',
40 | 'Programming Language :: Python',
41 | 'Programming Language :: Python :: 3',
42 | 'Programming Language :: Python :: 3.4',
43 | 'Programming Language :: Python :: 3.5',
44 | 'Programming Language :: Python :: 3.6',
45 | 'Programming Language :: Python :: 3.7',
46 | ],
47 | )
48 |
--------------------------------------------------------------------------------
/antispam/captcha/widgets.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.utils.safestring import mark_safe
3 |
4 |
5 | class ReCAPTCHA(forms.Widget):
6 | """
7 | Default reCAPTCHA v2 widget.
8 | (with "I'm not robot" checkbox)
9 | """
10 |
11 | def __init__(self, sitekey):
12 | """
13 | :param sitekey: site key (public key)
14 | """
15 | super(ReCAPTCHA, self).__init__()
16 |
17 | self.sitekey = sitekey
18 |
19 | def render(self, name, value, *args, **kwargs):
20 | """
21 | Returns this widget rendered as HTML.
22 | """
23 | return mark_safe(
24 | '' % {
25 | 'sitekey': self.sitekey
26 | })
27 |
28 | def value_from_datadict(self, data, files, name):
29 | """
30 | Given a dictionary of data and this widget's name, returns the value
31 | of this widget. Returns None if it's not provided.
32 | """
33 | return data.get('g-recaptcha-response', None)
34 |
35 |
36 | class InvisibleReCAPTCHA(ReCAPTCHA):
37 | """
38 | Invisible reCAPTCHA widget.
39 | """
40 |
41 | def render(self, name, value, *args, **kwargs):
42 | """
43 | Returns this widget rendered as HTML.
44 | """
45 |
46 | return mark_safe(
47 | '' % {
49 | 'name': name,
50 | 'sitekey': self.sitekey
51 | }
52 | )
53 |
--------------------------------------------------------------------------------
/antispam/akismet/client.py:
--------------------------------------------------------------------------------
1 | from akismet import Akismet
2 | from django.conf import settings
3 |
4 |
5 | def get_connection(api_key=None, blog=None, is_test=None):
6 | """
7 | Get Akismet client object.
8 |
9 | If no connection params provided, use params from django project settings.
10 |
11 | :param api_key: Akismet API key
12 | :param blog: blog base url
13 | :param is_test: test mode
14 | :rtype: akismet.Akismet
15 | :return: akismet client
16 | """
17 |
18 | if is_test is None:
19 | is_test = getattr(settings, 'AKISMET_TEST_MODE', False)
20 |
21 | return Akismet(
22 | api_key=api_key or getattr(settings, 'AKISMET_API_KEY'),
23 | blog=blog or getattr(settings, 'AKISMET_SITE_URL', None),
24 | is_test=is_test,
25 | )
26 |
27 |
28 | def check(request, comment):
29 | """
30 | Checks given comment to spam by Akismet.
31 |
32 | :type request: antispam.akismet.Request
33 | :type comment: antispam.akismet.Comment
34 | :return: True if comment is spam, otherwise False
35 | """
36 | params = {}
37 | params.update(request.as_params())
38 | params.update(comment.as_params())
39 |
40 | client = get_connection()
41 | return client.check(**params)
42 |
43 |
44 | def submit(request, comment, is_spam):
45 | """
46 | Submit given comment to Akismet.
47 |
48 | Information about the comment status must be provided (spam/not spam).
49 |
50 | :type request: antispam.akismet.Request
51 | :type comment: antispam.akismet.Comment
52 | :type is_spam: bool
53 | """
54 |
55 | params = {}
56 | params.update(request.as_params())
57 | params.update(comment.as_params())
58 |
59 | connection = get_connection()
60 | connection.submit(is_spam=is_spam, **params)
61 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | django-antispam
2 | ===============
3 |
4 | .. image:: https://travis-ci.org/mixkorshun/django-antispam.svg?branch=master
5 | :alt: build status
6 | :target: https://travis-ci.org/mixkorshun/django-antispam
7 | .. image:: https://codecov.io/gh/mixkorshun/django-antispam/branch/master/graph/badge.svg
8 | :alt: codecov
9 | :target: https://codecov.io/gh/mixkorshun/django-antispam
10 | .. image:: https://badge.fury.io/py/django-antispam.svg
11 | :alt: pypi
12 | :target: https://pypi.python.org/pypi/django-antispam
13 | .. image:: https://img.shields.io/badge/code%20style-pep8-orange.svg
14 | :alt: pep8
15 | :target: https://www.python.org/dev/peps/pep-0008/
16 | .. image:: https://img.shields.io/badge/License-MIT-yellow.svg
17 | :alt: MIT
18 | :target: https://opensource.org/licenses/MIT
19 |
20 | Various anti-spam protection tools for django applications.
21 |
22 | See the documentation_ for more details.
23 |
24 | Installation
25 | ------------
26 |
27 | The package can be installed using::
28 |
29 | pip install django-antispam
30 |
31 | Add the following settings::
32 |
33 | INSTALLED_APPS += (
34 | 'antispam',
35 |
36 | # 'antispam.akismet',
37 | # 'antispam.honeypot',
38 | # 'antispam.captcha',
39 | )
40 |
41 | # Akismet protection configuration (optional)
42 |
43 | AKISMET_API_KEY = ''
44 |
45 | AKISMET_SITE_URL = ''
46 |
47 | AKISMET_TEST_MODE = False
48 |
49 | # reCAPTCHA default configuration (optional)
50 |
51 | RECAPTCHA_SITEKEY = 'sitekey'
52 |
53 | RECAPTCHA_SECRETKEY = 'secretkey'
54 |
55 | RECAPTCHA_WIDGET = 'antispam.captcha.widgets.ReCAPTCHA'
56 |
57 | RECAPTCHA_TIMEOUT = 5
58 |
59 | RECAPTCHA_PASS_ON_ERROR = False
60 |
61 |
62 | Contributing
63 | ------------
64 |
65 | If you have any valuable contribution, suggestion or idea,
66 | please let us know as well because we will look into it.
67 |
68 | Pull requests are welcome too.
69 |
70 |
71 | .. _documentation: https://django-antispam.readthedocs.io/
72 |
--------------------------------------------------------------------------------
/tests/akismet/test_entities.py:
--------------------------------------------------------------------------------
1 | import time
2 | from unittest import TestCase
3 |
4 | from mock import Mock
5 |
6 | from antispam.akismet.entities import Request, Author, Site, Comment
7 |
8 |
9 | class RequestTests(TestCase):
10 | def test_to_params(self):
11 | req = Request('127.0.0.2', 'python/tests', 'https://localmachine')
12 |
13 | self.assertEqual({
14 | 'user_ip': '127.0.0.2',
15 | 'user_agent': 'python/tests',
16 | 'referrer': 'https://localmachine',
17 | }, req.as_params())
18 |
19 | def test_from_django_request(self):
20 | req = Request.from_django_request(Mock(META={
21 | 'REMOTE_ADDR': '127.0.0.2',
22 | 'HTTP_USER_AGENT': 'python/tests',
23 | 'HTTP_REFERRER': 'referrer',
24 | }))
25 |
26 | self.assertEqual('127.0.0.2', req.ip_address)
27 | self.assertEqual('python/tests', req.user_agent)
28 | self.assertEqual('referrer', req.referrer)
29 |
30 |
31 | class AuthorTests(TestCase):
32 | def test_to_params(self):
33 | author = Author('Mike', 'mike@mail.loc', 'http://mike.example.com',
34 | role='moderator')
35 |
36 | self.assertEqual({
37 | 'comment_author': 'Mike',
38 | 'comment_author_email': 'mike@mail.loc',
39 | 'comment_author_url': 'http://mike.example.com',
40 | 'user_role': 'moderator',
41 | }, author.as_params())
42 |
43 | def test_from_django_user(self):
44 | author = Author.from_django_user(Mock(
45 | get_full_name=Mock(return_value='Mike Hoff'),
46 | email='mike@mail.loc',
47 | is_staff=True
48 | ))
49 |
50 | self.assertEqual('Mike Hoff', author.name)
51 | self.assertEqual('mike@mail.loc', author.email)
52 | self.assertEqual(None, author.url)
53 | self.assertEqual('administrator', author.role)
54 |
55 |
56 | class SiteTests(TestCase):
57 | def test_to_params(self):
58 | site = Site('http://mike.example.com/', language_code='it')
59 |
60 | self.assertEqual({
61 | 'blog': 'http://mike.example.com/',
62 | 'blog_lang': 'it',
63 | }, site.as_params())
64 |
65 |
66 | class CommentTests(TestCase):
67 | def test_to_params(self):
68 | comment = Comment('', type='comment',
69 | permalink='http://mike.example.com/comment-1/')
70 |
71 | self.assertEqual({
72 | 'comment_content': '',
73 | 'comment_date': comment.created,
74 | 'comment_type': 'comment',
75 | 'permalink': 'http://mike.example.com/comment-1/',
76 | }, comment.as_params())
77 |
78 | def test_to_params_related_resources(self):
79 | author = Author('Mike', 'mike@mail.loc', 'http://mike.example.com',
80 | role='moderator')
81 | site = Site('http://mike.example.com/', language_code='it')
82 |
83 | comment = Comment('', author=author, site=site)
84 |
85 | params = comment.as_params()
86 |
87 | params = set(params.keys())
88 |
89 | author_params = set(author.as_params().keys())
90 | site_params = set(site.as_params().keys())
91 |
92 | self.assertTrue(author_params.issubset(params),
93 | 'all author params should be in comment params')
94 | self.assertTrue(site_params.issubset(params),
95 | 'all site params should be in comment params')
96 |
--------------------------------------------------------------------------------
/antispam/captcha/forms.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from django import forms
3 | from django.conf import settings
4 | from django.core.exceptions import ValidationError
5 | from django.utils.module_loading import import_string
6 | from django.utils.translation import ugettext_lazy as _
7 |
8 | from . import default_settings
9 |
10 |
11 | class ReCAPTCHA(forms.Field):
12 | """
13 | reCAPTCHA form field.
14 |
15 | Only one field can be used on page.
16 | """
17 | default_error_messages = {
18 | 'connection-error': _('Connection to reCAPTCHA server failed.'),
19 | 'invalid': _('reCAPTCHA invalid or expired. Please try again'),
20 | 'bad-request': _(
21 | 'reCAPTCHA cannot be checked due configuration problem.'),
22 | }
23 |
24 | def __init__(self, sitekey=None, secretkey=None, timeout=None,
25 | pass_on_error=None, **kwargs):
26 | """
27 | :param sitekey: site key (public)
28 | :param secretkey: secret key (private)
29 | :param timeout: connection to recaptcha service timeout
30 | :param pass_on_error: do not raise exception
31 | if recaptcha service is not working.
32 | """
33 | self.sitekey = sitekey or getattr(settings, 'RECAPTCHA_SITEKEY')
34 | self.secretkey = secretkey or getattr(settings, 'RECAPTCHA_SECRETKEY')
35 |
36 | if timeout is None:
37 | timeout = getattr(settings, 'RECAPTCHA_TIMEOUT',
38 | default_settings.RECAPTCHA_TIMEOUT)
39 | self.timeout = timeout
40 |
41 | if pass_on_error is None:
42 | pass_on_error = getattr(settings, 'RECAPTCHA_PASS_ON_ERROR',
43 | default_settings.RECAPTCHA_PASS_ON_ERROR)
44 | self.pass_on_error = pass_on_error
45 |
46 | if 'widget' not in kwargs:
47 | recaptcha_widget = import_string(
48 | getattr(settings, 'RECAPTCHA_WIDGET',
49 | default_settings.RECAPTCHA_WIDGET))
50 | kwargs['widget'] = recaptcha_widget(sitekey=self.sitekey)
51 | elif isinstance(kwargs['widget'], type):
52 | kwargs['widget'] = kwargs['widget'](sitekey=self.sitekey)
53 |
54 | super(ReCAPTCHA, self).__init__(**kwargs)
55 |
56 | def validate(self, value):
57 | """
58 | Validate reCAPTCHA value.
59 |
60 | :raise ValidationError with code="captcha-error"
61 | if reCAPTCHA service is unavailable or working incorrectly.
62 | :raise ValidationError with code="captcha-invalid"
63 | if reCAPTCHA validation failed.
64 | """
65 | super(ReCAPTCHA, self).validate(value)
66 |
67 | try:
68 | resp = requests.post(
69 | 'https://www.google.com/recaptcha/api/siteverify', {
70 | 'secret': self.secretkey,
71 | 'response': value
72 | }, timeout=self.timeout)
73 |
74 | resp.raise_for_status()
75 | except IOError:
76 | if self.pass_on_error:
77 | return
78 |
79 | raise ValidationError(self.error_messages['connection-error'],
80 | code='captcha-error')
81 |
82 | resp = resp.json()
83 |
84 | if not resp['success']:
85 | if 'missing-input-response' in resp['error-codes'] \
86 | or 'invalid-input-response' in resp['error-codes']:
87 | raise ValidationError(self.error_messages['invalid'],
88 | code='captcha-invalid')
89 | else:
90 | if self.pass_on_error:
91 | return
92 |
93 | raise ValidationError(self.error_messages['bad-request'],
94 | code='captcha-error')
95 |
--------------------------------------------------------------------------------
/docs/usage.rst:
--------------------------------------------------------------------------------
1 | .. _usage:
2 |
3 | Usage
4 | =====
5 |
6 | Honeypot field
7 | --------------
8 |
9 | Honeypot is a spam protection technique to detect and block automatic spam spiders on your website.
10 | Also known as **spamtrap** technique.
11 |
12 | .. seealso:: You can read more about this technique at `Wikipedia `_.
13 |
14 | Form protection is very simple, just add a ``HoneypotField`` to your form:
15 |
16 | .. code-block:: python
17 |
18 | from django import forms
19 | from antispam.honeypot.forms import HoneypotField
20 |
21 | class MyForm(forms.Form):
22 | name = forms.CharField()
23 | spam_honeypot_field = HoneypotField()
24 |
25 | HoneypotField uses standard form validation behaviour.
26 | If spam submit was detected - ``ValidationError`` with ``spam-protection`` code will be raised.
27 |
28 |
29 | Akismet
30 | -------
31 | .. image:: images/akismet.png
32 | :align: right
33 | :alt: Akismet logo
34 | :width: 250
35 |
36 | Akismet is an advanced hosted anti-spam service aimed at thwarting the underbelly of the web.
37 | It efficiently processes and analyzes masses of data from millions of sites and communities in real time.
38 |
39 | .. seealso:: You can read more at Akismet `official website `__.
40 |
41 | Use Akismet protection for your project:
42 |
43 | .. code-block:: python
44 |
45 | from django import forms
46 | from antispam import akismet
47 |
48 | class MyForm(forms.Form):
49 | name = forms.CharField()
50 | email = forms.EmailField()
51 |
52 | comment = forms.TextField()
53 |
54 | def __init__(self, **kwargs):
55 | self.request = kwargs.pop('request', None)
56 |
57 | super().__init__(**kwargs)
58 |
59 | def clean():
60 | if self.request and akismet.check(
61 | request=akismet.Request.from_django_request(self.request),
62 | comment=akismet.Comment(
63 | content=self.cleaned_data['comment'],
64 | type='comment',
65 |
66 | author=akismet.Author(
67 | name=self.cleaned_data['name'],
68 | email=self.cleaned_data['email']
69 | )
70 | )
71 | ):
72 | raise ValidationError('Spam detected', code='spam-protection')
73 |
74 | return super().clean()
75 |
76 |
77 | CAPTCHA
78 | -------
79 |
80 | CAPTCHA – Completely Automated Public Turing test to tell Computers and Humans Apart. We support CAPTCHA implementation
81 | called **reCAPTCHA V2**.
82 |
83 | reCAPTCHA V2
84 | ~~~~~~~~~~~~
85 |
86 | .. image:: images/recaptcha.png
87 | :align: right
88 | :alt: reCAPTCHA v2
89 | :width: 250
90 |
91 | **reCAPTCHA** is a free service that protects your website from spam and abuse. reCAPTCHA uses an advanced risk analysis engine
92 | and adaptive CAPTCHAs to keep automated software from engaging in abusive activities on your site.
93 |
94 | .. seealso:: You can read more at google reCAPTCHA `official website `__.
95 |
96 | To use reCAPTCHA protection in your project form, obtain keys `from Google `, add them to ``SETTINGS``:
97 |
98 | .. code-block:: python
99 |
100 | RECAPTCHA_SITEKEY = 'sitekey'
101 |
102 | RECAPTCHA_SECRETKEY = 'secretkey'
103 |
104 | and add ``ReCAPTCHA`` field:
105 |
106 | .. code-block:: python
107 |
108 | from django import forms
109 | from antispam.captcha.forms import ReCAPTCHA
110 |
111 | class MyForm(forms.Form):
112 | name = forms.CharField()
113 |
114 | captcha = ReCAPTCHA()
115 |
116 | **django-antispam** package provides 2 widgets of reCAPTCHA:
117 | * ``antispam.captcha.widgets.ReCAPTCHA`` - default reCAPTCHA v2 widget
118 | * ``antispam.captcha.widgets.InvisibleReCAPTCHA`` - invisible reCAPTCHA widget
119 |
120 | To display reCAPTCHA on website page, you should add reCAPTCHA script into the template:
121 |
122 | .. code-block:: django
123 |
124 | {% load recaptcha %}
125 |
126 | {% block head %}
127 | ...
128 |
129 | {% recaptcha_init %}
130 | {% endblock %}
131 |
--------------------------------------------------------------------------------
/tests/captcha/test_forms.py:
--------------------------------------------------------------------------------
1 | import json
2 | from unittest import TestCase
3 |
4 | from django.core.exceptions import ValidationError
5 | from mock import patch
6 | from requests import ConnectionError, Response
7 |
8 | from antispam.captcha import widgets
9 | from antispam.captcha.forms import ReCAPTCHA
10 |
11 |
12 | class ReCAPTCHATests(TestCase):
13 | def setUp(self):
14 | self.field = ReCAPTCHA(
15 | sitekey='my-sitekey',
16 | secretkey='my-secretkey',
17 | timeout=3,
18 | pass_on_error=False,
19 | widget=widgets.ReCAPTCHA,
20 | )
21 |
22 | def _get_response(self, json_data):
23 | resp = Response()
24 | resp.status_code = 200
25 | resp._content = json.dumps(json_data).encode('utf-8')
26 | return resp
27 |
28 | @patch('antispam.captcha.forms.settings')
29 | def test_field_defaults(self, settings):
30 | settings.RECAPTCHA_SITEKEY = 'sitekey-from-settings'
31 | settings.RECAPTCHA_SECRETKEY = 'secretkey-from-settings'
32 | settings.RECAPTCHA_WIDGET = 'antispam.captcha.widgets.InvisibleReCAPTCHA'
33 | settings.RECAPTCHA_TIMEOUT = 15
34 | settings.RECAPTCHA_PASS_ON_ERROR = True
35 |
36 | field = ReCAPTCHA()
37 |
38 | self.assertEqual('sitekey-from-settings', field.sitekey)
39 | self.assertEqual('secretkey-from-settings', field.secretkey)
40 |
41 | self.assertIsInstance(field.widget, widgets.InvisibleReCAPTCHA)
42 | self.assertEqual(field.sitekey, field.widget.sitekey)
43 |
44 | self.assertEqual(15, field.timeout)
45 | self.assertEqual(True, field.pass_on_error)
46 |
47 | @patch('antispam.captcha.forms.requests')
48 | def test_recaptcha_server_connection_error(self, requests):
49 | requests.post.side_effect = ConnectionError()
50 |
51 | with self.assertRaises(ValidationError) as e:
52 | self.field.validate('1234')
53 |
54 | self.assertEqual('captcha-error', e.exception.code)
55 |
56 | @patch('antispam.captcha.forms.requests')
57 | def test_recaptcha_internal_server_error(self, requests):
58 | resp = self._get_response({})
59 | resp.status_code = 500
60 | requests.post.return_value = resp
61 |
62 | with self.assertRaises(ValidationError) as e:
63 | self.field.validate('1234')
64 |
65 | self.assertEqual('captcha-error', e.exception.code)
66 |
67 | @patch('antispam.captcha.forms.requests')
68 | def test_recaptcha_validation_ok(self, requests):
69 | requests.post.return_value = self._get_response({'success': True})
70 |
71 | self.field.validate('1234')
72 | self.assertTrue(True)
73 |
74 | @patch('antispam.captcha.forms.requests')
75 | def test_recaptcha_validation_failed(self, requests):
76 | requests.post.return_value = self._get_response(
77 | {'success': False, 'error-codes': ['invalid-input-response']})
78 |
79 | with self.assertRaises(ValidationError) as e:
80 | self.field.validate('1234')
81 |
82 | self.assertEqual('captcha-invalid', e.exception.code)
83 |
84 | @patch('antispam.captcha.forms.requests')
85 | def test_recaptcha_validation_unexpected_error(self, requests):
86 | requests.post.return_value = self._get_response(
87 | {'success': False, 'error-codes': ['bad-request']})
88 |
89 | with self.assertRaises(ValidationError) as e:
90 | self.field.validate('1234')
91 |
92 | self.assertEqual('captcha-error', e.exception.code)
93 |
94 | @patch('antispam.captcha.forms.requests')
95 | def test_recaptcha_pass_on_error_request_error(self, requests):
96 | self.field.pass_on_error = True
97 |
98 | requests.post.side_effect = ConnectionError()
99 |
100 | self.field.validate('1234')
101 | self.assertTrue(True)
102 |
103 | @patch('antispam.captcha.forms.requests')
104 | def test_recaptcha_pass_on_error_unexpected_error(self, requests):
105 | self.field.pass_on_error = True
106 |
107 | requests.post.return_value = self._get_response(
108 | {'success': False, 'error-codes': ['bad-request']})
109 |
110 | self.field.validate('1234')
111 | self.assertTrue(True)
112 |
--------------------------------------------------------------------------------
/antispam/akismet/entities.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from .utils import get_client_ip
4 |
5 |
6 | class Request:
7 | """
8 | Akismet request.
9 | Contains request specific data.
10 | """
11 |
12 | @classmethod
13 | def from_django_request(cls, request):
14 | """
15 | Create Akismet request from django HttpRequest.
16 |
17 | :type request: django.http.HttpRequest
18 | """
19 | return cls(
20 | ip_address=get_client_ip(request),
21 | user_agent=request.META.get('HTTP_USER_AGENT', ''),
22 | referrer=request.META.get('HTTP_REFERRER', ''),
23 | )
24 |
25 | def __init__(self, ip_address=None, user_agent=None, referrer=None):
26 | """
27 | :param ip_address: request ip address
28 | :param user_agent: request user agent
29 | :param referrer: request HTTP_REFERER meta
30 | """
31 | self.ip_address = ip_address
32 | self.user_agent = user_agent
33 | self.referrer = referrer
34 |
35 | def as_params(self):
36 | """
37 | Converts object to Akismet request params.
38 |
39 | :rtype dict
40 | """
41 | return {
42 | 'user_ip': self.ip_address,
43 | 'user_agent': self.user_agent,
44 | 'referrer': self.referrer,
45 | }
46 |
47 |
48 | class Author:
49 | """
50 | Akismet author.
51 |
52 | Contains author specific data.
53 | """
54 |
55 | @classmethod
56 | def from_django_user(cls, user):
57 | """
58 | Create Akismet author from django user.
59 |
60 | :type user: django.contrib.auth.models.User
61 | """
62 |
63 | return cls(
64 | name=user.get_full_name(),
65 | email=user.email,
66 | role='administrator' if user.is_staff else None
67 | )
68 |
69 | def __init__(self, name=None, email=None, url=None, role=None):
70 | """
71 | :param name: user full name
72 | :param email: user email
73 | :param url: user website (url)
74 | :param role: user role, if administrator then Akismet
75 | should not check it for spam.
76 | """
77 | self.name = name
78 | self.email = email
79 | self.role = role
80 | self.url = url
81 |
82 | def as_params(self):
83 | """
84 | Converts object to Akismet request params.
85 |
86 | :rtype dict
87 | """
88 |
89 | return {
90 | 'comment_author': self.name,
91 | 'comment_author_email': self.email,
92 | 'comment_author_url': self.url,
93 | 'user_role': self.role,
94 | }
95 |
96 |
97 | class Site:
98 | """
99 | Akismet site (also known as `blog`).
100 | Contains site specific data.
101 | """
102 |
103 | def __init__(self, base_url=None, language_code=None):
104 | self.base_url = base_url
105 | self.language_code = language_code
106 |
107 | def as_params(self):
108 | """
109 | Converts object to Akismet request params.
110 |
111 | :rtype dict
112 | """
113 |
114 | return {
115 | 'blog': self.base_url,
116 | 'blog_lang': self.language_code,
117 | }
118 |
119 |
120 | class Comment:
121 | """
122 | Akismet comment.
123 | Contains comment specific data, including author and site.
124 | """
125 |
126 | def __init__(self, content, type=None, permalink=None, author=None,
127 | site=None):
128 | """
129 | :param content: comment text
130 | :param type: comment type (free form string relevant to comment type,
131 | for example: feedback, post, ...)
132 | :param permalink: link to comment on site
133 | :param author: comment author
134 | :param site: comment site(blog)
135 | """
136 | self.content = content
137 | self.type = type
138 | self.permalink = permalink
139 |
140 | self.author = author
141 | self.site = site
142 |
143 | self.created = datetime.utcnow()
144 |
145 | def as_params(self):
146 | """
147 | Converts object to Akismet request params.
148 |
149 | :rtype dict
150 | """
151 |
152 | params = {
153 | 'comment_type': self.type,
154 | 'comment_content': self.content,
155 | 'comment_date': self.created,
156 | 'permalink': self.permalink,
157 | }
158 |
159 | if self.site:
160 | params.update(self.site.as_params())
161 |
162 | if self.author:
163 | params.update(self.author.as_params())
164 |
165 | return params
166 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # django-antispam documentation build configuration file, created by
5 | # sphinx-quickstart on Mon Jun 12 21:23:00 2017.
6 | #
7 | # This file is execfile()d with the current directory set to its
8 | # containing dir.
9 | #
10 | # Note that not all possible configuration values are present in this
11 | # autogenerated file.
12 | #
13 | # All configuration values have a default; values that are commented out
14 | # serve to show the default.
15 |
16 | # If extensions (or modules to document with autodoc) are in another directory,
17 | # add these directories to sys.path here. If the directory is relative to the
18 | # documentation root, use os.path.abspath to make it absolute, like shown here.
19 | #
20 | import os
21 | import sys
22 | sys.path.insert(0, os.path.abspath('..'))
23 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.dj_settings'
24 |
25 |
26 | # -- General configuration ------------------------------------------------
27 |
28 | # If your documentation needs a minimal Sphinx version, state it here.
29 | #
30 | # needs_sphinx = '1.0'
31 |
32 | # Add any Sphinx extension module names here, as strings. They can be
33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
34 | # ones.
35 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
36 |
37 | # Add any paths that contain templates here, relative to this directory.
38 | templates_path = []
39 |
40 | # The suffix(es) of source filenames.
41 | # You can specify multiple suffix as a list of string:
42 | #
43 | # source_suffix = ['.rst', '.md']
44 | source_suffix = '.rst'
45 |
46 | # The master toctree document.
47 | master_doc = 'index'
48 |
49 | # General information about the project.
50 | project = 'django-antispam'
51 | copyright = '2017, Vladislav Bakin'
52 | author = 'Vladislav Bakin'
53 |
54 | # The version info for the project you're documenting, acts as replacement for
55 | # |version| and |release|, also used in various other places throughout the
56 | # built documents.
57 | #
58 | # The short X.Y version.
59 | version = '0.2.0'
60 | # The full version, including alpha/beta/rc tags.
61 | release = '0.2.0'
62 |
63 | # The language for content autogenerated by Sphinx. Refer to documentation
64 | # for a list of supported languages.
65 | #
66 | # This is also used if you do content translation via gettext catalogs.
67 | # Usually you set "language" from the command line for these cases.
68 | language = None
69 |
70 | # List of patterns, relative to source directory, that match files and
71 | # directories to ignore when looking for source files.
72 | # This patterns also effect to html_static_path and html_extra_path
73 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
74 |
75 | # The name of the Pygments (syntax highlighting) style to use.
76 | pygments_style = 'sphinx'
77 |
78 | # If true, `todo` and `todoList` produce output, else they produce nothing.
79 | todo_include_todos = False
80 |
81 |
82 | # -- Options for HTML output ----------------------------------------------
83 |
84 | # The theme to use for HTML and HTML Help pages. See the documentation for
85 | # a list of builtin themes.
86 | #
87 | html_theme = 'sphinx_rtd_theme'
88 |
89 | # Theme options are theme-specific and customize the look and feel of a theme
90 | # further. For a list of options available for each theme, see the
91 | # documentation.
92 | #
93 | # html_theme_options = {}
94 |
95 | # Add any paths that contain custom static files (such as style sheets) here,
96 | # relative to this directory. They are copied after the builtin static files,
97 | # so a file named "default.css" will overwrite the builtin "default.css".
98 | html_static_path = []
99 |
100 |
101 | # -- Options for HTMLHelp output ------------------------------------------
102 |
103 | # Output file base name for HTML help builder.
104 | htmlhelp_basename = 'django-antispamdoc'
105 |
106 |
107 | # -- Options for LaTeX output ---------------------------------------------
108 |
109 | latex_elements = {
110 | # The paper size ('letterpaper' or 'a4paper').
111 | #
112 | # 'papersize': 'letterpaper',
113 |
114 | # The font size ('10pt', '11pt' or '12pt').
115 | #
116 | # 'pointsize': '10pt',
117 |
118 | # Additional stuff for the LaTeX preamble.
119 | #
120 | # 'preamble': '',
121 |
122 | # Latex figure (float) alignment
123 | #
124 | # 'figure_align': 'htbp',
125 | }
126 |
127 | # Grouping the document tree into LaTeX files. List of tuples
128 | # (source start file, target name, title,
129 | # author, documentclass [howto, manual, or own class]).
130 | latex_documents = [
131 | (master_doc, 'django-antispam.tex', 'django-antispam Documentation',
132 | 'Vladislav Bakin', 'manual'),
133 | ]
134 |
135 |
136 | # -- Options for manual page output ---------------------------------------
137 |
138 | # One entry per manual page. List of tuples
139 | # (source start file, name, description, authors, manual section).
140 | man_pages = [
141 | (master_doc, 'django-antispam', 'django-antispam Documentation',
142 | [author], 1)
143 | ]
144 |
145 |
146 | # -- Options for Texinfo output -------------------------------------------
147 |
148 | # Grouping the document tree into Texinfo files. List of tuples
149 | # (source start file, target name, title, author,
150 | # dir menu entry, description, category)
151 | texinfo_documents = [
152 | (master_doc, 'django-antispam', 'django-antispam Documentation',
153 | author, 'django-antispam', 'One line description of project.',
154 | 'Miscellaneous'),
155 | ]
156 |
157 |
158 |
159 | # -- Options for Epub output ----------------------------------------------
160 |
161 | # Bibliographic Dublin Core info.
162 | epub_title = project
163 | epub_author = author
164 | epub_publisher = author
165 | epub_copyright = copyright
166 |
167 | # The unique identifier of the text. This can be a ISBN number
168 | # or the project homepage.
169 | #
170 | # epub_identifier = ''
171 |
172 | # A unique identification for the text.
173 | #
174 | # epub_uid = ''
175 |
176 | # A list of files that should not be packed into the epub file.
177 | epub_exclude_files = ['search.html']
178 |
179 | # autodoc
180 |
181 | autodoc_mock_imports = ["akismet"]
182 |
--------------------------------------------------------------------------------