Quick Start¶
Questions forms are basically class definitions, where each question is a form attribute:
from questions import Form
from questions import DropdownQuestion
from questions import TextQuestion
class PreferencesForm(Form):
email = TextQuestion(input_type="email")
email_format = DropdownQuestion(choices=["PDF", "HTML", "Plain Text"])
There are many kinds of questions, for different kinds of input types. Most questions have several possible parameters, but most of them are optional, so the question can be defined with just the parameters that are needed for each case, as in the example above.
To use a form, an instance has to be created. The Form constructor also accepts various parameters, but like the Question parameters, they are only used when they are needed:
prefs = PreferencesForm()
Generating a form from a JSON file¶
SurveyJS offers a free to use (but not open source) form creator. Questions
supports generating form classes from the JSON data that the form creator
exports. To do that, instead of defining the class, use the from_json
method of the Form
class:
from questions import Form
json = """
{
questions: [
{
type: "text",
name: "email",
inputType: "email"
},
{
type: "dropdown",
name: "email_format",
choices: ["PDF", "HTML", "Plain Text"]
}
]
}
"""
PreferencesForm = Form.from_json(json, "PreferencesForm")
The forms generated using the from_json
class method are dynamic types
that have no code equivalent. Questions includes a console script that can
generate actual Python code for these forms:
.. code-block:: console
$ generate_code {class_name} path/to/file.json
All the script needs is a class name for the generated form class, and the path to the JSON file with the SurveyJS form definition.
Displaying the forms¶
Questions generates a SurveyJS form, which requires a few Javascript and CSS resources to work. The simplest way to display a form is to use the SurveyJS CDN to serve all these resources, which requires no work and is the default form display mode. A full HTML page with all required resources and Javascript code can be generated like this:
prefs = PreferencesForm()
html = prefs.render_html()
This should be just enough for many small applications, but most applications will need to combine the form code with their own layout and resources. Again, this can be accomplished using the CDN or not. Here’s a simple example of how to integrate a Questions form into an existing web application using the CDN:
Python view code¶
To render a form in a template, the Python view code must pass the form instance to the template. In a generic Python web framework, this would look like the following:
def preferences_form():
prefs = PreferencesForm()
return {"form": prefs}
HTML template¶
In the template, using Jinja the form would be used like this:
<html>
<head>
<!-- the JS for the web application goes here -->
<!-- the form has a property with the required JS for SurveyJS -->
{% for script in form.js %}
<script src={{ script }} type="text/javascript"></script>
{% endfor %}
<!-- the script with the form definition and initialization -->
<script type="text/javascript">
{{ form.render_js() }}
</script>
<!-- the CSS for the web application goes here -->
<!-- the form has a property with the required CSS for SurveyJS -->
{% for stylesheet in form.css %}
<link href={{ stylesheet }} rel="stylesheet" />
{% endfor %}
</head>
<body>
<!-- the web application layout goes here -->
<div id="questions_form"></div>
</body>
</html>
What is needed to make the form work is to insert all the required JS and CSS
resources, followed by the form definition and initialization script. The only
thing needed on the HTML side, is the div
where the form will be inserted.
It must use the questions_form id, unless a different id is passed in
when creating the form, by using the html_id
parameter.
Displaying forms without using the CDN¶
When not using the SurveyJS CDN, there are two ways to display the forms. The first one requires downloading or installing all the resources under the same directory, and passing the URL for this directory to the form constructor:
prefs = PreferencesForm(resource_url="/static_resources")
This is the easiest way to do it, and requires no changes to the HTML above.
If your application includes many forms, you can set the resource URL for all
forms using the set_resource_url
class method of the Form
class:
from questions import Form
class PreferencesForm(Form):
email = TextQuestion(input_type="email")
email_format = DropdownQuestion(choices=["PDF", "HTML", "Plain Text"])
Form.set_resource_url("/static_resources")
prefs = PreferencesForm()
other = OtherForm()
In this example, both the prefs and other form instances will use the /static_resources URL for getting the form resources.
The other way to do this also requires downloading or installing all the required resources, but instead of using the resource_url parameter, remove the JS and CSS loops from the HTML template, and in their place put in the list of locally installed resources. See Installation to learn how this is done.
Panels¶
A panel is a container of form controls that are presented as a group. It’s like a question with multiple parts. To create a panel, a separate form has to be defined, and it is then passed in to the panel constructor:
from questions import Form
from questions import FormPanel
from questions import BooleanQuestion
from questions import DropdownQuestion
from questions import TextQuestion
class PreferencesForm(Form):
email = TextQuestion(input_type="email")
email_format = DropdownQuestion(choices=["PDF", "HTML", "Plain Text"])
class ProfileForm(Form):
receive_newsletter = BooleanQuestion(
title="Do you wish to receive our newsletter?",
required=True,
)
newsletter_panel = FormPanel(
PreferencesForm,
title="Newsletter Preferences",
visible_if="{receive_newsletter} == True",
)
In the example above, PreferencesForm
will act as a panel inside
ProfileForm
. Note that that the FormPanel
constructor takes the form
definition (the class) as the parameter, not an instance of the form. The
use of the visible_if
condition makes sure the newsletter preferences
panel will only be shown if the user elects to receive the newsletter.
It is possible to have a panel inside a panel, and even more nested panels if desired. However, be aware that multiple levels of nesting can be confusing for the user and require more complex code to get at the form data later.
Dynamic panels¶
A dynamic panel is also a container for questions with multiple parts, but it
has the added feature that copies of it can be dynamically added and removed
from a form. In this way a user can add two or more related panels, like for
example relatives, social media accounts, or previous illnesses. It is
defined in the same way as a regular panel, except the dynamic
parameter
is set to true:
from questions import Form
from questions import FormPanel
from questions import BooleanQuestion
from questions import DropdownQuestion
from questions import TextQuestion
class SocialMediaForm(Form):
service = DropdownQuestion(choices=["Twitter", "Instagram", "Snapchat"])
account = TextQuestion()
class ProfileForm(Form):
social_media = FormPanel(
SocialMediaForm,
title="Social Media Accounts",
dynamic=True,
panel_count=2,
)
The above form will allow the user to add any number of social accounts. Pay
attention to the panel_count
parameter, which signals that two panels will
be active when the form is first rendered.
Pages¶
Questions also allows the user to easily create multiple page forms. A page form is like a panel that will be presented on its own page. When a form has more than one page, Questions will add page navigation controls to move back and forth between the pages. The final page will show a complete button:
from questions import Form
from questions import FormPage
from questions import TextQuestion
from questions import DropdownQuestion
class PageOne(Form):
name = TextQuestion()
email = TextQuestion(input_type="email", required="True")
class PageTwo(Form):
country = DropdownQuestion(choices_by_url={"value_name": "name",
"url": "https://restcountries.eu/rest/v2/all"})
birthdate = TextQuestion(input_type="date")
class ProfileForm(Form):
page_one = FormPage(PageOne, title="Identification Information")
page_two = FormPage(PageTwo, title="Additional Information")
Although Questions will not complain if a page is added to another page, the nested page will be treated like a panel, not a page.
Accessing form data¶
Once a questions form is submitted, the data will be posted to the page URL. To get the form data, simply use you web framework’s way of accessing JSON data. For example, in Flask:
@app.route("/", methods=("POST",))
def post():
form_data = request.get_json()
The form data is returned in a dictionary format, a key for each form field, regardless of the page and panel structure of the form. A dynamic panel will be represented as a list of dictionaries. For example:
{
'name': 'John Smith',
'email': 'smith@smith.me',
'birthdate': '1980-05-08',
'country': 'US'
}
Since the data is returned as a single dictionary, it’s not allowed to use the same name for more than one field, even if the form has multiple pages.
Edit Forms¶
An edit form is a form that shows predetermined values at render time. The
user can then change only the desired values. This would be used to edit
objects stored in a database, for example. To set up an edit form in
Questions, simply pass in a dictionary with the data to the form rendering
method, using the form_data
parameter:
form = ProfileForm()
profile_data = {
'name': 'John Smith',
'email': 'smith@smith.me',
'birthdate': '1980-05-08',
'country': 'US'
}
questions_js = form.render_js(form_data=profile_data)
Here we are using a simple dictionary to set up the data, but of course the usual thing to do for an edit form would be to get the data from a database.
Updating objects with form data¶
Since we are on the subject of edit forms, it’s a good time to mention that Questions provides an utility method for updating objects with data coming from a form:
@app.route("/", methods=("POST",))
def post():
form = ProfileForm()
profile = User.get_profile("jsmith") # sample generic code
form_data = request.get_json()
form.update_object(profile, form_data)
The update_object
method does two things. First, it validates the data,
to avoid getting invalid data into the object. It then goes through all the
form fields and sets the corresponding attributes of the object with the
values from the form.
Validation¶
Form questions can have one or more validators assigned. The form data will be validated on the front end, and the form cannot be sent unless they all pass. Still, a user or bot could submit a Questions form directly to the Python view, bypassing the validation. This is why questions includes mirror validators that perform the same checks as the SurveyJS front end on the server side.
SurveyJS has five standard validators:
Numeric. Fails if the question answer is not a number, or if an entered number is outside the
min_value
andmax_value
range.Text. Fails the entered text length is outside the
min_length
andmax_length
range.Expression. Fails when
expression
returns false.Regex. Fails if the entered value does not fit a regular expression (
regex
).Email. Fails if the entered value is not a valid e-mail.
Questions allows the use of any of these validators, using its corresponding validator classes:
from questions import Form
from questions import DropdownQuestion
from questions import TextQuestion
from questions import ExpressionValidator
from questions import NumericValidator
class ValidatedForm(Form):
age = TextQuestion(
input_type="number",
validators=[
NumericValidator(
max_value=130,
message="We sincerely doubt that is your age",
)
]
)
tickets = DropdownQuestion(
choices=[1, 2, 3, 4, 5],
validators = [
ExpressionValidator(
expression="{age} > 18 or {tickets} < 2",
message="Minors can only buy one ticket",
)
]
)
Notice that the expression validator allows referring to any other question on the form, using the question name in brackets. This permits complex validations.
As mentioned above, validation will be performed in the front end, but it is
recommended to call the mirroring server side validation anyway, for safety.
To do that simply call the validate
method on the form data:
@app.route("/", methods=("POST",))
def post():
form = form.ValidatedForm()
form_data = request.get_json()
if form.validate(form_data):
# validation successful. Save data or something.
return redirect("success_page")
else:
return form.render_html(form_data=form_data)
This example demonstrates a common pattern for responding to form POST requests. If the validation is successful, the data is saved, and then we return a redirection to the success or thanks page. If validation fails, we redisplay the form with the data that was sent, and the errors will be highlighted.
Internationalization¶
SurveyJS supports many different languages, and questions
makes it easy to
tale advantage of that. Simply pass in the locale when instantiating a form:
form = YourFormSubclass(title="Formulaire en français", locale="fr")
The current list of supported locales is below.
ar
bg
ca
cs
da
de
en
es
et
fa
fi
fr
gr
he
hu
id
is
it
ja
ka
ko
lt
lv
nl
no
pl
pt
ro
ru
sv
sw
tg
th
tr
ua
zh-cn
zh-tw