Skip to content
Browse files

Attempt to integrate formhelpers with pyramid

  • Loading branch information...
0 parents commit 08a59acebe230f08ff3a66147bb42bacd6a5ddaa @tholo committed Sep 20, 2011
8 .gitignore
@@ -0,0 +1,8 @@
+/.coverage
+/build/
+/cover/
+/coverage.xml
+/data/
+/dist/
+/*.egg-info
+*.pyc
4 CHANGES.rst
@@ -0,0 +1,4 @@
+0.0
+---
+
+- Initial version
2 MANIFEST.in
@@ -0,0 +1,2 @@
+include *.txt *.ini *.cfg *.rst
+recursive-include formhelpers2 *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml
8 README.rst
@@ -0,0 +1,8 @@
+============
+formhelpers2
+============
+
+formhelpers2 is an attempt to use Mike Bayer's formhelpers_ with Pyramid_.
+
+.. _formhelpers: http://techspot.zzzeek.org/2008/07/01/better-form-generation-with-mako-and-pylons
+.. _Pyramid: http://pyramid.org
51 development.ini
@@ -0,0 +1,51 @@
+[app:main]
+use = egg:formhelpers2
+
+pyramid.reload_templates = true
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.debug_templates = true
+pyramid.default_locale_name = en
+pyramid.includes = pyramid_debugtoolbar
+
+mako.preprocessor = formhelpers2:mako.process_tags
+mako.directories = formhelpers2:templates
+#mako.module_directory = %(here)s/data/templates
+mako.strict_undefined = true
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 6543
+
+# Begin logging configuration
+
+[loggers]
+keys = root, formhelpers2
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_formhelpers2]
+level = DEBUG
+handlers =
+qualname = formhelpers2
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+
+# End logging configuration
17 formhelpers2/__init__.py
@@ -0,0 +1,17 @@
+from pyramid.config import Configurator
+from pyramid.session import UnencryptedCookieSessionFactoryConfig
+
+def main(global_config, **settings):
+ """
+ This function returns a Pyramid WSGI application.
+ """
+
+ my_session_factory = UnencryptedCookieSessionFactoryConfig('formhelpers2')
+ config = Configurator(settings=settings,
+ session_factory=my_session_factory)
+
+ config.add_static_view('static', 'formhelpers2:static')
+
+ config.scan()
+
+ return config.make_wsgi_app()
86 formhelpers2/mako.py
@@ -0,0 +1,86 @@
+import re
+
+
+class Form(object):
+ class FormContext(object):
+ pass
+
+ def __init__(self, schema, name):
+ self.schema = schema
+ self.name = name
+
+ def validate(self, data):
+ return self.schema.deserialize(data)
+
+ def render(self, data=None, errors=None, context=None, **kw):
+ if not context:
+ context = self.FormContext()
+ if not data:
+ data = self.schema.serialize()
+
+ setattr(context, self.name, dict(data.iteritems()))
+
+ if errors:
+ if hasattr(context, 'errors'):
+ context.errors.update(errors)
+ else:
+ context.errors = errors
+
+ for key, val in kw.items():
+ setattr(context, key, val)
+
+ return context
+
+
+tag_regexp = re.compile(r'<(\/)?%(\w+):(\w+)\s*(.*?)(\/)?>', re.S)
+attr_regexp = re.compile(
+ r"\s*(\w+)\s*=\s*(?:(?<!\\)'(.*?)(?<!\\)'|(?<!\\)\"(.*?)(?<!\\)\")")
+expr_regexp = re.compile(r'\${(.+?)}')
+
+
+def process_tags(source):
+ """Convert tags of the form <nsname:funcname attrs> into a <%call> tag.
+
+ This is a quick regexp approach that can be replaced with a full blown
+ XML parsing approach, if desired.
+
+ """
+ def process_exprs(t):
+ m = re.match(r'^\${(.+?)}$', t)
+ if m:
+ return m.group(1)
+
+ att = []
+
+ def replace_expr(m):
+ att.append(m.group(1))
+ return "%s"
+
+ t = expr_regexp.sub(replace_expr, t)
+ if att:
+ return "'%s' %% (%s)" % (t.replace("'", r"\'"), ",".join(att))
+ else:
+ return "'%s'" % t.replace("'", r"\'")
+
+ def cvt(match):
+ if bool(match.group(1)):
+ return "</%call>"
+
+ ns = match.group(2)
+ fname = match.group(3)
+ attrs = match.group(4)
+
+ attrs = dict([(key, process_exprs(val1 or val2))
+ for key, val1, val2 in attr_regexp.findall(attrs)])
+ args = attrs.pop("args", "")
+
+ attrs = ",".join(["%s=%s" % (key, value)
+ for key, value in attrs.iteritems()])
+
+ if bool(match.group(5)):
+ return """<%%call expr="%s.%s(%s)" args=%s/>""" % (
+ ns, fname, attrs, args)
+ else:
+ return """<%%call expr="%s.%s(%s)" args=%s>""" % (
+ ns, fname, attrs, args)
+ return tag_regexp.sub(cvt, source)
21 formhelpers2/static/style.css
@@ -0,0 +1,21 @@
+body { background-color: #fff; color: #333; }
+
+body, p {
+ font-family: verdana, arial, helvetica, sans-serif;
+ font-size: 12px;
+ line-height: 18px;
+}
+pre {
+ background-color: #eee;
+ padding: 10px;
+ font-size: 11px;
+ line-height: 13px;
+}
+
+a { color: #000; }
+a:visited { color: #666; }
+a:hover { color: #fff; background-color:#000; }
+
+.error-message{
+ color:red;
+}
20 formhelpers2/subscribers.py
@@ -0,0 +1,20 @@
+from pyramid.events import BeforeRender
+from pyramid.events import NewRequest
+from pyramid.events import subscriber
+from pyramid.httpexceptions import HTTPForbidden
+
+from webhelpers.html import tags
+
+
+@subscriber(BeforeRender)
+def add_renderer_globals(event):
+ event['h'] = tags
+
+
+@subscriber(NewRequest)
+def csrf_validation(event):
+ request = event.request
+ if request.method == 'POST':
+ token = request.POST.get('_csrf')
+ if token is None or token != request.session.get_csrf_token():
+ raise HTTPForbidden('Cross Site Request Forgery detected')
48 formhelpers2/templates/comment.mako
@@ -0,0 +1,48 @@
+<%namespace name="form" file="/form_tags.mako"/>
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+ <title>Mako Form Helpers</title>
+ <link rel="stylesheet" href="${request.static_url('formhelpers2:static/style.css')}" type="text/css" />
+</head>
+<body>
+
+<h3>Using Mako Helpers</h3>
+
+<%form:form name="comment_form" controller="comment" action="post">
+<div style="display: none;">
+ <input type="hidden" name="_csrf" value="${request.session.get_csrf_token()}" />
+</div>
+<table>
+ <tr>
+ <th colspan="2">Submit your Comment</th>
+ </tr>
+ <tr>
+ <td>Your Name:</td>
+ <td><%form:text name="name"/></td>
+ </tr>
+
+ <tr>
+ <td>How did you hear about this site ?</td>
+ <td>
+ <%form:select name="heard" options="${heard_choices}">
+ <%form:option value="">None</%form:option>
+ </%form:select>
+ </td>
+ </tr>
+
+ <tr>
+ <td>Comment:</td>
+ <td><%form:textarea name="comment"/></td>
+ </tr>
+
+ <tr>
+ <td colspan="2"><%form:submit/></td>
+ </tr>
+</table>
+</%form:form>
+
+</body>
+</html>
180 formhelpers2/templates/form_tags.mako
@@ -0,0 +1,180 @@
+<%doc>
+
+ Mako form tag library.
+
+</%doc>
+
+<%def name="errors(name)">\
+<%doc>
+ Given a field name, produce a stylized error message from the current
+ form_errors collection, if one is present. Else render nothing.
+</%doc>\
+% if hasattr(forms, 'errors') and name in forms.errors:
+<div class="error-message">${forms.errors[name]}</div>\
+% endif
+</%def>
+
+<%def name="form(name, url=None, multipart=False, **attrs)">
+<%doc>
+ Render an HTML <form> tag - the body contents will be rendered within.
+
+ name - the name of a dictionary placed on 'forms' which contains form values.
+ url - URL to be POSTed to
+</%doc><%
+ form = getattr(forms, name)
+ if not isinstance(form, dict):
+ raise Exception("No form dictionary found at forms.%s" % name)
+ forms._form = form
+ if not url:
+ url = request.url
+%>\
+${h.form(url, name=name, multipart=coerce_bool(multipart), **attrs)}\
+${caller.body()}\
+${h.end_form()}\
+<%
+ del forms._form
+%></%def>
+
+<%def name="text(name, **attrs)" decorator="render_error">\
+<%doc>
+ Render an HTML <input type="text"> tag.
+</%doc>\
+${h.text(name, value=form_value(forms, name), **attrs)}
+</%def>
+
+<%def name="upload(name, **attrs)" decorator="render_error">\
+<%doc>
+ Render an HTML <file> tag.
+</%doc>\
+${h.file(name, **attrs)}
+</%def>
+
+<%def name="hidden(name, **attrs)">\
+<%doc>
+ Render an HTML <input type="hidden"> tag.
+</%doc>\
+${h.hidden(name, value=form_value(forms, name), **attrs)}\
+</%def>
+
+<%def name="password(name, **attrs)" decorator="render_error">\
+<%doc>
+ Render an HTML <input type="password"> tag.
+</%doc>\
+${h.password(name, value=form_value(forms, name), **attrs)}
+</%def>
+
+<%def name="textarea(name, **attrs)" decorator="render_error">\
+<%doc>
+ Render an HTML <textarea></textarea> tag pair with embedded content.
+</%doc>\
+${h.textarea(name, content=form_value(forms, name), **attrs)}
+</%def>
+
+<%def name="select(name, options=None, **kw)" decorator="render_error">\
+<%doc>
+ Render an HTML <select> tag. Options within the tag
+ are generated using the "option" %def. Additional
+ items can be passed through the "options" argument -
+ these are rendered after the literal <%options> tags.
+</%doc>\
+<%
+ forms._select_options = tuples = []
+ if options is None:
+ options = []
+
+ capture(caller.body)
+
+ selected = form_value(forms, name)
+ if not selected:
+ i = 0
+ selected = []
+ while True:
+ v = form_value(forms, name + "-%d" % i)
+ if v:
+ selected.append(v)
+ i += 1
+ else:
+ break
+%>\
+${h.select(name, selected, [(t[0], t[1]) for t in tuples] + options, **kw)}\
+<%
+ del forms._select_options
+%></%def>
+
+<%def name="option(value)">\
+<%doc>
+ Render an HTML <option> tag. This is meant to be used with
+ the "select" %def and produces a special return value specific to
+ usage with that function.
+</%doc>\
+<%
+ forms._select_options.append((value, capture(caller.body).strip()))
+%></%def>
+
+<%def name="checkbox(name, value='true')" decorator="render_error">\
+<%doc>
+ Render an HTML <checkbox> tag. The value is rendered as 'true'
+ by default for usage with the StringBool validator.
+</%doc>
+${h.checkbox(name, value, checked=form_value(forms, name) == value)}\
+${errors(name)}
+</%def>
+
+<%def name="radio(name, value)" decorator="render_error">\
+<%doc>
+ Render an HTML <radio> tag.
+</%doc>
+${h.radio(name, value, checked=form_value(forms, name) == value)}\
+${errors(name)}
+</%def>
+
+<%def name="submit(value=None, name=None, **kwargs)">\
+<%doc>
+ Render an HTML <submit> tag.
+</%doc>\
+${h.submit(name=name, value=value, **kwargs)}\
+</%def>
+
+<%!
+ def render_error(fn):
+ """Decorate a form field to render an error message or asterisk."""
+
+ def decorate(context, name, *args, **kw):
+ error_message = get_error_message(context, name)
+
+ fn(name, *args, **kw)
+
+ if error_message:
+ context.write('<span id="%s_error" class="error-message">%s</span>' % (name, error_message))
+ return ''
+
+ return decorate
+
+ def get_error_message(context, name):
+ forms = context['forms']
+
+ if hasattr(forms, 'errors') and \
+ name in forms.errors:
+ error_message = forms.errors[name]
+ else:
+ error_message = None
+
+ return error_message
+
+ def coerce_bool(arg):
+ if isinstance(arg, basestring):
+ return eval(arg)
+ elif isinstance(arg, bool):
+ return arg
+ else:
+ raise ArgumentError("%r could not be coerced to boolean" % arg)
+
+ def form_value(forms, name):
+ try:
+ return forms._form.get(name)
+ except AttributeError:
+ raise Exception("Form tag used without a form "
+ "context present; ensure that forms.{form name} is "
+ "populated with a dict, and that this tag is enclosed "
+ "within the %form() tag from this library.")
+%>
11 formhelpers2/templates/thanks.mako
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+ <title>Mako Form Helpers</title>
+ <link rel="stylesheet" href="${request.static_url('formhelpers2:static/style.css')}" type="text/css" />
+</head>
+<body>
+Thanks for your comment, ${name} !
+</body>
+</html>
16 formhelpers2/tests.py
@@ -0,0 +1,16 @@
+import unittest
+
+from pyramid import testing
+
+class ViewTests(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def test_my_view(self):
+ from formhelpers2.views import comment
+ request = testing.DummyRequest()
+ info = comment(request)
+ self.assertTrue(hasattr(info['forms'], 'comment_form'))
42 formhelpers2/views.py
@@ -0,0 +1,42 @@
+import colander
+from pyramid.renderers import render_to_response
+from pyramid.view import view_config
+
+from formhelpers2.mako import Form
+
+HEARD_CHOICES = [
+ ('internet', 'the internet'),
+ ('friend', 'from a friend'),
+ ('radio', 'on the radio (really?)'),
+]
+
+
+class CommentForm(colander.MappingSchema):
+ name = colander.SchemaNode(
+ colander.String(),
+ default='')
+
+ heard = colander.SchemaNode(
+ colander.String(),
+ validator=colander.OneOf([c[0] for c in HEARD_CHOICES]),
+ default='internet')
+
+ comment = colander.SchemaNode(
+ colander.String(),
+ default='')
+
+
+@view_config(renderer='comment.mako')
+def comment(request):
+ form = Form(CommentForm(), 'comment_form')
+ if 'comment' in request.POST:
+ try:
+ appstruct = form.validate(request.POST)
+ return render_to_response('thanks.mako',
+ dict(name=appstruct['name']),
+ request=request)
+ except colander.Invalid, e:
+ return dict(forms=form.render(request.POST, e.asdict()),
+ heard_choices=HEARD_CHOICES)
+ return dict(forms=form.render(),
+ heard_choices=HEARD_CHOICES)
50 production.ini
@@ -0,0 +1,50 @@
+[app:main]
+use = egg:formhelpers2
+
+pyramid.reload_templates = false
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.debug_templates = false
+pyramid.default_locale_name = en
+
+mako.preprocessor = formhelpers2:mako.process_tags
+mako.directories = formhelpers2:templates
+mako.module_directory = %(here)s/data/templates
+mako.strict_undefined = true
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 6543
+
+# Begin logging configuration
+
+[loggers]
+keys = root, formhelpers2
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_formhelpers2]
+level = WARN
+handlers =
+qualname = formhelpers2
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+
+# End logging configuration
28 setup.cfg
@@ -0,0 +1,28 @@
+[nosetests]
+match = ^test
+nocapture = 1
+cover-package = formhelpers2
+with-coverage = 1
+cover-erase = 1
+nocapture = true
+
+[compile_catalog]
+directory = formhelpers2/locale
+domain = formhelpers2
+statistics = true
+
+[extract_messages]
+add_comments = TRANSLATORS:
+output_file = formhelpers2/locale/formhelpers2.pot
+width = 80
+
+[init_catalog]
+domain = formhelpers2
+input_file = formhelpers2/locale/formhelpers2.pot
+output_dir = formhelpers2/locale
+
+[update_catalog]
+domain = formhelpers2
+input_file = formhelpers2/locale/formhelpers2.pot
+output_dir = formhelpers2/locale
+previous = true
41 setup.py
@@ -0,0 +1,41 @@
+import os
+
+from setuptools import setup, find_packages
+
+here = os.path.abspath(os.path.dirname(__file__))
+README = open(os.path.join(here, 'README.rst')).read()
+CHANGES = open(os.path.join(here, 'CHANGES.rst')).read()
+
+requires = [
+ 'colander',
+ 'pyramid',
+ 'pyramid_debugtoolbar',
+ 'WebHelpers',
+ ]
+
+setup(name='formhelpers2',
+ version='0.0',
+ description='formhelpers2',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ "Programming Language :: Python",
+ "Framework :: Pylons",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
+ author='',
+ author_email='',
+ url='',
+ keywords='web pyramid pylons',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=requires,
+ tests_require=requires,
+ test_suite="formhelpers2",
+ entry_points = """\
+ [paste.app_factory]
+ main = formhelpers2:main
+ """,
+ paster_plugins=['pyramid'],
+ )

0 comments on commit 08a59ac

Please sign in to comment.
Something went wrong with that request. Please try again.