Commits

Ralph Bean committed e9c1987

Initial fork from tw2.forms

Comments (0)

Files changed (60)

+recursive-include tw2/forms/templates *
+recursive-include tw2/forms/static *

docs/appearance.rst

+Customising the Form
+====================
+
+You can change the form structure by editing the definition in the code. The widget browser shows all the widgets and options available in tw2.forms.
+
+To customise the appearance of the form, we need to use a stylesheet. Create ``myapp.css`` with this content::
+
+    h1 { color: red; }
+    th { font-weight: normal; }
+    tr.even { background-color: yellow; }
+    .required th { font-weight: bold; }
+    .error span { font-weight: bold; color: red; }
+
+Edit ``myapp.py`` and add to the ``TestPage`` class::
+
+    resources = [twc.CSSLink(filename='myapp.css')]
+
+When you view the form, you should see this:
+
+.. image:: tut2.png
+
+To customise the layout of the page, we can use our own template for the ``Page`` widget. Create ``mypage.html`` with this content::
+
+    <html>
+        <head><title>$w.title</title></head>
+        <body>
+            <h1>$w.title</h1>
+            This is some custom text
+            ${w.child.display()}
+        </body>
+    </html>
+
+Edit ``myapp.py`` and add to the ``TestPage`` class::
+
+    template = 'genshi:%s/myapp.html' % os.getcwd()
+
+The page should look this this:
+
+.. image:: tut3.png
+
+With these three techniques to use, you should be able to customise the form in almost any way you need. I encourage you to experiment with this for a while, before continuing with the tutorial. We will later cover validation, and creating your own widgets.
+
+.. note:: The tutorial used a simple approach for referring to the CSS and template files. This would not usually be used in a real application. See the design document for more information.
+
+# -*- coding: utf-8 -*-
+#
+# tw2.forms documentation build configuration file, created by
+# sphinx-quickstart on Thu Jun 25 14:20:18 2009.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# The contents of this file are pickled, so don't put values in the namespace
+# that aren't pickleable (module imports are okay, they're removed automatically).
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+# If your extensions are in another directory, add it here. If the directory
+# is relative to the documentation root, use os.path.abspath to make it
+# absolute, like shown here.
+#sys.path.append(os.path.abspath('.'))
+
+# General configuration
+# ---------------------
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['.templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'tw2.forms'
+copyright = u'2009, Paul Johnston, Alberto Valverde & Contributors'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '2.0.0a1'
+# The full version, including alpha/beta/rc tags.
+release = '2.0.0a1'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of documents that shouldn't be included in the build.
+#unused_docs = []
+
+# List of directories, relative to source directory, that shouldn't be searched
+# for source files.
+exclude_trees = ['.build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+
+# Options for HTML output
+# -----------------------
+
+# The style sheet to use for HTML and HTML Help pages. A file of that name
+# must exist either in Sphinx' static/ path, or in one of the custom paths
+# given in html_static_path.
+html_style = 'default.css'
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['.static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_use_modindex = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, the reST sources are included in the HTML build as _sources/<name>.
+#html_copy_source = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = ''
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'tw2formsdoc'
+
+
+# Options for LaTeX output
+# ------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, document class [howto/manual]).
+latex_documents = [
+  ('index', 'tw2forms.tex', ur'tw2.forms Documentation',
+   ur'Paul Johnston, Alberto Valverde & Contributors', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_use_modindex = True
+tw2.forms with Elixir
+=====================
+
+Elixir is a ORM; a library for interfacing with the database. This tutorial shows how to use tw2.forms with Elixir. It builds on standalone.py - TBD
+
+
+Model
+-----
+
+The first step is to define the database tables. We'll use Elixir as our object-relational mapper; this is an active record style ORM that builds on SQLAlchemy. Add the following to ``myapp.py``::
+
+    import elixir as el
+    el.metadata.connect('sqlite:///myapp.db')
+    el.options_defaults['shortnames'] = True
+
+    class People(el.Entity):
+        name = el.Field(el.String(100))
+        email = el.Field(el.String(100))
+        def __str__(self):
+            return self.name
+
+    class Status(el.Entity):
+        name = el.Field(el.String(100))
+        def __str__(self):
+            return self.name
+
+    class Order(el.Entity):
+        name = el.Field(el.String(100))
+        status = el.ManyToOne(Status)
+        customer = el.ManyToOne(People)
+        assignee = el.ManyToOne(People)
+        delivery = el.Field(el.Boolean)
+        address = el.Field(el.String(200))
+
+    el.setup_all()
+
+The next step is to actually create the database tables, and some test data. In the python interpreter, issue::
+
+    >>> from myapp2 import *
+    >>> el.metadata.create_all()
+
+    >>> jb = People(name='Joe Bloggs')
+    >>> jd = People(name='Jane Doe')
+    >>> sp = Status(name='Pending')
+    >>> sd = Status(name='Dispatched')
+    >>> Order(name='Garden furniture', status=sp, customer=jb, assignee=jd)
+    >>> Order(name='Barbeque', status=sd, customer=jd, assignee=jb)
+    >>> el.session.commit()
+
+
+Front Page
+----------
+
+The front page of the application needs to be a list of orders, so we can update the ``Index`` class as follows::
+
+    class Index(twc.Page):
+        class child(twf.GridLayout):
+            name = twf.LabelField()
+            status = twf.LabelField()
+            customer = twf.LabelField()
+            assignee = twf.LabelField()
+
+        def fetch_data(self, req):
+            self.value = Order.query.all()
+
+With all this done, restart the application, refresh the browser page, and you'll see the list of orders.
+
+
+Form Editing
+------------
+
+Users need to be able to click on an order to get further information. We'll build an inital version of the detail form using ToscaWidgets. Add the following to ``myapp.py``::
+
+    class OrderForm(twf.FormPage):
+        title = 'Order'
+        class child(twd.CustomisedForm):
+            class child(twd.HidingTableLayout):
+                id = twf.HiddenField()
+                name = twf.TextField()
+                status_id = twf.SingleSelectField(options=[str(r) for r in Status.query.all()])
+                customer_id = twf.SingleSelectField(options=[str(r) for r in People.query.all()])
+                assignee_id = twf.SingleSelectField(options=[str(r) for r in People.query.all()])
+                delivery = twf.CheckBox()
+                address = twf.TextArea()
+
+        def fetch_data(self, req):
+            self.value = Order.query.get(req.GET['id'])
+
+    mw.controllers.register(OrderForm, 'order')
+
+Users will need a link from the front page to the edit page. Update the ``Index`` class and add, at the beginning::
+
+    id = twf.LinkField(link='order?id=$', text='Edit', label=None)
+
+Have a look at this in your browser - you will now be able to navigate from the order list, to the order editing form. To make the form save when you click "submit", add the following to the ``Order`` class::
+
+    @classmethod
+    def validated_request(cls, req, data):
+        Order.query.get(id).from_dict(data)
+        # TBD: redirect
+
+You can now use your browser to edit orders in the system. This arrangement provides the basis for a highly functional system. In particular, validation can easily be added, with the error messages reported in a user-friendly way. It's also easy to adapt this to form a "create new order" function.
+.. tw2.forms documentation master file, created by sphinx-quickstart on Thu Jun 25 14:20:18 2009.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+tw2.forms
+=========
+
+This is a package of ToscaWidgets for creating forms.
+
+
+Contents:
+
+.. toctree::
+   :maxdepth: 2
+
+   validation
+   elixir
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+

docs/validation.rst

+Validation
+----------
+
+We can configure validation on form fields like this::
+
+    class child(twf.TableForm):
+        name = twf.TextField(validator=twc.Required)
+        group = twf.SingleSelectField(options=['', 'Red', 'Green', 'Blue'])
+        notes = twf.TextArea(validator=twc.StringLengthValidator(min=10))
+
+To enable validation we also need to modify the application to handle POST requests::
+
+    def app(environ, start_response):
+        req = wo.Request(environ)
+        resp = wo.Response(request=req, content_type="text/html; charset=UTF8")
+        if req.method == 'GET':
+            resp.body = MyForm.display().encode('utf-8')
+        elif req.method == 'POST':
+            try:
+                data = MyForm.validate(req.POST)
+                resp.body = 'Posted successfully ' + wo.html_escape(repr(data))
+            except twc.ValidationError, e:
+                resp.body = e.widget.display().encode('utf-8')
+        return resp(environ, start_response)
+
+If you submit the form with some invalid fields, you should see this:
+
+
+
+**Whole Form Message**
+
+If you want to display a message at the top of the form, when there are any errors, define the following validator::
+
+    class MyFormValidator(twc.Validator):
+        msgs = {
+            'childerror': ('form_childerror', 'There were problems with the details you entered. Review the messages below to correct your submission.'),
+        }
+
+And in your form::
+
+    validator = MyFormValidator()

examples/appearance.css

+h1 { color: red; }
+th { font-weight: normal; }
+tr.even { background-color: yellow; }
+.required th { font-weight: bold; }
+.error span { font-weight: bold; color: red; }

examples/appearance.html

+<html>
+    <head><title>$w.title</title></head>
+    <body>
+        <h1>$w.title</h1>
+        This is some custom text
+        ${w.child.display()}
+    </body>
+</html>

examples/appearance.py

+"""
+This app is the end result of the "customising appearance" tutorial
+"""
+import tw2.core as twc, tw2.forms as twf, os
+
+class Index(twf.FormPage):
+    title = 'tw2.forms Customising Appearance'
+    resources = [twc.CSSLink(filename='appearance.css')]
+    template = 'genshi:%s/appearance.html' % os.getcwd()
+    class child(twf.TableForm):
+        name = twf.TextField(validator=twc.Required)
+        group = twf.SingleSelectField(options=['Red', 'Green', 'Blue'])
+        notes = twf.TextArea()
+        submit = twf.SubmitButton(value='Go!')
+
+if __name__ == '__main__':
+    import wsgiref.simple_server as wrs
+    wrs.make_server('', 8000, twc.make_middleware(controller_prefix='/')).serve_forever()

examples/datagrid.py

+"""
+A data grid can be achieved using a GridLayout that contains LabelField widgets.
+"""
+import tw2.core as twc, tw2.forms as twf
+
+class Index(twc.Page):
+    title = 'Data Grid'
+    class child(twf.GridLayout):
+        extra_reps = 0
+        id = twf.LinkField(link='detail?id=$', text='View', label=None)
+        a = twf.LabelField()
+        b = twf.LabelField()
+
+    def fetch_data(self, req):
+        self.value = [{'id':1, 'a':'paj','b':'bob'}, {'id':2, 'a':'joe','b':'jill'}]
+
+if __name__ == '__main__':
+    import wsgiref.simple_server as wrs
+    wrs.make_server('', 8000, twc.make_middleware(controller_prefix='/')).serve_forever()

examples/deep_children.py

+import tw2.core as twc, tw2.forms as twf
+
+opts = ['Red', 'Yellow', 'Green', 'Blue']
+
+class Index(twf.FormPage):
+    title = 'tw2.forms Deep Children'
+    class child(twf.Form):
+        class child(twc.CompoundWidget):
+            class noname(twf.TableFieldSet):
+                id = None
+                legend = 'Contact Info'
+                name = twf.TextField()
+                email = twf.TextField(validator=twc.EmailValidator())
+            class noname2(twf.TableFieldSet):
+                id = None
+                legend = 'Work Info'
+                job_title = twf.TextField(validator=twc.Required)
+                location = twf.TextField()
+
+if __name__ == "__main__":
+    import wsgiref.simple_server as wrs
+    wrs.make_server('', 8000, twc.make_middleware(controller_prefix='/')).serve_forever()

examples/elixir.py

+import tw2.core as twc, tw2.forms as twf
+
+import elixir as el
+el.metadata.bind = 'sqlite:///elixir.db'
+el.options_defaults['shortnames'] = True
+
+
+class People(el.Entity):
+    name = el.Field(el.String(100))
+    email = el.Field(el.String(100))
+    def __str__(self):
+        return self.name
+
+class Status(el.Entity):
+    name = el.Field(el.String(100))
+    def __str__(self):
+        return self.name
+
+class Order(el.Entity):
+    name = el.Field(el.String(100))
+    status = el.ManyToOne(Status)
+    customer = el.ManyToOne(People)
+    assignee = el.ManyToOne(People)
+    delivery = el.Field(el.Boolean)
+    address = el.Field(el.String(200))
+    items = el.OneToMany('Item')
+
+class Item(el.Entity):
+    order = el.ManyToOne(Order)
+    code = el.Field(el.String(50))
+    description = el.Field(el.String(200))
+
+el.setup_all()
+
+
+
+class Index(twc.Page):
+    title = 'Orders'
+    class child(twf.GridLayout):
+        id = twf.LinkField(link='order?id=$', text='Edit', label=None)
+        name = twf.LabelField()
+        status = twf.LabelField()
+        customer = twf.LabelField()
+        assignee = twf.LabelField()
+
+    def fetch_data(self, req):
+        self.value = Order.query.all()
+
+
+class OrderForm(twf.FormPage):
+    title = 'Order'
+    class child(twf.Form):
+        class child(twf.TableLayout):
+            id = twf.HiddenField()
+            name = twf.TextField()
+            status_id = twf.SingleSelectField(options=[str(r) for r in Status.query.all()])
+            customer_id = twf.SingleSelectField(options=[str(r) for r in People.query.all()])
+            assignee_id = twf.SingleSelectField(options=[str(r) for r in People.query.all()])
+            delivery = twf.CheckBox()
+            address = twf.TextArea()
+
+    def fetch_data(self, req):
+        self.value = Order.query.get(req.GET['id'])
+
+
+if __name__ == '__main__':
+    import wsgiref.simple_server as wrs
+    wrs.make_server('', 8000, twc.make_middleware(controller_prefix='/')).serve_forever()

examples/grid_validation.py

+import tw2.core as twc, tw2.forms as twf
+
+class Index(twf.FormPage):
+    title = 'GridLayout Validation'
+    class child(twf.Form):
+        class child(twf.GridLayout):
+            repetitions = 5
+            name = twf.TextField(validator=twc.Required)
+            email = twf.TextField(validator=twc.EmailValidator())
+
+if __name__ == '__main__':
+    import wsgiref.simple_server as wrs
+    wrs.make_server('', 8000, twc.make_middleware(controller_prefix='/')).serve_forever()

examples/validation.py

+import tw2.core as twc, tw2.forms as twf
+
+opts = ['Red', 'Yellow', 'Green', 'Blue']
+
+class Index(twf.FormPage):
+    title = 'tw2.forms Validation'
+    class child(twf.Form):
+        class child(twf.TableLayout):
+            file = twf.FileField(validator=twf.FileValidator(required=True, extention='.html'))
+            email = twf.TextField(validator=twc.EmailValidator(required=True))
+#            confirm_email = twf.TextField()
+
+            class fred(twf.GridLayout):
+                repetitions = 3
+                class child(twf.RowLayout):
+                    bob = twf.TextField()
+                    rob = twf.TextField()
+                    validator = twc.MatchValidator('bob', 'rob')
+
+            select = twf.SingleSelectField(options=list(enumerate(opts)), validator=twc.Validator(required=True), item_validator=twc.IntValidator())
+#            msel = twf.MultipleSelectField(options=list(enumerate(opts)), validator=twc.Required, item_validator=twc.IntValidator())
+#            cbl = twf.CheckBoxList(options=list(enumerate(opts)), validator=twc.Required, item_validator=twc.IntValidator())
+#            rbl = twf.RadioButtonList(options=list(enumerate(opts)), validator=twc.Required, item_validator=twc.IntValidator())
+#            validator = twc.MatchValidator('email', 'confirm_email')
+#            a = twf.CheckBox(validator=twc.BoolValidator(required=True))
+#            b = twf.FileField()
+#            x = twf.TextField(validator=fe.validators.Regex('^\w+$'))
+
+if __name__ == "__main__":
+    twc.dev_server()
+[egg_info]
+tag_build = dev
+tag_date = true
+
+[nosetests]
+where = tests
+from setuptools import setup, find_packages
+
+setup(
+    name='tw2.jit',
+    version='0.1a1',
+    description='',
+    author='Ralph Bean',
+    author_email='ralph.bean@gmail.com',
+    url='',
+    install_requires=[
+        "tw2.core",
+        ## Add other requirements here
+        # "Genshi",
+        ],
+    packages=find_packages(exclude=['ez_setup', 'tests']),
+    namespace_packages = ['tw2'],
+    zip_safe=False,
+    include_package_data=True,
+    test_suite = 'nose.collector',
+    entry_points="""
+        [tw2.widgets]
+        # Register your widgets so they can be listed in the WidgetBrowser
+        tw2.jit = tw2.jit
+    """,
+    keywords = [
+        'toscawidgets.widgets',
+    ],
+    tests_require = ['BeautifulSoup'],
+    classifiers = [
+        'Development Status :: 1 - Alpha',
+        'Environment :: Web Environment',
+        'Environment :: Web Environment :: ToscaWidgets',
+        'Topic :: Software Development :: Libraries :: Python Modules',
+        'Topic :: Software Development :: Widget Sets',
+        'Intended Audience :: Developers',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+    ],
+)

tests/__init__.py

Empty file added.

tests/test_widgets.py

+from tw2.forms.widgets import *
+from webob import Request
+from tw2.core.testbase import assert_in_xml, assert_eq_xml, WidgetTest
+from nose.tools import raises
+from cStringIO import StringIO
+from tw2.core import EmptyField, IntValidator, ValidationError
+from cgi import FieldStorage
+
+import webob
+if hasattr(webob, 'NestedMultiDict'):
+    from webob import NestedMultiDict
+else:
+    from webob.multidict import NestedMultiDict
+
+class TestInputField(WidgetTest):
+    widget = InputField
+    attrs = {'type':'foo', 'css_class':'something'}
+    params = {'value':6}
+    expected = '<input type="foo" class="something" value="6"/>'
+
+class TestTextField(WidgetTest):
+    widget = TextField
+    attrs = {'css_class':'something', 'size':'60'}
+    params = {'value':6}
+    expected = '<input type="text" class="something" value="6" size="60"/>'
+
+class TestTextArea(WidgetTest):
+    widget = TextArea
+    attrs = {'css_class':'something', 'rows':6, 'cols':10}
+    params = {'value':'6'}
+    expected = '<textarea class="something" rows="6" cols="10">6</textarea>'
+    
+class TestCheckbox(WidgetTest):
+    widget = CheckBox
+    attrs = {'css_class':'something'}
+    params = {'value':True}
+    expected = '<input checked="checked" value="True" type="checkbox" class="something"/>'
+    
+    def test_value_false(self):
+        params = {'value':False}
+        expected = '<input value="False" type="checkbox" class="something">'
+        for engine in self._get_all_possible_engines():
+            yield self._check_rendering_vs_expected, engine, self.attrs, params, expected
+
+class TestRadioButton(WidgetTest):
+    widget = RadioButton
+    attrs = {'css_class':'something'}
+    params = {'checked':None}
+    expected = '<input type="radio" class="something"/>'
+
+    def test_checked(self):
+        params = {'checked':True}
+        expected = '<input checked="checked" type="radio" class="something">'
+        for engine in self._get_all_possible_engines():
+            yield self._check_rendering_vs_expected, engine, self.attrs, params, expected
+
+class TestPasswordField(WidgetTest):
+    widget = PasswordField
+    attrs = {'css_class':'something', 'id':'hid'}
+    expected = '<input type="password" class="something" id="hid" name="hid"/>'
+    validate_params = [[None, {'hid':'b'}, 'b']]
+
+    def test_no_value(self):
+        params = {'value':'something'}
+        for engine in self._get_all_possible_engines():
+            yield self._check_rendering_vs_expected, engine, self.attrs, params, self.expected
+
+class TestFileField(WidgetTest):
+    widget = FileField
+    attrs = {'css_class':'something', 'id':'hid', 'validator':FileValidator(extension="bdb", required=True)}
+    expected = '<input id="hid" type="file" class="something" name="hid"/>'
+    dummy_file = FieldStorage(StringIO(''))
+    dummy_file.filename = 'something.ext'
+    validate_params = [[None, {'hid':'b'}, None, ValidationError], [None, {'hid':dummy_file}, None, ValidationError]]
+
+class TestHiddenField(WidgetTest):
+    widget = HiddenField
+    attrs = {'css_class':'something', 'value':'info', 'name':'hidden_name', 'id':'hid'}
+    expected = '<input class="something" type="hidden" id="hid" value="info" name="hidden_name">'
+    validate_params = [[None, {'hid':'b'}, EmptyField]]
+
+class TestLabelField(WidgetTest):
+    widget = LabelField
+    attrs = {'css_class':'something', 'value':'info', 'name':'hidden_name', 'id':'hid'}
+    expected = '<span>info<input class="something" type="hidden" value="info" name="hidden_name" id="hid"/></span>'
+    validate_params = [[None, {'hid':'b'}, EmptyField]]
+
+class TestLinkField(WidgetTest):
+    widget = LinkField
+    attrs = {'css_class':'something', 'value':'info', 'name':'hidden_name', 'text':'some $', 'link':'/some/$'}
+    expected = '<a href="/some/info" class="something">some info</a>'
+    
+class TestButton(WidgetTest):
+    widget = Button
+    attrs = {'css_class':'something', 'value':'info', 'name':'hidden_name'}
+    expected = '<input class="something" type="button" value="info" name="hidden_name">'
+
+class TestSubmitButton(WidgetTest):
+    widget = SubmitButton
+    attrs = {'css_class':'something', 'value':'info', 'name':'hidden_name'}
+    expected = '<input class="something" type="submit" value="info" name="hidden_name">'
+
+class TestResetButton(WidgetTest):
+    widget = ResetButton
+    attrs = {'css_class':'something', 'value':'info', 'name':'hidden_name'}
+    expected = '<input class="something" type="reset" value="info" name="hidden_name">'
+
+class TestImageButton(WidgetTest):
+    widget = ImageButton
+    attrs = {'css_class':'something', 'value':'info', 'name':'hidden_name', 'link':'/somewhere.gif'}
+    expected = '<input src="/somewhere.gif" name="hidden_name" value="info" alt="" type="image" class="something">'
+
+class TestSingleSelectField(WidgetTest):
+    widget = SingleSelectField
+    attrs = {'css_class':'something', 
+             'options':((1, 'a'), (2, 'b'), (3, 'c')), 'id':'hid',
+             'validator':IntValidator(),
+             }
+    expected = """<select class="something" id="hid" name="hid">
+                        <option></option>
+                        <option value="1">a</option>
+                        <option value="2">b</option>
+                        <option value="3">c</option>
+                  </select>"""
+    validate_params = [[None, {'hid':''}, None],[None, {'hid':'1'}, 1]]
+
+    def test_option_group(self):
+        expected = """<select class="something">
+                          <option></option>
+                          <optgroup label="group">
+                              <option value=""></option>
+                              <option value="1">Red</option>
+                              <option value="2">Blue</option>
+                          </optgroup>
+                          <optgroup label="group2">
+                              <option value=""></option>
+                              <option value="Pink">Pink</option>
+                              <option value="Yellow">Yellow</option>
+                          </optgroup>
+                      </select>"""
+        attrs = {'css_class':'something', 'options':[('group', ['', (1, 'Red'), (2, 'Blue')]),
+                                                     ('group2', ['', 'Pink', 'Yellow'])]}
+        for engine in self._get_all_possible_engines():
+            yield self._check_rendering_vs_expected, engine, attrs, self.params, expected
+
+    def test_option_no_values(self):
+        expected = """<select class="something">
+                         <option></option>
+                         <option value="a">a</option>
+                         <option value="b">b</option>
+                         <option value="c">c</option>
+                      </select>"""
+        attrs = {'css_class':'something', 'options':('a', 'b','c')}
+        for engine in self._get_all_possible_engines():
+            yield self._check_rendering_vs_expected, engine, attrs, self.params, expected
+
+    def test_prompt_text(self):
+        expected = """<select>
+             <option >Pick one:</option>
+             <option value="a">a</option>
+             <option value="b">b</option>
+             <option value="c">c</option>
+            </select>"""
+        attrs = {'options':('a','b','c'), 'prompt_text':'Pick one:'}
+        for engine in self._get_all_possible_engines():
+            yield self._check_rendering_vs_expected, engine, attrs, self.params, expected
+
+
+class TestMultipleSelectField(WidgetTest):
+    widget = MultipleSelectField
+    attrs = {'css_class':'something', 'options':(('a',1), ('b', 2), ('c', 3)), 'id':"hid"}
+    expected = """<select class="something" multiple="multiple" id="hid" name="hid">
+                      <option value="a">1</option>
+                      <option value="b">2</option>
+                      <option value="c">3</option>
+                  </select>"""
+    validate_params = [[None, {'hid':'b'}, [u'b']]]
+
+class TestSelectionList(WidgetTest):
+    widget = SelectionList
+    attrs = {'css_class':'something', 'field_type':'test', 'options':(('a',1), ('b', 2), ('c', 3)), 'id':'something'}
+    expected = """<ul class="something" id="something" name="something">
+    <li>
+        <input type="test" name="something" value="a" id="something:0">
+        <label for="something:0">1</label>
+    </li><li>
+        <input type="test" name="something" value="b" id="something:1">
+        <label for="something:1">2</label>
+    </li><li>
+        <input type="test" name="something" value="c" id="something:2">
+        <label for="something:2">3</label>
+    </li>
+</ul>"""
+
+
+class TestRadioButtonList(WidgetTest):
+    widget = RadioButtonList
+    attrs = {'css_class':'something', 'options':(('a',1), ('b', 2), ('c', 3)), 'id':'something'}
+    expected = """<ul class="something" id="something" name="something">
+    <li>
+        <input type="radio" name="something" value="a" id="something:0">
+        <label for="something:0">1</label>
+    </li><li>
+        <input type="radio" name="something" value="b" id="something:1">
+        <label for="something:1">2</label>
+    </li><li>
+        <input type="radio" name="something" value="c" id="something:2">
+        <label for="something:2">3</label>
+    </li>
+</ul>"""
+
+class TestCheckBoxList(WidgetTest):
+    widget = CheckBoxList
+    attrs = {'css_class':'something', 'options':(('a',1), ('b', 2), ('c', 3)), 'id':'something'}
+    expected = """<ul class="something" id="something" name="something">
+    <li>
+        <input type="checkbox" name="something" value="a" id="something:0">
+        <label for="something:0">1</label>
+    </li><li>
+        <input type="checkbox" name="something" value="b" id="something:1">
+        <label for="something:1">2</label>
+    </li><li>
+        <input type="checkbox" name="something" value="c" id="something:2">
+        <label for="something:2">3</label>
+    </li>
+</ul>
+"""
+    def test_option_has_value(self):
+        expected = """<ul class="something" id="something" name="something">
+    <li>
+        <input type="checkbox" name="something" value="a" id="something:0" checked>
+        <label for="something:0">a</label>
+    </li><li>
+        <input type="checkbox" name="something" value="b" id="something:1">
+        <label for="something:1">b</label>
+    </li><li>
+        <input type="checkbox" name="something" value="c" id="something:2">
+        <label for="something:2">c</label>
+    </li>
+</ul>"""
+        attrs = {'css_class':'something', 'options':('a', 'b','c'), 'id':'something'}
+        params = {'value':'a',}
+        for engine in self._get_all_possible_engines():
+            yield self._check_rendering_vs_expected, engine, attrs, params, expected
+
+class TestSelectionTable(WidgetTest):
+    widget = SelectionTable
+    attrs = {'css_class':'something', 'field_type':'test', 'options':(('a',1), ('b', 2), ('c', 3)), 'id':'something'}
+    expected = """<table class="something" id="something" name="something">
+    <tbody>
+    <tr>
+        <td>
+            <input type="test" name="something" value="a" id="something:0">
+            <label for="something:0">1</label>
+        </td>
+    </tr><tr>
+        <td>
+            <input type="test" name="something" value="b" id="something:1">
+            <label for="something:1">2</label>
+        </td>
+    </tr><tr>
+        <td>
+            <input type="test" name="something" value="c" id="something:2">
+            <label for="something:2">3</label>
+        </td>
+    </tr>
+    </tbody>
+</table>"""
+
+    def test_option_leftover_chunk(self):
+        expected = """<table class="something" id="something" name="something">
+    <tbody>
+    <tr>
+        <td>
+            <input type="test" checked name="something" value="a" id="something:0">
+            <label for="something:0">a</label>
+        </td><td>
+            <input type="test" name="something" value="b" id="something:1">
+            <label for="something:1">b</label>
+        </td>
+    </tr><tr>
+        <td>
+            <input type="test" name="something" value="c" id="something:2">
+            <label for="something:2">c</label>
+        </td>
+        <td></td>
+    </tr>
+    </tbody>
+</table>"""
+        attrs = {'css_class':'something', 'field_type':'test', 'cols':2, 'options':(('group1', ('a', 'b')),'c'), 'id':'something'}
+        params = {'value':'a',}
+        for engine in self._get_all_possible_engines():
+            yield self._check_rendering_vs_expected, engine, attrs, params, expected
+
+class TestRadioButtonTable(WidgetTest):
+    widget = RadioButtonTable
+    attrs = {'css_class':'something', 'options':(('a',1), ('b', 2), ('c', 3)), 'id':'something'}
+    expected = """<table class="something" id="something" name="something">
+    <tbody>
+    <tr>
+        <td>
+            <input type="radio" name="something" value="a" id="something:0">
+            <label for="something:0">1</label>
+        </td>
+    </tr><tr>
+        <td>
+            <input type="radio" name="something" value="b" id="something:1">
+            <label for="something:1">2</label>
+        </td>
+    </tr><tr>
+        <td>
+            <input type="radio" name="something" value="c" id="something:2">
+            <label for="something:2">3</label>
+        </td>
+    </tr>
+    </tbody>
+</table>"""
+
+class TestCheckBoxTable(WidgetTest):
+    widget = CheckBoxTable
+    attrs = {'css_class':'something', 'options':(('a',1), ('b', 2), ('c', 3)), 'id':'something'}
+    expected = """<table class="something" id="something" name="something">
+    <tbody>
+    <tr>
+        <td>
+            <input type="checkbox" name="something" value="a" id="something:0">
+            <label for="something:0">1</label>
+        </td>
+    </tr><tr>
+        <td>
+            <input type="checkbox" name="something" value="b" id="something:1">
+            <label for="something:1">2</label>
+        </td>
+    </tr><tr>
+        <td>
+            <input type="checkbox" name="something" value="c" id="something:2">
+            <label for="something:2">3</label>
+        </td>
+    </tr>
+    </tbody>
+</table>"""
+    
+
+class TestListLayout(WidgetTest):
+    widget = ListLayout
+    attrs = {'children': [TextField(id='field1'),
+                          TextField(id='field2'),
+                          TextField(id='field3')]}
+    expected = """\
+<ul>
+    <li class="odd">
+        <label>Field1</label>
+        <input name="field1" id="field1" type="text">
+        <span id="field1:error" class="error"></span>
+    </li><li class="even">
+        <label>Field2</label>
+        <input name="field2" id="field2" type="text">
+        <span id="field2:error" class="error"></span>
+    </li><li class="odd">
+        <label>Field3</label>
+        <input name="field3" id="field3" type="text">
+        <span id="field3:error" class="error"></span>
+    </li>
+    <li class="error"><span id=":error" class="error"></span></li>
+</ul>"""
+    declarative = True
+    
+
+class TestTableLayout(WidgetTest):
+    widget = TableLayout
+    attrs = {'children': [TextField(id='field1'),
+                          TextField(id='field2'),
+                          TextField(id='field3')]}
+    expected = """<table>
+    <tr class="odd" id="field1:container">
+        <th>Field1</th>
+        <td>
+            <input name="field1" id="field1" type="text">
+            <span id="field1:error"></span>
+        </td>
+    </tr><tr class="even" id="field2:container">
+        <th>Field2</th>
+        <td>
+            <input name="field2" id="field2" type="text">
+            <span id="field2:error"></span>
+        </td>
+    </tr><tr class="odd" id="field3:container">
+        <th>Field3</th>
+        <td>
+            <input name="field3" id="field3" type="text">
+            <span id="field3:error"></span>
+        </td>
+    </tr>
+    <tr class="error"><td colspan="2">
+        <span id=":error"></span>
+    </td></tr>
+</table>"""
+    declarative = True
+
+class TestRowLayout(WidgetTest):
+    widget = RowLayout
+    attrs = {'children': [TextField(id='field1'),
+                          TextField(id='field2'),
+                          TextField(id='field3')],
+             'repetition': 1,
+             }
+    expected = """<tr class="even">
+    <td>
+        <input name="field1" id="field1" type="text">
+    </td><td>
+        <input name="field2" id="field2" type="text">
+    </td><td>
+        <input name="field3" id="field3" type="text">
+    </td>
+    <td>
+    </td>
+</tr>"""
+    declarative = True
+
+class TestGridLayout(WidgetTest):
+    widget = GridLayout
+    attrs = {'children': [TextField(id='field1'),
+                          TextField(id='field2'),
+                          TextField(id='field3')],
+             'repetition': 1,
+             }
+    expected = """<table>
+    <tr><th>Auto</th><th>Auto</th><th>Auto</th></tr>
+    <tr class="error"><td colspan="0" id=":error">
+    </td></tr>
+</table>"""
+    declarative = True
+
+class TestSpacer(WidgetTest):
+    widget = Spacer
+    attrs = {}
+    expected = """<div></div>"""
+
+class TestLabel(WidgetTest):
+    widget = Label
+    attrs = {'text':'something'}
+    expected = """<span>something</span>"""
+
+class TestTableForm(WidgetTest):
+    widget = TableForm
+    attrs = {'field1':TextField(id='field1'),
+             'field2':TextField(id='field2'),
+             'field3':TextField(id='field3'),
+             }
+    expected = """<form method="post" enctype="multipart/form-data">
+     <span class="error"></span>
+    <table>
+    <tr class="odd" id="field1:container">
+        <th>Field1</th>
+        <td>
+            <input name="field1" id="field1" type="text">
+            <span id="field1:error"></span>
+        </td>
+    </tr><tr class="even" id="field2:container">
+        <th>Field2</th>
+        <td>
+            <input name="field2" id="field2" type="text">
+            <span id="field2:error"></span>
+        </td>
+    </tr><tr class="odd" id="field3:container">
+        <th>Field3</th>
+        <td>
+            <input name="field3" id="field3" type="text">
+            <span id="field3:error"></span>
+        </td>
+    </tr>
+    <tr class="error"><td colspan="2">
+        <span id=":error"></span>
+    </td></tr>
+</table>
+    <input type="submit" id="submit" value="Save">
+</form>"""
+    declarative = True
+
+class TestListForm(WidgetTest):
+    widget = ListForm
+    attrs = {'field1':TextField(id='field1'),
+             'field2':TextField(id='field2'),
+             'field3':TextField(id='field3'),
+             }
+    expected = """<form method="post" enctype="multipart/form-data">
+     <span class="error"></span>
+    <ul >
+    <li class="odd">
+     <label>Field1</label>
+        <input name="field1" id="field1" type="text"/>
+        <span id="field1:error" class="error"></span>
+    </li>
+    <li class="even">
+     <label>Field2</label>
+        <input name="field2" id="field2" type="text"/>
+        <span id="field2:error" class="error"></span>
+    </li>
+    <li class="odd">
+     <label>Field3</label>
+        <input name="field3" id="field3" type="text"/>
+        <span id="field3:error" class="error"></span>
+    </li>
+    <li class="error"><span id=":error" class="error"></span></li>
+</ul>
+    <input type="submit" id="submit" value="Save"/>
+</form>"""
+    declarative = True
+
+class TestTableFieldset(WidgetTest):
+    widget = TableFieldSet
+    attrs = {'field1':TextField(id='field1'),
+             'field2':TextField(id='field2'),
+             'field3':TextField(id='field3'),
+             }
+    expected = """<fieldset>
+    <legend></legend>
+    <table>
+    <tr class="odd" id="field1:container">
+        <th>Field1</th>
+        <td>
+            <input name="field1" id="field1" type="text">
+            <span id="field1:error"></span>
+        </td>
+    </tr><tr class="even" id="field2:container">
+        <th>Field2</th>
+        <td>
+            <input name="field2" id="field2" type="text">
+            <span id="field2:error"></span>
+        </td>
+    </tr><tr class="odd" id="field3:container">
+        <th>Field3</th>
+        <td>
+            <input name="field3" id="field3" type="text">
+            <span id="field3:error"></span>
+        </td>
+    </tr>
+    <tr class="error"><td colspan="2">
+        <span id=":error"></span>
+    </td></tr>
+</table>
+</fieldset>"""
+    declarative = True
+
+class TestListFieldset(WidgetTest):
+    widget = ListFieldSet
+    attrs = {'field1':TextField(id='field1'),
+             'field2':TextField(id='field2'),
+             'field3':TextField(id='field3'),
+             }
+    expected = """<fieldset >
+    <legend></legend>
+    <ul >
+    <li class="odd">
+     <label>Field1</label>
+        <input name="field1" id="field1" type="text"/>
+        <span id="field1:error" class="error"></span>
+    </li>
+    <li class="even">
+     <label>Field2</label>
+        <input name="field2" id="field2" type="text"/>
+        <span id="field2:error" class="error"></span>
+    </li>
+    <li class="odd">
+     <label>Field3</label>
+        <input name="field3" id="field3" type="text"/>
+        <span id="field3:error" class="error"></span>
+    </li>
+    <li class="error"><span id=":error" class="error"></span></li>
+</ul>
+</fieldset>"""
+    declarative = True
+
+class TestFormPage(WidgetTest):
+    widget = FormPage
+    attrs = {'child':TableForm(children=[TextField(id='field1'),
+                                         TextField(id='field2'),
+                                         TextField(id='field3'),]),
+             'title':'some title'
+             }
+    expected = """<html>
+<head><title>some title</title></head>
+<body id="mytestwidget:page"><h1>some title</h1><form method="post" id="mytestwidget:form" enctype="multipart/form-data">
+     <span class="error"></span>
+    <table id="mytestwidget">
+    <tr class="odd" id="mytestwidget:field1:container">
+        <th>Field1</th>
+        <td>
+            <input name="mytestwidget:field1" id="mytestwidget:field1" type="text">
+            <span id="mytestwidget:field1:error"></span>
+        </td>
+    </tr><tr class="even" id="mytestwidget:field2:container">
+        <th>Field2</th>
+        <td>
+            <input name="mytestwidget:field2" id="mytestwidget:field2" type="text">
+            <span id="mytestwidget:field2:error"></span>
+        </td>
+    </tr><tr class="odd" id="mytestwidget:field3:container">
+        <th>Field3</th>
+        <td>
+            <input name="mytestwidget:field3" id="mytestwidget:field3" type="text">
+            <span id="mytestwidget:field3:error"></span>
+        </td>
+    </tr>
+    <tr class="error"><td colspan="2">
+        <span id="mytestwidget:error"></span>
+    </td></tr>
+</table>
+    <input type="submit" id="submit" value="Save">
+</form></body>
+</html>"""
+    
+    declarative = True
+    def test_request_get(self):
+        environ = {'REQUEST_METHOD': 'GET',
+                   }
+        req=Request(environ)
+        r = self.widget().request(req)
+        assert_eq_xml(r.body, """<html>
+<head><title>some title</title></head>
+<body id="mytestwidget:page"><h1>some title</h1><form method="post" id="mytestwidget:form" enctype="multipart/form-data">
+     <span class="error"></span>
+    <table id="mytestwidget">
+    <tr class="odd" id="mytestwidget:field1:container">
+        <th>Field1</th>
+        <td>
+            <input name="mytestwidget:field1" id="mytestwidget:field1" type="text">
+            <span id="mytestwidget:field1:error"></span>
+        </td>
+    </tr><tr class="even" id="mytestwidget:field2:container">
+        <th>Field2</th>
+        <td>
+            <input name="mytestwidget:field2" id="mytestwidget:field2" type="text">
+            <span id="mytestwidget:field2:error"></span>
+        </td>
+    </tr><tr class="odd" id="mytestwidget:field3:container">
+        <th>Field3</th>
+        <td>
+            <input name="mytestwidget:field3" id="mytestwidget:field3" type="text">
+            <span id="mytestwidget:field3:error"></span>
+        </td>
+    </tr>
+    <tr class="error"><td colspan="2">
+        <span id="mytestwidget:error"></span>
+    </td></tr>
+</table>
+    <input type="submit" id="submit" value="Save">
+</form></body>
+</html>""")
+
+    def _test_request_post_invalid(self):
+        # i have commented this because the post is in fact 
+        # valid, there are no arguments sent to the post, but the
+        # widget does not require them
+        environ = {'REQUEST_METHOD': 'POST',
+                   'wsgi.input': StringIO(''),
+
+                   }
+        req=Request(environ)
+        r = self.widget().request(req)
+        assert_eq_xml(r.body, """<html>
+<head><title>some title</title></head>
+<body id="mytestwidget:page"><h1>some title</h1><form method="post" id="mytestwidget:form" enctype="multipart/form-data">
+     <span class="error"></span>
+    <table id="mytestwidget">
+    <tr class="odd" id="mytestwidget:field1:container">
+        <th>Field1</th>
+        <td>
+            <input name="mytestwidget:field1" id="mytestwidget:field1" type="text">
+            <span id="mytestwidget:field1:error"></span>
+        </td>
+    </tr><tr class="even" id="mytestwidget:field2:container">
+        <th>Field2</th>
+        <td>
+            <input name="mytestwidget:field2" id="mytestwidget:field2" type="text">
+            <span id="mytestwidget:field2:error"></span>
+        </td>
+    </tr><tr class="odd" id="mytestwidget:field3:container">
+        <th>Field3</th>
+        <td>
+            <input name="mytestwidget:field3" id="mytestwidget:field3" type="text">
+            <span id="mytestwidget:field3:error"></span>
+        </td>
+    </tr>
+    <tr class="error"><td colspan="2">
+        <span id="mytestwidget:error"></span>
+    </td></tr>
+</table>
+    <input type="submit" id="submit" value="Save">
+</form></body>
+</html>""")
+
+    def test_request_post_valid(self):
+        environ = {'wsgi.input': StringIO(''),
+                   }
+        req=Request(environ)
+        req.method = 'POST'
+        req.body='mytestwidget:field1=a&mytestwidget:field2=b&mytestwidget:field3=c'
+        req.environ['CONTENT_LENGTH'] = str(len(req.body)) 
+        req.environ['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
+        
+        self.mw.config.debug = True
+        r = self.widget().request(req)
+        assert r.body == """Form posted successfully {'field2': u'b', 'field3': u'c', 'field1': u'a'}""", r.body
+__import__('pkg_resources').declare_namespace(__name__)

tw2/__init__.pyc

Binary file added.

tw2/forms/__init__.py

+"""
+This package contains the basic form widgets.
+"""
+
+from widgets import (Button, CheckBox, FieldSet, FileField, Form, HiddenField, ImageButton,
+    Label, Spacer, ListLayout, TableLayout, PasswordField,
+    RadioButton, ResetButton, SubmitButton, TextField, TextArea,
+    SingleSelectField, MultipleSelectField, RadioButtonList, CheckBoxList,
+    RadioButtonTable, CheckBoxTable, GridLayout, RowLayout, TableForm, ListForm,
+    TableFieldSet, ListFieldSet, FormPage, FileValidator,
+    LabelField, LinkField, InputField, SelectionField, MultipleSelectionField)

tw2/forms/__init__.pyc

Binary file added.

tw2/forms/samples.py

+"""
+Here you can create samples of your widgets by providing default parameters,
+inserting them in a container widget, mixing them with other widgets, etc...
+These samples will appear in the WidgetBrowser
+
+See http://toscawidgets.org/documentation/WidgetBrowser for more information
+"""
+
+import tw2.core as twc, widgets as twf
+
+options = ['Red', 'Orange', 'Yellow', 'Green', 'Blue']
+
+class DemoChildren(twc.CompoundWidget):
+    title = twf.TextField()
+    priority = twf.SingleSelectField(options=['', 'Normal', 'High'])
+    description = twf.TextArea()
+
+class DemoSingleSelectField(twf.SingleSelectField):
+    options = options
+
+class DemoMultipleSelectField(twf.MultipleSelectField):
+    options = options
+
+class DemoRadioButtonList(twf.RadioButtonList):
+    options = options
+
+class DemoCheckBoxList(twf.CheckBoxList):
+    options = options
+
+class DemoRadioButtonTable(twf.RadioButtonTable):
+    options = options
+    cols = 2
+
+class DemoCheckBoxTable(twf.CheckBoxTable):
+    options = options
+    cols = 2
+
+class DemoTableLayout(twf.TableLayout, DemoChildren):
+    pass
+
+class DemoListLayout(twf.ListLayout, DemoChildren):
+    pass
+
+class DemoSpacer(twf.TableLayout):
+    demo_for = twf.Spacer
+    title = twf.TextField()
+    xx = twf.Spacer()
+    description = twf.TextArea()
+
+class DemoLabel(twf.TableLayout):
+    demo_for = twf.Label
+    title = twf.TextField()
+    xx = twf.Label(text='Please enter as much information as possible in the description.')
+    description = twf.TextArea()
+
+class DemoFieldSet(twf.FieldSet):
+    legend = 'FieldSet'
+    child = DemoTableLayout()
+
+class DemoForm(twf.Form):
+    child = DemoTableLayout()
+
+class DemoButton(twf.Button):
+    value = 'Click me'
+    attrs = {'onclick': 'alert("Hello")'}
+
+class DemoGridLayout(twf.GridLayout):
+    id = 'x'
+    extra_reps = 3
+    title = twf.TextField()
+    priority = twf.SingleSelectField(options=['', 'Normal', 'High'])
+
+class DemoImageButton(twf.ImageButton):
+    modname = 'tw2.forms'
+    filename = 'static/edit-undo.png'

tw2/forms/static/dialog-warning.png

Added
New image

tw2/forms/static/edit-undo.png

Added
New image

tw2/forms/static/forms.css

+.error ul, .error table { float: left; }

tw2/forms/templates/__init__.py

Empty file added.

tw2/forms/templates/__init__.pyc

Binary file added.

tw2/forms/templates/fieldset.html

+<fieldset xmlns:py="http://genshi.edgewall.org/" py:attrs="w.attrs">
+    <legend py:content="w.legend" />
+    ${w.child.display()}
+</fieldset>

tw2/forms/templates/fieldset.mak

+<%namespace name="tw" module="tw2.core.mako_util"/>\
+<fieldset ${tw.attrs(attrs=w.attrs)}>
+    <legend>${w.legend or ''}</legend>
+    ${w.child.display() | n}
+</fieldset>

tw2/forms/templates/form.html

+<form xmlns:py="http://genshi.edgewall.org/" py:attrs="w.attrs">
+     <span class="error" py:content="w.error_msg"/>
+     <div py:if="w.help_msg" class="help">
+      <p>
+         ${w.help_msg}
+      </p>
+     </div>
+    ${w.child.display()}
+    <py:if test="w.submit">${w.submit.display()}</py:if>
+</form>

tw2/forms/templates/form.mak

+<%namespace name="tw" module="tw2.core.mako_util"/>\
+<form ${tw.attrs(attrs=w.attrs)}>
+     <span class="error">${w.error_msg or ''}</span>
+     % if w.help_msg:
+     <div class="help">
+      <p>
+         ${w.help_msg}
+      </p>
+     </div>
+     % endif
+    ${w.child.display() | n}
+   % if w.submit:
+    ${w.submit.display() | n}
+   % endif
+</form>

tw2/forms/templates/grid_layout.html

+<table xmlns:py="http://genshi.edgewall.org/" py:attrs="w.attrs">
+    <tr><th py:for="col in w.children[0].children_non_hidden">$col.label</th></tr>
+    <py:for each="row in w.children">${row.display()}</py:for>
+    <tr class="error"><td colspan="${len(w.children)}" id="${w.compound_id}:error">
+        ${w.error_msg}
+    </td></tr>
+</table>

tw2/forms/templates/grid_layout.mak

+<%namespace name="tw" module="tw2.core.mako_util"/>\
+<table ${tw.attrs(attrs=w.attrs)}>
+<tr>\
+% for col in w.children[0].children_non_hidden:
+    <th>${unicode(col.label)}</th>
+% endfor
+</tr>
+% for row in w.children:
+    ${row.display() | n}
+% endfor
+    <tr class="error"><td colspan="${str(len(w.children))}" id="${w.compound_id or ''}:error">
+        ${w.error_msg or ''}
+    </td></tr>
+</table>

tw2/forms/templates/input_field.html

+<input xmlns:py="http://genshi.edgewall.org/" py:attrs="w.attrs"/>

tw2/forms/templates/input_field.mak

+<%namespace name="tw" module="tw2.core.mako_util"/>\
+<input ${tw.attrs(attrs=w.attrs)}/>

tw2/forms/templates/label.html

+<span xmlns:py="http://genshi.edgewall.org/" py:attrs="w.attrs" py:content="w.text" />

tw2/forms/templates/label.mak

+<%namespace name="tw" module="tw2.core.mako_util"/>\
+<span ${tw.attrs(attrs=w.attrs)}>${w.text}</span>

tw2/forms/templates/label_field.html

+<span xmlns:py="http://genshi.edgewall.org/">$w.value<input py:attrs="w.attrs"/></span>

tw2/forms/templates/label_field.mak

+<%namespace name="tw" module="tw2.core.mako_util"/>\
+<span>${w.value or ''}<input ${tw.attrs(attrs=w.attrs)}/></span>

tw2/forms/templates/list_layout.html

+<ul xmlns:py="http://genshi.edgewall.org/" py:attrs="w.attrs">
+    <li py:for="i,c in enumerate(w.children)" class="${(i % 2 and 'even' or 'odd') + ((c.validator and c.validator.required) and ' required' or '') + (c.error_msg and ' error' or '')}" title="${w.hover_help and c.help_text or None}" py:attrs="c.container_attrs">
+        <py:if test="hasattr(c, 'type') and c.type != 'hidden'">
+        <label>$c.label</label>
+        </py:if>
+        ${c.display()}
+        <py:if test="not w.hover_help">$c.help_text</py:if>
+        <span id="${c.compound_id}:error" class="error" py:content="c.error_msg"/>
+    </li>
+    <li class="error"><span id="${w.compound_id}:error" class="error" py:content="w.error_msg"/></li>
+</ul>

tw2/forms/templates/list_layout.mak

+<%namespace name="tw" module="tw2.core.mako_util"/>\
+<ul \
+${tw.attrs(attrs=w.attrs)}>
+   % for i,c in enumerate(w.children):
+    <li \
+class="${(i % 2 and 'even' or 'odd') + ((c.validator and getattr(c.validator, 'required', None)) and ' required' or '') + (c.error_msg and ' error' or '')}"\
+     % if w.hover_help and c.help_text:
+title="${c.help_text}" \
+     % endif
+${tw.attrs(attrs=c.container_attrs)}\
+>
+     % if hasattr(c, 'type') and (c.type != 'hidden'):
+     <label>${c.label or ''}</label>
+     % endif
+        ${c.display() | n}
+        % if not w.hover_help:
+${c.help_text or ''}\
+        % endif
+        <span id="${c.compound_id or ''}:error" class="error">${c.error_msg or ''}</span>
+    </li>
+   % endfor
+    <li class="error"><span id="${w.compound_id or ''}:error" class="error">${w.error_msg or ''}</span></li>
+</ul>

tw2/forms/templates/row_layout.html

+<tr xmlns:py="http://genshi.edgewall.org/"  py:attrs="w.attrs">
+    <td py:for="c in w.children_non_hidden">
+        ${c.display()}
+        <py:if test="c.error_msg"><img src="${w.resources.error.link}" title="${c.error_msg}"/></py:if>
+    </td>
+    <td>
+        <py:for each="c in w.children_hidden">${c.display()}</py:for>
+        <py:if test="w.error_msg"><img src="${w.resources.error.link}" title="${w.error_msg}"/></py:if>
+    </td>
+</tr>

tw2/forms/templates/row_layout.mak

+<%namespace name="tw" module="tw2.core.mako_util"/>\
+<tr\
+ ${tw.attrs(attrs=w.attrs)}>
+   % for c in w.children_non_hidden:
+    <td>
+        ${c.display() | n}
+       % if c.error_msg:
+        <img src="${w.resources.error.link}" title="${c.error_msg}"/>
+       % endif
+    </td>
+   % endfor
+    <td>
+       % for c in w.children_hidden:
+        ${c.display() | n}
+       % endfor
+       % if w.error_msg:
+        <img src="${w.resources.error.link}" title="${w.error_msg}"/>
+       % endif
+    </td>
+</tr>

tw2/forms/templates/select_field.html

+<select xmlns:py="http://genshi.edgewall.org/" py:attrs="w.attrs">
+    <optgroup py:for="group, options in w.grouped_options"
+              py:strip="not group"
+              label="${group}" >
+        <option py:for="attrs, desc in options"
+                py:attrs="attrs"
+                py:content="desc" />
+    </optgroup>
+</select>

tw2/forms/templates/select_field.mak

+<%namespace name="tw" module="tw2.core.mako_util"/>\
+<select ${tw.attrs(attrs=w.attrs)}>
+    % for group, options in w.grouped_options:
+     % if group is not None:
+      <optgroup ${tw.attrs(attrs=dict(label=group))}>
+     % endif 
+        % for attrs, desc in options:
+         <option ${tw.attrs(attrs=attrs)}>${desc}</option>
+        % endfor
+     % if group is not None:
+      </optgroup>
+     % endif 
+    % endfor
+</select>

tw2/forms/templates/selection_list.html

+<ul xmlns:py="http://genshi.edgewall.org/" py:attrs="w.attrs">
+    <li py:for="group, opts in w.grouped_options" py:strip="not group">
+        <div class="group_header" py:if="group">$group</div>
+        <ul py:strip="not group">
+            <li py:for="attrs, desc in opts">
+                <input py:attrs="attrs" />
+                <label for="${attrs['id']}" py:content="desc" />
+            </li>
+        </ul>
+    </li>
+</ul>

tw2/forms/templates/selection_list.mak

+<%namespace name="tw" module="tw2.core.mako_util"/>\
+<ul ${tw.attrs(attrs=w.attrs)}>
+   % for group, opts in w.grouped_options:
+       % if group:
+        <li>
+        <div class="group_header">${group}</div>
+        <ul>
+       % endif   
+       % for attrs, desc in opts:
+        <li>
+            <input ${tw.attrs(attrs=attrs)}/>
+            <label for="${attrs['id']}">${desc}</label>
+        </li>
+       % endfor
+       % if group:
+        </li>
+        </ul>
+       % endif   
+   % endfor
+</ul>

tw2/forms/templates/selection_table.html

+<table xmlns:py="http://genshi.edgewall.org/" py:attrs="w.attrs" >
+    <tbody>
+    <tr py:for="row in w.options_rows">
+        <td py:for="attrs, desc in row">
+            <input py:attrs="attrs" />
+            <label for="${attrs['id']}" py:content="desc" />
+        </td>
+        <td py:for="j in xrange(w.cols - len(row))" />
+    </tr>
+    </tbody>
+</table>

tw2/forms/templates/selection_table.mak

+<%namespace name="tw" module="tw2.core.mako_util"/>\
+<table ${tw.attrs(attrs=w.attrs)}>
+    <tbody>
+   % for row in w.options_rows:
+    <tr>
+       % for attrs, desc in row:
+        <td>
+            <input ${tw.attrs(attrs=attrs)} />
+            <label for="${attrs['id']}">${desc}</label>
+        </td>
+       % endfor
+       % for j in xrange(w.cols - len(row)):
+        <td/>
+       % endfor
+    </tr>
+   % endfor
+    </tbody>
+</table>

tw2/forms/templates/spacer.html

+<div xmlns:py="http://genshi.edgewall.org/">&nbsp;</div>

tw2/forms/templates/spacer.mak

+<div>&nbsp;</div>

tw2/forms/templates/table_layout.html

+<table xmlns:py="http://genshi.edgewall.org/" py:attrs="w.attrs">
+    <tr py:for="i,c in enumerate(w.children_non_hidden)" class="${(i % 2 and 'even' or 'odd') + ((c.validator and c.validator.required) and ' required' or '') + (c.error_msg and ' error' or '')}" title="${w.hover_help and c.help_text or None}" py:attrs="c.container_attrs" id="${c.compound_id}:container">
+        <th py:if="c.label">$c.label</th>
+        <td py:attrs="(not c.label) and dict(colspan='2') or None">
+            ${c.display()}
+            <py:if test="not w.hover_help">$c.help_text</py:if>
+            <span id="${c.compound_id}:error" py:content="c.error_msg"/>
+        </td>
+    </tr>
+    <tr class="error"><td colspan="2">
+        <py:for each="c in w.children_hidden">${c.display()}</py:for>
+        <span id="${w.compound_id}:error" py:content="w.error_msg"/>
+    </td></tr>
+</table>

tw2/forms/templates/table_layout.mak

+<%namespace name="tw" module="tw2.core.mako_util"/>\
+<table ${tw.attrs(attrs=w.attrs)}>
+   % for i,c in enumerate(w.children_non_hidden):
+    <tr class="${(i % 2 and 'even' or 'odd') + ((c.validator and c.validator.required) and ' required' or '') + (c.error_msg and ' error' or '')}" \
+     %if w.hover_help and c.help_text:
+      title="${c.help_text}" \
+     %endif
+${tw.attrs(attrs=c.container_attrs)} \
+id="${c.compound_id or ''}:container">
+       % if c.label:
+        <th>${c.label}</th>
+       % endif
+        <td \
+       % if not c.label:
+colspan="2"\
+       % endif
+>
+            ${c.display() | n}
+           % if not w.hover_help:
+            ${c.help_text or ''}
+           % endif
+            <span id="${c.compound_id or ''}:error">${c.error_msg or ''}</span>
+        </td>
+    </tr>
+   % endfor
+    <tr class="error"><td colspan="2">
+       % for c in w.children_hidden:
+        ${c.display() | n}
+       % endfor
+        <span id="${w.compound_id or ''}:error">${w.error_msg or ''}</span>
+    </td></tr>
+</table>

tw2/forms/templates/textarea.html

+<textarea xmlns:py="http://genshi.edgewall.org/" py:attrs="w.attrs">$w.value</textarea>

tw2/forms/templates/textarea.mak

+<%namespace name="tw" module="tw2.core.mako_util"/>\
+<textarea ${tw.attrs(attrs=w.attrs)}>${w.value or ''}</textarea>

tw2/forms/widgets.py

+import tw2.core as twc, re, itertools, webob, cgi
+
+#--
+# Basic Fields
+#--
+class FormField(twc.Widget):
+    name = twc.Variable('dom name', request_local=False, attribute=True, default=property(lambda s: s.compound_id))
+
+class InputField(FormField):
+    type = twc.Variable('Type of input field', default=twc.Required, attribute=True)
+    value = twc.Param(attribute=True)
+    template = "tw2.forms.templates.input_field"
+
+
+class TextField(InputField):
+    size = twc.Param('Size of the field', default=None, attribute=True)
+    type = 'text'
+
+
+class TextArea(FormField):
+    rows = twc.Param('Number of rows', default=None, attribute=True)
+    cols = twc.Param('Number of columns', default=None, attribute=True)
+    template = "tw2.forms.templates.textarea"
+
+
+class CheckBox(InputField):
+    type = "checkbox"
+    validator = twc.BoolValidator
+    def prepare(self):
+        super(CheckBox, self).prepare()
+        self.safe_modify('attrs')
+        self.attrs['checked'] = self.value and 'checked' or None
+        self.value = None
+
+
+class RadioButton(InputField):
+    type = "radio"
+    checked = twc.Param('Whether the field is selected', attribute=True)
+
+
+class PasswordField(InputField):
+    """
+    A password field. This never displays a value passed into the widget,
+    although it does redisplay entered values on validation failure. If no
+    password is entered, this validates as EmptyField.
+    """
+    type = 'password'
+    def prepare(self):
+        super(PasswordField, self).prepare()
+        self.safe_modify('attrs')
+        self.attrs['value'] = None
+    def _validate(self, value, state=None):
+        value = super(PasswordField, self)._validate(value, state)
+        return value or twc.EmptyField
+
+
+class FileValidator(twc.Validator):
+    """Base class for file validators
+
+    `extension`
+        Allowed extension for the file
+    """
+    extension = None
+    msgs = {
+        'required': ('file_required', 'Select a file'),
+        'badext': "File name must have '$extension' extension",
+    }
+
+    def validate_python(self, value, outer_call=None):
+        if isinstance(value, cgi.FieldStorage):
+            if self.extension is not None and not value.filename.endswith(self.extension):
+                raise twc.ValidationError('badext', self)
+        elif value:    
+            raise twc.ValidationError('corrupt', self)
+        elif self.required:
+            raise twc.ValidationError('required', self)
+
+
+class FileField(InputField):
+    type = "file"
+    validator = FileValidator
+
+    def _validate(self, value, state=None):
+        try:
+            return super(FileField, self)._validate(value, state)
+        except twc.ValidationError:
+            self.value = None
+            raise
+
+
+class HiddenField(InputField):
+    """
+    A hidden field. The default validator avoids the value being included in 
+    validated data. This helps prevent against parameter tampering attacks.
+    """
+    type = 'hidden'
+    validator = twc.BlankValidator
+    
+
+class LabelField(InputField):
+    """
+    A read-only label showing the value of a field. The value is stored in a hidden field, so it remains through validation failures. However, the value is never included in validated data.
+    """
+    type = 'hidden'
+    template = "tw2.forms.templates.label_field"
+    validator = twc.BlankValidator
+
+
+class LinkField(twc.Widget):
+    """
+    A dynamic link based on the value of a field. If either *link* or *text* contain a $, it is replaced with the field value.
+    """
+    template = "tw2.forms.templates.link_field"
+    link = twc.Variable('Link target', default='')
+    text = twc.Variable('Link text', default='')
+    value = twc.Variable("Value to replace $ with in the link/text")
+    validator = twc.BlankValidator
+
+    def prepare(self):
+        super(LinkField, self).prepare()
+        self.safe_modify('attrs')
+        self.attrs['href'] = self.link.replace('$', unicode(self.value or ''))
+        self.text = self.value and self.text.replace('$', unicode(self.value)) or ''
+
+
+class Button(InputField):
+    """Generic button. You can override the text using `value` and define a JavaScript action using `attrs['onclick']`.
+    """
+    type = "button"
+    id = None
+
+
+class SubmitButton(Button):
+    """Button to submit a form."""
+    type = "submit"
+    name = None
+
+
+class ResetButton(Button):
+    """Button to clear the values in a form."""
+    type = "reset"
+
+
+class ImageButton(twc.Link, InputField):
+    type = "image"
+    width = twc.Param('Width of image in pixels', attribute=True, default=None)
+    height = twc.Param('Height of image in pixels', attribute=True, default=None)
+    alt = twc.Param('Alternate text', attribute=True, default='')
+    src = twc.Variable(attribute=True)
+
+    def prepare(self):
+        super(ImageButton, self).prepare()
+        self.src = self.link
+        self.safe_modify('attrs')