diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..508a30c
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,5 @@
+Contributors based on gitlog:
+Joe Jasinski
+Jin Sun
+amatellanes
+Pablo Recio (pablorecio)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ebc8d00
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,31 @@
+Copyright 2014 (c) Imaginary Landscape LLC
+All rights reserved.
+
+Major updates to the project and name change made by Imaginary Landscape LLC.
+All licensing follows the below 3-clause BSD license.
+
+
+Copyright (c) Praekelt Foundation
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of Praekelt Foundation nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL PRAEKELT FOUNDATION BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..15f1435
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,9 @@
+global-include LICENSE README.md setup.py LICENSE MANIFEST.in
+include LICENSE
+include README.md
+include MANIFEST.in
+include setup.py
+recursive-include demo *
+recursive-include nocaptcha_recaptcha *
+recursive-exclude * __pycache__
+recursive-exclude * *.py[co]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0d02c03
--- /dev/null
+++ b/README.md
@@ -0,0 +1,97 @@
+[![Build Status](https://travis-ci.org/ImaginaryLandscape/django-nocaptcha-recaptcha.svg?branch=master)](https://travis-ci.org/ImaginaryLandscape/django-nocaptcha-recaptcha)
+
+# SUMMARY
+
+Add new-style Google ReCaptcha widgets to your Django forms simply by adding a
+NoReCaptchaField field to said forms.
+
+# ABOUT
+
+In late 2014, Google updated their ReCaptcha service, changing its API. The update significantly
+changes the appearance and function of ReCaptcha. This has been referred to as
+ReCaptcha 2 or "nocaptcha recaptcha".
+
+This module is intended to be a successor to django-recaptcha to support the new style
+Google Recaptcha. It borrows a lot of the logic from the django-recaptcha, but has been
+updated to support the Google change.
+
+For the Google documentation for this service, visit the following:
+
+ https://developers.google.com/recaptcha/intro
+
+The original django-recaptcha project is located at the following location:
+
+ https://github.com/praekelt/django-recaptcha
+
+# FEATURES
+
+ - Implements Google's New "NoCaptcha ReCaptcha Field"
+ - Uses the fallback option for browsers without JavaScript
+ - Easy to add to a Form via a FormField
+ - Works similar to django-recaptcha
+ - Working demo projects
+ - Works with Python 2.7 and 3.4
+
+# INSTALL
+
+ pip install django-nocaptcha-recaptcha
+
+# CONFIGURE
+
+Add nocaptcha_recaptcha to your INSTALLED_APPS setting
+
+Add the following to settings.py
+
+ Required settings:
+ NORECAPTCHA_SITE_KEY (string) = the Google provided site_key
+ NORECAPTCHA_SECRET_KEY (string) = the Google provided secret_key
+
+ Optional Settings:
+ NORECAPTCHA_VERIFY_URL (string) = reCaptcha api endpoint for verification.
+ Best to leave this as the default setting.
+ Default is https://www.google.com/recaptcha/api/siteverify
+ NORECAPTCHA_WIDGET_TEMPLATE (string) = location for the widget template.
+ Default is nocaptcha_recaptcha/widget.html
+
+
+Add the field to a form that you want to protect.
+
+ from nocaptcha_recaptcha.fields import NoReCaptchaField
+
+ class DemoForm(forms.Form):
+ .....
+ captcha = NoReCaptchaField()
+
+
+Add Google's JavaScript library to your base template or elsewhere, so it is
+available on the page containing the django form.
+
+
+
+
+(optional)
+You can customize the field.
+
+- You can add attributes to the g-recaptcha div tag through the following
+
+ captcha = NoReCaptchaField(gtag_attrs={'data-theme':'dark'}))
+
+- You can override the template for the widget like you would any
+ other django template.
+
+
+# DEMO PROJECT
+
+The demo project includes a fully working example of this module.
+To use it, run the following:
+
+ cd demo
+ export NORECAPTCHA_SITE_KEY=""
+ export NORECAPTCHA_SECRET_KEY=""
+ ./manage.py runserver
+
+ # in a browser, visit http://localhost:8000
+
+# TESTING
+
+ python setup.py test
diff --git a/demo/demo/__init__.py b/demo/demo/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/demo/demo/settings.py b/demo/demo/settings.py
new file mode 100644
index 0000000..d763a55
--- /dev/null
+++ b/demo/demo/settings.py
@@ -0,0 +1,212 @@
+# Django settings for demo project.
+import os
+import sys
+from django import VERSION
+
+PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ))
+sys.path.insert(0, os.path.join(PROJECT_ROOT, '..', '..'))
+
+DEBUG = True
+
+ADMINS = (
+ # ('Your Name', 'your_email@example.com'),
+)
+
+MANAGERS = ADMINS
+
+DATABASES = {
+ 'default': {
+ # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': 'demo.sqlite3', # Or path to database file if using sqlite3.
+ # The following settings are not used with sqlite3:
+ 'USER': '',
+ 'PASSWORD': '',
+ 'HOST': '',
+ # Empty for localhost through domain sockets or '127.0.0.1' for
+ # localhost through TCP.
+ 'PORT': '', # Set to empty string for default.
+ }
+}
+
+
+# Hosts/domain names that are valid for this site; required if DEBUG is False
+# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts
+ALLOWED_HOSTS = []
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# In a Windows environment this must be set to your system time zone.
+TIME_ZONE = 'America/Chicago'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# If you set this to False, Django will not format dates, numbers and
+# calendars according to the current locale.
+USE_L10N = True
+
+# If you set this to False, Django will not use timezone-aware datetimes.
+USE_TZ = True
+
+# Absolute filesystem path to the directory that will hold user-uploaded files.
+# Example: "/var/www/example.com/media/"
+MEDIA_ROOT = ''
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash.
+# Examples: "http://example.com/media/", "http://media.example.com/"
+MEDIA_URL = ''
+
+# Absolute path to the directory static files should be collected to.
+# Don't put anything in this directory yourself; store your static files
+# in apps' "static/" subdirectories and in STATICFILES_DIRS.
+# Example: "/var/www/example.com/static/"
+STATIC_ROOT = ''
+
+# URL prefix for static files.
+# Example: "http://example.com/static/", "http://static.example.com/"
+STATIC_URL = '/static/'
+
+# Additional locations of static files
+STATICFILES_DIRS = (
+ # Put strings here, like "/home/html/static" or "C:/www/django/static".
+ # Always use forward slashes, even on Windows.
+ # Don't forget to use absolute paths, not relative paths.
+)
+
+# List of finder classes that know how to find static files in
+# various locations.
+STATICFILES_FINDERS = (
+ 'django.contrib.staticfiles.finders.FileSystemFinder',
+ 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+ # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
+)
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = 'x$47^-hmv-kaa0trcc*ry%+b^^2f)$rs#cl)6j&!)j2c&h%88e'
+
+
+MIDDLEWARE_CLASSES = (
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ # Uncomment the next line for simple clickjacking protection:
+ # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+)
+
+ROOT_URLCONF = 'demo.urls'
+
+# Python dotted path to the WSGI application used by Django's runserver.
+WSGI_APPLICATION = 'demo.wsgi.application'
+
+
+if VERSION >= (1, 8, 0):
+
+ TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [
+ os.path.join(PROJECT_ROOT, 'templates'),
+ ],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.contrib.auth.context_processors.auth',
+ 'django.template.context_processors.i18n',
+ 'django.template.context_processors.request',
+ 'django.template.context_processors.media',
+ 'django.template.context_processors.static',
+ 'django.contrib.messages.context_processors.messages',
+ 'example.apps.core.context_processors.site',
+ ],
+ },
+ },
+ ]
+else:
+
+ TEMPLATE_DEBUG = DEBUG
+
+ # List of callables that know how to import templates from various sources.
+ TEMPLATE_LOADERS = (
+ 'django.template.loaders.filesystem.Loader',
+ 'django.template.loaders.app_directories.Loader',
+ # 'django.template.loaders.eggs.Loader',
+ )
+
+ TEMPLATE_DIRS = (
+ # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
+ # Always use forward slashes, even on Windows.
+ # Don't forget to use absolute paths, not relative paths.
+ os.path.join(PROJECT_ROOT, 'templates'),
+ )
+
+
+INSTALLED_APPS = (
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'django.contrib.admin',
+ # Uncomment the next line to enable admin documentation:
+ # 'django.contrib.admindocs',
+
+ # Only needed for running unit tests
+ 'nocaptcha_recaptcha',
+)
+
+
+SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer'
+
+# A sample logging configuration. The only tangible logging
+# performed by this configuration is to send an email to
+# the site admins on every HTTP 500 error when DEBUG=False.
+# See http://docs.djangoproject.com/en/dev/topics/logging for
+# more details on how to customize your logging configuration.
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'filters': {
+ 'require_debug_false': {
+ '()': 'django.utils.log.RequireDebugFalse'
+ }
+ },
+ 'handlers': {
+ 'mail_admins': {
+ 'level': 'ERROR',
+ 'filters': ['require_debug_false'],
+ 'class': 'django.utils.log.AdminEmailHandler'
+ },
+ 'console': {
+ 'level': 'DEBUG',
+ 'class': 'logging.StreamHandler',
+ },
+ },
+ 'loggers': {
+ 'django.request': {
+ 'handlers': ['mail_admins'],
+ 'level': 'ERROR',
+ 'propagate': True,
+ },
+ 'iscapeauth': {
+ 'handlers': ['console'],
+ 'level': "DEBUG",
+ 'propogate': True,
+ }
+ }
+}
+
+NORECAPTCHA_SITE_KEY = os.environ.get('NORECAPTCHA_SITE_KEY', "")
+NORECAPTCHA_SECRET_KEY = os.environ.get('NORECAPTCHA_SECRET_KEY', "")
diff --git a/demo/demo/templates/index.html b/demo/demo/templates/index.html
new file mode 100644
index 0000000..e02b2b4
--- /dev/null
+++ b/demo/demo/templates/index.html
@@ -0,0 +1,14 @@
+
+
+ reCAPTCHA demo: Simple page
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demo/demo/templates/success.html b/demo/demo/templates/success.html
new file mode 100644
index 0000000..7af069f
--- /dev/null
+++ b/demo/demo/templates/success.html
@@ -0,0 +1,8 @@
+
+
+ reCAPTCHA demo: Simple page
+
+
+Success!
+
+
\ No newline at end of file
diff --git a/demo/demo/urls.py b/demo/demo/urls.py
new file mode 100644
index 0000000..0307794
--- /dev/null
+++ b/demo/demo/urls.py
@@ -0,0 +1,18 @@
+from django.contrib import admin
+from django.conf.urls import include, url
+from django.views.generic import TemplateView
+
+from . import views
+
+admin.autodiscover()
+
+urlpatterns = [
+
+ url(r'^$', views.DemoView.as_view(template_name="index.html"), {},
+ name="index"),
+ url(r'^success/$', TemplateView.as_view(template_name="success.html"), {},
+ name="success"),
+
+ # Uncomment the next line to enable the admin:
+ url(r'^admin/', include(admin.site.urls)),
+]
diff --git a/demo/demo/views.py b/demo/demo/views.py
new file mode 100644
index 0000000..6d2c4b4
--- /dev/null
+++ b/demo/demo/views.py
@@ -0,0 +1,18 @@
+from django import forms
+from django.core.urlresolvers import reverse_lazy
+from django.views.generic import FormView
+
+from nocaptcha_recaptcha.fields import NoReCaptchaField
+
+
+class DemoForm(forms.Form):
+ username = forms.CharField(required=True)
+ captcha = NoReCaptchaField(gtag_attrs={'data-theme': 'dark'})
+
+
+class DemoView(FormView):
+ form_class = DemoForm
+ success_url = reverse_lazy('success')
+
+ def form_valid(self, form):
+ return super(DemoView, self).form_valid(form)
\ No newline at end of file
diff --git a/demo/demo/wsgi.py b/demo/demo/wsgi.py
new file mode 100644
index 0000000..8d4d998
--- /dev/null
+++ b/demo/demo/wsgi.py
@@ -0,0 +1,33 @@
+"""
+WSGI config for demo project.
+
+This module contains the WSGI application used by Django's development server
+and any production WSGI deployments. It should expose a module-level variable
+named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
+this application via the ``WSGI_APPLICATION`` setting.
+
+Usually you will have the standard Django WSGI application here, but it also
+might make sense to replace the whole Django WSGI application with a custom one
+that later delegates to the Django one. For example, you could introduce WSGI
+middleware here, or combine a Django application with an application of another
+framework.
+
+"""
+import os
+
+# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
+# if running multiple sites in the same mod_wsgi process. To fix this, use
+# mod_wsgi daemon mode with each site in its own daemon process, or use
+# os.environ["DJANGO_SETTINGS_MODULE"] = "demo.settings"
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings")
+
+# This application object is used by any WSGI server configured to use this
+# file. This includes Django's development server, if the WSGI_APPLICATION
+# setting points here.
+from django.core.wsgi import get_wsgi_application
+
+application = get_wsgi_application()
+
+# Apply WSGI middleware here.
+# from helloworld.wsgi import HelloWorldApplication
+# application = HelloWorldApplication(application)
diff --git a/demo/manage.py b/demo/manage.py
new file mode 100755
index 0000000..86cc0b0
--- /dev/null
+++ b/demo/manage.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings")
+
+ from django.core.management import execute_from_command_line
+
+ execute_from_command_line(sys.argv)
diff --git a/demo/requirements.txt b/demo/requirements.txt
new file mode 100644
index 0000000..b16808d
--- /dev/null
+++ b/demo/requirements.txt
@@ -0,0 +1,3 @@
+Django>=1.5,<1.11
+mock>=1.0.1
+six>=1.8.0
diff --git a/nocaptcha_recaptcha/__init__.py b/nocaptcha_recaptcha/__init__.py
new file mode 100644
index 0000000..50404c7
--- /dev/null
+++ b/nocaptcha_recaptcha/__init__.py
@@ -0,0 +1,4 @@
+__version__ = '0.0.19'
+
+from .fields import NoReCaptchaField
+from .widgets import NoReCaptchaWidget
diff --git a/nocaptcha_recaptcha/_compat.py b/nocaptcha_recaptcha/_compat.py
new file mode 100644
index 0000000..22c1f91
--- /dev/null
+++ b/nocaptcha_recaptcha/_compat.py
@@ -0,0 +1,18 @@
+import sys
+
+PY2 = sys.version_info[0] == 2
+if PY2:
+ text_type = unicode
+ from urllib2 import Request, urlopen
+ from urllib import urlencode
+else:
+ from urllib.request import Request, urlopen
+ from urllib.parse import urlencode
+
+ text_type = str
+
+
+def want_bytes(s, encoding='utf-8', errors='strict'):
+ if isinstance(s, text_type):
+ s = s.encode(encoding, errors)
+ return s
diff --git a/nocaptcha_recaptcha/client.py b/nocaptcha_recaptcha/client.py
new file mode 100644
index 0000000..84dcadd
--- /dev/null
+++ b/nocaptcha_recaptcha/client.py
@@ -0,0 +1,117 @@
+import logging
+
+import django
+
+try:
+ import json
+except ImportError:
+ from django.utils import simplejson as json
+
+from django.conf import settings
+from django.template.loader import render_to_string
+from django.utils.translation import get_language
+from django.utils.encoding import force_text
+
+from ._compat import want_bytes, urlencode, Request, urlopen, PY2
+
+logger = logging.getLogger(__name__)
+
+DEFAULT_VERIFY_URL = "https://www.google.com/recaptcha/api/siteverify"
+DEFAULT_FALLBACK_URL = "https://www.google.com/recaptcha/api/fallback"
+DEFAULT_WIDGET_TEMPLATE = 'nocaptcha_recaptcha/widget.html'
+
+VERIFY_URL = getattr(settings, "NORECAPTCHA_VERIFY_URL",
+ DEFAULT_VERIFY_URL)
+
+FALLBACK_URL = getattr(settings, "NORECAPTCHA_FALLBACK_URL",
+ DEFAULT_FALLBACK_URL)
+
+WIDGET_TEMPLATE = getattr(settings, "NORECAPTCHA_WIDGET_TEMPLATE",
+ DEFAULT_WIDGET_TEMPLATE)
+
+
+class RecaptchaResponse(object):
+ def __init__(self, is_valid, error_codes=None):
+ self.is_valid = is_valid
+ self.error_codes = error_codes
+
+
+def displayhtml(site_key, gtag_attrs, js_params):
+ """Gets the HTML to display for reCAPTCHA
+
+ site_key -- The public api key provided by Google ReCaptcha
+ """
+
+ if 'hl' not in js_params:
+ js_params['hl'] = get_language()[:2]
+
+ return render_to_string(
+ WIDGET_TEMPLATE,
+ {
+ 'fallback_url': FALLBACK_URL,
+ 'site_key': site_key,
+ 'js_params': js_params,
+ 'gtag_attrs': gtag_attrs,
+ })
+
+
+def submit(g_nocaptcha_response_value, secret_key, remoteip):
+ """
+ Submits a reCAPTCHA request for verification. Returns RecaptchaResponse
+ for the request
+
+ recaptcha_response_field -- The value of recaptcha_response_field
+ from the form
+ secret_key -- your reCAPTCHA private key
+ remoteip -- the user's ip address
+ """
+
+ if not (g_nocaptcha_response_value and len(g_nocaptcha_response_value)):
+ return RecaptchaResponse(
+ is_valid=False,
+ error_codes=['incorrect-captcha-sol']
+ )
+
+ params = urlencode({
+ 'secret': want_bytes(secret_key),
+ 'remoteip': want_bytes(remoteip),
+ 'response': want_bytes(g_nocaptcha_response_value),
+ })
+
+ if not PY2:
+ params = params.encode('utf-8')
+
+ req = Request(
+ url=VERIFY_URL, data=params,
+ headers={
+ 'Content-type': 'application/x-www-form-urlencoded',
+ 'User-agent': 'noReCAPTCHA Python'
+ }
+ )
+
+ httpresp = urlopen(req)
+
+ try:
+ res = force_text(httpresp.read())
+ return_values = json.loads(res)
+ except (ValueError, TypeError):
+ return RecaptchaResponse(
+ is_valid=False,
+ error_codes=['json-read-issue']
+ )
+ except:
+ return RecaptchaResponse(
+ is_valid=False,
+ error_codes=['unknown-network-issue']
+ )
+ finally:
+ httpresp.close()
+
+ return_code = return_values.get("success", False)
+ error_codes = return_values.get('error-codes', [])
+ logger.debug("%s - %s" % (return_code, error_codes))
+
+ if return_code is True:
+ return RecaptchaResponse(is_valid=True)
+ else:
+ return RecaptchaResponse(is_valid=False, error_codes=error_codes)
diff --git a/nocaptcha_recaptcha/fields.py b/nocaptcha_recaptcha/fields.py
new file mode 100644
index 0000000..738a295
--- /dev/null
+++ b/nocaptcha_recaptcha/fields.py
@@ -0,0 +1,75 @@
+import os
+import sys
+
+from django import forms
+from django.conf import settings
+
+try:
+ from django.utils.encoding import smart_unicode
+except ImportError:
+ from django.utils.encoding import smart_text as smart_unicode
+
+from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext_lazy as _
+
+from . import client
+from .widgets import NoReCaptchaWidget
+
+
+class NoReCaptchaField(forms.CharField):
+ default_error_messages = {
+ 'captcha_invalid': _('Incorrect, please try again.')
+ }
+
+ def __init__(self, site_key=None, secret_key=None,
+ gtag_attrs={}, js_params={}, *args, **kwargs):
+ """
+ site_key = the Google provided site_key
+ secret_key = the Google provided secret_key
+ gtag_attrs = html input attributes to provide
+ to the g-recaptcha tag
+ js_params = parameters to passed to the javascript backend
+
+ See: https://developers.google.com/recaptcha/docs/display
+ """
+ site_key = site_key if site_key else \
+ settings.NORECAPTCHA_SITE_KEY
+ self.secret_key = secret_key if secret_key else \
+ settings.NORECAPTCHA_SECRET_KEY
+
+ self.widget = NoReCaptchaWidget(
+ site_key=site_key, gtag_attrs=gtag_attrs, js_params=js_params)
+ self.required = True
+ super(NoReCaptchaField, self).__init__(*args, **kwargs)
+
+ def get_remote_ip(self):
+ """
+ Return the remote IP from the request.
+ First check the REMOTE_ADDR header and then the
+ HTTP_X_FORWARDED_FOR header.
+ """
+ f = sys._getframe()
+ while f:
+ if 'request' in f.f_locals:
+ request = f.f_locals['request']
+ if request:
+ remote_ip = request.META.get('REMOTE_ADDR', '')
+ forwarded_ip = request.META.get('HTTP_X_FORWARDED_FOR', '')
+ ip = remote_ip if not forwarded_ip else forwarded_ip
+ return ip
+ f = f.f_back
+
+ def clean(self, value):
+ super(NoReCaptchaField, self).clean(value)
+ g_nocaptcha_response_value = smart_unicode(value)
+ if os.environ.get('NORECAPTCHA_TESTING', None) == 'True' \
+ and g_nocaptcha_response_value == 'PASSED':
+ return value
+
+ check_captcha = client.submit(
+ g_nocaptcha_response_value, secret_key=self.secret_key,
+ remoteip=self.get_remote_ip())
+
+ if not check_captcha.is_valid:
+ raise ValidationError(self.error_messages['captcha_invalid'])
+ return value
diff --git a/nocaptcha_recaptcha/models.py b/nocaptcha_recaptcha/models.py
new file mode 100644
index 0000000..e69de29
diff --git a/nocaptcha_recaptcha/templates/nocaptcha_recaptcha/widget.html b/nocaptcha_recaptcha/templates/nocaptcha_recaptcha/widget.html
new file mode 100644
index 0000000..17d6d11
--- /dev/null
+++ b/nocaptcha_recaptcha/templates/nocaptcha_recaptcha/widget.html
@@ -0,0 +1,21 @@
+
+
\ No newline at end of file
diff --git a/nocaptcha_recaptcha/tests.py b/nocaptcha_recaptcha/tests.py
new file mode 100644
index 0000000..9b0205c
--- /dev/null
+++ b/nocaptcha_recaptcha/tests.py
@@ -0,0 +1,79 @@
+import os
+import json
+
+from django.forms import Form
+from django.test import TestCase
+
+import mock
+
+from nocaptcha_recaptcha import fields, client
+
+
+class TestForm(Form):
+ captcha = fields.NoReCaptchaField(gtag_attrs={'data-theme': 'dark'})
+
+
+class TestCase(TestCase):
+ def setUp(self):
+ os.environ['NORECAPTCHA_TESTING'] = 'True'
+
+ def test_envvar_enabled(self):
+ form_params = {'g-recaptcha-response': 'PASSED'}
+ form = TestForm(form_params)
+ self.assertTrue(form.is_valid())
+
+ def test_envvar_disabled(self):
+ os.environ['NORECAPTCHA_TESTING'] = 'False'
+ form_params = {'g-recaptcha-response': 'PASSED'}
+ form = TestForm(form_params)
+ self.assertFalse(form.is_valid())
+
+ @mock.patch('nocaptcha_recaptcha.client.urlopen')
+ def test_client_submit_empty_input(self, mock_urlopen):
+ """
+ Should return False if input is empty string
+ """
+ result = client.submit('', '', '')
+ self.assertFalse(result.is_valid)
+ self.assertEqual(['incorrect-captcha-sol'], result.error_codes)
+
+ @mock.patch('nocaptcha_recaptcha.client.urlopen')
+ def test_client_submit_correct(self, mock_urlopen):
+ """
+ Should return True if response is correct
+ """
+ mock_resp = mock.Mock()
+ mock_resp.read.return_value = json.dumps(
+ {'success': True, 'error-codes': []})
+ mock_urlopen.return_value = mock_resp
+ result = client.submit('a', 'a', 'a')
+ self.assertTrue(result.is_valid)
+ self.assertEqual(result.error_codes, None)
+
+ @mock.patch('nocaptcha_recaptcha.client.urlopen')
+ def test_client_submit_response_not_json(self, mock_urlopen):
+ """
+ Should return json read error if response is not json
+ """
+ mock_resp = mock.Mock()
+ mock_resp.read.return_value = "{'success': True, 'error-codes': []}"
+ mock_urlopen.return_value = mock_resp
+ result = client.submit('a', 'a', 'a')
+ self.assertFalse(result.is_valid)
+ self.assertEqual(result.error_codes, ['json-read-issue'])
+
+ @mock.patch('nocaptcha_recaptcha.client.urlopen')
+ def test_client_submit_response_incorrect(self, mock_urlopen):
+ """
+ Should return false if response is incorrect
+ """
+ mock_resp = mock.Mock()
+ mock_resp.read.return_value = json.dumps(
+ {'success': False, 'error-codes': ['ERROR']})
+ mock_urlopen.return_value = mock_resp
+ result = client.submit('a', 'a', 'a')
+ self.assertFalse(result.is_valid)
+ self.assertEqual(result.error_codes, ['ERROR'])
+
+ def tearDown(self):
+ del os.environ['NORECAPTCHA_TESTING']
diff --git a/nocaptcha_recaptcha/views.py b/nocaptcha_recaptcha/views.py
new file mode 100644
index 0000000..e69de29
diff --git a/nocaptcha_recaptcha/widgets.py b/nocaptcha_recaptcha/widgets.py
new file mode 100644
index 0000000..95380e5
--- /dev/null
+++ b/nocaptcha_recaptcha/widgets.py
@@ -0,0 +1,24 @@
+from django import forms
+from django.conf import settings
+from django.utils.safestring import mark_safe
+
+from . import client
+
+
+class NoReCaptchaWidget(forms.widgets.Widget):
+ g_nocaptcha_response = 'g-recaptcha-response'
+
+ def __init__(self, site_key=None,
+ gtag_attrs={}, js_params={}, *args, **kwargs):
+ self.site_key = site_key if site_key else \
+ settings.NORECAPTCHA_SITE_KEY
+ super(NoReCaptchaWidget, self).__init__(*args, **kwargs)
+ self.gtag_attrs = gtag_attrs
+ self.js_params = js_params
+
+ def render(self, name, value, gtag_attrs=None, **kwargs):
+ return mark_safe(u'%s' % client.displayhtml(
+ self.site_key, self.gtag_attrs, self.js_params))
+
+ def value_from_datadict(self, data, files, name):
+ return data.get(self.g_nocaptcha_response, None)
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..40c19d7
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,38 @@
+import os
+from setuptools import setup, find_packages
+
+
+def read(fname):
+ return open(os.path.join(os.path.dirname(__file__), fname)).read()
+
+
+setup(
+ name='django-nocaptcha-recaptcha',
+ version='0.0.20',
+ description='Django nocaptcha recaptcha form field/widget app.',
+ long_description=read('README.md'),
+ author='Imaginary Landscape',
+ author_email='jjasinski@imgescape.com',
+ keywords=['django', 'recaptcha', 'field', 'nocaptcha'],
+ license='BSD',
+ url='https://github.com/ImaginaryLandscape/django-nocaptcha-recaptcha',
+ packages=find_packages(),
+ tests_require=[
+ 'mock',
+ ],
+ test_suite="setuptest.setuptest.SetupTestSuite",
+ include_package_data=True,
+ classifiers=[
+ "Framework :: Django",
+ "License :: OSI Approved :: BSD License",
+ "Operating System :: OS Independent",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python :: 2.6",
+ "Programming Language :: Python :: 2.7",
+ "Programming Language :: Python :: 3.4",
+ "Programming Language :: Python :: 3.5",
+ "Programming Language :: Python :: 3.6",
+ "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
+ ],
+ zip_safe=False,
+)
diff --git a/test_settings.py b/test_settings.py
new file mode 100644
index 0000000..4ff3137
--- /dev/null
+++ b/test_settings.py
@@ -0,0 +1,23 @@
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': 'test.sqlite',
+ }
+}
+
+INSTALLED_APPS = [
+ 'nocaptcha_recaptcha',
+]
+
+MIDDLEWARE_CLASSES = [
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.locale.LocaleMiddleware',
+ 'django.middleware.doc.XViewMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+]
+
+NORECAPTCHA_SECRET_KEY = 'privkey'
+NORECAPTCHA_SITE_KEY = 'pubkey'