├── 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('', script) 12 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to django-antispam's documentation! 2 | =========================================== 3 | 4 | Various anti-spam protection tools for django applications. 5 | 6 | Documentation 7 | ============= 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | 12 | getstarted 13 | usage 14 | api/modules 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | -------------------------------------------------------------------------------- /docs/api/antispam.honeypot.rst: -------------------------------------------------------------------------------- 1 | antispam\.honeypot 2 | ================== 3 | 4 | antispam\.honeypot\.forms 5 | ------------------------- 6 | 7 | .. automodule:: antispam.honeypot.forms 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | antispam\.honeypot\.widgets 13 | --------------------------- 14 | 15 | .. automodule:: antispam.honeypot.widgets 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | -------------------------------------------------------------------------------- /antispam/captcha/templatetags/recaptcha.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.utils.safestring import mark_safe 3 | 4 | register = template.Library() 5 | 6 | 7 | @register.simple_tag 8 | def recaptcha_init(): 9 | """ 10 | Render reCAPTCHA script tag. 11 | """ 12 | url = 'https://www.google.com/recaptcha/api.js' 13 | 14 | return mark_safe( 15 | '' % 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 | --------------------------------------------------------------------------------