=========== 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 :doc:`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: .. code-block:: html+jinja {% for script in form.js %} {% endfor %} {% for stylesheet in form.css %} {% endfor %}
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. .. _Jinja: https://jinja.palletsprojects.com/ 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 :class:`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 :doc:`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. For conditions, like ``visble_if``, it is necessary to use the ``panel.`` prefix to create a reference to the current element of a dynamic panel. For example:: "{panel.service} == 'Instagram'" It is also possible to check the values of dynamic panel elements by referring to them by indexes. However, this requires passing in a ``name`` parameter to the ``FormPanel`` element. Once that is done, you can refer to a specific panel number by using the passed in name and an index. For example, for the following condition, the checking will be done based on the first element of the dynamic panel named ``social_media``:: "{social_media[0].service} == 'Instagram'" 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.com/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`` and ``max_value`` range. - `Text`. Fails the entered text length is outside the ``min_length`` and ``max_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