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()
---
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