Automatic form generation

Websauna comes with an automatic form generator for SQLAlchemy models.

An example of automatically generated form - the question edit page from the tutorial:


Form generation

The core part of form generation is websauna.system.form.fieldmapper.ColumnToFieldMapper() which maps SQLAlchemy columns to Colander schema.

See for interface.

See websauna.system.crud.views.FormView.create_form() for a usage example how to call the field mapper.

See classes in websauna.system.user.adminviews for examples how to customize automatically included fields.

Internally form generation uses highly modified colanderalchemy library.


Due to high customization this dependency is likely to go away.

Customizing automatically generated forms

There are several ways to customize automatic form generation based on your use case.

Edit includes attribute

This is the most common way to customize CRUD forms. Each CRUD class comes with includes attribute which lists fields which are pulled from the SQLAlchemy columns, or other Python object properties, to a form.

Edit websauna.system.crud.views.FormView.includes() attribute to include the list of fields to include. This list can contain

  • string: String presents a name of a column that goes to form. You can omit the names of the columns you don’t want to show on the form.

  • colander.SchemaNode - add custom field and customize widgets for existing columns. Rememeber to have name attribute matching to a column.


import colander

from websauna.system.admin import views as admin_views
from websauna.system.form.fields import defer_widget_values
from websauna.system.user.schemas import group_vocabulary
from websauna.system.user.schemas import GroupSet

 class UserEdit(admin_views.Edit):
    """Edit one user."""

    includes = admin_views.Edit.includes + [
                colander.SchemaNode(colander.String(), name='username'),  # Make username required field
                colander.SchemaNode(colander.String(), name='full_name', missing=""),
                colander.SchemaNode(GroupSet(), name="groups", widget=defer_widget_values(deform.widget.CheckboxChoiceWidget, group_vocabulary, css_class="groups"))

Subclass CRUD view and override form_generator

This applies for automatic CRUD.

Subclass your form from

Override websauna.system.crud.views.FormView.form_generator() and pass a callable to edit generated colander.SchemaNode in place.


from websauna.system.core.viewconfig import view_overrides
from websauna.system.crud.formgenerator import SQLAlchemyFormGenerator
from websauna.system.crud import views

def customizer(**kwargs):
    request = kwargs["request"]
    schema = kwargs["schema"]
    if request.user:
        # Do nothing, we know the name of the logged in user already
        schema.add(colander.SchemaNode(colander.String(), label="Leave your name for feedback", name="anonymous_visitor_name", missing="", widget=deform.widget.TextInputWidget()))

# This view applies to imaginary CommentCRUD which manages SQLAlchemy Comment model
class CommentAddView(views.Add):

    # Pass a callable to SQLAlchemyFormGenerator
    form_generator = SQLAlchemyFormGenerator(customize_schema=customizer)

Rolling out your own view with field mapper

You can also write everything from scratch and call field mapper.


from uuid import UUID

from pyramid.httpexceptions import HTTPFound
from websauna.system.core import messages
from websauna.system.http import Request
from websauna.system.form.fieldmapper import EditMode
from websauna.system.form.csrf import add_csrf
from websauna.system.core.route import simple_route
from websauna.utils.slug import slug_to_uuid

from myapp.model import Question

def detail(request: Request):
    # Convert base64 encoded UUID string from request path to Python UUID object
    question_uuid = slug_to_uuid(request.matchdict["question_uuid"])

    question = request.dbsession.query(Question).filter_by(uuid=question_uuid).first()
    if not question:
        raise HTTPNotFound()

    # Generate a form from SQLAlchemy model
    # includes not set -> include all fields on SQLALchemy model
    schema =, request, None, Question, includes=None, nested=nested)

    # In this point use schema.add(), schema["question_text"], e.g. to edit the schema

    # Make sure we have CSRF token as a hidden field

    schema = self.bind_schema(schema, request=request)

    if request.method == "POST":

        controls = self.request.POST.items()

            appstruct = form.validate(controls)

            # Validation passed -> edit obj
            question.question_text = appstruct["question_text"]
            question.published_at = appstruct["published_at"]
            messages.add(kind="success", msg_id="question-edit-saved", "Your edit was saved")
            return HttpFound(request.route_url("home"))

        except deform.ValidationFailure as e:
            # Whoops, bad things happened, render form with validation errors
            rendered_form = e.render()
        rendered_form = form.render()

    # Load widget CSS/JS
    form.resource_registry.pull_in_resources(request, form)

    return locals()

See also websauna.system.crud.views.FormView.create_form.

Override field_mapper attribute

Inherit from a crud view and override websauna.system.crud.views.FormView.field_mapper with your own instance of websauna.system.form.fieldmapper.ColumnToFieldMapper.