Edit this page on GitHub

Recipes

This section contains miscellaneous recipes for solving problems in docassemble.

Require a checkbox to be checked

Using validation code

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

Using datatype: checkboxes

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

Use a variable to track when an interview has been completed

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

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

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

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

Exit interview with a hyperlink rather than a redirect

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

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

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

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

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

Ensure two fields match

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

Progresive disclosure

modules:
  - .progressivedisclosure
---
features:
  css: progressivedisclosure.css
---
template: fruit_explanation
subject: |
  Tell me more about fruit
content: |
  ##### What is a fruit?
  
  A fruit is the the sweet and
  fleshy product of a tree or
  other plant that contains
  seed and can be eaten as food.
---
template: favorite_explanation
subject: |
  Explain favorites
content: |
  ##### What is a favorite?

  If you have a favorite something,
  that means you like it more than
  you like other things of a similar
  nature.
Screenshot of progressive-disclosure example

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

a span.pdcaretopen {
    display: inline;
}

a span.pdcaretclosed {
    display: none;
}

a.collapsed .pdcaretopen {
    display: none;
}

a.collapsed .pdcaretclosed {
    display: inline;
}

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

import re

__all__ = ['prog_disclose']

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

This uses the collapse feature of Bootstrap.

New object or existing object

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

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

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

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

  The employee is ${ employee }.

  The customers are ${ customers }.

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

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

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

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

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

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

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

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

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

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

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

    My favorite vegetable is
    ${ favorite_vegetable }.

    I solemnly swear that the
    foregoing is true and
    correct.

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

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

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

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

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

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

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

Multi-user interview for getting a client’s signature

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

{{ signature }}

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

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

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

Validating uploaded files

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

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

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.