Someone posted a comment in my last blog entry about ‘why another form library?’ (as predicted I suppose). My personal opinion is that it’s about easy of use but that is a difficult thing to describe. So I thought I’d challenge myself to port a Tosca Widget widget to see what it would look like in formish. I decided to include the ReCAPTCHA widget to make it a little more interesting.
First thing was to write the Widget python code which went roughly as follows. First we set up the widget header with some constants..
import urllib, urllib2
from validatish import ConvertError
from convertish.convert import string_converter
class ReCAPTCHA(widgets.Input):
_template = 'ReCAPTCHA'
API_SSL_SERVER="https://api-secure.recaptcha.net"
API_SERVER="http://api.recaptcha.net"
VERIFY_SERVER="api-verify.recaptcha.net"
USER_AGENT = "reCAPTCHA Formish"
def __init__(self, publickey, privatekey, environ, **k):
self.use_ssl = k.pop('use_ssl', False)
widgets.Input.__init__(self, **k)
self.publickey = publickey
self.privatekey = privatekey
self.remoteip = environ.get('REMOTE_ADDR', '127.0.0.1')
This was the easy bit.. next we need to add the recaptcha code..
def convert(self, schema_type, data):
request_params = urllib.urlencode({
'privatekey': self.privatekey,
'remoteip' : self.remoteip,
'challenge': data['recaptcha_challenge_field'][0],
'response' : data['recaptcha_response_field'][0],
})
request = urllib2.Request (
url = "http://%s/verify"%self.VERIFY_SERVER,
data = request_params,
headers = { "Content-type": "application/x-www-form-urlencoded",
"User-agent": self.USER_AGENT })
response = urllib2.urlopen(request)
return_code = response.read().splitlines()[0]
response.close()
if (return_code == "true"):
return True
else:
raise ConvertError('reCAPTCHA failed')
This code just prepares a request to make to the captcha server and reads the result. Now this would be it for the python code but unfortunately captcha widgets don’t let you define your own name for the fields. This is just what the pre_parse_request method is for. It lets you munge some code up front if needed. In this case we move the captcha data into the right request field names.
def pre_parse_request(self, schema_type, request_data, full_request_data):
""" reCaptcha won't let you use your own field names so we move them """
return {'recaptcha_challenge_field':full_request_data['recaptcha_challenge_field'],
'recaptcha_response_field': full_request_data['recaptcha_response_field'],}
Finally we need a template
<%page args="field" />
<%
if field.error:
args = "k=%s&error=incorrect-captcha-sol"%field.widget.publickey
else:
args = "k=%s"%field.widget.publickey
if field.widget.use_ssl:
apiserver = field.widget.API_SSL_SERVER
else:
apiserver = field.widget.API_SERVER
%>
<script type="text/javascript" src="${apiserver}/challenge?${args|n}"></script>
<noscript>
<iframe src="${apiserver}/noscript?${args|n}" height="300" width="500" frameborder="0"></iframe><br />
<textarea name="${field.name}.recaptcha_challenge_field" rows="3" cols="40"></textarea>
<input type="hidden" name="${field.name}.recaptcha_response_field" value="manual_challenge" />
</noscript>
So the widget just works out if there is an error and prepares the appropriate args and also plans for ssl if needed.
And using the widget is as simple as .. (ReCAPTCHA isn’t in the current release as I only wrote it tonight)
import schemaish, formish
schema = schemaish.Structure()
schema.add('recaptcha', schemaish.Boolean())
form = formish.Form(schema, 'form')
publickey = '6LcSqgQAAAAAAA1A6MJZXGpY35ZsdvwxvsEq0KQD'
privatekey = '6LcSqgQAA....................7ugn72Hi2va'
form['recaptcha'].widget = formish.ReCAPTCHA(publickey, privatekey, request.environ)
Have a look at the widget in action at http://ish.io:8891/ReCAPTCHA .
The Tosca Widget equivalent follows ..
Widget
from tw.api import Widget, JSLink, CSSLink
from genshi import HTML
from recaptcha.client.captcha import *
from tw.forms import InputField, FormField, ContainerMixin
class ReCaptchaWidget(InputField):
engine_name='genshi'
params = ['captcha_response','public_key', 'server', 'use_ssl', 'error_param']
template = """<div><script type="text/javascript" src="${server}/challenge?k=${public_key}${error_param}"></script>
<noscript>
<iframe src="${server}/noscript?k=${public_key}${error_param}" height="300" width="500" frameborder="0"></iframe><br />
<textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
<input type='hidden' name='recaptcha_response_field' value='manual_challenge' />
</noscript></div>
"""
def __init__(self, id=None, public_key=None, use_ssl=False, error_param=None, parent=None, children=[], **kw):
self.public_key = public_key
self.error_param = ''
if error_param:
self.error_param = '&error=%s'%error_param
self.server = API_SERVER
if use_ssl:
self.server = API_SSL_SERVER
super(ReCaptchaWidget, self).__init__('recaptcha_response_field', parent, children, **kw)
def update_params(self, d):
d['captcha_response'] = HTML(displayhtml(self.public_key))
return super(ReCaptchaWidget, self).update_params(d)
Validator
class ReCaptchaValidator(FancyValidator):
"""
@see formencode.validators.FieldsMatch
"""
messages = {
'incorrect': _("Incorrect value."),
'missing': _("Missing value."),
}
verify_server = "api-verify.recaptcha.net"
__unpackargs__ = ('*', 'field_names')
validate_partial_form = True
validate_partial_python = None
validate_partial_other = None
def __init__(self, private_key, remote_ip, *args, **kw):
super(ReCaptchaValidator, self).__init__(args, kw)
self.private_key = private_key
self.remote_ip = remote_ip
self.field_names = ['recaptcha_challenge_field',
'recaptcha_response_field']
def validate_partial(self, field_dict, state):
for name in self.field_names:
if not field_dict.has_key(name):
return
self.validate_python(field_dict, state)
def validate_python(self, field_dict, state):
challenge = field_dict['recaptcha_challenge_field']
response = field_dict['recaptcha_response_field']
if response == '' or challenge == '':
error = Invalid(self.message('missing', state), field_dict, state)
error.error_dict = {'recaptcha_response_field':'Missing value'}
raise error
params = urllib.urlencode({
'privatekey': self.private_key,
'remoteip' : self.remote_ip,
'challenge': challenge,
'response' : response,
})
request = urllib2.Request(
url = "http://%s/verify" % self.verify_server,
data = params,
headers = {"Content-type": "application/x-www-form-urlencoded",
"User-agent": "reCAPTCHA Python"
}
)
httpresp = urllib2.urlopen(request)
return_values = httpresp.read().splitlines();
httpresp.close();
return_code = return_values[0]
if not return_code == "true":
error = Invalid(self.message('incorrect', state), field_dict, state)
error.error_dict = {'recaptcha_response_field':self.message('incorrect', state)}
raise error
return True
Using the Widget
from tw.forms import TableForm
from tw.recaptcha import ReCaptchaWidget
from tw.recaptcha.validator import ReCaptchaValidator
from tw.api import WidgetsList
from formencode import Schema, NoDefault
from formencode.validators import NotEmpty
class MyForm(TableForm):
class fields(WidgetsList):
recaptcha_response_field = ReCaptchaWidget(public_key='<your_public_key>')
class FilteringSchema(Schema):
filter_extra_fields = False
allow_extra_fields = True
ignore_key_missing = False
captchaForm = MyForm(validator=validator)
The Tosca Widget one is fine and accomplishes the job well but I personally think the formish version is a little simpler to understand. I am completely and utterly biased though so I would appreciate any body elses comments.. (and I realise the Tosca Widget has some gettext and a little redundant field checking).
If anybody is interested in preparing a newforms version of this I would be interested in seeing it..
p.s. I’ll repeat again that I really like many aspects of Tosca Widgets and this isn’t meant to be a competition. Tosca widgets can do some amazing magic with javascript and css and also does some special tricks to allow you to work with Stacked Object Proxies (I think). I also admit that I haven’t used Tosca Widgets in anger and it may have many features that only come into their own under certain situations.
Anyway .. comments?

