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 }.

Progressive 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.
---
question: |
  What is your favorite fruit?
subquestion: |
  Everybody has a favorite fruit.
  
  ${ prog_disclose(fruit_explanation) }

  Don't you have a favorite fruit?

  ${ prog_disclose(favorite_explanation) }

  You must have a favorite.
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().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

Mail merge

Here is an example interview that assembles a document for every row in a Google Sheet.

modules:
  - docassemble.demo.google_sheets
---
objects:
  - court: DAList.using(object_type=Person, auto_gather=False)
---
code: |
  court.clear()
  for row in read_sheet('court_list'):
    item = court.appendObject()
    item.name.text = row['Name']
    item.address.address = row['Address']
    item.address.city = row['City']
    item.address.state = row['State']
    item.address.zip = row['Zip']
    item.address.county = row['County']
    del item
  court.gathered = True
---
attachment:
  name: |
    ${ court[i].address.county } court
  filename: |
    ${ space_to_underscore(court[i].address.county) }_court_info
  variable name: court[i].info_sheet
  content: |
    [BOLDCENTER] ${ court[i] }
    
    [NOINDENT] Your friendly court for
    ${ court[i].address.county }
    is located at:

    ${ court[i].address_block() }
---
mandatory: True
question: |
  Court information
subquestion: |
  Here are information sheets
  for each court in your state.
attachment code: |
  [item.info_sheet for item in court]
Screenshot of google-sheet-3 example

Documents based on objects

This example is similar to the mail merge example in that it uses a single template to create multiple documents. In this case, however, the same template is used to generate a document for two different objects.

objects:
  - plaintiff: Individual
  - defendant: Individual
---
code: |
  plaintiff.opponent = defendant
  defendant.opponent = plaintiff
---
code: |
  title = "Summary of case"
---
question: |
  What is the name of the plaintiff?
fields:
  - Name: plaintiff.name.first
---
question: |
  What is the name of the defendant?
fields:
  - Name: defendant.name.first
---
generic object: Individual
attachment:
  variable name: x.document
  name: Document for ${ x.name.first }
  docx template file: generic-document.docx
---
mandatory: True
question: |
  Here are your documents.
attachment code: |
  [plaintiff.document, defendant.document]
Screenshot of generic-document example

This makes use of the generic object modifier. The template file generic-document.docx refers to the person using the variable x.

Inserting Jinja2 with Jinja2

If you use Jinja2 to insert Jinja2 template tags into a document assembled through docx template file, you will find that the tags in the included text will not be evaluated. However, you can conduct your document assembly in two stages, so that first you assemble a template and then you use the DOCX output as the input for another assembly.

code: |
  inserted_paragraph = "My favorite fruit is {{ favorite_fruit }} and I want the world to know."
---
question: |
  What is your favorite fruit?
fields:
  - Favorite fruit: favorite_fruit
---
attachment:
  variable name: the_template
  docx template file: twostage.docx
  valid formats:
    - docx
---
mandatory: True
question: |
  All done
attachment:
  name: A Document
  filename: a_document
  docx template file:
    code: |
      the_template.docx
Screenshot of twostage example

Altering metadata of generated DOCX files

This example demonstrates using the docx package to modify the core document properties of a generated DOCX file.

attachment:
  variable name: assembled_file
  docx template file: docx-with-metadata.docx
  valid formats:
    - docx
---
mandatory: True
code: |
  assembled_file
  user_name
  from docx import Document
  docx = Document(assembled_file.path())
  docx.core_properties.author = user_name
  docx.save(assembled_file.path())
  del docx
---
mandatory: True
question: Your document
attachment code: assembled_file
---
question: |
  What planet are you from?
fields:
  - Planet: planet
---
question: |
  What is your name?
fields:
  - Name: user_name
Screenshot of docxproperties example

Note that this interview uses Python code in a code block that should ideally go into a module file. The docx variable is an object from a third party module and is not able to be pickled. The code works this way in this interview because the code block ensures that the variable user_name is defined before the docx variable is created, and it deletes the docx variable with del docx before the code block finishes. If the variable user_name was undefined, docassemble would try to save the variable docx in the interview answers before asking about user_name, and this would result in a pickling error. If the docx variable only existed inside of a function in a module, there would be no problem with pickling.

Log out a user who has been idle for too long

Create a static file called idle.js with the following contents.

var idleTime = 0;
var idleInterval;
$(document).on('daPageLoad', function(){
    idleInterval = setInterval(idleTimerIncrement, 60000);
    $(document).mousemove(function (e) {
        idleTime = 0;
    });
    $(document).keypress(function (e) {
        idleTime = 0;
    });
});

function idleTimerIncrement() {
    idleTime = idleTime + 1;
    if (idleTime > 60){
        url_action_perform('log_user_out');
        clearInterval(idleInterval);
    }
}

In your interview, include idle.js in a features block.

features:
  javascript: idle.js
---
mandatory: True
code: |
  welcome_screen_seen
  final_screen
---
question: |
  Welcome to the interview.
field: welcome_screen_seen
---
event: final_screen
question: |
  You are done with the interview.
---
event: log_user_out
code: |
  command('logout')

This logs the user out after 60 minutes of inactivity in the browser. To use a different number of minutes, edit the line if (idleTime > 60){.

Seeing the progress of a running background task

Since background tasks run in a separate Celery process, there is no simple way to get information from them while they are running.

However, Redis lists provide a helpful mechanism for keeping track of log messages.

Here is an example that uses a DARedis object to store log messages about a long-running background task. It uses check in to poll the server for new log messages.

objects:
  r: DARedis
---
mandatory: True
code: |
  log_key = r.key('log:' + user_info().session)
  messages = list()
---
mandatory: True
code: |
  if the_task.ready():
    last_messages_retrieved
    final_screen
  else:
    waiting_screen
---
code: |
  the_task = background_action('bg_task', 'refresh', additional=value_to_add)
---
question: |
  How much shall I add to 553?
fields:
  - Number: value_to_add
    datatype: integer
---
event: bg_task
code: |
  import time
  r.rpush(log_key, 'Waking up.')
  time.sleep(10)
  r.rpush(log_key, 'Ok, I am awake now.')
  value = 553 + action_argument('additional')
  time.sleep(17)
  r.rpush(log_key, 'I did the hard work.')
  time.sleep(14)
  r.rpush(log_key, 'Ok, I am done.')
  background_response_action('bg_resp', ans=value)
---
event: bg_resp
code: |
  answer = action_argument('ans')
  background_response()
---
event: waiting_screen
question: |
  Your process is running.
subquestion: |
  #### Message log
  
  <ul class="list-group" id="logMessages">
  </ul>
check in: get_log
---
event: get_log
code: |
  import json
  new_messages = ''
  while True:
    message = r.lpop(log_key)
    if message:
      messages.append(message.decode())
      new_messages += '<li class="list-group-item">' + message.decode() + '</li>'
      continue
    break
  background_response('$("#logMessages").append(' + json.dumps(new_messages) + ')', 'javascript')
---
code: |
  while True:
    message = r.lpop(log_key)
    if message:
      messages.append(message.decode())
      continue
    break
  last_messages_retrieved = True
---
event: final_screen
question: |
  The answer is ${ answer }.
subquestion: |
  #### Message log
  
  <ul class="list-group" id="logMessages">
  % for message in messages:
    <li class="list-group-item">${ message }</li>
  % endfor
  </ul>
Screenshot of background-tail example

Since the task in this case (adding one number to another) is not actually long-running, the interview uses time.sleep() to make it artificially long-running.

Sending information from Python to JavaScript

If you use JavaScript in your interviews, and you want your JavaScript to have knowledge about the interview answers, you can use get_interview_variables(), but it is slow because it uses Ajax. If you only want a few pieces of information to be available to your JavaScript code, there are a few methods you can use.

One method is to use the script modifier.

imports:
  - json
---
question: |
  What is your favorite color?
fields:
  - Color: favorite_color
---
question: |
  What is your favorite fruit?
fields:
  - Fruit: favorite_fruit
script: |
  <script>
    var myColor = ${ json.dumps(favorite_color) };
    console.log("I know your favorite color is " + myColor);
  </script>
---
mandatory: True
question: |
  Your favorites
subquestion: |
  Your favorite fruit is
  ${ favorite_fruit }.

  Your favorite color is
  ${ favorite_color }.
Screenshot of pytojs-script example

Note that the variable is only guaranteed to be defined on the screen showing the question that includes the script modifier. While the value will persist from screen to screen, this is only because screen loads use Ajax and the JavaScript variables are not cleared out when a new screen loads. But a browser refresh will clear the JavaScript variables.

Another method is to use the "javascript" form of the log() function.

imports:
  - json
---
question: |
  What is your favorite color?
fields:
  - Color: favorite_color
---
initial: True
code: |
  log("var myColor = " + json.dumps(favorite_color) + ";", "javascript")
---
question: |
  What is your favorite fruit?
fields:
  - Fruit: favorite_fruit
script: |
  <script>
    console.log("I know that your favorite color is " + myColor);
  </script>
---
mandatory: True
question: |
  Your favorites
subquestion: |
  Your favorite fruit is
  ${ favorite_fruit }.

  Your favorite color is
  ${ favorite_color }.
Screenshot of pytojs-log example

In this example, the log() function is called from a code block that has initial set to True. Thus, you can rely on the myColor variable being defined on every screen of the interview after favorite_color gets defined.

Another method is to pass the values of Python variables to the browser using the DOM, and then use JavaScript to retrieve the values.

imports:
  - json
---
question: |
  What are your favorites?
fields:
  - Color: color
  - Flavor: flavor
---
question: |
  What is your favorite fruit?
subquestion: |
  <div id="myinfo"
       data-color=${ json.dumps(color) } 
       data-flavor=${ json.dumps(flavor) }
       class="d-none"></div>
fields:
  - Fruit: favorite_fruit
script: |
  <script>
    var myInfo = $("#myinfo").data();
    console.log("You like " + myInfo.color + " things that taste like " + myInfo.flavor + "."); 
  </script>
---
mandatory: True
question: |
  Your favorite fruit is
  ${ favorite_fruit }.
Screenshot of pytojs-dom example

All of these methods are read-only. If you want to be able to change variables using JavaScript, and also have the values saved to the interview answers, you can insert <input type="hidden"> elements onto a page that has a “Continue” button.

imports:
  - json
---
question: |
  What is your favorite color?
fields:
  - Color: favorite_color
---
question: |
  What is your favorite fruit?
subquestion: |
  <input type="hidden"
         name="${ encode_name('favorite_color') }"
         value=${ json.dumps(favorite_color) }>
fields:
  - Fruit: favorite_fruit
script: |
  <script>
    var myColor = val('favorite_color');
    console.log("You said you liked " + myColor);
    setField('favorite_color', 'dark ' + myColor);
    console.log("But now you like " + val('favorite_color'));
  </script>
---
mandatory: True
question: |
  Your favorites
subquestion: |
  Your favorite fruit is
  ${ favorite_fruit }.

  Your favorite color is
  ${ favorite_color }.
Screenshot of pytojs-hidden example

This example uses the encode_name() function to convert the variable name to the appropriate field name. For more information on manipulating the docassemble front end, see the section on custom front ends. The example above works for easily for text fields, but other data types will require more work.

Running actions with Ajax

Here is an example of using JavaScript to run an action using Ajax.

code: |
  favorite_fruit = "apples"
---
id: guess favorite fruit
mandatory: True
question: |
  Guess my favorite fruit.
fields:
  - Your guess: guess
  - note: |
      ${ action_button_html("#", id_tag="getFavoriteFruit", label="Verify", size="md", color="primary") }
script: |
  <script>
    $(document).on('daPageLoad', function(){
      // hide the Continue button
      // and disable the form for
      // this question
      if ($(".question-guess-favorite-fruit").length > 0){
        $(".da-field-buttons").remove();
        $("#daform").off().on('submit', function(event){
          event.preventDefault();
          return false;
        });
      };
    });
    $("#getFavoriteFruit").click(function(event){
      event.preventDefault();
      if (!/\S/.test(val("guess"))){
        flash("You need to guess something!", "danger", true);
        return false;
      }
      flash("Verifying . . .", "info", true);
      action_call("verify_favorite_fruit", {"fruit": val("guess")}, function(data){
        if (data.success){
          flash("You're right!", "info", true);
        }
        else {
          flash("You're totally wrong.  I actually like " + data.fruit + ".", "danger", true);
        }
      });
      return false;
    });
  </script>
---
event: verify_favorite_fruit
code: |
  # No need to save the interview
  # answers after this action.
  set_save_status('ignore')
  
  # Pretend we have to think
  # about the answer.
  import time
  time.sleep(1)
  
  if favorite_fruit.lower() == action_argument('fruit').lower():
    success = True
  else:
    success = False
  json_response(dict(success=success, fruit=favorite_fruit))
Screenshot of ajax example

The features used in this example include:

Collating assembled and uploaded documents

Here is an interview that uses pdf_concatenate() to bring assembled documents and user-provided documents in a single PDF file.

objects:
  - favorite: DADict.using(object_type=DAObject, keys=['fruit', 'vegetable', 'fungus'], gathered=True)
  - cover_page: DAList
---
question: |
  What is your favorite ${ i }?
fields:
  - "${ capitalize(i) }": favorite[i].variety
  - "Do you have documentation of this?": favorite[i].has_documentation
    datatype: yesnoradio
  - "Attach documentation": favorite[i].documentation
    datatype: file
    show if: favorite[i].has_documentation
---
attachment:
  variable name: summary_document
  content: |
    [BOLDCENTER] Your Favorite Things
    
    % for key, val in favorite.items():
    Your favorite ${ key } is
    ${ val.variety }.
    % if val.has_documentation:
    See Exhibit
    ${ alpha(val.documentation_reference) }.
    % endif
    
    % endfor
---
attachment:
  variable name: cover_page[i]
  docx template file: exhibit_insert.docx
---
mandatory: True
code: |
  information_gathered
  bundle
  final_screen
---
code: |
  indexno = 0
  for item in favorite.values():
    item.variety
    if item.has_documentation:
      item.documentation
      item.documentation_reference = indexno
      indexno += 1
  information_gathered = True
---
code: |
  document_list = list()
  document_list.append(summary_document)
  for item in favorite.values():
    if item.has_documentation:
      document_list.append(cover_page[item.documentation_reference])
      document_list.append(item.documentation)
  bundle = pdf_concatenate(document_list, filename="Favorites.pdf")
---
event: final_screen
question: |
  Your documents
subquestion: |
  ${ action_button_html(bundle.url_for(), color='link', label='Download bundle', icon='file-pdf') }
Screenshot of collate example

The Exhibit labeling sheets are generated by the template exhibit_insert.docx.

Payment processing with Stripe

First, sign up for a Stripe account.

From the dashboard, obtain your test API keys. There are two keys: a “publishable key” and a “secret key.” Put these into your Configuration as follows:

stripe public key: pk_test_ZjkQYPUU0pjQibxamUq28PlM00381Pd25e
stripe secret key: sk_test_YW41CYyivW0Vo7EN0mFD5i4P01ZLeQAPS8

The stripe public key is the “publishable key.” The stripe secret key is the “secret key.”

Confirm that you have the stripe package installed by checking the list of packages under “Package Management”. If stripe is not listed, follow the directions for installing a package. stripe is available on PyPI.

Create a Python module called dastripe.py with the following contents:

import stripe
import json
from docassemble.base.util import word, get_config, action_argument, DAObject, prevent_going_back
from docassemble.base.standardformatter import BUTTON_STYLE, BUTTON_CLASS

stripe.api_key = get_config('stripe secret key')

__all__ = ['DAStripe']

class DAStripe(DAObject):
  def init(self, *pargs, **kwargs):
    if get_config('stripe public key') is None or get_config('stripe secret key') is None:
      raise Exception("In order to use a DAStripe object, you need to set stripe public key and stripe secret key in your Configuration.")
    super().init(*pargs, **kwargs)
    if not hasattr(self, 'button_label'):
      self.button_label = "Pay"
    if not hasattr(self, 'button_color'):
      self.button_color = "primary"
    if not hasattr(self, 'error_message'):
      self.error_message = "Please try another payment method."
    self.is_setup = False

  def setup(self):
    float(self.amount)
    str(self.currency)
    self.intent = stripe.PaymentIntent.create(
      amount=int(float('%.2f' % self.amount)*100.0),
      currency=self.currency,
    )
    self.is_setup = True

  @property
  def html(self):
    if not self.is_setup:
      self.setup()
    return """\
<div id="stripe-card-element" class="mt-2"></div>
<div id="stripe-card-errors" class="mt-2 mb-2 text-alert" role="alert"></div>
<button class="btn """ + BUTTON_STYLE + self.button_color + " " + BUTTON_CLASS + '"' + """ id="stripe-submit">""" + word(self.button_label) + """</button>"""

  @property
  def javascript(self):
    if not self.is_setup:
      self.setup()
    billing_details = dict()
    try:
      billing_details['name'] = str(self.payor)
    except:
      pass
    address = dict()
    try:
      address['postal_code'] = self.payor.billing_address.zip
    except:
      pass
    try:
      address['line1'] = self.payor.billing_address.address
      address['line2'] = self.payor.billing_address.formatted_unit()
      address['city'] = self.payor.billing_address.city
      if hasattr(self.payor.billing_address, 'country'):
        address['country'] = address.billing_country
      else:
        address['country'] = 'US'
    except:
      pass
    if len(address):
      billing_details['address'] = address
    try:
      billing_details['email'] = self.payor.email
    except:
      pass
    try:
      billing_details['phone'] = self.payor.phone_number
    except:
      pass
    return """\
<script>
  var stripe = Stripe(""" + json.dumps(get_config('stripe public key')) + """);
  var elements = stripe.elements();
  var style = {
    base: {
      color: "#32325d",
    }
  };
  var card = elements.create("card", { style: style });
  card.mount("#stripe-card-element");
  card.addEventListener('change', ({error}) => {
    const displayError = document.getElementById('stripe-card-errors');
    if (error) {
      displayError.textContent = error.message;
    } else {
      displayError.textContent = '';
    }
  });
  var submitButton = document.getElementById('stripe-submit');
  submitButton.addEventListener('click', function(ev) {
    stripe.confirmCardPayment(""" + json.dumps(self.intent.client_secret) + """, {
      payment_method: {
        card: card,
        billing_details: """ + json.dumps(billing_details) + """
      }
    }).then(function(result) {
      if (result.error) {
        flash(result.error.message + "  " + """ + json.dumps(word(self.error_message)) + """, "danger");
      } else {
        if (result.paymentIntent.status === 'succeeded') {
          action_perform(""" + json.dumps(self.instanceName + '.success') + """, {result: result})
        }
      }
    });
  });
</script>
    """
  @property
  def paid(self):
    if not self.is_setup:
      self.setup()
    if hasattr(self, "payment_successful") and self.payment_successful:
      return True
    if not hasattr(self, 'result'):
      self.demand
    payment_status = stripe.PaymentIntent.retrieve(self.intent.id)
    if payment_status.amount_received == self.intent.amount:
      self.payment_successful = True
      return True
    return False
  def process(self):
    self.result = action_argument('result')
    self.paid
    prevent_going_back()

Create an interview YAML file (called, e.g., teststripe.yml) with the following contents:

modules:
  - .dastripe
---
features:
  javascript: https://js.stripe.com/v3/
---
objects:
  - payment: DAStripe.using(payor=client, currency='usd')
  - client: Individual
  - client.billing_address: Address
---
mandatory: True
code: |
  # Payor information may be required for some payment methods.
  client.name.first
  # client.billing_address.address
  # client.phone_number
  # client.email
  if not payment.paid:
    payment_screen
  favorite_fruit
  final_screen
---
question: |
  What is your name?
fields:
  - First: client.name.first
  - Last: client.name.last
---
question: |
  What is your phone number?
fields:
  - Phone: client.phone_number
---
question: |
  What is your e-mail address?
fields:
  - Phone: client.email
---
question: |
  What is your billing address?
fields:
  - Address: client.billing_address.address
    address autocomplete: True
  - Unit: client.billing_address.unit
    required: False
  - City: client.billing_address.city
  - State: client.billing_address.state
    code: states_list()
  - Zip: client.billing_address.zip
---
question: |
  How much do you want to pay?
fields:
  - Amount: payment.amount
    datatype: currency
---
event: payment.demand
question: |
  Payment
subquestion: |
  You need to pay up.  Enter your credit card information here.

  ${ payment.html }
script: |
  ${ payment.javascript }
---
event: payment.success
code: |
  payment.process()
---
question: |
  What is your favorite fruit?
fields:
  - Fruit: favorite_fruit
---
event: final_screen
question: Your favorite fruit
subquestion: |
  It is my considered opinion
  that your favorite fruit is
  ${ favorite_fruit }.

Test the interview with a testing card number and adapt it to your particular use case.

The attributes of the DAStripe object (known as payment in this example) that can be set are:

  • payment.currency: this is the currency that the payment will use. Set this to 'usd' for U.S. dollars. See supported accounts and settlement currencies for information about which currencies are available.
  • payment.payor: this contains information about the person who is paying. You can set this to an Individual or Person with a .billing_address (an Address), a name, a .phone_number, and an .email. This information will not be sought through dependency satisfaction; it will only be used if it exists. Thus, if you want to send this information (which may be required for the payment to go through), make sure your interview logic gathers it.
  • payment.amount: the amount of the payment to be made, in whatever currency you are using for the payment.
  • payment.button_label: the label for the “Pay” button. The default is “Pay.”
  • payment.button_color: the Bootstrap color for the “Pay” button. The default is primary.
  • payment.error_message: the error message that the user will see at the top of the screen if the credit card is not accepted. The default is “Please try another payment method.”

The attribute .paid returns True if the payment has been made or False if it has not. It also triggers the payment process. If payment.amount is not known, it will be sought.

The user is asked for their credit card information on a “special screen” tagged with event: request.demand. The variable request.demand is sought behind the scenes when the interview logic evaluates request.paid.

The request.demand page needs to include ${ payment.html } in the subquestion and ${ payment.javascript } in the script. The JavaScript produced by ${ payment.javascript } assumes that the file https://js.stripe.com/v3/ has been loaded in the browser already; this can be accomplished through a features block containing a javascript reference.

The “Pay” button is labeled “Pay” by default, but this can be customized with the request.button_label attribute. This value is passed through word(), so you can use the words translation system to translate it.

If the payment is not successful, the user will see the error message reported by Stripe, followed by the value of request.error_message, which is 'Please try another payment method.' by default. The value of request.error_message is passed through word(), so you can use the words translation system to translate it.

If the payment is successful, the JavaScript on the page performs the request.success “action” in the interview. Your interview needs to provide a code block that handles this action. The action needs to call payment.process(). This will save the data returned by Stripe and will also call the Stripe API to verify that payment was actually made. The code block for the “action” will run to the end, so the next thing it will do is evaluate the normal interview logic. When request.paid is encountered, it will evaluate to True.

The Stripe API is only called once to verify that the payment was actually made. Subsequent evaluations of request.paid will return True immediately without calling the API again.

Thus, the interview logic for the process of requiring a payment is just two lines of code:

if not payment.paid:
  payment_screen

Payment processing is a very complicated subject, so this recipe should only be considered a starting point. The advantage of this design is that it keeps a lot of the complexity of payment processing out of the interview YAML and hides it in the module.

If you want to have the billing address on the same screen where the credit card number is entered, you could use custom HTML forms, or a fields block in which the Continue button is hidden.

When you are satisfied that your payment process work correctly, you can set your stripe public key and stripe secret key to your “live” Stripe API keys on your production server.

Integration with form.io, AWS Lambda, and the docassemble API

This recipe shows how you can set up form.io to send a webhook request to an AWS Lambda function, which in turn calls the docassemble API to start an interview session, inject data into the interview answers of the session, and then send a notification to someone to let them know that a session has been started.

On your server, create the following interview, calling it fromformio.yml.

mandatory: True
code: |
  multi_user = True
---
code: |
  start_data = None
---
mandatory: |
  start_data is not None
code: |
  send_email(to='[email protected]', template=start_of_session)
---
template: start_of_session
subject: Session started
content: |
  A session has started.  [Go to it now](${ interview_url() }).
---
mandatory: True
question: |
  The start_data is:

  `${ repr(start_data) }`

Create an AWS Lambda function and trigger it with an HTTP REST API that is authenticated with an API key.

AWS Lambda

AWS Lambda API key

Add a layer that provides the requests module. Then write a function like the following.

lambda function code

import json
import os
import requests

def lambda_handler(event, context):
    try:
        start_data = json.loads(event['body'])
    except:
        return { 'statusCode': 400, 'body': json.dumps('Could not read JSON from the HTTP body') }
    key = os.environ['daapikey']
    root = os.environ['baseurl']
    i = os.environ['interview']
    r = requests.get(root + '/api/session/new', params={'key': key, 'i': i})
    if r.status_code != 200:
        return { 'statusCode': 400, 'body': json.dumps('Error creating new session at ' + root + '/api/session/new')}
    session = r.json()['session']
    r = requests.post(root + '/api/session', data={'key': key,
                                                   'i': i,
                                                   'session': session,
                                                   'variables': json.dumps({'start_data': start_data, 'event_data': event}),
                                                   'question': '1'})
    if r.status_code != 200:
        return { 'statusCode': 400, 'body': json.dumps('Error writing data to new session')}
    return { 'statusCode': 204, 'body': ''}

Set the environment variables so that your provide the function with an API key for the docassemble API (which you can set up in your Profile), the URL of your server, and the name of the interview you want to run (in this case, fromformio.yml is a Playground interview).

AWS Lambda

Go to form.io and create a form that looks like this:

form.io form

Attach a “webhook” action that sends a POST request to your AWS Lambda endpoint. Add the API key for the HTTP REST API trigger as the x-api-key header.

form.io action

Under Forms, click the “Use” button next to the form that you created and try submitting the form. The e-mail recipient designated in your YAML code should receive an e-mail containing the text:

A session has started. Go to it now.

where “Go to it now” is a hyperlink to an interview session. When the e-mail recipient clicks the link, they will resume an interview session in which the variable start_data contains the information from the form.io form.

interview session

Converting the result of object questions

When you gather objects using datatype: object_checkboxes or one of the other object-based data types, you might not want the variable you are setting to use object references. You can use the validation code feature to apply a transformation to the variable you are defining.

question: |
  Which fruits does ${ person[i] } like?
fields:
  - Fruits: person[i].fruit_preferences
    datatype: object_checkboxes
    choices:
      - fruit_data
validation code: |
  person[i].fruit_preferences = person[i].fruit_preferences.copy_deep(person[i].instanceName + '.fruit_preferences')
---
question: |
  How many seeds does
  ${ person[i].possessive('ideal ' + person[i].fruit_preferences[j].name.text) }
  have?
fields:
  Seeds: person[i].fruit_preferences[j].seeds
continue button field: person[i].fruit_preferences[j].preferred_seeds_verified
Screenshot of object-checkboxes-copy example

The result is that the fruit_preferences objects are copies of the original fruit_data object and have separate instanceNames.

Repeatable session with defaults

If you want to restart an interview session and use the answers from the just-finished session as default values for the new session, you can accomplish this using the depends on feature.

mandatory: True
code: |
  version = 0
---
event: new_version
code: |
  version += 1
---
depends on: version
question: |
  What is your favorite fruit?
fields:
  - Fruit: favorite_fruit
---
depends on: version
question: |
  What is your favorite vegetable?
fields:
  - Fruit: favorite_vegetable
---
depends on: version
question: |
  What is your favorite apple?
fields:
  - Fruit: favorite_apple
---
mandatory: True
question: |
  Summary
subquestion: |
  You like ${ favorite_fruit }
  and ${ favorite_vegetable }.
  % if favorite_fruit == 'apple':

  Your favorite apple is
  ${ favorite_apple }
  % endif
action buttons:
  - label: Try again
    action: new_version
    color: primary
Screenshot of repeatable example

When the variable counter is incremented by the new_version action, all of the variables set by question blocks that use depends on: counter will be undefined, but the old values will be remembered and offered as defaults when the question blocks are encountered again.

Universal document assembler

Here is an example of using the catchall questions feature to provide an interview that can ask questions to define arbitrary variables that are referenced in a document.

features:
  use catchall: True
---
generic object: DACatchAll
question: |
  What is ${ x.object_name() }?
fields:
  - no label: x.value
validation code: |
  define(x.instanceName, x.value)
---
if: |
  x.context == 'float' or (x.context == 'add' and isinstance(x.operand, float))
generic object: DACatchAll
question: |
  How much is ${ x.object_name() }?
fields:
  - Amount: x.value
    datatype: currency
validation code: |
  define(x.instanceName, x.value)
---
if: |
  x.context == 'int' or (x.context == 'add' and isinstance(x.operand, int))
generic object: DACatchAll
question: |
  How much is ${ x.object_name() }?
fields:
  - Amount: x.value
    datatype: integer
validation code: |
  define(x.instanceName, x.value)
---
if: |
  x.context == 'str' and x.instanceName.lower() == 'signature'
generic object: DACatchAll
question: |
  Sign your name
signature: x.value
validation code: |
  define(x.instanceName, x.value)
---
question: |
  Please attach a .docx file that you would like to assemble.
subquestion: |
  Place variables in brackets like so:

  > The quick brown {{ animal }} jumped 
  > over the {{ adjective }} {{ other_animal }}.

  To enter a signature, use `{{ signature }}`.

  To see how this works, try uploading this [test file].

  [test file]: ${ DAStaticFile(filename='universal-test.docx').url_for() }
fields:
  - no label: attached_template
    datatype: file
    accept: |
      "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
---
mandatory: True
question: Here is your document.
attachment:
  - docx template file:
      code: attached_template
Screenshot of universal-document example

Adjust Language for Second or Third Person

You may want to have a single interview that can be used either by a person for themselves, or by a person who is assisting another person. In the following example, there is an object named client that is of the type [Individual]. The variable user_is_client indicates whether the user is the client, or the user is assisting a third party.

Here is the text of the question and subquestion written in second person:

question: |
  Should your attorney be compensated for out-of-pocket expenses
  out of your property?
subquestion: |
  Your attorney may incur expenses in administering your property.
  If you allow your attorney to be compensated for out-of-pocket
  expenses out of your property, that may make the attorney's life
  easier.

  Do you want your attorney to be compensated for out-of-pocket
  expenses out of your property?
yesno: should_be_compensated

Here is how you might convert that text so that it will work properly if the user is the client, or if the client is someone else:

initial: True
code: |
  if user_is_client:
    set_info(user=client)
---
question: |
  Should ${ client.possessive('attorney') } be compensated for
  out-of-pocket expenses out of ${ client.possessive('property') }?
subquestion: |
  ${ client.possessive('attorney', capitalize=True) }
  may incur expenses in administering
  ${ client.pronoun_possessive('property') }.
  If
  ${ client.subject() }
  ${ client.does_verb("allow") }
  ${ client.pronoun_possessive('attorney') }
  to be compensated for out-of-pocket expenses out of
  ${ client.pronoun_possessive('property') },
  that may make the attorney's life easier.

  ${ client.do_question('want', capitalize=True) }
  ${ client.pronoun_possessive('attorney') }
  to be compensated for out-of-pocket expenses out of
  ${ client.pronoun_possessive('property') }?
yesno: should_be_compensated

This one block can now do two different things, and is still relatively readable.

Possessives

The first mention of the client in the sentence should use the client’s name. If the first mention of the client in the sentence is possessive, you should use ${ client.possessive('object') } to generate either “Client Name’s object” or “your object”.

Capitalization

Any of the language functions can be modified with capitalize=True if they are being used at the start of a sentence.

Allow user to read a document before signing it

This interview uses depends on to force the reassembly of a document after the user has viewed a draft version with a “sign here” sticker in place of the signature.

objects:
  - user: Individual
  - friend: Individual
---
mandatory: True
code: |
  user.name.first
  friend.name.first
  prized_collection
  draft_screen_shown
  user.signature
  document_finalized
  final_screen
---
attachment:
  variable name: instrument
  name: Transfer of Ownership
  filename: Transfer-of-Ownership
  pdf template file: |
    Transfer-of-Ownership.pdf
  fields:
    - "grantor": ${ user }
    - "grantee": ${ friend }
    - "collection": ${ prized_collection }
    - "signature": ${ user.signature if final else "[FILE sign-here.png]" }
# If the variable "final" changes,
# the document needs to be reassembled
depends on: final
---
# What it means for the document
# to be finalized
code: |
  # Put the document into final mode
  final = True
  # Submit the document somewhere
  # send_email(...)
  # Prevent duplicate submissions
  prevent_going_back()
  document_finalized = True
---
# Set an initial value for final
code: |
  final = False
---
question: |
  What is your name?
fields:
  - First Name: user.name.first
  - Last Name: user.name.last
---
question: |
  What is your best friend's name?
fields:
  - First Name: friend.name.first
  - Last Name: friend.name.last
---
question: What objects do you collect?
fields:
  - Collection: prized_collection
    hint: baseball cards, fine china
---
question: |
  Please sign your name below.
signature: user.signature
under: |
  ${ user }
---
question: Your draft document
subquestion: |
  Please review the attached document and
  press Continue if you agree to sign it.
continue button field: draft_screen_shown
attachment code: instrument
---
event: final_screen
question: Congratulations!
subquestion: |
  You have now transferred everything
  you own to ${ friend }.

  Here is your signed document.
attachment code: instrument
Screenshot of signature-preview example

Gathering multiple signatures on a document

This interview sends a document out for signature to an arbitrary number of signers, and then e-mails the document to the original user when all signatures have been provided.

objects:
  - user: Person
  - witnesses: DAList.using(object_type=Person, there_are_any=True, complete_attribute='complete')
  - signatories: DADict.using(auto_gather=False)
---
mandatory: True
code: |
  multi_user = True
---
mandatory: True
code: |
  user.name.text
  user.agrees_to_sign
  user.signature
  witnesses.gather()
  emails_sent
  told_to_wait
---
code: |
  told_to_wait = True
  force_ask('user.status')
---
code: |
  user.code
  signatories.gather()
  background_action('send_out_for_signature')
  emails_sent = True
---
mandatory: True
code: |
  process_action()
  force_ask('unauthorized')
---
question: |
  What is your name and e-mail address?
fields:
  - Name: user.name.text
  - E-mail: user.email
---
question: |
  What is your favorite fruit?
fields:
  - Fruit: user.favorite_fruit
---
question: |
  If you attest that your
  favorite fruit is
  ${ user.favorite_fruit },
  press Continue.
subquestion: |
  On the next page, you will sign
  your name to an attestation
  to this effect.
continue button field: user.agrees_to_sign
---
question: |
  What is the name and e-mail
  address
  % if i == 0:
  of a
  % else:
  of the ${ ordinal(i) }
  % endif
  witness who can attest that
  you like
  ${ user.favorite_fruit }?
fields:
  - Name: witnesses[i].name.text
  - E-mail: witnesses[i].email
---
question: |
  Besides ${ witnesses }, can anyone
  else attest that you like
  ${ user.favorite_fruit }?
yesno: witnesses.there_is_another
---
code: |
  witnesses[i].name.text
  witnesses[i].signed
  witnesses[i].signature
  witnesses[i].signature_date
  witnesses[i].complete = True
---
generic object: Person
code: |
  import random
  import string
  x.code = ''.join(random.choice(string.ascii_lowercase) for i in range(10))
---
code: |
  witnesses[i].sign_url = interview_url_action('request_signature', code=witnesses[i].code)
---
code: |
  for witness in witnesses:
    signatories[witness.code] = witness
  signatories.gathered = True
---
event: send_out_for_signature
code: |
  for witness in witnesses:
    witness.notified
  user.notified
  background_response()
---
code: |
  send_email(to=user, template=initial_email_template)
  user.notified = True
---
code: |
  send_email(to=witness, template=witness.email_template)
  witnesses[i].notified = True
---
template: initial_email_template
subject: |
  Your Declaration of Favorite Fruit is out for signature.
content: |
  ${ user },

  % if witnesses.number() == 1:
  I have e-mailed your
  witness to request a
  signature on the
  Declaration of Favorite Fruit.

  I will e-mail you when
  the witness has signed,
  but in the meantime you can
  [check the status].
  % else:
  I have e-mailed your
  witnesses
  to request that they sign the
  Declaration of Favorite Fruit.

  I will e-mail you when
  all the witnesses have
  signed.  You can
  [check the status] of your
  document to see who still has
  not signed.
  % endif

  [check the status]: ${ interview_url_action('status_page', code=user.code) }
---
template: witnesses[i].email_template
subject: |
  Your signature needed on
  a document for ${ user }
content: |
  Dear ${ witnesses[i] },

  You are requested to
  witness a sworn statement
  attesting that ${ user }
  likes
  ${ user.favorite_fruit }.

  Please [click here] to
  view the document you are
  being asked to sign.

  [click here]: ${ witnesses[i].sign_url }
---
attachment:
  name: |
    ${ user.possessive('Declaration of Favorite Fruit') }
  filename: |
    fruit_declaration_${ space_to_underscore(user) }
  docx template file: fruit_declaration.docx
  variable name: statement
---
event: unauthorized
code: |
  response('Page not found', response_code=404)
---
event: status_page
code: |
  if action_argument('code') != user.code:
    force_ask('unauthorized')
  force_ask('user.status')
---
event: request_signature
code: |
  if action_argument('code') not in signatories:
    force_ask('unauthorized')
  witness = signatories[action_argument('code')]
  if witness.signed:
    force_ask(witness.instanceName + '.thanked')
  force_ask(witness.instanceName + '.willing_to_sign',
            witness.instanceName + '.signature',
            { 'recompute': [ witness.instanceName + '.signature_obtained' ] },
            witness.instanceName + '.thanked')
---
reconsider: statement
question: |
  Are you willing to sign a
  statement attesting that
  ${ user } likes
  ${ user.favorite_fruit }?
subquestion: |
  You can
  [preview the statement](${ statement.pdf.url_for() })
  now.

  If you are willing to sign,
  press Continue.  On
  the next screen, you will
  provide your signature.
continue button field: witnesses[i].willing_to_sign
---
generic object: Person
question: |
  Sign your name
signature: x.signature
under: |
  ${ x }
---
code: |
  for witness in witnesses:
    if not witness.signed:
      all_signatures_in = False
      break
  else:
    final_email_sent
    all_signatures_in = True
---
code: |
  background_action('send_final_email')
  final_email_sent = True
---
event: send_final_email
code: |
  send_email(to=user, template=final_email_template, attachments=statement)
  background_response()
---
template: final_email_template
subject: Your Declaration of Favorite Fruit has been signed by all parties
content: |
  ${ user },

  Your Declaration of Favorite
  Fruit has now been signed by
  all of your witnesses.
  Please see attached.
---
code: |
  user.signature_date = today()
---
only sets: witnesses[i].signature_obtained
code: |
  witnesses[i].signed = True
  witnesses[i].signature_date = today()
  witnesses[i].signature_obtained = True
---
code: |
  witnesses[i].signed = False
---
code: |
  witnesses[i].signature_date = "_________________"
---
code: |
  witnesses[i].signature = "____________________________"
---
event: user.status
reconsider:
  - all_signatures_in
  - statement
question: |
  % if all_signatures_in:
  Your Declaration has been signed.
  % else:
  Your Declaration is out for signature.
  % endif
subquestion: |
  % if not all_signatures_in:
    % for witness in witnesses:
      % if witness.signed:
  ${ witness } has signed.

      % else:
  ${ witness } has not signed yet.

      % endif
    % endfor
  % endif
attachment code: statement
---
event: witnesses[i].thanked
reconsider:
  - all_signatures_in
  - statement
question: |
  Thank you for witnessing
  ${ user.possessive('Declaration of Favorite Fruit') }
subquestion: |
  If you would like a signed
  copy for your records, you
  may download it below.

  % if not all_signatures_in:
  Please note that not everyone
  has signed the document yet.
  % endif
attachment code: statement
Screenshot of multi-signature example

This interview is somewhat complicated, but there are security reasons for the use of complicated features; otherwise signers could manipulate URLs and sign on behalf of other people.

Validating international phone numbers

Here is a simple way to validate international phone numbers.

objects:
  - user: Individual
---
question: |
  Is your mobile phone number
  based in the United States
  or Canada?
field: user.phone_international
buttons:
  - U.S. or Canada: False
  - Another country: True
---
question: |
  What country is associated
  with your mobile phone number?
field: user.phone_country
dropdown:
  code: countries_list()
---
code: |
  if not user.phone_international:
    user.phone_country = 'US'
---
question: |
  What is your phone number?
subquestion: |
  % if user.phone_international:
  Enter your phone number as you
  would dial it in
  ${ country_name(user.phone_country) }.
  % endif
fields:
  - Phone number: user.mobile_number
    validate: |
      lambda y: phone_number_is_valid(y, country=user.phone_country) or validation_error("Please enter a valid " + country_name(user.phone_country) + " phone number." )
---
mandatory: True
question: |
  I will text you at
  `${ user.sms_number(country=user.phone_country) }`.
Screenshot of phone-number example

Here is a way to validate international phone numbers using a single screen.

objects:
  - user: Individual
---
question: |
  What is your phone number?
fields:
  - label: |
      Is your mobile phone number
      based in the United States
      or Canada?
    field: user.phone_international
    datatype: noyesradio
    default: False
  - label: |
      What country is associated
      with your mobile phone number?
    field: user.phone_country
    code: countries_list()
    show if: user.phone_international
  - label: |
      Enter your phone number
    field: user.mobile_number
validation code: |
  if user.phone_international:
    if not phone_number_is_valid(user.mobile_number, country=user.phone_country):
      validation_error('Please enter a valid phone number for ' + country_name(user.phone_country) + '.', field='user.mobile_number')
  else:
    if not phone_number_is_valid(user.mobile_number):
      validation_error('Please enter a valid phone number.', field='user.mobile_number')
---
code: |
  if not user.phone_international:
    user.phone_country = 'US'
---
mandatory: True
question: |
  I will text you at
  `${ user.sms_number(country=user.phone_country) }`.
Screenshot of phone-number-2 example

Customizing the date input

If you don’t like the way that web browsers implement date inputs, you can customize the way dates are displayed.

features:
  javascript: datereplace.js
---
question: |
  When were you born?
fields:
  - Date of birth: date_of_birth
    datatype: date
Screenshot of customdate example

This interview uses a JavaScript file datereplace.js. The JavaScript converts a regular date input element into a hidden element and then adds name-less elements to the DOM. This approach preserves default values.

Running side processes outside of the interview logic

Normally, the order of questions in a docassemble interview is determined by the interview logic: questions are asked to obtain the definitions of undefined variables, and the interview ends when all the necessary variables have been defined.

Sometimes, however, you might want to send the user through a logical process that is not driven by the need to definitions of undefined variables. For example, you might want to:

  • Ask a series of questions again to make sure the user has a second chance to get the answers right;
  • Ask the user specific follow-up questions after the user makes an edit to an answer.
  • Give the user the option of going through a process one or more times.

The following interview is an example of the latter. At the end of the interview logic, the user has the option of pressing a button in order to go through a repeatable multi-step process that contains conditional logic.

objects:
  - user: Individual
  - court_filing_unit: DAEmailRecipient.using(name='Prothonotary of the Court', address='[email protected]')
---
question: |
  Please provide your name and
  e-mail address.
fields:
  - First Name: user.name.first
  - Last Name: user.name.last
  - E-mail address: user.email
    datatype: email
---
question: |
  What is your favorite fruit?
fields:
  - Fruit: user.favorite_fruit
---
attachment:
  name: |
    Praecipe to Declare Favorite Fruit
  filename: Praecipe_Favorite_Fruit_${ space_to_underscore(user) }
  content: |
    [BOLDCENTER] Praecipe to Declare
    Favorite Fruit

    I, ${ user }, on ${ today() }, hereby
    inform all interested parties that my
    favorite fruit is
    ${ user.favorite_fruit }.
  variable name: court_document
---
mandatory: True
question: |
  Your Praecipe to Declare Favorite Fruit
subquestion: |
  % if task_performed('filed_in_court'):
  You have filed your **Praecipe to Declare
  Favorite Fruit** in court.
  
  Follow up with the court if you have
  any questions about the legal process.
  % else:
  When you are ready to file your Praecipe
  to Declare Favorite Fruit in court, press
  this button.
  
  ${ action_button_html(url_action('filing_process'),
                        label='Submit this to the court',
                        size='md',
                        color='warning') }

  Until then, take your time and look
  over the document to make sure you
  really want to file it.
  % endif
attachment code: court_document
---
event: filing_process
code: |
  if task_performed('filed_in_court'):
    # This is unlikely to happen but
    # still worth protecting against.
    log('You have already filed the document.', 'danger')
  else:
    force_ask('wants_to_file',
              'email_address_confirmed',
              'really_wants_to_file',
              {'recompute': ['document_filed']},
              'end_of_filing_process')
---
question: |
  Are you completely and totally
  sure you want to file your Praecipe
  to Declare Favorite Fruit?
subquestion: |
  Once you file your Praecipe, you
  can't take it back.
field: wants_to_file
buttons:
  - "Yes, I am sure I want to file.": True
  - "On second thought, I am not ready": False
---
if: wants_to_file
question: |
  What is your e-mail address?
subquestion: |
  I am going to e-mail your document
  to the court.  I need to cc you on
  the e-mail so you have it for your
  records.

  Please confirm that this is your
  e-mail address.
fields:
  - E-mail: user.email
continue button field: email_address_confirmed
---
if: wants_to_file
question: |
  Are you completely and totally sure
  beyond a shadow of a doubt that you
  wish to file your Praecipe in court?
subquestion: |
  You can [take one last look at it]
  before submitting.

  [take one last look at it]: ${ court_document.pdf.url_for() }
field: really_wants_to_file
buttons:
  - "Submit the document now": True
  - "Get me out of here!": False
---
template: filing_template
subject: |
  E-filing: Praecipe to Declare
  Favorite Fruit on behalf of
  ${ user }
content: |
  To the Prothonotary:

  Please accept the attached
  Praecipe to Declare Favorite Fruit,
  which I am filing on behalf of
  ${ user }.

  Thank you,

  A local docassemble server

  Cc: ${ user } via e-mail at ${ user.email }
---
code: |
  if wants_to_file and really_wants_to_file:
    document_filed = send_email(to=court_filing_unit,
                                cc=user,
                                template=filing_template,
                                attachments=court_document,
                                task='filed_in_court')
    if document_filed:
      prevent_going_back()
    else:
      log('There was a problem with the e-mail system.  Your document was not filed.', 'danger')
  else:
    document_filed = False
---
if: wants_to_file and really_wants_to_file and document_filed
question: |
  Congratulations!
subquestion: |
  Your document is now on its way to the
  court.
continue button field: end_of_filing_process
Screenshot of courtfile example

This takes advantage of an important feature of force_ask(). If you give force_ask() a variable and there is no question in the interview YAML that can be asked in order to define that variable, then force_ask() will ignore that variable. The call to force_ask() lists all of the questions that might be asked in the process, and the if modifiers are used to indicate under what conditions the questions should be asked.