Have you registered for Docacon 2019 yet?
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

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.

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.

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:

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:

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

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:
  - .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.
---
template: favorite_explanation
subject: |
  Explain favorites
content: |
  ##### What is a favorite?

  If you have a favorite something,
  that means you like it more than
  you like other things of a similar
  nature.
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.

E-mailing the user a link for resuming the interview later

If you want users to be able to resume their interviews later, but you don’t want to use the username and password system, you can e-mail your users a URL created with interview_url().

default screen parts:
  under: |
    % if show_save_resume_message:
    [Save and resume later](${ url_action('save_and_resume') })
    % endif
---
mandatory: True
code: |
  target = 'normal'
  show_save_resume_message = True
  multi_user = True
---
mandatory: True
scan for variables: False
code: |
  if target == 'save_and_resume':
    if wants_email:
      if email_sent:
        log("We sent an e-mail to your e-mail address.", "info")
      else:
        log("There was a problem with e-mailing.", "danger")
      show_save_resume_message = False
    undefine('wants_email')
    undefine('email_sent')
    target = 'normal'
  final_screen
---
question: |
  What is your favorite fruit?
fields:
  - Favorite fruit: favorite_fruit
---
question: |
  What is your favorite vegetable?
fields:
  - Favorite vegetable: favorite_vegetable
---
question: |
  What is your favorite legume?
fields:
  - Favorite legume: favorite_legume
---
event: final_screen
question: |
  I would like you to cook a
  ${ favorite_fruit },
  ${ favorite_vegetable }, and
  ${ favorite_legume } stew.
---
event: save_and_resume
code: |
  target = 'save_and_resume'
---
code: |
  send_email(to=user_email_address, template=save_resume_template)
  email_sent = True
---
question: |
  How to resume your interview later
subquestion: |
  If you want to resume your interview later, we can
  e-mail you a link that you can click on to resume
  your interview at a later time.
fields:
  - no label: wants_email
    input type: radio
    choices:
      - "Ok, e-mail me": True
      - "No thanks": False
    default: True
  - E-mail address: user_email_address
    datatype: email
    show if: wants_email
under: ""
---
template: save_resume_template
subject: |
  Your interview
content: |
  To resume your interview, 
  [click here](${ interview_url() }).
Screenshot of save-continue-later example

E-mailing or texting the user a link for purposes of using the touchscreen

Using a desktop computer is generally very good for answering questions, but it is difficult to write a signature using a mouse.

Here is an example of an interview that allows the user to use a desktop computer for answering questions, but use a mobile device with a touchscreen for writing the signature.

include:
  - docassemble.demo:data/questions/examples/signature-diversion.yml
---
mandatory: True
question: |
  Here is your document.
attachment:
  name: Summary of food
  filename: food
  content: |
    [BOLDCENTER] Food Attestation

    My name is ${ user }.
    
    My favorite fruit is
    ${ favorite_fruit }.

    My favorite vegetable is
    ${ favorite_vegetable }.

    I solemnly swear that the
    foregoing is true and
    correct.

    ${ user.signature.show(width="2in") }

    ${ user }
Screenshot of food-with-sig example

This interview includes a YAML file called signature-diversion.yml, the contents of which are:

mandatory: True
code: |
  multi_user = True
---
question: |
  Sign your name
subquestion: |
  % if not device().is_touch_capable:
  Please sign your name below with your mouse.
  % endif
signature: user.signature
under: |
  ${ user }
---
sets: user.signature
code: |
  signature_intro
  if not device().is_touch_capable and user.has_mobile_device:
    if user.can_text:
      sig_diversion_sms_message_sent
      sig_diversion_post_sms_screen
    elif user.can_email:
      sig_diversion_email_message_sent
      sig_diversion_post_email_screen
---
question: |
  Do you have a mobile device?
yesno: user.has_mobile_device
---
question: |
  Can you receive text messages on your mobile device?
yesno: user.can_text
---
question: |
  Can you receive e-mail messages on your mobile device?
yesno: user.can_email
---
code: |
  send_sms(user, body="Click on this link to sign your name: " + interview_url_action('mobile_sig'))
  sig_diversion_sms_message_sent = True
---
code: |
  send_email(user, template=sig_diversion_email_template)
  sig_diversion_email_message_sent = True
---
template: sig_diversion_email_template
subject: Sign your name with your mobile device
content: |
  Make sure you are using your
  mobile device.  Then
  [click here](${ interview_url_action('mobile_sig') })
  to sign your name with
  the touchscreen.
---
question: |
  What is your e-mail address?
fields:
  - E-mail: user.email
---
question: |
  What is your mobile number?
fields:
  - Number: user.phone_number
---
event: sig_diversion_post_sms_screen
question: |
  Check your text messages.
subquestion: |
  We just sent you a text message containing a link.  Click the link
  and sign your name.

  Once we have your signature, you will move on automatically.
reload: 5
---
event: sig_diversion_post_email_screen
question: |
  Check your e-mail on your mobile device.
subquestion: |
  We just sent you an email containing a link.  With your mobile
  device, click the link and sign your name.

  Once we have your signature, you will move on automatically.
reload: 5
---
event: mobile_sig
need: user.signature
question: |
  Thanks!
subquestion: |
  We got your signature:

  ${ user.signature }
  
  You can now resume the interview on your computer.

Multi-user interview for getting a client’s signature

This is an example of a multi-user interview where one person (e.g., an attorney) writes a document that they want a second person (e.g, a client) to sign. It is a multi-user interview (with multi_user set to True). The attorney inputs the attorney’s e-mail address and uploads a DOCX file containing:

{{ signature }}

where the client’s signature should go. The attorney then receives a hyperlink that the attorney can send to the client.

When the client clicks on the link, the client can read the unsigned document, then agree to sign it, then sign it, then download the signed document. After the client signs the document, it is e-mailed to the attorney’s e-mail address.

mandatory: True
code: |
  multi_user = True
  signature = '(Your signature will go here)'
---
mandatory: True
code: |
  intro_seen
  email_address
  template_file
  notified_of_url
  agrees_to_sign
  signature_reset
  signature
  document_emailed
  final_screen
---
code: |
  notified_of_url = True
  prevent_going_back()
  force_ask('screen_with_link')
Screenshot of sign example

Validating uploaded files

Here is an interview that makes the user upload a different file if the file the user uploads is too large.

question: |
  Please upload a file.
fields:
  File: uploaded_file
  datatype: file
validation code: |
  if uploaded_file.size_in_bytes() > 100000:
    validation_error("That file is way too big!  Upload a smaller file.")
Screenshot of upload-file-size example