Edit this page on GitHub

Recipes

This section contains miscellaneous recipes for solving problems in docassemble.

Require a checkbox to be checked

Using validation code

modules:
  - docassemble.base.util
---
question: |
  You must agree to the terms of service.
fields:
  - I agree to the terms of service: agrees_to_tos
    datatype: yesnowide
validation code: |
  if not agrees_to_tos:
    validation_error("You cannot continue until you agree to the terms of service.")
---
mandatory: True
need: agrees_to_tos
question: All done.

Using datatype: checkboxes

question: |
  You must agree to the terms of service.
fields:
  - no label: agrees_to_tos
    datatype: checkboxes
    minlength: 1
    choices:
      - I agree to the terms of service
    validation messages:
      minlength: |
        You cannot continue unless you check this checkbox.
---
mandatory: True
need: agrees_to_tos
question: All done

Use a variable to track when an interview has been completed

One way to track whether an interview is completed is to set a variable when the interview is done. That way, you can inspect the interview answers and test for the presence of this variable.

modules:
  - docassemble.base.util
---
objects:
  - user: Individual
---
question: |
  What is your name?
fields:
  - First name: user.name.first
  - Last name: user.name.last
---
mandatory: True
code: |
  user.name.first
  user_finish_time
  final_screen
---
code: |
  user_finish_time = current_datetime()
---
event: final_screen
question: |
  Goodbye, user!
buttons:
  Exit: exit

You could also use Redis to store the status of an interview.

modules:
  - docassemble.base.util
---
objects:
  - user: Individual
  - r: DARedis
---
question: |
  What is your name?
fields:
  - First name: user.name.first
  - Last name: user.name.last
---
mandatory: True
code: |
  interview_marked_as_started
  user.name.first
  interview_marked_as_finished
  final_screen
---
code: |
  redis_key = user_info().filename + ':' + user_info().session
---
code: |
  r.set(redis_key, 'started')
  interview_marked_as_started = True
---
code: |
  r.set(redis_key, 'finished')
  interview_marked_as_finished = True
---
event: final_screen
question: |
  Goodbye, user!
buttons:
  Exit: exit

Exit interview with a hyperlink rather than a redirect

Suppose you have a final screen in your interview that looks like this:

modules:
  - docassemble.base.util
---
mandatory: True
code: |
  kick_out
---
event: kick_out
question: Bye
buttons:
  - Exit: exit
    url: https://example.com

When the user clicks the “Exit” button, an Ajax request is sent to the docassemble server, the interview logic is run again, and then when the browser processes the response, the browser is redirected by JavaScript to the url (https://example.com).

If you would rather that the button act as a hyperlink, where clicking the button sends the user directly to the URL, you can make the button this way:

modules:
  - docassemble.base.util
---
mandatory: True
code: |
  kick_out
---
event: kick_out
question: Bye
subquestion: |
  ${ action_button_html("https://example.com", size='md', color='primary', label='Exit', new_window=False) }

Ensure two fields match

modules:
  - docassemble.base.util
---
question: |
  What is your e-mail address?
fields:
  - E-mail: email_address_first
    datatype: email
  - note: |
      Please enter your e-mail address again.
    datatype: email
  - E-mail: email_address
    datatype: email
  - note: |
      Make sure the e-mail addresses match.
    js hide if: |
      val('email_address') != '' && val('email_address_first') == val('email_address')
  - note: |
      <span class="text-success">E-mail addresses match!</span>
    js show if: |
      val('email_address') != '' && val('email_address_first') == val('email_address')
validation code: |
  if email_address_first != email_address:
    validation_error("You cannot continue until you confirm your e-mail address")
---
mandatory: True
question: |
  Your e-mail address is ${ email_address }.

Progresive disclosure

modules:
  - docassemble.base.util
  - .progressivedisclosure
---
features:
  css: progressivedisclosure.css
---
template: fruit_explanation
subject: |
  Tell me more about fruit
content: |
  ##### What is a fruit?
  
  A fruit is the the sweet and
  fleshy product of a tree or
  other plant that contains
  seed and can be eaten as food.
---
question: |
  What is your favorite fruit?
subquestion: |
  Everybody has a favorite fruit.
  
  ${ prog_disclose(fruit_explanation) }
fields:
  - Favorite fruit: favorite_fruit
Screenshot of progressive-disclosure example

Add progressivedisclosure.css to the “static” data folder of your package.

a span.pdcaretopen {
    display: inline;
}

a span.pdcaretclosed {
    display: none;
}

a.collapsed .pdcaretopen {
    display: none;
}

a.collapsed .pdcaretclosed {
    display: inline;
}

Add progressivedisclosure.py as a Python module file in your package.

import re

__all__ = ['prog_disclose']

def prog_disclose(template, classname=None):
    if classname is None:
        classname = ' bg-light'
    else:
        classname = ' ' + classname.strip()
    the_id = re.sub(r'[^A-Za-z0-9]', '', template.instanceName)
    return u"""\
<a class="collapsed" data-toggle="collapse" href="#{}" role="button" aria-expanded="false" aria-controls="collapseExample"><span class="pdcaretopen"><i class="fas fa-caret-down"></i></span><span class="pdcaretclosed"><i class="fas fa-caret-right"></i></span> {}</a>
<div class="collapse" id="{}"><div class="card card-body{} pb-1">{}</div></div>\
""".format(the_id, template.subject_as_html(trim=True), the_id, classname, template.content_as_html())

This uses the collapse feature of Bootstrap.

New object or existing object

The object datatype combined with the disable others can be used to present a single question that asks the user either to select an object from a list or to enter information about a new object.

Another way to do this is to use show if to show or hide fields.

This recipe gives an example of how to do this in an interview that asks about individuals.

objects:
  - boss: Individual
  - employee: Individual
  - customers: DAList.using(object_type=Individual)
---
mandatory: True
question: |
  Summary
subquestion: |
  The boss is ${ boss }.

  The employee is ${ employee }.

  The customers are ${ customers }.

  % if boss in customers or employee in customers:
  Either the boss or the employee is also a customer.
  % else:
  Neither the boss nor the employee is also a customer.
  % endif
---
question: Are there any customers?
yesno: customers.there_are_any
---
question: Is there another customer?
yesno: customers.there_is_another
---
code: |
  people = ([boss] if defined('boss') and boss.name.defined() else []) \
         + ([employee] if defined('employee') and employee.name.defined() else []) \
         + customers.complete_elements()
---
reconsider:
  - people
question: |
  Who is the boss?
fields:
  - Existing or New: boss.existing_or_new
    datatype: radio
    default: Existing
    choices:
      - Existing
      - New
  - Person: boss
    show if:
      variable: boss.existing_or_new
      is: Existing
    datatype: object
    choices: people
  - First Name: boss.name.first
    show if:
      variable: boss.existing_or_new
      is: New
  - Last Name: boss.name.last
    show if:
      variable: boss.existing_or_new
      is: New
  - Birthday: boss.birthdate
    datatype: date
    show if:
      variable: boss.existing_or_new
      is: New
---
reconsider:
  - people
question: |
  Who is the employee?
fields:
  - Existing or New: employee.existing_or_new
    datatype: radio
    default: Existing
    choices:
      - Existing
      - New
  - Person: employee
    show if:
      variable: employee.existing_or_new
      is: Existing
    datatype: object
    choices: people
  - First Name: employee.name.first
    show if:
      variable: employee.existing_or_new
      is: New
  - Last Name: employee.name.last
    show if:
      variable: employee.existing_or_new
      is: New
  - Birthday: employee.birthdate
    datatype: date
    show if:
      variable: employee.existing_or_new
      is: New
---
reconsider:
  - people
question: |
  Who is the ${ ordinal(i) } customer?
fields:
  - Existing or New: customers[i].existing_or_new
    datatype: radio
    default: Existing
    choices:
      - Existing
      - New
  - Person: customers[i]
    show if:
      variable: customers[i].existing_or_new
      is: Existing
    datatype: object
    choices: people
  - First Name: customers[i].name.first
    show if:
      variable: customers[i].existing_or_new
      is: New
  - Last Name: customers[i].name.last
    show if:
      variable: customers[i].existing_or_new
      is: New
  - Birthday: customers[i].birthdate
    datatype: date
    show if:
      variable: customers[i].existing_or_new
      is: New
Screenshot of new-or-existing example

This recipe keeps a master list of individuals in an object called people. Since this list changes throughout the interview, it is re-calculated whenever a question is asked that uses people.

When individuals are treated as unitary objects, you can do things like use Python’s in operator to test whether an individual is a part of a list. This recipe illustrates this by testing whether boss is part of customers or employee is part of customers.